/*   Copyright 2004 The Apache Software Foundation
 *
 *   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.xmlbeans.impl.xpathgen;

import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlCursor.TokenType;
import org.apache.xmlbeans.XmlObject;

import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;

/**
 * Generates an XPath String that points to a given position in an XML document
 */
public class XPathGenerator
{
    /**
     * Generates an XPath pointing to the position in the document indicated by {@code node}.
     * <p>If the {@code context} parameter is null, the XPath is absolute, otherwise the
     * XPath will be relative to the position indicated by {@code context}.</p>
     * <p>Note: the cursor position for the {@code node} parameter is not preserved</p>
     * @param node the position in the document that the generated path will point to
     * @param context the context node; the generated path will be relative to it if not null and if
     * pointing to an element on the path from the document root to {@code node}
     * @param nsctx a namespace context that will be used to obtain prefixes; a (non-default)
     * namespace mapping must be available for all required namespace URIs
     * @return the generated path as a {@code String}
     * @throws XPathGenerationException if the path could not be generated: the cursor is in a bad
     * position (like over a comment) or no prefix mapping was found for one of the namespace URIs
     */
    public static String generateXPath(XmlCursor node, XmlCursor context, NamespaceContext nsctx)
        throws XPathGenerationException
    {
        if (node == null)
            throw new IllegalArgumentException("Null node");
        if (nsctx == null)
            throw new IllegalArgumentException("Null namespace context");
        TokenType tt = node.currentTokenType();
        if (context != null && node.isAtSamePositionAs(context))
            return ".";
        switch (tt.intValue())
        {
            case TokenType.INT_ATTR:
                QName name = node.getName();
                node.toParent();
                String pathToParent = generateInternal(node, context, nsctx);
                return pathToParent + '/' + '@' + qnameToString(name, nsctx);
            case TokenType.INT_NAMESPACE:
                name = node.getName();
                node.toParent();
                pathToParent = generateInternal(node, context, nsctx);
                String prefix = name.getLocalPart();
                if (prefix.isEmpty())
                    return pathToParent + "/@xmlns";
                else
                    return pathToParent + "/@xmlns:" + prefix;
            case TokenType.INT_START:
            case TokenType.INT_STARTDOC:
                return generateInternal(node, context, nsctx);
            case TokenType.INT_TEXT:
                int nrOfTextTokens = countTextTokens(node);
                node.toParent();
                pathToParent = generateInternal(node, context, nsctx);
                if (nrOfTextTokens == 0)
                    return pathToParent + "/text()";
                else
                    return pathToParent + "/text()[position()=" + nrOfTextTokens + ']';
            default:
                throw new XPathGenerationException("Cannot generate XPath for cursor position: " +
                    tt.toString());
        }
    }

    private static String generateInternal(XmlCursor node, XmlCursor context, NamespaceContext nsctx)
        throws XPathGenerationException
    {
        if (node.isStartdoc())
            return "";
        if (context != null && node.isAtSamePositionAs(context))
            return ".";
        assert node.isStart();
        QName name = node.getName();
        int elemIndex = 0, i = 1;
        try (XmlCursor d = node.newCursor()) {
            if (!node.toParent())
                return "/" + name;
            node.push();
            if (!node.toChild(name))
                throw new IllegalStateException("Must have at least one child with name: " + name);
            do
            {
                if (node.isAtSamePositionAs(d))
                    elemIndex = i;
                else
                    i++;
            } while (node.toNextSibling(name));
            node.pop();
        }
        String pathToParent = generateInternal(node, context, nsctx);
        return  i == 1 ? pathToParent + '/' + qnameToString(name, nsctx) :
            pathToParent + '/' + qnameToString(name, nsctx) + '[' + elemIndex + ']';
    }

    private static String qnameToString(QName qname, NamespaceContext ctx)
        throws XPathGenerationException
    {
        String localName = qname.getLocalPart();
        String uri = qname.getNamespaceURI();
        if (uri.isEmpty())
            return localName;
        String prefix = qname.getPrefix();
        if (prefix != null && prefix.length() > 0)
        {
            // Try to use the same prefix if it maps to the right URI
            String mappedUri = ctx.getNamespaceURI(prefix);
            if (uri.equals(mappedUri))
                return prefix + ':' + localName;
        }
        // The prefix is not specified, or it is not mapped to the right URI
        prefix = ctx.getPrefix(uri);
        if (prefix == null)
            throw new XPathGenerationException("Could not obtain a prefix for URI: " + uri);
        if (prefix.isEmpty())
            throw new XPathGenerationException("Can not use default prefix in XPath for URI: " + uri);
        return prefix + ':' + localName;
    }

    /**
     * Computes how many text nodes the
     * @param c the position in the document
     * @return how many text nodes occur before the position determined by {@code c}
     */
    private static int countTextTokens(XmlCursor c)
    {
        int k = 0;
        int l = 0;
        try (XmlCursor d = c.newCursor()) {
            c.push();
            c.toParent();
            TokenType tt = c.toFirstContentToken();
            while (!tt.isEnd())
            {
                if (tt.isText())
                {
                    if (c.comparePosition(d) > 0)
                        // We have moved after the initial position
                        l++;
                    else
                        k++;
                }
                else if (tt.isStart())
                    c.toEndToken();
                tt = c.toNextToken();
            }
        }
        c.pop();
        return l == 0 ? 0 : k;
    }

    public static void main(String[] args) throws org.apache.xmlbeans.XmlException
    {
        String xml =
            "<root>\n" +
                "<ns:a xmlns:ns=\"http://a.com\"><b foo=\"value\">text1<c/>text2<c/>text3<c>text</c>text4</b></ns:a>\n" +
                "</root>";
        NamespaceContext ns = new NamespaceContext() {
            public String getNamespaceURI(String prefix)
            {
                if ("ns".equals(prefix))
                    return "http://a.com";
                else
                    return null;
            }
            public String getPrefix(String namespaceUri)
            {
                return null;
            }
            public java.util.Iterator getPrefixes(String namespaceUri)
            {
                return null;
            }
        };

        try (XmlCursor c = XmlObject.Factory.parse(xml).newCursor()) {
            c.toFirstContentToken(); // on <root>
            c.toFirstContentToken(); // on <a>
            c.toFirstChild();        // on <b>
            c.toFirstChild();        // on <c>
            c.push(); System.out.println(generateXPath(c, null, ns)); c.pop();
            c.toNextSibling();
            c.toNextSibling();       // on the last <c>
            c.push(); System.out.println(generateXPath(c, null, ns)); c.pop();
            try (XmlCursor d = c.newCursor()) {
                d.toParent();
                c.push(); System.out.println(generateXPath(c, d, ns)); c.pop();
                d.toParent();
                c.push(); System.out.println(generateXPath(c, d, ns)); c.pop();
                c.toFirstContentToken(); // on text content of the last <c>
                c.push(); System.out.println(generateXPath(c, d, ns)); c.pop();
                c.toParent();
                c.toPrevToken();         // on text content before the last <c>
                c.push(); System.out.println(generateXPath(c, d, ns)); c.pop();
                c.toParent();            // on <b>
                c.push(); System.out.println(generateXPath(c, d, ns)); c.pop();
                c.toFirstAttribute();    // on the "foo" attribute
                c.push(); System.out.println(generateXPath(c, d, ns)); c.pop();
                c.toParent();
                c.toParent();
                c.toNextToken();         // on the "xmlns:ns" attribute
                c.push(); System.out.println(generateXPath(c, d, ns)); c.pop();
            }
            c.push(); System.out.println(generateXPath(c, null, ns)); c.pop();
        }
    }
}
