/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Rhino code, released
 * May 6, 1999.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1997-2000
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Igor Bukanov
 *   Milen Nankov
 *   David P. Caldwell <inonit@inonit.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.mozilla.javascript.xmlimpl;

import org.mozilla.javascript.*;

class XMLName extends Ref {
    static final long serialVersionUID = 3832176310755686977L;

    private static boolean isNCNameStartChar(int c) {
        if ((c & ~0x7F) == 0) {
            // Optimize for ASCII and use A..Z < _ < a..z
            if (c >= 'a') {
                return c <= 'z';
            } else if (c >= 'A') {
                if (c <= 'Z') {
                    return true;
                }
                return c == '_';
            }
        } else if ((c & ~0x1FFF) == 0) {
            return (0xC0 <= c && c <= 0xD6)
            || (0xD8 <= c && c <= 0xF6)
            || (0xF8 <= c && c <= 0x2FF)
            || (0x370 <= c && c <= 0x37D)
            || 0x37F <= c;
        }
        return (0x200C <= c && c <= 0x200D)
        || (0x2070 <= c && c <= 0x218F)
        || (0x2C00 <= c && c <= 0x2FEF)
        || (0x3001 <= c && c <= 0xD7FF)
        || (0xF900 <= c && c <= 0xFDCF)
        || (0xFDF0 <= c && c <= 0xFFFD)
        || (0x10000 <= c && c <= 0xEFFFF);
    }

    private static boolean isNCNameChar(int c) {
        if ((c & ~0x7F) == 0) {
            // Optimize for ASCII and use - < . < 0..9 < A..Z < _ < a..z
            if (c >= 'a') {
                return c <= 'z';
            } else if (c >= 'A') {
                if (c <= 'Z') {
                    return true;
                }
                return c == '_';
            } else if (c >= '0') {
                return c <= '9';
            } else {
                return c == '-' || c == '.';
            }
        } else if ((c & ~0x1FFF) == 0) {
            return isNCNameStartChar(c) || c == 0xB7
                || (0x300 <= c && c <= 0x36F);
        }
        return isNCNameStartChar(c) || (0x203F <= c && c <= 0x2040);
    }

    //    This means "accept" in the parsing sense
    //    See ECMA357 13.1.2.1
    static boolean accept(Object nameObj) {
        String name;
        try {
            name = ScriptRuntime.toString(nameObj);
        } catch (EcmaError ee) {
            if ("TypeError".equals(ee.getName())) {
                return false;
            }
            throw ee;
        }

        // See http://w3.org/TR/xml-names11/#NT-NCName
        int length = name.length();
        if (length != 0) {
            if (isNCNameStartChar(name.charAt(0))) {
                for (int i = 1; i != length; ++i) {
                    if (!isNCNameChar(name.charAt(i))) {
                        return false;
                    }
                }
                return true;
            }
        }

        return false;
    }

    private XmlNode.QName qname;
    private boolean isAttributeName;
    private boolean isDescendants;
    private XMLObjectImpl xmlObject;

    private XMLName() {
    }

    static XMLName formStar() {
        XMLName rv = new XMLName();
        rv.qname = XmlNode.QName.create(null, null);
        return rv;
    }

    /** @deprecated */
    static XMLName formProperty(XmlNode.Namespace namespace, String localName) {
        if (localName != null && localName.equals("*")) localName = null;
        XMLName rv = new XMLName();
        rv.qname = XmlNode.QName.create(namespace, localName);
        return rv;
    }

    /** TODO: marked deprecated by original author */
    static XMLName formProperty(String uri, String localName) {
        return formProperty(XmlNode.Namespace.create(uri), localName);
    }

    /** TODO: marked deprecated by original implementor */
    static XMLName create(String defaultNamespaceUri, String name) {
        if (name == null)
            throw new IllegalArgumentException();

        int l = name.length();
        if (l != 0) {
            char firstChar = name.charAt(0);
            if (firstChar == '*') {
                if (l == 1) {
                    return XMLName.formStar();
                }
            } else if (firstChar == '@') {
                XMLName xmlName = XMLName.formProperty("", name.substring(1));
                xmlName.setAttributeName();
                return xmlName;
            }
        }

        return XMLName.formProperty(defaultNamespaceUri, name);
    }

    static XMLName create(XmlNode.QName qname, boolean attribute, boolean descendants) {
        XMLName rv = new XMLName();
        rv.qname = qname;
        rv.isAttributeName = attribute;
        rv.isDescendants = descendants;
        return rv;
    }

    /** @deprecated */
    static XMLName create(XmlNode.QName qname) {
        return create(qname, false, false);
    }

    void initXMLObject(XMLObjectImpl xmlObject) {
        if (xmlObject == null) throw new IllegalArgumentException();
        if (this.xmlObject != null) throw new IllegalStateException();
        this.xmlObject = xmlObject;
    }

    String uri() {
        if (qname.getNamespace() == null) return null;
        return qname.getNamespace().getUri();
    }

    String localName() {
        if (qname.getLocalName() == null) return "*";
        return qname.getLocalName();
    }

    private void addDescendantChildren(XMLList list, XML target) {
        XMLName xmlName = this;
        if (target.isElement()) {
            XML[] children = target.getChildren();
            for (int i=0; i<children.length; i++) {
                if (xmlName.matches( children[i] )) {
                    list.addToList( children[i] );
                }
                addDescendantChildren(list, children[i]);
            }
        }
    }

    void addMatchingAttributes(XMLList list, XML target) {
        XMLName name = this;
        if (target.isElement()) {
            XML[] attributes = target.getAttributes();
            for (int i=0; i<attributes.length; i++) {
                if (name.matches( attributes[i]) ) {
                    list.addToList( attributes[i] );
                }
            }
        }
    }

    private void addDescendantAttributes(XMLList list, XML target) {
        if (target.isElement()) {
            addMatchingAttributes(list, target);
            XML[] children = target.getChildren();
            for (int i=0; i<children.length; i++) {
                addDescendantAttributes(list, children[i]);
            }
        }
    }

    XMLList matchDescendantAttributes(XMLList rv, XML target) {
        rv.setTargets(target, null);
        addDescendantAttributes(rv, target);
        return rv;
    }

    XMLList matchDescendantChildren(XMLList rv, XML target) {
        rv.setTargets(target, null);
        addDescendantChildren(rv, target);
        return rv;
    }

    void addDescendants(XMLList rv, XML target) {
        XMLName xmlName = this;
        if (xmlName.isAttributeName()) {
            matchDescendantAttributes(rv, target);
        } else {
            matchDescendantChildren(rv, target);
        }
    }

    private void addAttributes(XMLList rv, XML target) {
        addMatchingAttributes(rv, target);
    }

    void addMatches(XMLList rv, XML target) {
        if (isDescendants()) {
            addDescendants(rv, target);
        } else if (isAttributeName()) {
            addAttributes(rv, target);
        } else {
            XML[] children = target.getChildren();
            if (children != null) {
                for (int i=0; i<children.length; i++) {
                    if (this.matches(children[i])) {
                        rv.addToList( children[i] );
                    }
                }
            }
            rv.setTargets(target, this.toQname());
        }
    }

    XMLList getMyValueOn(XML target) {
        XMLList rv = target.newXMLList();
        addMatches(rv, target);
        return rv;
    }

    void setMyValueOn(XML target, Object value) {
        // Special-case checks for undefined and null
        if (value == null) {
            value = "null";
        } else if (value instanceof Undefined) {
            value = "undefined";
        }

        XMLName xmlName = this;
        // Get the named property
        if (xmlName.isAttributeName()) {
            target.setAttribute(xmlName, value);
        } else if (xmlName.uri() == null && xmlName.localName().equals("*")) {
            target.setChildren(value);
        } else {
            // Convert text into XML if needed.
            XMLObjectImpl xmlValue = null;

            if (value instanceof XMLObjectImpl) {
                xmlValue = (XMLObjectImpl)value;

                // Check for attribute type and convert to textNode
                if (xmlValue instanceof XML) {
                    if (((XML)xmlValue).isAttribute()) {
                        xmlValue = target.makeXmlFromString(xmlName,
                                xmlValue.toString());
                    }
                }

                if (xmlValue instanceof XMLList) {
                    for (int i = 0; i < xmlValue.length(); i++) {
                        XML xml = ((XMLList) xmlValue).item(i);

                        if (xml.isAttribute()) {
                            ((XMLList)xmlValue).replace(i, target.makeXmlFromString(xmlName, xml.toString()));
                        }
                    }
                }
            } else {
                xmlValue = target.makeXmlFromString(xmlName, ScriptRuntime.toString(value));
            }

            XMLList matches = target.getPropertyList(xmlName);

            if (matches.length() == 0) {
                target.appendChild(xmlValue);
            } else {
                // Remove all other matches
                for (int i = 1; i < matches.length(); i++) {
                    target.removeChild(matches.item(i).childIndex());
                }

                // Replace first match with new value.
                XML firstMatch = matches.item(0);
                target.replace(firstMatch.childIndex(), xmlValue);
            }
        }
    }

    @Override
    public boolean has(Context cx) {
        if (xmlObject == null) {
            return false;
        }
        return xmlObject.hasXMLProperty(this);
    }

    @Override
    public Object get(Context cx) {
        if (xmlObject == null) {
            throw ScriptRuntime.undefReadError(Undefined.instance,
                toString());
        }
        return xmlObject.getXMLProperty(this);
    }

    @Override
    public Object set(Context cx, Object value) {
        if (xmlObject == null) {
            throw ScriptRuntime.undefWriteError(Undefined.instance,
                toString(),
                value);
        }
        // Assignment to descendants causes parse error on bad reference
        // and this should not be called
        if (isDescendants) throw Kit.codeBug();
        xmlObject.putXMLProperty(this, value);
        return value;
    }

    @Override
    public boolean delete(Context cx) {
        if (xmlObject == null) {
            return true;
        }
        xmlObject.deleteXMLProperty(this);
        return !xmlObject.hasXMLProperty(this);
    }

    @Override
    public String toString() {
        //return qname.localName();
        StringBuffer buff = new StringBuffer();
        if (isDescendants) buff.append("..");
        if (isAttributeName) buff.append('@');
        if (uri() == null) {
            buff.append('*');
            if(localName().equals("*")) {
                return buff.toString();
            }
        } else {
            buff.append('"').append(uri()).append('"');
        }
        buff.append(':').append(localName());
        return buff.toString();
    }

    final XmlNode.QName toQname() {
        return this.qname;
    }

    final boolean matchesLocalName(String localName) {
        return localName().equals("*") || localName().equals(localName);
    }

    final boolean matchesElement(XmlNode.QName qname) {
        if (this.uri() == null || this.uri().equals(qname.getNamespace().getUri())) {
            if (this.localName().equals("*") || this.localName().equals(qname.getLocalName())) {
                return true;
            }
        }
        return false;
    }

    final boolean matches(XML node) {
        XmlNode.QName qname = node.getNodeQname();
        String nodeUri = null;
        if (qname.getNamespace() != null) {
            nodeUri = qname.getNamespace().getUri();
        }
        if (isAttributeName) {
            if (node.isAttribute()) {
                if (this.uri() == null || this.uri().equals(nodeUri)) {
                    if (this.localName().equals("*") || this.localName().equals(qname.getLocalName())) {
                        return true;
                    }
                }
                return false;
            } else {
                //    TODO    Could throw exception maybe, should not call this method on attribute name with arbitrary node type
                //            unless we traverse all attributes and children habitually
                return false;
            }
        } else {
            if ( this.uri() == null || ((node.isElement()) && this.uri().equals(nodeUri)) ) {
                if (localName().equals("*")) return true;
                if (node.isElement()) {
                    if (localName().equals(qname.getLocalName())) return true;
                }
            }
            return false;
        }
    }

    /* TODO: marked deprecated by original author */
    boolean isAttributeName() {
        return isAttributeName;
    }

    // TODO Fix whether this is an attribute XMLName at construction?
    // Marked deprecated by original author
    void setAttributeName() {
//        if (isAttributeName) throw new IllegalStateException();
        isAttributeName = true;
    }

    /* TODO: was marked deprecated by original author */
    boolean isDescendants() {
        return isDescendants;
    }

    //    TODO    Fix whether this is an descendant XMLName at construction?
    /** @deprecated */
    void setIsDescendants() {
//        if (isDescendants) throw new IllegalStateException();
        isDescendants = true;
    }
}
