/*
 * Copyright (c) 2017 XMLmind Software. All rights reserved.
 *
 * Author: Hussein Shafie
 *
 * This file is part of the XMLmind Assembly Processor project.
 * For conditions of distribution and use, see the accompanying legal.txt file.
 */
package com.xmlmind.xinclude;

import org.w3c.dom.Element;
import org.w3c.dom.Document;
import static javax.xml.XMLConstants.XML_NS_URI;
import com.xmlmind.util.StringUtil;
import com.xmlmind.util.XMLText;
import com.xmlmind.domutil.DOMUtil;

/**
 * A simple xpointer conforming to the 
 * <a href="http://www.w3.org/TR/xptr-element/">element() scheme</a>.
 *
 * @see ElementScheme
 */
/*package*/ final class ElementXPointer {
    /**
     * Returns a simple xpointer conforming to the <tt>element()</tt> 
     * scheme constructed by parsing specified specification.
     * Returns <code>null</code> if specification has syntax errors. Shorthand
     * notation (example: <tt>#foo</tt> instead of <tt>#element(foo)</tt>) is
     * also supported.
     */
    public static ElementXPointer parse(String spec) {
        ElementXPointer xpointer;

        if (spec.indexOf('(') < 0) {
            // Shorthand notation ---

            if (!XMLText.isName(spec)) {
                // An ID must match the Name production.
                return null;
            }

            xpointer = new ElementXPointer(spec, /*descendant*/ null);
        } else {
            int start = spec.indexOf("element(");
            if (start < 0) {
                // We only support the 'element()' scheme.
                return null;
            }

            int end = spec.indexOf(')', start);
            if (end < 0) {
                // Syntax error.
                return null;
            }

            // '(' and ')' are not allowed in an ID ==> there is no need to
            // unescape them.
            String data = spec.substring(start+8, end);

            xpointer = parseSchemeData(data);
        }

        return xpointer;
    }

    /**
     * Returns a simple xpointer by parsing specified 
     * <tt>element()</tt> scheme data.
     * Returns <code>null</code> if the scheme data has syntax errors. 
     * @see #parse
     */
    private static final ElementXPointer parseSchemeData(String data) {
        String[] split = StringUtil.split(data, '/');

        String id = null;
        int[] descendant = new int[split.length];
        int descendantCount = 0;

        for (int i = 0; i < split.length; ++i) {
            String part = split[i];

            if (i == 0) {
                if (part.length() > 0) {
                    id = part;
                    if (!XMLText.isName(id)) {
                        return null;
                    }
                }
            } else {
                int rank;
                try {
                    rank = Integer.parseInt(part);
                } catch (NumberFormatException ignored) {
                    rank = -1;
                }
                if (rank <= 0) {
                    return null;
                }

                descendant[descendantCount++] = rank;
            }
        }
        if (id == null && descendantCount == 0) {
            return null;
        }

        int[] descendant2 = new int[descendantCount];
        System.arraycopy(descendant, 0, descendant2, 0, descendantCount);

        return new ElementXPointer(id, descendant2);
    }

    // -----------------------------------------------------------------------

    /**
     * ID part of this xpointer if any; <code>null</code> otherwise.
     */
    public final String id;

    /**
     * Location part of this xpointer if any; an empty array otherwise.
     */
    public final int[] descendant;

    private String idAttrNamespace, idAttrLocalName;

    // -----------------------------------------------------------------------

    /**
     * Constructs an XPointer pointing to specified descendant of element with
     * specified ID. Both ID and descendant location may not be
     * <code>null</code>.
     * <p>Example 1: for "<tt>element(foo/1/2)</tt>", pass id=foo and
     * descendant=[1,2].
     * <p>Example 2: for "<tt>element(/1/2)</tt>", pass id=<code>null</code>
     * and descendant=[1,2].
     * <p>Example 3: for "<tt>foo</tt>", pass id=foo and 
     * descendant=<code>null</code>.
     */
    public ElementXPointer(String id, int[] descendant) {
        this.id = id;
        this.descendant = (descendant == null)? new int[0] : descendant;

        idAttrNamespace = XML_NS_URI;
        idAttrLocalName = "id";
    }

    /**
     * Specifies the name of the ID attribute.
     * <p>By default, it's "<tt>xml:id</tt>".
     *
     * @see #getIdAttribute
     */
    public void setIdAttribute(String namespace, String localName) {
        idAttrNamespace = namespace;
        idAttrLocalName = localName;
    }

    /**
     * Returns a string pair containing the namespace URI and local name
     * of the ID attribute.
     *
     * @see #setIdAttribute
     */
    public String[] getIdAttribute() {
        return new String[] { idAttrNamespace, idAttrLocalName };
    }

    /**
     * Returns the string representation of this XPointer. This representation
     * can be parsed.
     */
    @Override
    public String toString() {
        if (descendant.length == 0) {
            // Shorthand notation.
            return id;
        } else {
            StringBuilder buffer = new StringBuilder();
            
            buffer.append("element(");
            if (id != null)
                buffer.append(id);
            for (int i = 0; i < descendant.length; ++i) {
                buffer.append('/');
                buffer.append(Integer.toString(descendant[i]));
            }
            buffer.append(')');

            return buffer.toString();
        }
    }

    /**
     * Find in specified document the element selected by this xpointer.
     *
     * @param doc document to be searched
     * @return found element or <code>null</code>
     */
    public Element findElement(Document doc) {
        Element element;
        int offset;
        if (id == null) {
            if (descendant.length == 0 || descendant[0] != 1) {
                // Invalid ElementXPointer.
                return null;
            }

            element = doc.getDocumentElement();
            offset = 1;
        } else {
            element = DOMUtil.findElementByAttribute(doc.getDocumentElement(), 
                                                     idAttrNamespace,
                                                     idAttrLocalName, id);
            if (element == null) {
                // Id not found.
                return null;
            }

            offset = 0;
        }

        final int descendantCount = descendant.length;
        for (int i = offset; i < descendantCount; ++i) {
            int rank = descendant[i];

            element = DOMUtil.getNthChildElement(element, rank-1);
            if (element == null) {
                // No such child element.
                return null;
            }
        }

        return element;
    }
}
