/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.harmony.xml;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

/**
 * Fast, partial XmlPullParser implementation based upon Expat. Does not
 * support validation or {@code DOCTYPE} processing.
 */
public class ExpatPullParser implements XmlPullParser {
    /**
     * This feature is identified by http://xmlpull.org/v1/doc/features.html#relaxed
     * If this feature is supported that means that XmlPull parser will be
     * lenient when checking XML well formedness.
     * NOTE: use it only if XML input is not well-formed and in general usage
     * if this feature is discouraged
     * NOTE: as there is no definition of what is relaxed XML parsing
     * therefore what parser will do completely depends on implementation used
     */
    public static final String FEATURE_RELAXED =
            "http://xmlpull.org/v1/doc/features.html#relaxed";

    private static final int BUFFER_SIZE = 8096;

    private static final String NOT_A_START_TAG = "This is not a start tag.";

    private Document document;
    private boolean processNamespaces = false;
    private boolean relaxed = false;

    public void setFeature(String name, boolean state)
            throws XmlPullParserException {
        if (name == null) {
            // Required by API.
            throw new IllegalArgumentException("Null feature name");
        }

        if (name.equals(FEATURE_PROCESS_NAMESPACES)) {
            processNamespaces = state;
            return;
        }

        if (name.equals(FEATURE_RELAXED)) {
            relaxed = true;
            return;
        }

        // You're free to turn these features off because we don't support them.
        if (!state && (name.equals(FEATURE_REPORT_NAMESPACE_ATTRIBUTES)
                || name.equals(FEATURE_PROCESS_DOCDECL)
                || name.equals(FEATURE_VALIDATION))) {
            return;
        }

        throw new XmlPullParserException("Unsupported feature: " + name);
    }

    public boolean getFeature(String name) {
        if (name == null) {
            // Required by API.
            throw new IllegalArgumentException("Null feature name");
        }

        // We always support namespaces, but no other features.
        return name.equals(FEATURE_PROCESS_NAMESPACES) && processNamespaces;
    }

    /**
     * Returns true if this parser processes namespaces.
     *
     * @see #setNamespaceProcessingEnabled(boolean)
     */
    public boolean isNamespaceProcessingEnabled() {
        return processNamespaces;
    }

    /**
     * Enables or disables namespace processing. Set to false by default.
     *
     * @see #isNamespaceProcessingEnabled()
     */
    public void setNamespaceProcessingEnabled(boolean processNamespaces) {
        this.processNamespaces = processNamespaces;
    }

    public void setProperty(String name, Object value)
            throws XmlPullParserException {
        if (name == null) {
            // Required by API.
            throw new IllegalArgumentException("Null feature name");
        }

        // We don't support any properties.
        throw new XmlPullParserException("Properties aren't supported.");
    }

    public Object getProperty(String name) {
        return null;
    }

    public void setInput(Reader in) throws XmlPullParserException {
        this.document = new CharDocument(in, processNamespaces);
    }

    public void setInput(InputStream in, String encodingName)
            throws XmlPullParserException {
        this.document = new ByteDocument(in, encodingName, processNamespaces);
    }

    public String getInputEncoding() {
        return this.document.getEncoding();
    }

    /**
     * Not supported.
     *
     * @throws UnsupportedOperationException always
     */
    public void defineEntityReplacementText(String entityName,
            String replacementText) throws XmlPullParserException {
        throw new UnsupportedOperationException();
    }

    public int getNamespaceCount(int depth) throws XmlPullParserException {
        return document.currentEvent.namespaceStack.countAt(depth);
    }

    public String getNamespacePrefix(int pos) throws XmlPullParserException {
        String prefix = document.currentEvent.namespaceStack.prefixAt(pos);
        @SuppressWarnings("StringEquality")
        boolean hasPrefix = prefix != "";
        return hasPrefix ? prefix : null;
    }

    public String getNamespaceUri(int pos) throws XmlPullParserException {
        return document.currentEvent.namespaceStack.uriAt(pos);
    }

    public String getNamespace(String prefix) {
        // In XmlPullParser API, null == default namespace.
        if (prefix == null) {
            // Internally, we use empty string instead of null.
            prefix = "";
        }

        return document.currentEvent.namespaceStack.uriFor(prefix);
    }

    public int getDepth() {
        return this.document.getDepth();
    }

    public String getPositionDescription() {
        return "line " + getLineNumber() + ", column " + getColumnNumber();
    }

    /**
     * Not supported.
     *
     * @return {@literal -1} always
     */
    public int getLineNumber() {
        // We would have to record the line number in each event.
        return -1;
    }

    /**
     * Not supported.
     *
     * @return {@literal -1} always
     */
    public int getColumnNumber() {
        // We would have to record the column number in each event.
        return -1;
    }

    public boolean isWhitespace() throws XmlPullParserException {
        if (getEventType() != TEXT) {
            throw new XmlPullParserException("Not on text.");
        }

        String text = getText();

        if (text.length() == 0) {
            return true;
        }

        int length = text.length();
        for (int i = 0; i < length; i++) {
            if (!Character.isWhitespace(text.charAt(i))) {
                return false;
            }
        }

        return true;
    }

    public String getText() {
        final StringBuilder builder = this.document.currentEvent.getText();
        return builder == null ? null : builder.toString();
    }

    public char[] getTextCharacters(int[] holderForStartAndLength) {
        final StringBuilder builder = this.document.currentEvent.getText();

        final int length = builder.length();
        char[] characters = new char[length];
        builder.getChars(0, length, characters, 0);

        holderForStartAndLength[0] = 0;
        holderForStartAndLength[1] = length;

        return characters;
    }

    public String getNamespace() {
        return this.document.currentEvent.getNamespace();
    }

    public String getName() {
        return this.document.currentEvent.getName();
    }

    /**
     * Not supported.
     *
     * @throws UnsupportedOperationException always
     */
    public String getPrefix() {
        throw new UnsupportedOperationException();
    }

    public boolean isEmptyElementTag() throws XmlPullParserException {
        return this.document.isCurrentElementEmpty();
    }

    public int getAttributeCount() {
        return this.document.currentEvent.getAttributeCount();
    }

    public String getAttributeNamespace(int index) {
        return this.document.currentEvent.getAttributeNamespace(index);
    }

    public String getAttributeName(int index) {
        return this.document.currentEvent.getAttributeName(index);
    }

    /**
     * Not supported.
     *
     * @throws UnsupportedOperationException always
     */
    public String getAttributePrefix(int index) {
        throw new UnsupportedOperationException();
    }

    public String getAttributeType(int index) {
        return "CDATA";
    }

    public boolean isAttributeDefault(int index) {
        return false;
    }

    public String getAttributeValue(int index) {
        return this.document.currentEvent.getAttributeValue(index);
    }

    public String getAttributeValue(String namespace, String name) {
        return this.document.currentEvent.getAttributeValue(namespace, name);
    }

    public int getEventType() throws XmlPullParserException {
        return this.document.currentEvent.getType();
    }

    public int next() throws XmlPullParserException, IOException {
        return this.document.dequeue();
    }

    /**
     * Not supported.
     *
     * @throws UnsupportedOperationException always
     */
    public int nextToken() throws XmlPullParserException, IOException {
        throw new UnsupportedOperationException();
    }

    public void require(int type, String namespace, String name)
            throws XmlPullParserException, IOException {
        if (type != getEventType()
                || (namespace != null && !namespace.equals(getNamespace()))
                || (name != null && !name.equals(getName()))) {
            throw new XmlPullParserException("expected "
                    + TYPES[type] + getPositionDescription());
        }
    }

    public String nextText() throws XmlPullParserException, IOException {
        if (this.document.currentEvent.getType() != START_TAG)
            throw new XmlPullParserException("Not on start tag.");

        int next = this.document.dequeue();
        switch (next) {
            case TEXT: return getText();
            case END_TAG: return "";
            default: throw new XmlPullParserException(
                "Unexpected event type: " + TYPES[next]);
        }
    }

    public int nextTag() throws XmlPullParserException, IOException {
        int eventType = next();
        if (eventType == TEXT && isWhitespace()) {
            eventType = next();
        }
        if (eventType != START_TAG && eventType != END_TAG) {
            throw new XmlPullParserException(
                "Expected start or end tag", this, null);
        }
        return eventType;
    }

    /**
     * Immutable namespace stack. Pushing a new namespace on to the stack
     * only results in one object allocation. Most operations are O(N) where
     * N is the stack size. Accessing recently pushed namespaces, like those
     * for the current element, is significantly faster.
     */
    static class NamespaceStack {

        /** An empty stack. */
        static final NamespaceStack EMPTY = new NamespaceStack();

        private final NamespaceStack parent;
        private final String prefix;
        private final String uri;
        private final int index;
        private final int depth;

        /**
         * Constructs an actual namespace stack node. Internally, the nodes
         * and the stack are one in the same making for a very efficient
         * implementation. The user just sees an immutable stack and the
         * builder.
         */
        private NamespaceStack(NamespaceStack parent, String prefix,
                String uri, int depth) {
            this.parent = parent;
            this.prefix = prefix;
            this.uri = uri;
            this.index = parent.index + 1;
            this.depth = depth;
        }

        /**
         * Constructs a dummy node which only serves to point to the bottom
         * of the stack. Using an actual node instead of null simplifies the
         * code.
         */
        private NamespaceStack() {
            this.parent = null;
            this.prefix = null;
            this.uri = null;

            // This node has an index of -1 since the actual first node in the
            // stack has index 0.
            this.index = -1;

            // The actual first node will have a depth of 1.
            this.depth = 0;
        }

        String uriFor(String prefix) {
            for (NamespaceStack node = this; node.index >= 0;
                    node = node.parent) {
                if (node.prefix.equals(prefix)) {
                    return node.uri;
                }
            }

            // Not found.
            return null;
        }

        /**
         * Gets the prefix at the given index in the stack.
         */
        String prefixAt(int index) {
            return nodeAt(index).prefix;
        }

        /**
         * Gets the URI at the given index in the stack.
         */
        String uriAt(int index) {
            return nodeAt(index).uri;
        }

        private NamespaceStack nodeAt(int index) {
            if (index > this.index) {
                throw new IndexOutOfBoundsException("Index > size.");
            }
            if (index < 0) {
                throw new IndexOutOfBoundsException("Index < 0.");
            }

            NamespaceStack node = this;
            while (index != node.index) {
                node = node.parent;
            }
            return node;
        }

        /**
         * Gets the size of the stack at the given element depth.
         */
        int countAt(int depth) {
            if (depth > this.depth) {
                throw new IndexOutOfBoundsException("Depth > maximum.");
            }
            if (depth < 0) {
                throw new IndexOutOfBoundsException("Depth < 0.");
            }

            NamespaceStack node = this;
            while (depth < node.depth) {
                node = node.parent;
            }
            return node.index + 1;
        }

        /** Builds a NamespaceStack. */
        static class Builder {

            NamespaceStack top = EMPTY;

            /**
             * Pushes a namespace onto the stack.
             *
             * @param depth of the element upon which the namespace was
             *  declared
             */
            void push(String prefix, String uri, int depth) {
                top = new NamespaceStack(top, prefix, uri, depth);
            }

            /**
             * Pops all namespaces from the given element depth.
             */
            void pop(int depth) {
                // Remove all nodes at the specified depth.
                while (top != null && top.depth == depth) {
                    top = top.parent;
                }
            }

            /** Returns the current stack. */
            NamespaceStack build() {
                return top;
            }
        }
    }

    /**
     * Base class for events. Implements event chaining and defines event API
     * along with common implementations which can be overridden.
     */
    static abstract class Event {

        /** Element depth at the time of this event. */
        final int depth;

        /** The namespace stack at the time of this event. */
        final NamespaceStack namespaceStack;

        /** Next event in the queue. */
        Event next = null;

        Event(int depth, NamespaceStack namespaceStack) {
            this.depth = depth;
            this.namespaceStack = namespaceStack;
        }

        void setNext(Event next) {
            this.next = next;
        }

        Event getNext() {
            return next;
        }

        StringBuilder getText() {
            return null;
        }

        String getNamespace() {
            return null;
        }

        String getName() {
            return null;
        }

        int getAttributeCount() {
            return -1;
        }

        String getAttributeNamespace(int index) {
            throw new IndexOutOfBoundsException(NOT_A_START_TAG);
        }

        String getAttributeName(int index) {
            throw new IndexOutOfBoundsException(NOT_A_START_TAG);
        }

        String getAttributeValue(int index) {
            throw new IndexOutOfBoundsException(NOT_A_START_TAG);
        }

        abstract int getType();

        String getAttributeValue(String namespace, String name) {
            throw new IndexOutOfBoundsException(NOT_A_START_TAG);
        }

        public int getDepth() {
            return this.depth;
        }
    }

    static class StartDocumentEvent extends Event {

        public StartDocumentEvent() {
            super(0, NamespaceStack.EMPTY);
        }

        @Override
        int getType() {
            return START_DOCUMENT;
        }
    }

    static class StartTagEvent extends Event {

        final String name;
        final String namespace;
        final Attributes attributes;
        final boolean processNamespaces;

        StartTagEvent(String namespace,
                String name,
                ExpatParser expatParser,
                int depth,
                NamespaceStack namespaceStack,
                boolean processNamespaces) {
            super(depth, namespaceStack);
            this.namespace = namespace;
            this.name = name;
            this.attributes = expatParser.cloneAttributes();
            this.processNamespaces = processNamespaces;
        }

        @Override
        String getNamespace() {
            return namespace;
        }

        @Override
        String getName() {
            return name;
        }

        @Override
        int getAttributeCount() {
            return attributes.getLength();
        }

        @Override
        String getAttributeNamespace(int index) {
            return attributes.getURI(index);
        }

        @Override
        String getAttributeName(int index) {
            return processNamespaces ? attributes.getLocalName(index)
                    : attributes.getQName(index);
        }

        @Override
        String getAttributeValue(int index) {
            return attributes.getValue(index);
        }

        @Override
        String getAttributeValue(String namespace, String name) {
            if (namespace == null) {
                namespace = "";
            }

            return attributes.getValue(namespace, name);
        }

        @Override
        int getType() {
            return START_TAG;
        }
    }

    static class EndTagEvent extends Event {

        final String namespace;
        final String localName;

        EndTagEvent(String namespace, String localName, int depth,
                NamespaceStack namespaceStack) {
            super(depth, namespaceStack);
            this.namespace = namespace;
            this.localName = localName;
        }

        @Override
        String getName() {
            return this.localName;
        }

        @Override
        String getNamespace() {
            return this.namespace;
        }

        @Override
        int getType() {
            return END_TAG;
        }
    }

    static class TextEvent extends Event {

        final StringBuilder builder;

        public TextEvent(int initialCapacity, int depth,
                NamespaceStack namespaceStack) {
            super(depth, namespaceStack);
            this.builder = new StringBuilder(initialCapacity);
        }

        @Override
        int getType() {
            return TEXT;
        }

        @Override
        StringBuilder getText() {
            return this.builder;
        }

        void append(char[] text, int start, int length) {
            builder.append(text, start, length);
        }
    }

    static class EndDocumentEvent extends Event {

        EndDocumentEvent() {
            super(0, NamespaceStack.EMPTY);
        }

        @Override
        Event getNext() {
            throw new IllegalStateException("End of document.");
        }

        @Override
        void setNext(Event next) {
            throw new IllegalStateException("End of document.");
        }

        @Override
        int getType() {
            return END_DOCUMENT;
        }
    }

    /**
     * Encapsulates the parsing context of the current document.
     */
    abstract class Document {

        final String encoding;
        final ExpatParser parser;
        final boolean processNamespaces;

        TextEvent textEvent = null;
        boolean finished = false;

        Document(String encoding, boolean processNamespaces) {
            this.encoding = encoding;
            this.processNamespaces = processNamespaces;

            ExpatReader xmlReader = new ExpatReader();
            xmlReader.setContentHandler(new SaxHandler());

            this.parser = new ExpatParser(
                    encoding, xmlReader, processNamespaces, null, null);
        }

        /** Namespace stack builder. */
        NamespaceStack.Builder namespaceStackBuilder
                = new NamespaceStack.Builder();

        Event currentEvent = new StartDocumentEvent();
        Event last = currentEvent;

        /**
         * Sends some more XML to the parser.
         */
        void pump() throws IOException, XmlPullParserException {
            if (this.finished) {
                return;
            }

            int length = buffer();

            // End of document.
            if (length == -1) {
                this.finished = true;
                if (!relaxed) {
                    try {
                        parser.finish();
                    } catch (SAXException e) {
                        throw new XmlPullParserException(
                            "Premature end of document.", ExpatPullParser.this, e);
                    }
                }
                add(new EndDocumentEvent());
                return;
            }

            if (length == 0) {
                return;
            }

            flush(parser, length);
        }

        /**
         * Reads data into the buffer.
         *
         * @return the length of data buffered or {@code -1} if we've reached
         *  the end of the data.
         */
        abstract int buffer() throws IOException;

        /**
         * Sends buffered data to the parser.
         *
         * @param parser the parser to flush to
         * @param length of data buffered
         */
        abstract void flush(ExpatParser parser, int length)
                throws XmlPullParserException;

        /**
         * Adds an event.
         */
        void add(Event event) {
            // Flush pre-exising text event if necessary.
            if (textEvent != null) {
                last.setNext(textEvent);
                last = textEvent;
                textEvent = null;
            }

            last.setNext(event);
            last = event;
        }

        /**
         * Moves to the next event in the queue.
         *
         * @return type of next event
         */
        int dequeue() throws XmlPullParserException, IOException {
            Event next;

            while ((next = currentEvent.getNext()) == null) {
                pump();
            }

            currentEvent.next = null;
            currentEvent = next;

            return currentEvent.getType();
        }

        String getEncoding() {
            return this.encoding;
        }

        int getDepth() {
            return currentEvent.getDepth();
        }

        /**
         * Returns true if we're on a start element and the next event is
         * its corresponding end element.
         *
         * @throws XmlPullParserException if we aren't on a start element
         */
        boolean isCurrentElementEmpty() throws XmlPullParserException {
            if (currentEvent.getType() != START_TAG) {
                throw new XmlPullParserException(NOT_A_START_TAG);
            }

            Event next;

            try {
                while ((next = currentEvent.getNext()) == null) {
                    pump();
                }
            } catch (IOException ex) {
                throw new XmlPullParserException(ex.toString());
            }

            return next.getType() == END_TAG;
        }

        private class SaxHandler implements ContentHandler {

            int depth = 0;

            public void startPrefixMapping(String prefix, String uri)
                    throws SAXException {
                // Depth + 1--we aren't actually in the element yet.
                namespaceStackBuilder.push(prefix, uri, depth + 1);
            }

            public void startElement(String uri, String localName, String qName,
                    Attributes attributes) {
                String name = processNamespaces ? localName : qName;

                add(new StartTagEvent(uri, name, parser, ++this.depth,
                        namespaceStackBuilder.build(), processNamespaces));
            }

            public void endElement(String uri, String localName, String qName) {
                String name = processNamespaces ? localName : qName;

                int depth = this.depth--;
                add(new EndTagEvent(uri, name, depth,
                        namespaceStackBuilder.build()));
                namespaceStackBuilder.pop(depth);
            }

            public void characters(char[] ch, int start, int length) {
                // Ignore empty strings.
                if (length == 0) {
                    return;
                }

                // Start a new text event if necessary.
                if (textEvent == null) {
                    textEvent = new TextEvent(length, this.depth,
                            namespaceStackBuilder.build());
                }

                // Append to an existing text event.
                textEvent.append(ch, start, length);
            }

            public void setDocumentLocator(Locator locator) {}
            public void startDocument() throws SAXException {}
            public void endDocument() throws SAXException {}
            public void endPrefixMapping(String prefix) throws SAXException {}
            public void ignorableWhitespace(char[] ch, int start, int length)
                    throws SAXException {}
            public void processingInstruction(String target, String data)
                    throws SAXException {}
            public void skippedEntity(String name) throws SAXException {}
        }
    }

    class CharDocument extends Document {

        final char[] buffer = new char[BUFFER_SIZE / 2];
        final Reader in;

        CharDocument(Reader in, boolean processNamespaces) {
            super("UTF-16", processNamespaces);
            this.in = in;
        }

        @Override
        int buffer() throws IOException {
            return in.read(buffer);
        }

        @Override
        void flush(ExpatParser parser, int length)
                throws XmlPullParserException {
            try {
                parser.append(buffer, 0, length);
            } catch (SAXException e) {
                throw new XmlPullParserException(
                        "Error parsing document.", ExpatPullParser.this, e);
            }
        }
    }

    class ByteDocument extends Document {

        final byte[] buffer = new byte[BUFFER_SIZE];
        final InputStream in;

        ByteDocument(InputStream in, String encoding,
                boolean processNamespaces) {
            super(encoding, processNamespaces);
            this.in = in;
        }

        @Override
        int buffer() throws IOException {
            return in.read(buffer);
        }

        @Override
        void flush(ExpatParser parser, int length)
                throws XmlPullParserException {
            try {
                parser.append(buffer, 0, length);
            } catch (SAXException e) {
                throw new XmlPullParserException(
                        "Error parsing document.", ExpatPullParser.this, e);
            }
        }
    }
}
