/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.harmony.xml.dom;

import com.google.j2objc.annotations.Weak;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Attr;
import org.w3c.dom.CharacterData;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.TypeInfo;
import org.w3c.dom.UserDataHandler;

/**
 * A straightforward implementation of the corresponding W3C DOM node.
 *
 * <p>Some fields have package visibility so other classes can access them while
 * maintaining the DOM structure.
 *
 * <p>This class represents a Node that has neither a parent nor children.
 * Subclasses may have either.
 *
 * <p>Some code was adapted from Apache Xerces.
 */
public abstract class NodeImpl implements Node {

    private static final NodeList EMPTY_LIST = new NodeListImpl();

    static final TypeInfo NULL_TYPE_INFO = new TypeInfo() {
        public String getTypeName() {
            return null;
        }
        public String getTypeNamespace() {
            return null;
        }
        public boolean isDerivedFrom(
                String typeNamespaceArg, String typeNameArg, int derivationMethod) {
            return false;
        }
    };

    /**
     * The containing document. This is non-null except for DocumentTypeImpl
     * nodes created by the DOMImplementation.
     */
    @Weak DocumentImpl document;

    NodeImpl(DocumentImpl document) {
        this.document = document;
    }

    public Node appendChild(Node newChild) throws DOMException {
        throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
    }

    public final Node cloneNode(boolean deep) {
        return document.cloneOrImportNode(UserDataHandler.NODE_CLONED, this, deep);
    }

    public NamedNodeMap getAttributes() {
        return null;
    }

    public NodeList getChildNodes() {
        return EMPTY_LIST;
    }

    public Node getFirstChild() {
        return null;
    }

    public Node getLastChild() {
        return null;
    }

    public String getLocalName() {
        return null;
    }

    public String getNamespaceURI() {
        return null;
    }

    public Node getNextSibling() {
        return null;
    }

    public String getNodeName() {
        return null;
    }

    public abstract short getNodeType();

    public String getNodeValue() throws DOMException {
        return null;
    }

    public final Document getOwnerDocument() {
        return document == this ? null : document;
    }

    public Node getParentNode() {
        return null;
    }

    public String getPrefix() {
        return null;
    }

    public Node getPreviousSibling() {
        return null;
    }

    public boolean hasAttributes() {
        return false;
    }

    public boolean hasChildNodes() {
        return false;
    }

    public Node insertBefore(Node newChild, Node refChild) throws DOMException {
        throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
    }

    public boolean isSupported(String feature, String version) {
        return DOMImplementationImpl.getInstance().hasFeature(feature, version);
    }

    public void normalize() {
    }

    public Node removeChild(Node oldChild) throws DOMException {
        throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
    }

    public Node replaceChild(Node newChild, Node oldChild) throws DOMException {
        throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
    }

    public final void setNodeValue(String nodeValue) throws DOMException {
        switch (getNodeType()) {
            case CDATA_SECTION_NODE:
            case COMMENT_NODE:
            case TEXT_NODE:
                ((CharacterData) this).setData(nodeValue);
                return;

            case PROCESSING_INSTRUCTION_NODE:
                ((ProcessingInstruction) this).setData(nodeValue);
                return;

            case ATTRIBUTE_NODE:
                ((Attr) this).setValue(nodeValue);
                return;

            case ELEMENT_NODE:
            case ENTITY_REFERENCE_NODE:
            case ENTITY_NODE:
            case DOCUMENT_NODE:
            case DOCUMENT_TYPE_NODE:
            case DOCUMENT_FRAGMENT_NODE:
            case NOTATION_NODE:
                return; // do nothing!

            default:
                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
                        "Unsupported node type " + getNodeType());
        }
    }

    public void setPrefix(String prefix) throws DOMException {
    }

    /**
     * Validates the element or attribute namespace prefix on this node.
     *
     * @param namespaceAware whether this node is namespace aware
     * @param namespaceURI this node's namespace URI
     */
    static String validatePrefix(String prefix, boolean namespaceAware, String namespaceURI) {
        if (!namespaceAware) {
            throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
        }

        if (prefix != null) {
            if (namespaceURI == null
                    || !DocumentImpl.isXMLIdentifier(prefix)
                    || "xml".equals(prefix) && !"http://www.w3.org/XML/1998/namespace".equals(namespaceURI)
                    || "xmlns".equals(prefix) && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) {
                throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
            }
        }

        return prefix;
    }

    /**
     * Sets {@code node} to be namespace-aware and assigns its namespace URI
     * and qualified name.
     *
     * @param node an element or attribute node.
     * @param namespaceURI this node's namespace URI. May be null.
     * @param qualifiedName a possibly-prefixed name like "img" or "html:img".
     */
    static void setNameNS(NodeImpl node, String namespaceURI, String qualifiedName) {
        if (qualifiedName == null) {
            throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName);
        }

        String prefix = null;
        int p = qualifiedName.lastIndexOf(":");
        if (p != -1) {
            prefix = validatePrefix(qualifiedName.substring(0, p), true, namespaceURI);
            qualifiedName = qualifiedName.substring(p + 1);
        }

        if (!DocumentImpl.isXMLIdentifier(qualifiedName)) {
            throw new DOMException(DOMException.INVALID_CHARACTER_ERR, qualifiedName);
        }

        switch (node.getNodeType()) {
        case ATTRIBUTE_NODE:
            if ("xmlns".equals(qualifiedName)
                    && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) {
                throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName);
            }

            AttrImpl attr = (AttrImpl) node;
            attr.namespaceAware = true;
            attr.namespaceURI = namespaceURI;
            attr.prefix = prefix;
            attr.localName = qualifiedName;
            break;

        case ELEMENT_NODE:
            ElementImpl element = (ElementImpl) node;
            element.namespaceAware = true;
            element.namespaceURI = namespaceURI;
            element.prefix = prefix;
            element.localName = qualifiedName;
            break;

        default:
            throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
                    "Cannot rename nodes of type " + node.getNodeType());
        }
    }

    /**
     * Sets {@code node} to be not namespace-aware and assigns its name.
     *
     * @param node an element or attribute node.
     */
    static void setName(NodeImpl node, String name) {
        int prefixSeparator = name.lastIndexOf(":");
        if (prefixSeparator != -1) {
            String prefix = name.substring(0, prefixSeparator);
            String localName = name.substring(prefixSeparator + 1);
            if (!DocumentImpl.isXMLIdentifier(prefix) || !DocumentImpl.isXMLIdentifier(localName)) {
                throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
            }
        } else if (!DocumentImpl.isXMLIdentifier(name)) {
            throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
        }

        switch (node.getNodeType()) {
        case ATTRIBUTE_NODE:
            AttrImpl attr = (AttrImpl) node;
            attr.namespaceAware = false;
            attr.localName = name;
            break;

        case ELEMENT_NODE:
            ElementImpl element = (ElementImpl) node;
            element.namespaceAware = false;
            element.localName = name;
            break;

        default:
            throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
                    "Cannot rename nodes of type " + node.getNodeType());
        }
    }

    public final String getBaseURI() {
        switch (getNodeType()) {
            case DOCUMENT_NODE:
                return sanitizeUri(((Document) this).getDocumentURI());

            case ELEMENT_NODE:
                Element element = (Element) this;
                String uri = element.getAttributeNS(
                        "http://www.w3.org/XML/1998/namespace", "base"); // or "xml:base"

                try {
                    // if this node has no base URI, return the parent's.
                    if (uri == null || uri.isEmpty()) {
                        return getParentBaseUri();
                    }

                    // if this node's URI is absolute, return it
                    if (new URI(uri).isAbsolute()) {
                        return uri;
                    }

                    // this node has a relative URI. Try to resolve it against the
                    // parent, but if that doesn't work just give up and return null.
                    String parentUri = getParentBaseUri();
                    if (parentUri == null) {
                        return null;
                    }

                    return new URI(parentUri).resolve(uri).toString();
                } catch (URISyntaxException e) {
                    return null;
                }

            case PROCESSING_INSTRUCTION_NODE:
                return getParentBaseUri();

            case NOTATION_NODE:
            case ENTITY_NODE:
                // When we support these node types, the parser should
                // initialize a base URI field on these nodes.
                return null;

            case ENTITY_REFERENCE_NODE:
                // TODO: get this value from the parser, falling back to the
                // referenced entity's baseURI if that doesn't exist
                return null;

            case DOCUMENT_TYPE_NODE:
            case DOCUMENT_FRAGMENT_NODE:
            case ATTRIBUTE_NODE:
            case TEXT_NODE:
            case CDATA_SECTION_NODE:
            case COMMENT_NODE:
                return null;

            default:
                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
                        "Unsupported node type " + getNodeType());
        }
    }

    private String getParentBaseUri() {
        Node parentNode = getParentNode();
        return parentNode != null ? parentNode.getBaseURI() : null;
    }

    /**
     * Returns the sanitized input if it is a URI, or {@code null} otherwise.
     */
    private String sanitizeUri(String uri) {
        if (uri == null || uri.length() == 0) {
            return null;
        }
        try {
            return new URI(uri).toString();
        } catch (URISyntaxException e) {
            return null;
        }
    }

    public short compareDocumentPosition(Node other)
            throws DOMException {
        throw new UnsupportedOperationException(); // TODO
    }

    public String getTextContent() throws DOMException {
        return getNodeValue();
    }

    void getTextContent(StringBuilder buf) throws DOMException {
        String content = getNodeValue();
        if (content != null) {
            buf.append(content);
        }
    }

    public final void setTextContent(String textContent) throws DOMException {
        switch (getNodeType()) {
            case DOCUMENT_TYPE_NODE:
            case DOCUMENT_NODE:
                return; // do nothing!

            case ELEMENT_NODE:
            case ENTITY_NODE:
            case ENTITY_REFERENCE_NODE:
            case DOCUMENT_FRAGMENT_NODE:
                // remove all existing children
                Node child;
                while ((child = getFirstChild()) != null) {
                    removeChild(child);
                }
                // create a text node to hold the given content
                if (textContent != null && textContent.length() != 0) {
                    appendChild(document.createTextNode(textContent));
                }
                return;

            case ATTRIBUTE_NODE:
            case TEXT_NODE:
            case CDATA_SECTION_NODE:
            case PROCESSING_INSTRUCTION_NODE:
            case COMMENT_NODE:
            case NOTATION_NODE:
                setNodeValue(textContent);
                return;

            default:
                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
                        "Unsupported node type " + getNodeType());
        }
    }

    public boolean isSameNode(Node other) {
        return this == other;
    }

    /**
     * Returns the element whose namespace definitions apply to this node. Use
     * this element when mapping prefixes to URIs and vice versa.
     */
    private NodeImpl getNamespacingElement() {
        switch (this.getNodeType()) {
            case ELEMENT_NODE:
                return this;

            case DOCUMENT_NODE:
                return (NodeImpl) ((Document) this).getDocumentElement();

            case ENTITY_NODE:
            case NOTATION_NODE:
            case DOCUMENT_FRAGMENT_NODE:
            case DOCUMENT_TYPE_NODE:
                return null;

            case ATTRIBUTE_NODE:
                return (NodeImpl) ((Attr) this).getOwnerElement();

            case TEXT_NODE:
            case CDATA_SECTION_NODE:
            case ENTITY_REFERENCE_NODE:
            case PROCESSING_INSTRUCTION_NODE:
            case COMMENT_NODE:
                return getContainingElement();

            default:
                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
                        "Unsupported node type " + getNodeType());
        }
    }

    /**
     * Returns the nearest ancestor element that contains this node.
     */
    private NodeImpl getContainingElement() {
        for (Node p = getParentNode(); p != null; p = p.getParentNode()) {
            if (p.getNodeType() == ELEMENT_NODE) {
                return (NodeImpl) p;
            }
        }
        return null;
    }

    public final String lookupPrefix(String namespaceURI) {
        if (namespaceURI == null) {
            return null;
        }

        // the XML specs define some prefixes (like "xml" and "xmlns") but this
        // API is explicitly defined to ignore those.

        NodeImpl target = getNamespacingElement();
        for (NodeImpl node = target; node != null; node = node.getContainingElement()) {
            // check this element's namespace first
            if (namespaceURI.equals(node.getNamespaceURI())
                    && target.isPrefixMappedToUri(node.getPrefix(), namespaceURI)) {
                return node.getPrefix();
            }

            // search this element for an attribute of this form:
            //   xmlns:foo="http://namespaceURI"
            if (!node.hasAttributes()) {
                continue;
            }
            NamedNodeMap attributes = node.getAttributes();
            for (int i = 0, length = attributes.getLength(); i < length; i++) {
                Node attr = attributes.item(i);
                if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())
                        || !"xmlns".equals(attr.getPrefix())
                        || !namespaceURI.equals(attr.getNodeValue())) {
                    continue;
                }
                if (target.isPrefixMappedToUri(attr.getLocalName(), namespaceURI)) {
                    return attr.getLocalName();
                }
            }
        }

        return null;
    }

    /**
     * Returns true if the given prefix is mapped to the given URI on this
     * element. Since child elements can redefine prefixes, this check is
     * necessary: {@code
     * <foo xmlns:a="http://good">
     *   <bar xmlns:a="http://evil">
     *     <a:baz />
     *   </bar>
     * </foo>}
     *
     * @param prefix the prefix to find. Nullable.
     * @param uri the URI to match. Non-null.
     */
    boolean isPrefixMappedToUri(String prefix, String uri) {
        if (prefix == null) {
            return false;
        }

        String actual = lookupNamespaceURI(prefix);
        return uri.equals(actual);
    }

    public final boolean isDefaultNamespace(String namespaceURI) {
        String actual = lookupNamespaceURI(null); // null yields the default namespace
        return namespaceURI == null
                ? actual == null
                : namespaceURI.equals(actual);
    }

    public final String lookupNamespaceURI(String prefix) {
        NodeImpl target = getNamespacingElement();
        for (NodeImpl node = target; node != null; node = node.getContainingElement()) {
            // check this element's namespace first
            String nodePrefix = node.getPrefix();
            if (node.getNamespaceURI() != null) {
                if (prefix == null // null => default prefix
                        ? nodePrefix == null
                        : prefix.equals(nodePrefix)) {
                    return node.getNamespaceURI();
                }
            }

            // search this element for an attribute of the appropriate form.
            //    default namespace: xmlns="http://resultUri"
            //          non default: xmlns:specifiedPrefix="http://resultUri"
            if (!node.hasAttributes()) {
                continue;
            }
            NamedNodeMap attributes = node.getAttributes();
            for (int i = 0, length = attributes.getLength(); i < length; i++) {
                Node attr = attributes.item(i);
                if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())) {
                    continue;
                }
                if (prefix == null // null => default prefix
                        ? "xmlns".equals(attr.getNodeName())
                        : "xmlns".equals(attr.getPrefix()) && prefix.equals(attr.getLocalName())) {
                    String value = attr.getNodeValue();
                    return value.length() > 0 ? value : null;
                }
            }
        }

        return null;
    }

    /**
     * Returns a list of objects such that two nodes are equal if their lists
     * are equal. Be careful: the lists may contain NamedNodeMaps and Nodes,
     * neither of which override Object.equals(). Such values must be compared
     * manually.
     */
    private static List<Object> createEqualityKey(Node node) {
        List<Object> values = new ArrayList<Object>();
        values.add(node.getNodeType());
        values.add(node.getNodeName());
        values.add(node.getLocalName());
        values.add(node.getNamespaceURI());
        values.add(node.getPrefix());
        values.add(node.getNodeValue());
        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
            values.add(child);
        }

        switch (node.getNodeType()) {
            case DOCUMENT_TYPE_NODE:
                DocumentTypeImpl doctype = (DocumentTypeImpl) node;
                values.add(doctype.getPublicId());
                values.add(doctype.getSystemId());
                values.add(doctype.getInternalSubset());
                values.add(doctype.getEntities());
                values.add(doctype.getNotations());
                break;

            case ELEMENT_NODE:
                Element element = (Element) node;
                values.add(element.getAttributes());
                break;
        }

        return values;
    }

    public final boolean isEqualNode(Node arg) {
        if (arg == this) {
            return true;
        }

        List<Object> listA = createEqualityKey(this);
        List<Object> listB = createEqualityKey(arg);

        if (listA.size() != listB.size()) {
            return false;
        }

        for (int i = 0; i < listA.size(); i++) {
            Object a = listA.get(i);
            Object b = listB.get(i);

            if (a == b) {
                continue;

            } else if (a == null || b == null) {
                return false;

            } else if (a instanceof String || a instanceof Short) {
                if (!a.equals(b)) {
                    return false;
                }

            } else if (a instanceof NamedNodeMap) {
                if (!(b instanceof NamedNodeMap)
                        || !namedNodeMapsEqual((NamedNodeMap) a, (NamedNodeMap) b)) {
                    return false;
                }

            } else if (a instanceof Node) {
                if (!(b instanceof Node)
                        || !((Node) a).isEqualNode((Node) b)) {
                    return false;
                }

            } else {
                throw new AssertionError(); // unexpected type
            }
        }

        return true;
    }

    private boolean namedNodeMapsEqual(NamedNodeMap a, NamedNodeMap b) {
        if (a.getLength() != b.getLength()) {
            return false;
        }
        for (int i = 0; i < a.getLength(); i++) {
            Node aNode = a.item(i);
            Node bNode = aNode.getLocalName() == null
                    ? b.getNamedItem(aNode.getNodeName())
                    : b.getNamedItemNS(aNode.getNamespaceURI(), aNode.getLocalName());
            if (bNode == null || !aNode.isEqualNode(bNode)) {
                return false;
            }
        }
        return true;
    }

    public final Object getFeature(String feature, String version) {
        return isSupported(feature, version) ? this : null;
    }

    public final Object setUserData(String key, Object data, UserDataHandler handler) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }
        Map<String, UserData> map = document.getUserDataMap(this);
        UserData previous = data == null
                ? map.remove(key)
                : map.put(key, new UserData(data, handler));
        return previous != null ? previous.value : null;
    }

    public final Object getUserData(String key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }
        Map<String, UserData> map = document.getUserDataMapForRead(this);
        UserData userData = map.get(key);
        return userData != null ? userData.value : null;
    }

    static class UserData {
        final Object value;
        final UserDataHandler handler;
        UserData(Object value, UserDataHandler handler) {
            this.value = value;
            this.handler = handler;
        }
    }
}
