package com.ctc.wstx.sr;

import javax.xml.namespace.QName;

/**
 * Container class that is constructed with enough raw attribute information,
 * to be able to lazily construct full attribute objects, to be accessed
 * via Iterator, or fully-qualified name.
 *<p>
 * Implementation note: code for using Map-like structure is unfortunately
 * cut'n pasted from {@link AttributeCollector}. Problem
 * with refactoring is that it's 90% the same code, but not 100%.
 *<p>
 * Although instances of this class are constructed by stream readers,
 * it is actually used by element event objects.
 */
public final class ElemAttrs
{
    private final static int OFFSET_LOCAL_NAME = 0;
    private final static int OFFSET_NS_URI = 1;
    private final static int OFFSET_NS_PREFIX = 2;
    private final static int OFFSET_VALUE = 3;
    
    /**
     * Array that contains 4 Strings for each attribute;
     * localName, URI, prefix, value. Can be used to lazily construct
     * structure(s) needed to return Iterator for accessing all
     * attributes.
     */
    private final String[] mRawAttrs;

    /**
     * Raw offset (in <code>mRawAttrs</code>) of the first attribute
     * instance that was created through default value expansion.
     */
    private final int mDefaultOffset;

    /*
    //////////////////////////////////////////////////////////////
    // Information that defines "Map-like" data structure used for
    // quick access to attribute values by fully-qualified name
    // (only used for "long" lists)
    //////////////////////////////////////////////////////////////
     */

    // // // For full explanation, see source for AttributeCollector

    private final int[] mAttrMap;

    private final int mAttrHashSize;

    private final int mAttrSpillEnd;

    /**
     * Method called to create "short" attribute list; list that has
     * only few entries, and can thus be searched for attributes using
     * linear search, without using any kind of Map structure.
     *<p>
     * Currently the limit is 4 attributes; 1, 2 or 3 attribute lists are
     * considered short, 4 or more 'long'.
     *
     * @param rawAttrs Array that contains 4 Strings for each attribute;
     *    localName, URI, prefix, value. Can be used to lazily construct
     *    structure(s) needed to return Iterator for accessing all
     *    attributes.
     * @param defOffset Index of the first default attribute, if any;
     *    number of all attributes if none
     */
    public ElemAttrs(String[] rawAttrs, int defOffset)
    {
        mRawAttrs = rawAttrs;
        mAttrMap = null;
        mAttrHashSize = 0;
        mAttrSpillEnd = 0;
        mDefaultOffset = (defOffset << 2);
    }

    /**
     * Method called to create "long" attribute list; list that has
     * a few entries, and efficient access by fully-qualified name should
     * not be done by linear search.
     *
     * @param rawAttrs Array that contains 4 Strings for each attribute;
     *    localName, URI, prefix, value. Can be used to lazily construct
     *    structure(s) needed to return Iterator for accessing all
     *    attributes.
     */
    public ElemAttrs(String[] rawAttrs, int defOffset,
                     int[] attrMap, int hashSize, int spillEnd)
    {
        mRawAttrs = rawAttrs;
        mDefaultOffset = (defOffset << 2);

        mAttrMap = attrMap;
        mAttrHashSize = hashSize;
        mAttrSpillEnd = spillEnd;
    }

    /*
    ////////////////////////////////////////////////////
    // Public API
    ////////////////////////////////////////////////////
     */

    public String[] getRawAttrs() {
        return mRawAttrs;
    }

    public int findIndex(QName name)
    {
        // Can/need to use linear search?
        if (mAttrMap == null) {
            String ln = name.getLocalPart();
            String uri = name.getNamespaceURI();
            boolean defaultNs = (uri == null || uri.length() == 0);
            String[] raw = mRawAttrs;

            for (int i = 0, len = raw.length; i < len; i += 4) {
                if (!ln.equals(raw[i])) {
                    continue;
                }
                String thisUri = raw[i+OFFSET_NS_URI];
                if (defaultNs) {
                    if (thisUri == null || thisUri.length() == 0) {
                        return i;
                    }
                } else { // non-default NS
                    if (thisUri != null &&
                        (thisUri == uri || thisUri.equals(uri))) {
                        return i;
                    }
                }
            }
            return -1;
        }

        // Ok, better use the Map...
        return findMapIndex(name.getNamespaceURI(), name.getLocalPart());
    }

    public int getFirstDefaultOffset() {
        return mDefaultOffset;
    }

    public boolean isDefault(int ix) {
        return (ix >= mDefaultOffset);
    }

    /*
    ////////////////////////////////////////////////////
    // Internal methods
    ////////////////////////////////////////////////////
     */

    /**
     *<p>
     * Note: this method is very similar to
     * {@link com.ctc.wstx.sr.AttributeCollector#getAttrValue}; basically
     * most of it was cut'n pasted. Would be nice to refactor, but it's
     * bit hard to do that since data structures are not 100% identical
     * (mostly attribute storage, not Map structure itself).
     */
    private final int findMapIndex(String nsURI, String localName)
    {
        // Primary hit?
        int hash = localName.hashCode();
        if (nsURI == null) {
            nsURI = ""; // just to simplify comparisons -- array contains nulls
        } else if (nsURI.length() > 0) {
            hash ^= nsURI.hashCode();
        }
        int ix = mAttrMap[hash & (mAttrHashSize - 1)];
        if (ix == 0) { // nothing in here; no spills either
            return -1;
        }
        // Index is "one off" (since 0 indicates 'null), 4 Strings per attr
        ix = (ix - 1) << 2;

        // Is primary candidate match?
        String[] raw = mRawAttrs;
        String thisName = raw[ix];
        /* Equality first, since although equals() checks that too, it's
         * very likely to match (if interning Strings), and we can save
         * a method call.
         */
        if (thisName == localName || thisName.equals(localName)) {
            String thisURI = raw[ix+OFFSET_NS_URI];
            if (thisURI == nsURI) {
                return ix;
            }
            if (thisURI == null) {
                if (nsURI.length() == 0) {
                    return ix;
                }
            } else if (thisURI.equals(nsURI)) {            
                return ix;
            }
        }

        /* Nope, need to traverse spill list, which has 2 entries for
         * each spilled attribute id; first for hash value, second index.
         */
        for (int i = mAttrHashSize, len = mAttrSpillEnd; i < len; i += 2) {
            if (mAttrMap[i] != hash) {
                continue;
            }
            /* Note: spill indexes are not off-by-one, since there's no need
             * to mask 0
             */
            ix = mAttrMap[i+1] << 2; // ... but there are 4 Strings for each attr
            thisName = raw[ix];
            if (thisName == localName || thisName.equals(localName)) {
                String thisURI = raw[ix+1];
                if (thisURI == nsURI) {
                    return ix;
                }
                if (thisURI == null) {
                    if (nsURI.length() == 0) {
                        return ix;
                    }
                } else if (thisURI.equals(nsURI)) {            
                    return ix;
                }
            }
        }

        return -1;
    }
}
