/*
 * 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.jdom;

import java.util.List;
import java.util.Locale;

import org.apache.commons.jxpath.JXPathAbstractFactoryException;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathException;
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.util.TypeUtils;
import org.jdom.Attribute;
import org.jdom.CDATA;
import org.jdom.Comment;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.ProcessingInstruction;
import org.jdom.Text;

/**
 * A Pointer that points to a DOM node.
 */
public class JDOMNodePointer extends NodePointer {

    private static final long serialVersionUID = -6346532297491082651L;
    /** XML ns uri */
    public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
    /** XMLNS ns uri */
    public static final String XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";

    /**
     * Tests whether two strings are == or .equals()
     *
     * @param s1 string 1
     * @param s2 string 2
     * @return true if equal
     */
    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);
    }

    /**
     * Find the nearest occurrence of the specified attribute on the specified and enclosing elements.
     *
     * @param n        current node
     * @param attrName attribute name
     * @param ns       Namespace
     * @return attribute value
     */
    protected static String findEnclosingAttribute(Object n, final String attrName, final Namespace ns) {
        while (n != null) {
            if (n instanceof Element) {
                final Element e = (Element) n;
                final String attr = e.getAttributeValue(attrName, ns);
                if (attr != null && !attr.isEmpty()) {
                    return attr;
                }
            }
            n = nodeParent(n);
        }
        return null;
    }

    /**
     * Gets the local name of the specified node.
     *
     * @param node to check
     * @return String local name
     */
    public static String getLocalName(final Object node) {
        if (node instanceof Element) {
            return ((Element) node).getName();
        }
        if (node instanceof Attribute) {
            return ((Attribute) node).getName();
        }
        return null;
    }

    /**
     * Gets the ns uri of the specified node.
     *
     * @param node Node to check
     * @return String
     */
    private static String getNamespaceURI(final Object node) {
        if (node instanceof Element) {
            final Element element = (Element) node;
            String ns = element.getNamespaceURI();
            if ("".equals(ns)) {
                ns = null;
            }
            return ns;
        }
        return null;
    }

    /**
     * Gets the prefix from a given node.
     *
     * @param node to check
     * @return String
     */
    public static String getPrefix(final Object node) {
        if (node instanceof Element) {
            final String prefix = ((Element) node).getNamespacePrefix();
            return prefix == null || prefix.isEmpty() ? null : prefix;
        }
        if (node instanceof Attribute) {
            final String prefix = ((Attribute) node).getNamespacePrefix();
            return prefix == null || prefix.isEmpty() ? null : prefix;
        }
        return null;
    }

    /**
     * Gets the parent of the specified node.
     *
     * @param node to check
     * @return parent Element
     */
    private static Element nodeParent(final Object node) {
        if (node instanceof Element) {
            final Object parent = ((Element) node).getParent();
            return parent instanceof Element ? (Element) parent : null;
        }
        if (node instanceof Text) {
            return (Element) ((Text) node).getParent();
        }
        if (node instanceof CDATA) {
            return (Element) ((CDATA) node).getParent();
        }
        if (node instanceof ProcessingInstruction) {
            return (Element) ((ProcessingInstruction) node).getParent();
        }
        if (node instanceof Comment) {
            return (Element) ((Comment) node).getParent();
        }
        return null;
    }

    /**
     * Execute test against node on behalf of pointer.
     *
     * @param pointer Pointer
     * @param node    to test
     * @param test    to execute
     * @return true if node passes test
     */
    public static boolean testNode(final NodePointer pointer, final Object node, final NodeTest test) {
        if (test == null) {
            return true;
        }
        if (test instanceof NodeNameTest) {
            if (!(node instanceof Element)) {
                return false;
            }
            final NodeNameTest nodeNameTest = (NodeNameTest) test;
            final QName testName = nodeNameTest.getNodeName();
            final String namespaceURI = nodeNameTest.getNamespaceURI();
            final boolean wildcard = nodeNameTest.isWildcard();
            final String testPrefix = testName.getPrefix();
            if (wildcard && testPrefix == null) {
                return true;
            }
            if (wildcard || testName.getName().equals(getLocalName(node))) {
                final String nodeNS = getNamespaceURI(node);
                return equalStrings(namespaceURI, nodeNS) || nodeNS == null && equalStrings(testPrefix, getPrefix(node));
            }
            return false;
        }
        if (test instanceof NodeTypeTest) {
            switch (((NodeTypeTest) test).getNodeType()) {
            case Compiler.NODE_TYPE_NODE:
                return true;
            case Compiler.NODE_TYPE_TEXT:
                return node instanceof Text || node instanceof CDATA;
            case Compiler.NODE_TYPE_COMMENT:
                return node instanceof Comment;
            case Compiler.NODE_TYPE_PI:
                return node instanceof ProcessingInstruction;
            default:
                return false;
            }
        }
        if (test instanceof ProcessingInstructionTest && node instanceof ProcessingInstruction) {
            final String testPI = ((ProcessingInstructionTest) test).getTarget();
            final String nodePI = ((ProcessingInstruction) node).getTarget();
            return testPI.equals(nodePI);
        }
        return false;
    }

    /** Node, like a bean. */
    private final Object node;

    /** Optional ID, may be null. */
    private final String id;

    /** Local namespace resolver. */
    private NamespaceResolver localNamespaceResolver;

    /**
     * Constructs a new JDOMNodePointer.
     *
     * @param parent NodePointer
     * @param node   pointed
     */
    public JDOMNodePointer(final NodePointer parent, final Object node) {
        super(parent);
        this.node = node;
        this.id = null;
    }

    /**
     * Constructs a new JDOMNodePointer.
     *
     * @param node   node, like a bean.
     * @param locale Locale
     */
    public JDOMNodePointer(final Object node, final Locale locale) {
        super(null, locale);
        this.node = node;
        this.id = null;
    }

    /**
     * Constructs a new JDOMNodePointer.
     *
     * @param node   pointed
     * @param locale Locale
     * @param id     String id
     */
    public JDOMNodePointer(final Object node, final Locale locale, final String id) {
        super(null, locale);
        this.node = node;
        this.id = id;
    }

    /**
     * Add the specified content to this element.
     *
     * @param content List
     */
    private void addContent(final List content) {
        final Element element = (Element) node;
        final int count = content.size();
        for (int i = 0; i < count; i++) {
            Object child = content.get(i);
            if (child instanceof Element) {
                child = ((Element) child).clone();
                element.addContent((Element) child);
            } else if (child instanceof Text) {
                child = ((Text) child).clone();
                element.addContent((Text) child);
            } else if (node instanceof CDATA) {
                child = ((CDATA) child).clone();
                element.addContent((CDATA) child);
            } else if (node instanceof ProcessingInstruction) {
                child = ((ProcessingInstruction) child).clone();
                element.addContent((ProcessingInstruction) child);
            } else if (node instanceof Comment) {
                child = ((Comment) child).clone();
                element.addContent((Comment) child);
            }
        }
    }

    @Override
    public String asPath() {
        if (id != null) {
            return "id('" + escape(id) + "')";
        }
        final StringBuilder buffer = new StringBuilder();
        if (parent != null) {
            buffer.append(parent.asPath());
        }
        if (node instanceof Element) {
            // If the parent pointer is not a JDOMNodePointer, it is
            // the parent's responsibility to produce the node test part
            // of the path
            if (parent instanceof JDOMNodePointer) {
                if (buffer.length() == 0 || buffer.charAt(buffer.length() - 1) != '/') {
                    buffer.append('/');
                }
                final String nsURI = getNamespaceURI();
                final String ln = getLocalName(node);
                if (nsURI == null) {
                    buffer.append(ln);
                    buffer.append('[');
                    buffer.append(getRelativePositionByQName()).append(']');
                } else {
                    final String prefix = getNamespaceResolver().getPrefix(nsURI);
                    if (prefix != null) {
                        buffer.append(prefix);
                        buffer.append(':');
                        buffer.append(ln);
                        buffer.append('[');
                        buffer.append(getRelativePositionByQName());
                    } else {
                        buffer.append("node()");
                        buffer.append('[');
                        buffer.append(getRelativePositionOfElement());
                    }
                    buffer.append(']');
                }
            }
        } else if (node instanceof Text || node instanceof CDATA) {
            buffer.append("/text()");
            buffer.append('[').append(getRelativePositionOfTextNode()).append(']');
        } else if (node instanceof ProcessingInstruction) {
            buffer.append("/processing-instruction(\'").append(((ProcessingInstruction) node).getTarget()).append("')");
            buffer.append('[').append(getRelativePositionOfPI()).append(']');
        }
        return buffer.toString();
    }

    @Override
    public NodeIterator attributeIterator(final QName qName) {
        return new JDOMAttributeIterator(this, qName);
    }

    @Override
    public NodeIterator childIterator(final NodeTest test, final boolean reverse, final NodePointer startWith) {
        return new JDOMNodeIterator(this, test, reverse, startWith);
    }

    @Override
    public int compareChildNodePointers(final NodePointer pointer1, final NodePointer pointer2) {
        final Object node1 = pointer1.getBaseValue();
        final Object node2 = pointer2.getBaseValue();
        if (node1 == node2) {
            return 0;
        }
        if (node1 instanceof Attribute && !(node2 instanceof Attribute)) {
            return -1;
        }
        if (!(node1 instanceof Attribute) && node2 instanceof Attribute) {
            return 1;
        }
        if (node1 instanceof Attribute && node2 instanceof Attribute) {
            final List list = ((Element) getNode()).getAttributes();
            final int length = list.size();
            for (int i = 0; i < length; i++) {
                final Object n = list.get(i);
                if (n == node1) {
                    return -1;
                }
                if (n == node2) {
                    return 1;
                }
            }
            return 0; // Should not happen
        }
        if (!(node instanceof Element)) {
            throw new IllegalStateException("JXPath internal error: compareChildNodes called for " + node);
        }
        final List children = ((Element) node).getContent();
        final int length = children.size();
        for (int i = 0; i < length; i++) {
            final Object n = children.get(i);
            if (n == node1) {
                return -1;
            }
            if (n == node2) {
                return 1;
            }
        }
        return 0;
    }

    @Override
    public NodePointer createAttribute(final JXPathContext context, final QName qName) {
        if (!(node instanceof Element)) {
            return super.createAttribute(context, qName);
        }
        final Element element = (Element) node;
        final String prefix = qName.getPrefix();
        if (prefix != null) {
            final String namespaceUri = getNamespaceResolver().getNamespaceURI(prefix);
            if (namespaceUri == null) {
                throw new JXPathException("Unknown namespace prefix: " + prefix);
            }
            final Namespace ns = Namespace.getNamespace(prefix, namespaceUri);
            final Attribute attr = element.getAttribute(qName.getName(), ns);
            if (attr == null) {
                element.setAttribute(qName.getName(), "", ns);
            }
        } else {
            final Attribute attr = element.getAttribute(qName.getName());
            if (attr == null) {
                element.setAttribute(qName.getName(), "");
            }
        }
        final NodeIterator it = attributeIterator(qName);
        it.setPosition(1);
        return it.getNodePointer();
    }

    @Override
    public NodePointer createChild(final JXPathContext context, final QName qName, int index) {
        if (index == WHOLE_COLLECTION) {
            index = 0;
        }
        final boolean success = getAbstractFactory(context).createObject(context, this, node, qName.toString(), index);
        if (success) {
            NodeTest nodeTest;
            final String prefix = qName.getPrefix();
            final String namespaceURI = prefix == null ? null : context.getNamespaceURI(prefix);
            nodeTest = new NodeNameTest(qName, namespaceURI);
            final 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() + "/" + qName + "[" + (index + 1) + "]");
    }

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

    @Override
    public boolean equals(final Object object) {
        if (object == this) {
            return true;
        }
        if (!(object instanceof JDOMNodePointer)) {
            return false;
        }
        final JDOMNodePointer other = (JDOMNodePointer) object;
        return node == other.node;
    }

    @Override
    public Object getBaseValue() {
        return node;
    }

    @Override
    public Object getImmediateNode() {
        return node;
    }

    /**
     * Gets the language of this element.
     *
     * @return String language
     */
    protected String getLanguage() {
        return findEnclosingAttribute(node, "lang", Namespace.XML_NAMESPACE);
    }

    @Override
    public int getLength() {
        return 1;
    }

    @Override
    public QName getName() {
        String ns = null;
        String ln = null;
        if (node instanceof Element) {
            ns = ((Element) node).getNamespacePrefix();
            if (ns != null && ns.isEmpty()) {
                ns = null;
            }
            ln = ((Element) node).getName();
        } else if (node instanceof ProcessingInstruction) {
            ln = ((ProcessingInstruction) node).getTarget();
        }
        return new QName(ns, ln);
    }

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

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

    @Override
    public String getNamespaceURI(final String prefix) {
        if (prefix.equals("xml")) {
            return Namespace.XML_NAMESPACE.getURI();
        }
        Element element = null;
        if (node instanceof Document) {
            element = ((Document) node).getRootElement();
        }
        if (node instanceof Element) {
            element = (Element) node;
        }
        if (element == null) {
            return null;
        }
        final Namespace ns = element.getNamespace(prefix);
        return ns == null ? null : ns.getURI();
    }

    /**
     * Gets relative position of this among like-named siblings.
     *
     * @return 1..n
     */
    private int getRelativePositionByQName() {
        if (node instanceof Element) {
            final Object parent = ((Element) node).getParent();
            if (!(parent instanceof Element)) {
                return 1;
            }
            final List children = ((Element) parent).getContent();
            int count = 0;
            for (final Object child : children) {
                if (child instanceof Element && matchesQName((Element) child)) {
                    count++;
                }
                if (child == node) {
                    break;
                }
            }
            return count;
        }
        return 1;
    }

    /**
     * Gets relative position of this among all siblings.
     *
     * @return 1..n
     */
    private int getRelativePositionOfElement() {
        final Object parent = ((Element) node).getParent();
        if (parent == null) {
            return 1;
        }
        List children;
        if (parent instanceof Element) {
            children = ((Element) parent).getContent();
        } else {
            children = ((Document) parent).getContent();
        }
        int count = 0;
        for (final Object child : children) {
            if (child instanceof Element) {
                count++;
            }
            if (child == node) {
                break;
            }
        }
        return count;
    }

    /**
     * Gets the relative position of this among same-target processing instruction siblings.
     *
     * @return 1..n
     */
    private int getRelativePositionOfPI() {
        final String target = ((ProcessingInstruction) node).getTarget();
        final Element parent = (Element) ((ProcessingInstruction) node).getParent();
        if (parent == null) {
            return 1;
        }
        final List children = parent.getContent();
        int count = 0;
        for (final Object child : children) {
            if (child instanceof ProcessingInstruction && (target == null || target.equals(((ProcessingInstruction) child).getTarget()))) {
                count++;
            }
            if (child == node) {
                break;
            }
        }
        return count;
    }

    /**
     * Gets the relative position of this among sibling text nodes.
     *
     * @return 1..n
     */
    private int getRelativePositionOfTextNode() {
        Element parent;
        if (node instanceof Text) {
            parent = (Element) ((Text) node).getParent();
        } else {
            parent = (Element) ((CDATA) node).getParent();
        }
        if (parent == null) {
            return 1;
        }
        final List children = parent.getContent();
        int count = 0;
        for (final Object child : children) {
            if (child instanceof Text || child instanceof CDATA) {
                count++;
            }
            if (child == node) {
                break;
            }
        }
        return count;
    }

    @Override
    public Object getValue() {
        if (node instanceof Element) {
            final StringBuilder buf = new StringBuilder();
            for (final NodeIterator children = childIterator(null, false, null); children.setPosition(children.getPosition() + 1);) {
                final NodePointer ptr = children.getNodePointer();
                if (ptr.getImmediateNode() instanceof Element || ptr.getImmediateNode() instanceof Text) {
                    buf.append(ptr.getValue());
                }
            }
            return buf.toString();
        }
        if (node instanceof Comment) {
            String text = ((Comment) node).getText();
            if (text != null) {
                text = text.trim();
            }
            return text;
        }
        String result = null;
        if (node instanceof Text) {
            result = ((Text) node).getText();
        }
        if (node instanceof ProcessingInstruction) {
            result = ((ProcessingInstruction) node).getData();
        }
        final boolean trim = !"preserve".equals(findEnclosingAttribute(node, "space", Namespace.XML_NAMESPACE));
        return result != null && trim ? result.trim() : result;
    }

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

    @Override
    public boolean isCollection() {
        return false;
    }

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

    @Override
    public boolean isLeaf() {
        if (node instanceof Element) {
            return ((Element) node).getContent().isEmpty();
        }
        if (node instanceof Document) {
            return ((Document) node).getContent().isEmpty();
        }
        return true;
    }

    private boolean matchesQName(final Element element) {
        if (getNamespaceURI() != null) {
            final String ns = getNamespaceURI(element);
            if (ns == null || !ns.equals(getNamespaceURI())) {
                return false;
            }
        }
        return element.getName().equals(((Element) node).getName());
    }

    @Override
    public NodeIterator namespaceIterator() {
        return new JDOMNamespaceIterator(this);
    }

    @Override
    public NodePointer namespacePointer(final String prefix) {
        return new JDOMNamespacePointer(this, prefix);
    }

    @Override
    public void remove() {
        final Element parent = nodeParent(node);
        if (parent == null) {
            throw new JXPathException("Cannot remove root JDOM node");
        }
        parent.getContent().remove(node);
    }

    @Override
    public void setValue(final Object value) {
        if (node instanceof Text) {
            final String string = (String) TypeUtils.convert(value, String.class);
            if (string != null && !string.isEmpty()) {
                ((Text) node).setText(string);
            } else {
                nodeParent(node).removeContent((Text) node);
            }
        } else {
            final Element element = (Element) node;
            element.getContent().clear();
            if (value instanceof Element) {
                final Element valueElement = (Element) value;
                addContent(valueElement.getContent());
            } else if (value instanceof Document) {
                final Document valueDocument = (Document) value;
                addContent(valueDocument.getContent());
            } else if (value instanceof Text || value instanceof CDATA) {
                final String string = ((Text) value).getText();
                element.addContent(new Text(string));
            } else if (value instanceof ProcessingInstruction) {
                final ProcessingInstruction pi = (ProcessingInstruction) ((ProcessingInstruction) value).clone();
                element.addContent(pi);
            } else if (value instanceof Comment) {
                final Comment comment = (Comment) ((Comment) value).clone();
                element.addContent(comment);
            } else {
                final String string = (String) TypeUtils.convert(value, String.class);
                if (string != null && !string.isEmpty()) {
                    element.addContent(new Text(string));
                }
            }
        }
    }

    @Override
    public boolean testNode(final NodeTest test) {
        return testNode(this, node, test);
    }
}
