/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.commons.jxpath.ri.model.dom;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.jxpath.JXPathAbstractFactoryException;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathException;
import org.apache.commons.jxpath.Pointer;
import org.apache.commons.jxpath.ri.Compiler;
import org.apache.commons.jxpath.ri.NamespaceResolver;
import org.apache.commons.jxpath.ri.QName;
import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
import org.apache.commons.jxpath.ri.compiler.NodeTest;
import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
import org.apache.commons.jxpath.ri.compiler.ProcessingInstructionTest;
import org.apache.commons.jxpath.ri.model.NodeIterator;
import org.apache.commons.jxpath.ri.model.NodePointer;
import org.apache.commons.jxpath.ri.model.beans.NullPointer;
import org.apache.commons.jxpath.util.TypeUtils;
import org.w3c.dom.Attr;
import org.w3c.dom.Comment;
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;

/**
 * A Pointer that points to a DOM node. Because a DOM Node is not guaranteed Serializable,
 * a DOMNodePointer instance may likewise not be properly Serializable.
 *
 * @author Dmitri Plotnikov
 * @version $Revision: 668329 $ $Date: 2008-06-16 16:59:48 -0500 (Mon, 16 Jun 2008) $
 */
public class DOMNodePointer extends NodePointer {

    private static final long serialVersionUID = -8751046933894857319L;

    private Node node;
    private Map namespaces;
    private String defaultNamespace;
    private String id;
    private NamespaceResolver localNamespaceResolver;

    /** XML namespace URI */
    public static final String XML_NAMESPACE_URI =
            "http://www.w3.org/XML/1998/namespace";

    /** XMLNS namespace URI */
    public static final String XMLNS_NAMESPACE_URI =
            "http://www.w3.org/2000/xmlns/";

    /**
     * Create a new DOMNodePointer.
     * @param node pointed at
     * @param locale Locale
     */
    public DOMNodePointer(Node node, Locale locale) {
        super(null, locale);
        this.node = node;
    }

    /**
     * Create a new DOMNodePointer.
     * @param node pointed at
     * @param locale Locale
     * @param id string id
     */
    public DOMNodePointer(Node node, Locale locale, String id) {
        super(null, locale);
        this.node = node;
        this.id = id;
    }

    /**
     * Create a new DOMNodePointer.
     * @param parent pointer
     * @param node pointed
     */
    public DOMNodePointer(NodePointer parent, Node node) {
        super(parent);
        this.node = node;
    }

    public boolean testNode(NodeTest test) {
        return testNode(node, test);
    }

    /**
     * Test a Node.
     * @param node to test
     * @param test to execute
     * @return true if node passes test
     */
    public static boolean testNode(Node node, NodeTest test) {
        if (test == null) {
            return true;
        }
        if (test instanceof NodeNameTest) {
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                return false;
            }

            NodeNameTest nodeNameTest = (NodeNameTest) test;
            QName testName = nodeNameTest.getNodeName();
            String namespaceURI = nodeNameTest.getNamespaceURI();
            boolean wildcard = nodeNameTest.isWildcard();
            String testPrefix = testName.getPrefix();
            if (wildcard && testPrefix == null) {
                return true;
            }
            if (wildcard
                || testName.getName()
                        .equals(DOMNodePointer.getLocalName(node))) {
                String nodeNS = DOMNodePointer.getNamespaceURI(node);
                return equalStrings(namespaceURI, nodeNS) || nodeNS == null
                        && equalStrings(testPrefix, getPrefix(node));
            }
            return false;
        }
        if (test instanceof NodeTypeTest) {
            int nodeType = node.getNodeType();
            switch (((NodeTypeTest) test).getNodeType()) {
                case Compiler.NODE_TYPE_NODE :
                    return true;
                case Compiler.NODE_TYPE_TEXT :
                    return nodeType == Node.CDATA_SECTION_NODE
                        || nodeType == Node.TEXT_NODE;
                case Compiler.NODE_TYPE_COMMENT :
                    return nodeType == Node.COMMENT_NODE;
                case Compiler.NODE_TYPE_PI :
                    return nodeType == Node.PROCESSING_INSTRUCTION_NODE;
                default:
                    return false;
            }
        }
        if (test instanceof ProcessingInstructionTest
                && node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
            String testPI = ((ProcessingInstructionTest) test).getTarget();
            String nodePI = ((ProcessingInstruction) node).getTarget();
            return testPI.equals(nodePI);
        }
        return false;
    }

    /**
     * Test string equality.
     * @param s1 String 1
     * @param s2 String 2
     * @return true if == or .equals()
     */
    private static boolean equalStrings(String s1, String s2) {
        if (s1 == s2) {
            return true;
        }
        s1 = s1 == null ? "" : s1.trim();
        s2 = s2 == null ? "" : s2.trim();
        return s1.equals(s2);
    }

    public QName getName() {
        String ln = null;
        String ns = null;
        int type = node.getNodeType();
        if (type == Node.ELEMENT_NODE) {
            ns = DOMNodePointer.getPrefix(node);
            ln = DOMNodePointer.getLocalName(node);
        }
        else if (type == Node.PROCESSING_INSTRUCTION_NODE) {
            ln = ((ProcessingInstruction) node).getTarget();
        }
        return new QName(ns, ln);
    }

    public String getNamespaceURI() {
        return getNamespaceURI(node);
    }

    public NodeIterator childIterator(NodeTest test, boolean reverse,
            NodePointer startWith) {
        return new DOMNodeIterator(this, test, reverse, startWith);
    }

    public NodeIterator attributeIterator(QName name) {
        return new DOMAttributeIterator(this, name);
    }

    public NodePointer namespacePointer(String prefix) {
        return new NamespacePointer(this, prefix);
    }

    public NodeIterator namespaceIterator() {
        return new DOMNamespaceIterator(this);
    }

    public synchronized NamespaceResolver getNamespaceResolver() {
        if (localNamespaceResolver == null) {
            localNamespaceResolver = new NamespaceResolver(super.getNamespaceResolver());
            localNamespaceResolver.setNamespaceContextPointer(this);
        }
        return localNamespaceResolver;
    }

    public String getNamespaceURI(String prefix) {
        if (prefix == null || prefix.equals("")) {
            return getDefaultNamespaceURI();
        }

        if (prefix.equals("xml")) {
            return XML_NAMESPACE_URI;
        }

        if (prefix.equals("xmlns")) {
            return XMLNS_NAMESPACE_URI;
        }

        String namespace = null;
        if (namespaces == null) {
            namespaces = new HashMap();
        }
        else {
            namespace = (String) namespaces.get(prefix);
        }

        if (namespace == null) {
            String qname = "xmlns:" + prefix;
            Node aNode = node;
            if (aNode instanceof Document) {
                aNode = ((Document) aNode).getDocumentElement();
            }
            while (aNode != null) {
                if (aNode.getNodeType() == Node.ELEMENT_NODE) {
                    Attr attr = ((Element) aNode).getAttributeNode(qname);
                    if (attr != null) {
                        namespace = attr.getValue();
                        break;
                    }
                }
                aNode = aNode.getParentNode();
            }
            if (namespace == null || namespace.equals("")) {
                namespace = NodePointer.UNKNOWN_NAMESPACE;
            }
        }

        namespaces.put(prefix, namespace);
        if (namespace == UNKNOWN_NAMESPACE) {
            return null;
        }

        // TBD: We are supposed to resolve relative URIs to absolute ones.
        return namespace;
    }

    public String getDefaultNamespaceURI() {
        if (defaultNamespace == null) {
            Node aNode = node;
            if (aNode instanceof Document) {
                aNode = ((Document) aNode).getDocumentElement();
            }
            while (aNode != null) {
                if (aNode.getNodeType() == Node.ELEMENT_NODE) {
                    Attr attr = ((Element) aNode).getAttributeNode("xmlns");
                    if (attr != null) {
                        defaultNamespace = attr.getValue();
                        break;
                    }
                }
                aNode = aNode.getParentNode();
            }
        }
        if (defaultNamespace == null) {
            defaultNamespace = "";
        }
        // TBD: We are supposed to resolve relative URIs to absolute ones.
        return defaultNamespace.equals("") ? null : defaultNamespace;
    }

    public Object getBaseValue() {
        return node;
    }

    public Object getImmediateNode() {
        return node;
    }

    public boolean isActual() {
        return true;
    }

    public boolean isCollection() {
        return false;
    }

    public int getLength() {
        return 1;
    }

    public boolean isLeaf() {
        return !node.hasChildNodes();
    }

    /**
     * Returns true if the xml:lang attribute for the current node
     * or its parent has the specified prefix <i>lang</i>.
     * If no node has this prefix, calls <code>super.isLanguage(lang)</code>.
     * @param lang ns to test
     * @return boolean
     */
    public boolean isLanguage(String lang) {
        String current = getLanguage();
        return current == null ? super.isLanguage(lang)
                : current.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH));
    }

    /**
     * Find the nearest occurrence of the specified attribute
     * on the specified and enclosing elements.
     * @param n current node
     * @param attrName attribute name
     * @return attribute value
     */
    protected static String findEnclosingAttribute(Node n, String attrName) {
        while (n != null) {
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                Element e = (Element) n;
                String attr = e.getAttribute(attrName);
                if (attr != null && !attr.equals("")) {
                    return attr;
                }
            }
            n = n.getParentNode();
        }
        return null;
    }

    /**
     * Get the language attribute for this node.
     * @return String language name
     */
    protected String getLanguage() {
        return findEnclosingAttribute(node, "xml:lang");
    }

    /**
     * Sets contents of the node to the specified value. If the value is
     * a String, the contents of the node are replaced with this text.
     * If the value is an Element or Document, the children of the
     * node are replaced with the children of the passed node.
     * @param value to set
     */
    public void setValue(Object value) {
        if (node.getNodeType() == Node.TEXT_NODE
            || node.getNodeType() == Node.CDATA_SECTION_NODE) {
            String string = (String) TypeUtils.convert(value, String.class);
            if (string != null && !string.equals("")) {
                node.setNodeValue(string);
            }
            else {
                node.getParentNode().removeChild(node);
            }
        }
        else {
            NodeList children = node.getChildNodes();
            int count = children.getLength();
            for (int i = count; --i >= 0;) {
                Node child = children.item(i);
                node.removeChild(child);
            }

            if (value instanceof Node) {
                Node valueNode = (Node) value;
                if (valueNode instanceof Element
                    || valueNode instanceof Document) {
                    children = valueNode.getChildNodes();
                    for (int i = 0; i < children.getLength(); i++) {
                        Node child = children.item(i);
                        node.appendChild(child.cloneNode(true));
                    }
                }
                else {
                    node.appendChild(valueNode.cloneNode(true));
                }
            }
            else {
                String string = (String) TypeUtils.convert(value, String.class);
                if (string != null && !string.equals("")) {
                    Node textNode =
                        node.getOwnerDocument().createTextNode(string);
                    node.appendChild(textNode);
                }
            }
        }
    }

    public NodePointer createChild(JXPathContext context, QName name, int index) {
        if (index == WHOLE_COLLECTION) {
            index = 0;
        }
        boolean success =
            getAbstractFactory(context).createObject(
                context,
                this,
                node,
                name.toString(),
                index);
        if (success) {
            NodeTest nodeTest;
            String prefix = name.getPrefix();
            String namespaceURI = prefix == null ? null : context
                    .getNamespaceURI(prefix);
            nodeTest = new NodeNameTest(name, namespaceURI);

            NodeIterator it = childIterator(nodeTest, false, null);
            if (it != null && it.setPosition(index + 1)) {
                return it.getNodePointer();
            }
        }
        throw new JXPathAbstractFactoryException(
                "Factory could not create a child node for path: " + asPath()
                        + "/" + name + "[" + (index + 1) + "]");
    }

    public NodePointer createChild(JXPathContext context, QName name,
            int index, Object value) {
        NodePointer ptr = createChild(context, name, index);
        ptr.setValue(value);
        return ptr;
    }

    public NodePointer createAttribute(JXPathContext context, QName name) {
        if (!(node instanceof Element)) {
            return super.createAttribute(context, name);
        }
        Element element = (Element) node;
        String prefix = name.getPrefix();
        if (prefix != null) {
            String ns = null;
            NamespaceResolver nsr = getNamespaceResolver();
            if (nsr != null) {
                ns = nsr.getNamespaceURI(prefix);
            }
            if (ns == null) {
                throw new JXPathException(
                    "Unknown namespace prefix: " + prefix);
            }
            element.setAttributeNS(ns, name.toString(), "");
        }
        else {
            if (!element.hasAttribute(name.getName())) {
                element.setAttribute(name.getName(), "");
            }
        }
        NodeIterator it = attributeIterator(name);
        it.setPosition(1);
        return it.getNodePointer();
    }

    public void remove() {
        Node parent = node.getParentNode();
        if (parent == null) {
            throw new JXPathException("Cannot remove root DOM node");
        }
        parent.removeChild(node);
    }

    public String asPath() {
        if (id != null) {
            return "id('" + escape(id) + "')";
        }

        StringBuffer buffer = new StringBuffer();
        if (parent != null) {
            buffer.append(parent.asPath());
        }
        switch (node.getNodeType()) {
            case Node.ELEMENT_NODE :
                // If the parent pointer is not a DOMNodePointer, it is
                // the parent's responsibility to produce the node test part
                // of the path
                if (parent instanceof DOMNodePointer) {
                    if (buffer.length() == 0
                            || buffer.charAt(buffer.length() - 1) != '/') {
                        buffer.append('/');
                    }
                    String ln = DOMNodePointer.getLocalName(node);
                    String nsURI = getNamespaceURI();
                    if (nsURI == null) {
                        buffer.append(ln);
                        buffer.append('[');
                        buffer.append(getRelativePositionByName()).append(']');
                    }
                    else {
                        String prefix = getNamespaceResolver().getPrefix(nsURI);
                        if (prefix != null) {
                            buffer.append(prefix);
                            buffer.append(':');
                            buffer.append(ln);
                            buffer.append('[');
                            buffer.append(getRelativePositionByName());
                            buffer.append(']');
                        }
                        else {
                            buffer.append("node()");
                            buffer.append('[');
                            buffer.append(getRelativePositionOfElement());
                            buffer.append(']');
                        }
                    }
                }
            break;
            case Node.TEXT_NODE :
            case Node.CDATA_SECTION_NODE :
                buffer.append("/text()");
                buffer.append('[');
                buffer.append(getRelativePositionOfTextNode()).append(']');
                break;
            case Node.PROCESSING_INSTRUCTION_NODE :
                buffer.append("/processing-instruction(\'");
                buffer.append(((ProcessingInstruction) node).getTarget()).append("')");
                buffer.append('[');
                buffer.append(getRelativePositionOfPI()).append(']');
                break;
            case Node.DOCUMENT_NODE :
                // That'll be empty
                break;
            default:
                break;
        }
        return buffer.toString();
    }

    /**
     * Get relative position of this among like-named siblings.
     * @return 1..n
     */
    private int getRelativePositionByName() {
        int count = 1;
        Node n = node.getPreviousSibling();
        while (n != null) {
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                String nm = n.getNodeName();
                if (nm.equals(node.getNodeName())) {
                    count++;
                }
            }
            n = n.getPreviousSibling();
        }
        return count;
    }

    /**
     * Get relative position of this among all siblings.
     * @return 1..n
     */
    private int getRelativePositionOfElement() {
        int count = 1;
        Node n = node.getPreviousSibling();
        while (n != null) {
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                count++;
            }
            n = n.getPreviousSibling();
        }
        return count;
    }

    /**
     * Get the relative position of this among sibling text nodes.
     * @return 1..n
     */
    private int getRelativePositionOfTextNode() {
        int count = 1;
        Node n = node.getPreviousSibling();
        while (n != null) {
            if (n.getNodeType() == Node.TEXT_NODE
                || n.getNodeType() == Node.CDATA_SECTION_NODE) {
                count++;
            }
            n = n.getPreviousSibling();
        }
        return count;
    }

    /**
     * Get the relative position of this among same-target processing instruction siblings.
     * @return 1..n
     */
    private int getRelativePositionOfPI() {
        int count = 1;
        String target = ((ProcessingInstruction) node).getTarget();
        Node n = node.getPreviousSibling();
        while (n != null) {
            if (n.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE
                && ((ProcessingInstruction) n).getTarget().equals(target)) {
                count++;
            }
            n = n.getPreviousSibling();
        }
        return count;
    }

    public int hashCode() {
        return node.hashCode();
    }

    public boolean equals(Object object) {
        return object == this || object instanceof DOMNodePointer && node == ((DOMNodePointer) object).node;
    }

    /**
     * Get any prefix from the specified node.
     * @param node the node to check
     * @return String xml prefix
     */
    public static String getPrefix(Node node) {
        String prefix = node.getPrefix();
        if (prefix != null) {
            return prefix;
        }

        String name = node.getNodeName();
        int index = name.lastIndexOf(':');
        return index < 0 ? null : name.substring(0, index);
    }

    /**
     * Get the local name of the specified node.
     * @param node node to check
     * @return String local name
     */
    public static String getLocalName(Node node) {
        String localName = node.getLocalName();
        if (localName != null) {
            return localName;
        }

        String name = node.getNodeName();
        int index = name.lastIndexOf(':');
        return index < 0 ? name : name.substring(index + 1);
    }

    /**
     * Get the ns uri of the specified node.
     * @param node Node to check
     * @return String ns uri
     */
    public static String getNamespaceURI(Node node) {
        if (node instanceof Document) {
            node = ((Document) node).getDocumentElement();
        }

        Element element = (Element) node;

        String uri = element.getNamespaceURI();
        if (uri != null) {
            return uri;
        }

        String prefix = getPrefix(node);
        String qname = prefix == null ? "xmlns" : "xmlns:" + prefix;

        Node aNode = node;
        while (aNode != null) {
            if (aNode.getNodeType() == Node.ELEMENT_NODE) {
                Attr attr = ((Element) aNode).getAttributeNode(qname);
                if (attr != null) {
                    return attr.getValue();
                }
            }
            aNode = aNode.getParentNode();
        }
        return null;
    }

    public Object getValue() {
        if (node.getNodeType() == Node.COMMENT_NODE) {
            String text = ((Comment) node).getData();
            return text == null ? "" : text.trim();
        }
        return stringValue(node);
    }

    /**
     * Get the string value of the specified node.
     * @param node Node to check
     * @return String
     */
    private String stringValue(Node node) {
        int nodeType = node.getNodeType();
        if (nodeType == Node.COMMENT_NODE) {
            return "";
        }
        boolean trim = !"preserve".equals(findEnclosingAttribute(node, "xml:space"));
        if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) {
            String text = node.getNodeValue();
            return text == null ? "" : trim ? text.trim() : text;
        }
        if (nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
            String text = ((ProcessingInstruction) node).getData();
            return text == null ? "" : trim ? text.trim() : text;
        }
        NodeList list = node.getChildNodes();
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < list.getLength(); i++) {
            Node child = list.item(i);
            buf.append(stringValue(child));
        }
        return buf.toString();
    }

    /**
     * Locates a node by ID.
     * @param context starting context
     * @param id to find
     * @return Pointer
     */
    public Pointer getPointerByID(JXPathContext context, String id) {
        Document document = node.getNodeType() == Node.DOCUMENT_NODE ? (Document) node
                : node.getOwnerDocument();
        Element element = document.getElementById(id);
        return element == null ? (Pointer) new NullPointer(getLocale(), id)
                : new DOMNodePointer(element, getLocale(), id);
    }

    public int compareChildNodePointers(NodePointer pointer1,
            NodePointer pointer2) {
        Node node1 = (Node) pointer1.getBaseValue();
        Node node2 = (Node) pointer2.getBaseValue();
        if (node1 == node2) {
            return 0;
        }

        int t1 = node1.getNodeType();
        int t2 = node2.getNodeType();
        if (t1 == Node.ATTRIBUTE_NODE && t2 != Node.ATTRIBUTE_NODE) {
            return -1;
        }
        if (t1 != Node.ATTRIBUTE_NODE && t2 == Node.ATTRIBUTE_NODE) {
            return 1;
        }
        if (t1 == Node.ATTRIBUTE_NODE && t2 == Node.ATTRIBUTE_NODE) {
            NamedNodeMap map = ((Node) getNode()).getAttributes();
            int length = map.getLength();
            for (int i = 0; i < length; i++) {
                Node n = map.item(i);
                if (n == node1) {
                    return -1;
                }
                if (n == node2) {
                    return 1;
                }
            }
            return 0; // Should not happen
        }

        Node current = node.getFirstChild();
        while (current != null) {
            if (current == node1) {
                return -1;
            }
            if (current == node2) {
                return 1;
            }
            current = current.getNextSibling();
        }
        return 0;
    }
}
