/*
 * Copyright (c) 2007 Henri Sivonen
 * Copyright (c) 2008-2011 Mozilla Foundation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a 
 * copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the 
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 */

package nu.validator.htmlparser.impl;

import nu.validator.htmlparser.annotation.Auto;
import nu.validator.htmlparser.annotation.IdType;
import nu.validator.htmlparser.annotation.Local;
import nu.validator.htmlparser.annotation.NsUri;
import nu.validator.htmlparser.annotation.Prefix;
import nu.validator.htmlparser.annotation.QName;
import nu.validator.htmlparser.common.Interner;
import nu.validator.htmlparser.common.XmlViolationPolicy;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * Be careful with this class. QName is the name in from HTML tokenization.
 * Otherwise, please refer to the interface doc.
 * 
 * @version $Id: AttributesImpl.java 206 2008-03-20 14:09:29Z hsivonen $
 * @author hsivonen
 */
public final class HtmlAttributes implements Attributes {

    // [NOCPP[

    private static final AttributeName[] EMPTY_ATTRIBUTENAMES = new AttributeName[0];

    private static final String[] EMPTY_STRINGS = new String[0];

    // ]NOCPP]

    public static final HtmlAttributes EMPTY_ATTRIBUTES = new HtmlAttributes(
            AttributeName.HTML);

    private int mode;

    private int length;

    private @Auto AttributeName[] names;

    private @Auto String[] values; // XXX perhaps make this @NoLength?
    
    // CPPONLY: private @Auto int[] lines; // XXX perhaps make this @NoLength?

    // [NOCPP[

    private String idValue;

    private int xmlnsLength;

    private AttributeName[] xmlnsNames;

    private String[] xmlnsValues;

    // ]NOCPP]

    public HtmlAttributes(int mode) {
        this.mode = mode;
        this.length = 0;
        /*
         * The length of 5 covers covers 98.3% of elements
         * according to Hixie, but lets round to the next power of two for
         * jemalloc.
         */
        this.names = new AttributeName[8];
        this.values = new String[8];
        // CPPONLY: this.lines = new int[8];

        // [NOCPP[

        this.idValue = null;

        this.xmlnsLength = 0;

        this.xmlnsNames = HtmlAttributes.EMPTY_ATTRIBUTENAMES;

        this.xmlnsValues = HtmlAttributes.EMPTY_STRINGS;

        // ]NOCPP]
    }
    /*
    public HtmlAttributes(HtmlAttributes other) {
        this.mode = other.mode;
        this.length = other.length;
        this.names = new AttributeName[other.length];
        this.values = new String[other.length];
        // [NOCPP[
        this.idValue = other.idValue;
        this.xmlnsLength = other.xmlnsLength;
        this.xmlnsNames = new AttributeName[other.xmlnsLength];
        this.xmlnsValues = new String[other.xmlnsLength];
        // ]NOCPP]
    }
    */

    void destructor() {
        clear(0);
    }
    
    /**
     * Only use with a static argument
     * 
     * @param name
     * @return
     */
    public int getIndex(AttributeName name) {
        for (int i = 0; i < length; i++) {
            if (names[i] == name) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Only use with static argument.
     * 
     * @see org.xml.sax.Attributes#getValue(java.lang.String)
     */
    public String getValue(AttributeName name) {
        int index = getIndex(name);
        if (index == -1) {
            return null;
        } else {
            return getValueNoBoundsCheck(index);
        }
    }

    public int getLength() {
        return length;
    }

    /**
     * Variant of <code>getLocalName(int index)</code> without bounds check.
     * @param index a valid attribute index
     * @return the local name at index
     */
    public @Local String getLocalNameNoBoundsCheck(int index) {
        // CPPONLY: assert index < length && index >= 0: "Index out of bounds";
        return names[index].getLocal(mode);
    }

    /**
     * Variant of <code>getURI(int index)</code> without bounds check.
     * @param index a valid attribute index
     * @return the namespace URI at index
     */
    public @NsUri String getURINoBoundsCheck(int index) {
        // CPPONLY: assert index < length && index >= 0: "Index out of bounds";
        return names[index].getUri(mode);
    }

    /**
     * Variant of <code>getPrefix(int index)</code> without bounds check.
     * @param index a valid attribute index
     * @return the namespace prefix at index
     */
    public @Prefix String getPrefixNoBoundsCheck(int index) {
        // CPPONLY: assert index < length && index >= 0: "Index out of bounds";
        return names[index].getPrefix(mode);
    }

    /**
     * Variant of <code>getValue(int index)</code> without bounds check.
     * @param index a valid attribute index
     * @return the attribute value at index
     */
    public String getValueNoBoundsCheck(int index) {
        // CPPONLY: assert index < length && index >= 0: "Index out of bounds";
        return values[index];
    }

    /**
     * Variant of <code>getAttributeName(int index)</code> without bounds check.
     * @param index a valid attribute index
     * @return the attribute name at index
     */
    public AttributeName getAttributeNameNoBoundsCheck(int index) {
        // CPPONLY: assert index < length && index >= 0: "Index out of bounds";
        return names[index];
    }

    // CPPONLY: /**
    // CPPONLY: * Obtains a line number without bounds check.
    // CPPONLY: * @param index a valid attribute index
    // CPPONLY: * @return the line number at index or -1 if unknown
    // CPPONLY: */
    // CPPONLY: public int getLineNoBoundsCheck(int index) {
    // CPPONLY: assert index < length && index >= 0: "Index out of bounds";
    // CPPONLY: return lines[index];
    // CPPONLY: }

    // [NOCPP[
    
    /**
     * Variant of <code>getQName(int index)</code> without bounds check.
     * @param index a valid attribute index
     * @return the QName at index
     */
    public @QName String getQNameNoBoundsCheck(int index) {
        return names[index].getQName(mode);
    }

    /**
     * Variant of <code>getType(int index)</code> without bounds check.
     * @param index a valid attribute index
     * @return the attribute type at index
     */
    public @IdType String getTypeNoBoundsCheck(int index) {
        return (names[index] == AttributeName.ID) ? "ID" : "CDATA";
    }

    public int getIndex(String qName) {
        for (int i = 0; i < length; i++) {
            if (names[i].getQName(mode).equals(qName)) {
                return i;
            }
        }
        return -1;
    }
    
    public int getIndex(String uri, String localName) {
        for (int i = 0; i < length; i++) {
            if (names[i].getLocal(mode).equals(localName)
                    && names[i].getUri(mode).equals(uri)) {
                return i;
            }
        }
        return -1;
    }

    public @IdType String getType(String qName) {
        int index = getIndex(qName);
        if (index == -1) {
            return null;
        } else {
            return getType(index);
        }
    }

    public @IdType String getType(String uri, String localName) {
        int index = getIndex(uri, localName);
        if (index == -1) {
            return null;
        } else {
            return getType(index);
        }
    }
    
    public String getValue(String qName) {
        int index = getIndex(qName);
        if (index == -1) {
            return null;
        } else {
            return getValue(index);
        }
    }

    public String getValue(String uri, String localName) {
        int index = getIndex(uri, localName);
        if (index == -1) {
            return null;
        } else {
            return getValue(index);
        }
    }
    
    public @Local String getLocalName(int index) {
        if (index < length && index >= 0) {
            return names[index].getLocal(mode);
        } else {
            return null;
        }
    }
    
    public @QName String getQName(int index) {
        if (index < length && index >= 0) {
            return names[index].getQName(mode);
        } else {
            return null;
        }
    }

    public @IdType String getType(int index) {
        if (index < length && index >= 0) {
            return (names[index] == AttributeName.ID) ? "ID" : "CDATA";
        } else {
            return null;
        }
    }

    public AttributeName getAttributeName(int index) {
        if (index < length && index >= 0) {
            return names[index];
        } else {
            return null;
        }
    }

    public @NsUri String getURI(int index) {
        if (index < length && index >= 0) {
            return names[index].getUri(mode);
        } else {
            return null;
        }
    }

    public @Prefix String getPrefix(int index) {
        if (index < length && index >= 0) {
            return names[index].getPrefix(mode);
        } else {
            return null;
        }
    }

    public String getValue(int index) {
        if (index < length && index >= 0) {
            return values[index];
        } else {
            return null;
        }
    }

    public String getId() {
        return idValue;
    }

    public int getXmlnsLength() {
        return xmlnsLength;
    }

    public @Local String getXmlnsLocalName(int index) {
        if (index < xmlnsLength && index >= 0) {
            return xmlnsNames[index].getLocal(mode);
        } else {
            return null;
        }
    }

    public @NsUri String getXmlnsURI(int index) {
        if (index < xmlnsLength && index >= 0) {
            return xmlnsNames[index].getUri(mode);
        } else {
            return null;
        }
    }

    public String getXmlnsValue(int index) {
        if (index < xmlnsLength && index >= 0) {
            return xmlnsValues[index];
        } else {
            return null;
        }
    }
    
    public int getXmlnsIndex(AttributeName name) {
        for (int i = 0; i < xmlnsLength; i++) {
            if (xmlnsNames[i] == name) {
                return i;
            }
        }
        return -1;
    }
    
    public String getXmlnsValue(AttributeName name) {
        int index = getXmlnsIndex(name);
        if (index == -1) {
            return null;
        } else {
            return getXmlnsValue(index);
        }
    }
    
    public AttributeName getXmlnsAttributeName(int index) {
        if (index < xmlnsLength && index >= 0) {
            return xmlnsNames[index];
        } else {
            return null;
        }
    }

    // ]NOCPP]

    void addAttribute(AttributeName name, String value
            // [NOCPP[
            , XmlViolationPolicy xmlnsPolicy
            // ]NOCPP]
            // CPPONLY: , int line
    ) throws SAXException {
        // [NOCPP[
        if (name == AttributeName.ID) {
            idValue = value;
        }

        if (name.isXmlns()) {
            if (xmlnsNames.length == xmlnsLength) {
                int newLen = xmlnsLength == 0 ? 2 : xmlnsLength << 1;
                AttributeName[] newNames = new AttributeName[newLen];
                System.arraycopy(xmlnsNames, 0, newNames, 0, xmlnsNames.length);
                xmlnsNames = newNames;
                String[] newValues = new String[newLen];
                System.arraycopy(xmlnsValues, 0, newValues, 0, xmlnsValues.length);
                xmlnsValues = newValues;
            }
            xmlnsNames[xmlnsLength] = name;
            xmlnsValues[xmlnsLength] = value;
            xmlnsLength++;
            switch (xmlnsPolicy) {
                case FATAL:
                    // this is ugly
                    throw new SAXException("Saw an xmlns attribute.");
                case ALTER_INFOSET:
                    return;
                case ALLOW:
                    // fall through
            }
        }

        // ]NOCPP]

        if (names.length == length) {
            int newLen = length << 1; // The first growth covers virtually
            // 100% of elements according to
            // Hixie
            AttributeName[] newNames = new AttributeName[newLen];
            System.arraycopy(names, 0, newNames, 0, names.length);
            names = newNames;
            String[] newValues = new String[newLen];
            System.arraycopy(values, 0, newValues, 0, values.length);
            values = newValues;
            // CPPONLY: int[] newLines = new int[newLen];
            // CPPONLY: System.arraycopy(lines, 0, newLines, 0, lines.length);
            // CPPONLY: lines = newLines;
        }
        names[length] = name;
        values[length] = value;
        // CPPONLY: lines[length] = line;
        length++;
    }

    void clear(int m) {
        for (int i = 0; i < length; i++) {
            names[i].release();
            names[i] = null;
            Portability.releaseString(values[i]);
            values[i] = null;
        }
        length = 0;
        mode = m;
        // [NOCPP[
        idValue = null;
        for (int i = 0; i < xmlnsLength; i++) {
            xmlnsNames[i] = null;
            xmlnsValues[i] = null;
        }
        xmlnsLength = 0;
        // ]NOCPP]
    }
    
    /**
     * This is used in C++ to release special <code>isindex</code>
     * attribute values whose ownership is not transferred.
     */
    void releaseValue(int i) {
        Portability.releaseString(values[i]);        
    }
    
    /**
     * This is only used for <code>AttributeName</code> ownership transfer
     * in the isindex case to avoid freeing custom names twice in C++.
     */
    void clearWithoutReleasingContents() {
        for (int i = 0; i < length; i++) {
            names[i] = null;
            values[i] = null;
        }
        length = 0;
    }

    boolean contains(AttributeName name) {
        for (int i = 0; i < length; i++) {
            if (name.equalsAnother(names[i])) {
                return true;
            }
        }
        // [NOCPP[
        for (int i = 0; i < xmlnsLength; i++) {
            if (name.equalsAnother(xmlnsNames[i])) {
                return true;
            }
        }
        // ]NOCPP]
        return false;
    }

    public void adjustForMath() {
        mode = AttributeName.MATHML;
    }

    public void adjustForSvg() {
        mode = AttributeName.SVG;
    }

    public HtmlAttributes cloneAttributes(Interner interner)
            throws SAXException {
        assert (length == 0
                // [NOCPP[
                && xmlnsLength == 0
                // ]NOCPP]
                )
                || mode == 0 || mode == 3;
        HtmlAttributes clone = new HtmlAttributes(0);
        for (int i = 0; i < length; i++) {
            clone.addAttribute(names[i].cloneAttributeName(interner),
                    Portability.newStringFromString(values[i])
                    // [NOCPP[
                    , XmlViolationPolicy.ALLOW
                    // ]NOCPP]
                    // CPPONLY: , lines[i]
            );
        }
        // [NOCPP[
        for (int i = 0; i < xmlnsLength; i++) {
            clone.addAttribute(xmlnsNames[i], xmlnsValues[i],
                    XmlViolationPolicy.ALLOW);
        }
        // ]NOCPP]
        return clone; // XXX!!!
    }

    public boolean equalsAnother(HtmlAttributes other) {
        assert mode == 0 || mode == 3 : "Trying to compare attributes in foreign content.";
        int otherLength = other.getLength();
        if (length != otherLength) {
            return false;
        }
        for (int i = 0; i < length; i++) {
            // Work around the limitations of C++
            boolean found = false;
            // The comparing just the local names is OK, since these attribute
            // holders are both supposed to belong to HTML formatting elements
            @Local String ownLocal = names[i].getLocal(AttributeName.HTML);
            for (int j = 0; j < otherLength; j++) {
                if (ownLocal == other.names[j].getLocal(AttributeName.HTML)) {
                    found = true;
                    if (!Portability.stringEqualsString(values[i], other.values[j])) {
                        return false;
                    }
                }
            }
            if (!found) {
                return false;
            }
        }
        return true;
    }
    
    // [NOCPP[
    
    void processNonNcNames(TreeBuilder<?> treeBuilder, XmlViolationPolicy namePolicy) throws SAXException {
        for (int i = 0; i < length; i++) {
            AttributeName attName = names[i];
            if (!attName.isNcName(mode)) {
                String name = attName.getLocal(mode);
                switch (namePolicy) {
                    case ALTER_INFOSET:
                        names[i] = AttributeName.create(NCName.escapeName(name));
                        // fall through
                    case ALLOW:
                        if (attName != AttributeName.XML_LANG) {
                            treeBuilder.warn("Attribute \u201C" + name + "\u201D is not serializable as XML 1.0.");
                        }
                        break;
                    case FATAL:
                        treeBuilder.fatal("Attribute \u201C" + name + "\u201D is not serializable as XML 1.0.");
                        break;
                }
            }
        }
    }
    
    public void merge(HtmlAttributes attributes) throws SAXException {
        int len = attributes.getLength();
        for (int i = 0; i < len; i++) {
            AttributeName name = attributes.getAttributeNameNoBoundsCheck(i);
            if (!contains(name)) {
                addAttribute(name, attributes.getValueNoBoundsCheck(i), XmlViolationPolicy.ALLOW);
            }
        }
    }


    // ]NOCPP]
    
}
