/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.oxm;

import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

import javax.xml.namespace.QName;

import org.eclipse.persistence.internal.oxm.mappings.Field;

/**
 * INTERNAL:
 * <p><b>Purpose</b>:  Represents a token from an XPath statement.</p>
 * <p>For example the following XPath statment a/b[2]/text() corresponds to three
 * XPathFragments:  "a", "b[2]", and "text()".</p>
 * <p><b>Responsibilities</b>:<ul>
 * <li>Maintain name, namespace, and prefix information.</li>
 * <li>Maintain information about the corresponding node type.</li>
 * <li>Maintain index information if any.  The XPathFragment corresponding to
 * b[2] would have an index value of 2.</li>
 * </ul>
 */
public class XPathFragment<XML_FIELD extends Field> {

    public static final XPathFragment TEXT_FRAGMENT = new XPathFragment(Constants.TEXT);
    public static final String SELF_XPATH = ".";
    public static final XPathFragment SELF_FRAGMENT = new XPathFragment(SELF_XPATH);
    public static final XPathFragment ANY_FRAGMENT = null;
    public static final Charset CHARSET = Charset.forName(Constants.DEFAULT_XML_ENCODING);

    private XPathFragment nextFragment;
    private XML_FIELD xmlField;
    private String xpath;
    protected boolean hasAttribute = false;
    private boolean hasText = false;
    private boolean hasNamespace = false;
    private boolean containsIndex = false;
    private int indexValue = -1;//if containsIndex, then this is the value of the index.
    private boolean shouldExecuteSelectNodes = false;
    private String shortName;
    private String prefix;
    private byte[] prefixBytes;
    private String localName;
    private byte[] localNameBytes;
    private String namespaceURI;
    protected boolean nameIsText = false;
    protected boolean isSelfFragment = false;
    private QName leafElementType;
    private boolean generatedPrefix = false;
    private XPathPredicate predicate;

    private boolean namespaceAware;
    private char namespaceSeparator;

    private Set<String> attributeCollisionSet;
    private Set<String> nonAttributeCollisionSet;

    public XPathFragment() {
        setNamespaceAware(true);
        namespaceSeparator = Constants.COLON;
    }

    public XPathFragment(String xpathString) {
        this(xpathString, Constants.COLON, true);
    }

    public XPathFragment(String xpathString, char namespaceSeparator, boolean namespaceAware) {
        this.namespaceSeparator = namespaceSeparator;
        setNamespaceAware(namespaceAware);
        setXPath(xpathString);
    }

    public void setPredicate(XPathPredicate condition) {
        this.predicate = condition;
    }

    public boolean isNamespaceAware() {
        return namespaceAware;
    }

    public void setNamespaceAware(boolean isNamespaceAware) {
        this.namespaceAware = isNamespaceAware;
    }

    public char getNamespaceSeparator() {
        return this.namespaceSeparator;
    }

    public void setNamespaceSeparator(char namespaceSeparator) {
        this.namespaceSeparator = namespaceSeparator;
    }

    public XPathPredicate getPredicate() {
        return predicate;
    }


    public XPathFragment getNextFragment() {
        return nextFragment;
    }

    public void setNextFragment(XPathFragment nextFragment) {
        this.nextFragment = nextFragment;
    }

    public void setXPath(String xpathString) {

        xpath = xpathString;
        shortName = xpathString;

        // handle case:  company[name/text()="Oracle"]
        if(xpathString.length() > 0){
            if ((xpath.indexOf('[') != -1) && (xpath.indexOf(']') == -1)) {
                setShouldExecuteSelectNodes(true);
                return;
            }

            // handle case:  ancestor::*/jaxb:class/@name
            if (xpath.indexOf("::") != -1) {
                setShouldExecuteSelectNodes(true);
                return;
            }

            if (xpathString.charAt(0) == '@') {
                hasAttribute = true;
                shortName = xpathString.substring(1).intern();
                indexValue = hasIndex(xpathString);
                setupNamespaceInformation(shortName);
                return;
            }

            if (xpathString.charAt(0) == '/') {
                setShouldExecuteSelectNodes(true);
                shortName = xpathString.substring(xpathString.lastIndexOf('/') + 1).intern();
                indexValue = hasIndex(xpathString);
                setupNamespaceInformation(shortName);
                return;
            }
        }

        if (xpathString.equals(Constants.TEXT)) {
            nameIsText = true;
            shortName = xpathString.intern();
            return;
        } else {
            nameIsText = false;
        }

        // handle "self" xpath
        if (xpathString.equals(SELF_XPATH)) {
            isSelfFragment = true;
            shortName = xpathString.intern();
            return;
        }

        indexValue = hasIndex(xpathString);
        setupNamespaceInformation(shortName);
    }

    private void setupNamespaceInformation(String xpathString) {
        int nsindex = xpathString.indexOf(namespaceSeparator);
        if (nsindex != -1) {
            hasNamespace = true;
            localName = xpathString.substring(nsindex + 1).intern();
            prefix = xpathString.substring(0, nsindex).intern();
        } else {
            localName = xpathString.intern();
        }
    }

    public boolean isAttribute() {
        return hasAttribute;
    }

    public void setAttribute(boolean isAttribute) {
        hasAttribute = isAttribute;
    }

    public String getShortName() {
        if(shortName == null){
            if(prefix !=null && prefix.length() >0){
                  shortName = prefix + Constants.COLON + localName;
              }else{
                  shortName = localName;
              }
        }
        return shortName;
    }

    public String getPrefix() {
        return prefix;
    }

    public byte[] getPrefixBytes() {
        if(null == prefixBytes && null != prefix) {
            prefixBytes = prefix.getBytes(CHARSET);
        }
        return prefixBytes;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
        resetShortName();
    }

    public String getLocalName() {
        return localName;
    }

    public byte[] getLocalNameBytes() {
        if(null == localNameBytes && null != localName) {
            localNameBytes = localName.getBytes(CHARSET);
        }
        return localNameBytes;
    }

    public void setLocalName(String localName) {
        this.localName = localName;
        resetShortName();
    }

    public String getNamespaceURI() {
        return namespaceURI;
    }

    public void setNamespaceURI(String namespaceURI) {
        if (isSelfFragment || namespaceURI !=null && namespaceURI.length() == 0) {
            this.namespaceURI = null;
        } else {
            this.namespaceURI = namespaceURI;
        }
    }

    private int hasIndex(String xpathString) {
        int index = -1;
        int startindex = xpathString.lastIndexOf('[');
        if ((startindex != -1) && (xpathString.lastIndexOf(']') != -1)) {
            StringTokenizer st = new StringTokenizer(xpathString, "[]");
            String element = st.nextToken();

            while(st.hasMoreTokens()) {
                String indexString = st.nextToken();
                try {
                    index = Integer.parseInt(indexString);
                    setContainsIndex(true);
                } catch (NumberFormatException e) {
                    int equalsOffset = indexString.indexOf('=');
                    if (equalsOffset >= 0) {
                        XPathFragment xPathFragment = new XPathFragment(indexString.substring(0, equalsOffset));
                        String value = indexString.substring(equalsOffset + 2, indexString.length() - 1);
                        predicate = new XPathPredicate(xPathFragment, value);
                    } else {
                        setContainsIndex(true);
                    }
                    setShouldExecuteSelectNodes(true);
                }
            }
            shortName = element;

        } else {
            index = -1;
        }
        return index;
    }

    public int getIndexValue() {
        return indexValue;
    }

    public void setIndexValue(int indexValue) {
        this.indexValue = indexValue;
    }

    public String getXPath() {
        return xpath;
    }

    public boolean hasNamespace() {
        return hasNamespace;
    }

    /**
     * INTERNAL:
     * Indicates if the xpath is "."
     *
     * @return true if the xpath is ".", false otherwise
     */
    public boolean isSelfFragment() {
        return isSelfFragment;
    }

    public boolean nameIsText() {
        return nameIsText;
    }

    public void setHasText(boolean hasText) {
        this.hasText = hasText;
    }

    public boolean getHasText() {
        return hasText;
    }

    public void setContainsIndex(boolean containsIndex) {
        this.containsIndex = containsIndex;
    }

    public boolean containsIndex() {
        return containsIndex;
    }

    public void setShouldExecuteSelectNodes(boolean newShouldExecuteSelectNodes) {
        this.shouldExecuteSelectNodes = newShouldExecuteSelectNodes;
    }

    public boolean shouldExecuteSelectNodes() {
        return shouldExecuteSelectNodes;
    }

    public boolean equals(Object object) {
        return equals(object, false);
    }
    public boolean equals(Object object, boolean ignorePredicate) {
        if (null == object) {
            return false;
        } else if (this == object) {
            return true;
        }
        try {
            XPathFragment xPathFragment = (XPathFragment) object;
            if (hasAttribute && !xPathFragment.hasAttribute) {
                return false;
            }
            if (nameIsText && xPathFragment.nameIsText) {
                return true;
            }
            if (nameIsText != xPathFragment.nameIsText) {
                return false;
            }
            if ((null == localName && null != xPathFragment.localName) || (null != localName && null == xPathFragment.localName)) {
                return false;
            }
            if (null != localName && !localName.equals(xPathFragment.localName)) {
                return false;
            }
            if (namespaceAware && xPathFragment.isNamespaceAware()) {
                if ((null == namespaceURI && null != xPathFragment.namespaceURI) || (null != namespaceURI && null == xPathFragment.namespaceURI)) {
                    return false;
                }
                if (null != namespaceURI && !namespaceURI.equals(xPathFragment.namespaceURI)) {
                    return false;
                }
            }
            if (indexValue != xPathFragment.indexValue) {
                return false;
            }
            if(!ignorePredicate) {
                if (null == predicate && null != xPathFragment.predicate) {
                    return false;
                }
                if (null != predicate && !predicate.equals(xPathFragment.predicate)) {
                    return false;
                }
            }
        } catch (ClassCastException e) {
            return false;
        }
        return true;
    }

    public int hashCode() {
        if(null == localName) {
            return 1;
        } else {
            return localName.hashCode();
        }
    }

    public QName getLeafElementType() {
        return leafElementType;
    }

    public boolean hasLeafElementType() {
        return leafElementType != null;
    }

    public void setLeafElementType(QName type) {
        leafElementType = type;
    }

    public void setGeneratedPrefix(boolean isGenerated) {
        generatedPrefix = isGenerated;
    }

    public boolean isGeneratedPrefix() {
        return generatedPrefix;
    }

    public XML_FIELD getXMLField() {
        return this.xmlField;
    }

    public void setXMLField(XML_FIELD field) {
        this.xmlField = field;
    }

    private void resetShortName(){
        shortName = null;
    prefixBytes = null;
    localNameBytes = null;
    }

    /**
     * INTERNAL:
     * Gets auxiliary set for determining collisions during case insensitive unmarshalling.
     *
     * @param isAttribute Determine if retrieving an element or an attribute collision set.
     * @return
     *      Set containing localNames of attributes or elements of an XPathFragment.
     */
    public Set<String> getChildrenCollisionSet(boolean isAttribute) {
        return isAttribute ? getAttributeCollisionSet() : getNonAttributeCollisionSet();
    }

    private Set<String> getAttributeCollisionSet() {
        if (attributeCollisionSet == null)
            attributeCollisionSet = new HashSet<String>();
        return attributeCollisionSet;
    }

    private Set<String> getNonAttributeCollisionSet() {
        if (nonAttributeCollisionSet == null)
            nonAttributeCollisionSet = new HashSet<String>();
        return nonAttributeCollisionSet;
    }

}
