package com.shazam.axmlparser;

import com.google.common.io.LittleEndianDataInputStream;

import java.io.DataInput;
import java.io.IOException;
import java.io.InputStream;

@SuppressWarnings("ALL")
public final class AXMLParser {

    /**
     * Types of returned tags.
     * Values are compatible to those in XmlPullParser.
     */
    public static final int
            START_DOCUMENT = 0,
            END_DOCUMENT = 1,
            START_TAG = 2,
            END_TAG = 3,
            TEXT = 4;

    /**
     * Creates object and reads file info.
     * Call next() to read first tag.
     *
     * @param stream the input stream
     * @throws IOException when any I/O errors occur
     */
    public AXMLParser(InputStream stream) throws IOException {
        m_stream = stream;
        doStart();
    }

    /**
     * Closes parser:
     * * closes (and nulls) underlying stream
     * * nulls dynamic data
     * * moves object to 'closed' state, where methods
     * return invalid values and next() throws IOException.
     */
    public final void close() {
        if (m_stream == null) {
            return;
        }
        try {
            m_stream.close();
        } catch (IOException e) {
        }
        if (m_nextException == null) {
            m_nextException = new IOException("Closed.");
        }
        m_stream = null;
        resetState();
    }

    public final int next() throws IOException {
        if (m_nextException != null) {
            throw m_nextException;
        }
        try {
            return doNext();
        } catch (IOException e) {
            m_nextException = e;
            resetState();
            throw e;
        }
    }

    /**
     * Returns current tag type.
     *
     * @return the tag type
     */
    public final int getType() {
        return m_tagType;
    }

    public final String getName() {
        if (m_tagName == -1) {
            return null;
        }
        return getString(m_tagName);
    }

    public final int getLineNumber() {
        return m_tagSourceLine;
    }

    public final int getAttributeCount() {
        if (m_tagAttributes == null) {
            return -1;
        }
        return m_tagAttributes.length;
    }

    /**
     * Returns attribute namespace.
     *
     * @param index the index
     * @return the attribute namespace at index
     */
    public final String getAttributeNamespace(int index) {
        return getString(getAttribute(index).namespace);
    }

    /**
     * Returns attribute name.
     *
     * @param index the index
     * @return the attribute name at index
     */
    public final String getAttributeName(int index) {
        return getString(getAttribute(index).name);
    }

    /**
     * Returns attribute resource ID.
     *
     * @param index the index
     * @return the attribute resource ID at index
     */
    public final int getAttributeResourceID(int index) {
        int resourceIndex = getAttribute(index).name;
        if (m_resourceIDs == null ||
                resourceIndex < 0 || resourceIndex >= m_resourceIDs.length) {
            return 0;
        }
        return m_resourceIDs[resourceIndex];
    }

    /**
     * Returns type of attribute value.
     * See TypedValue.TYPE_ values.
     *
     * @param index the index
     * @return the attribute type at index
     */
    public final int getAttributeValueType(int index) {
        return getAttribute(index).valueType;
    }

    /**
     * For attributes of type TypedValue.TYPE_STRING returns
     * string value. For other types returns empty string.
     *
     * @param index the index
     * @return the attribute string at index
     */
    public final String getAttributeValueString(int index) {
        return getString(getAttribute(index).valueString);
    }

    /**
     * Returns integer attribute value.
     * This integer interpreted according to attribute type.
     *
     * @param index the index
     * @return the attribute at index
     */
    public final int getAttributeValue(int index) {
        return getAttribute(index).value;
    }

    private static final class TagAttribute {
        public int namespace;
        public int name;
        public int valueString;
        public int valueType;
        public int value;
    }

    private final void resetState() {
        m_tagType = -1;
        m_tagSourceLine = -1;
        m_tagName = -1;
        m_tagAttributes = null;
    }

    private final void doStart() throws IOException {
        ReadUtil.readCheckType(m_stream, AXML_CHUNK_TYPE);
        /*chunk size*/
        ReadUtil.readInt(m_stream);

        m_strings = StringBlock.read(new ExtDataInput((DataInput) new LittleEndianDataInputStream(m_stream)));

        ReadUtil.readCheckType(m_stream, RESOURCEIDS_CHUNK_TYPE);
        int chunkSize = ReadUtil.readInt(m_stream);
        if (chunkSize < 8 || (chunkSize % 4) != 0) {
            throw new IOException("Invalid resource ids size (" + chunkSize + ").");
        }
        m_resourceIDs = ReadUtil.readIntArray(m_stream, chunkSize / 4 - 2);

        resetState();
    }

    private final int doNext() throws IOException {
        if (m_tagType == END_DOCUMENT) {
            return END_DOCUMENT;
        }

        m_tagType = (ReadUtil.readInt(m_stream) & 0xFF);/*other 3 bytes?*/
        /*some source length*/
        ReadUtil.readInt(m_stream);
        m_tagSourceLine = ReadUtil.readInt(m_stream);
        /*0xFFFFFFFF*/
        ReadUtil.readInt(m_stream);

        m_tagName = -1;
        m_tagAttributes = null;

        switch (m_tagType) {
            case START_DOCUMENT: {
                /*namespace?*/
                ReadUtil.readInt(m_stream);
                /*name?*/
                ReadUtil.readInt(m_stream);
                break;
            }
            case START_TAG: {
                /*0xFFFFFFFF*/
                ReadUtil.readInt(m_stream);
                m_tagName = ReadUtil.readInt(m_stream);
                /*flags?*/
                ReadUtil.readInt(m_stream);
                int attributeCount = ReadUtil.readInt(m_stream);
                /*?*/
                ReadUtil.readInt(m_stream);
                m_tagAttributes = new TagAttribute[attributeCount];
                for (int i = 0; i != attributeCount; ++i) {
                    TagAttribute attribute = new TagAttribute();
                    attribute.namespace = ReadUtil.readInt(m_stream);
                    attribute.name = ReadUtil.readInt(m_stream);
                    attribute.valueString = ReadUtil.readInt(m_stream);
                    attribute.valueType = (ReadUtil.readInt(m_stream) >>> 24);/*other 3 bytes?*/
                    attribute.value = ReadUtil.readInt(m_stream);
                    m_tagAttributes[i] = attribute;
                }
                break;
            }
            case END_TAG: {
                /*0xFFFFFFFF*/
                ReadUtil.readInt(m_stream);
                m_tagName = ReadUtil.readInt(m_stream);
                break;
            }
            case TEXT: {
                m_tagName = ReadUtil.readInt(m_stream);
                /*?*/
                ReadUtil.readInt(m_stream);
                /*?*/
                ReadUtil.readInt(m_stream);
                break;
            }
            case END_DOCUMENT: {
                /*namespace?*/
                ReadUtil.readInt(m_stream);
                /*name?*/
                ReadUtil.readInt(m_stream);
                break;
            }
            default: {
                throw new IOException("Invalid tag type (" + m_tagType + ").");
            }
        }
        return m_tagType;
    }

    private final TagAttribute getAttribute(int index) {
        if (m_tagAttributes == null) {
            throw new IndexOutOfBoundsException("Attributes are not available.");
        }
        if (index >= m_tagAttributes.length) {
            throw new IndexOutOfBoundsException("Invalid attribute index (" + index + ").");
        }
        return m_tagAttributes[index];
    }

    private final String getString(int index) {
        if (index == -1) {
            return "";
        }
        return m_strings.getString(index);
    }

    private InputStream m_stream;

    private StringBlock m_strings;
    private int[] m_resourceIDs;

    private IOException m_nextException;

    private int m_tagType;
    private int m_tagSourceLine;
    private int m_tagName;
    private TagAttribute[] m_tagAttributes;

    private static final int
            AXML_CHUNK_TYPE = 0x00080003,
            RESOURCEIDS_CHUNK_TYPE = 0x00080180;
}