package org.apache.synapse.util;

import java.io.IOException;
import java.io.Reader;
import java.util.Collections;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.apache.axiom.om.impl.EmptyOMLocation;
import org.apache.axiom.om.impl.llom.util.NamespaceContextImpl;
import org.apache.commons.io.IOUtils;

public class WrappedTextNodeStreamReader implements XMLStreamReader {
    private final static Location EMPTY_LOCATION = new EmptyOMLocation();
    
    private final QName wrapperElementName;
    private final Reader reader;
    private int eventType = -1;
    private char[] charData;
    private int charDataLength;
    private NamespaceContext namespaceContext;
    
    public WrappedTextNodeStreamReader(QName wrapperElementName, Reader reader) {
        this.wrapperElementName = wrapperElementName;
        this.reader = reader;
    }

    public Object getProperty(String name) throws IllegalArgumentException {
        // We don't define any properties
        return null;
    }

    public String getEncoding() {
        // Encoding is not known (not relevant?)
        return null;
    }

    public String getCharacterEncodingScheme() {
        // Encoding is not known (not relevant?)
        return null;
    }

    public String getVersion() {
        // Version is not relevant
        return null;
    }

    public boolean standaloneSet() {
        return false;
    }

    public boolean isStandalone() {
        return true;
    }

    public Location getLocation() {
        // We do not support location information
        return EMPTY_LOCATION;
    }

    public int next() throws XMLStreamException {
        // Determine next event type based on current event type. If current event type
        // is START_ELEMENT or CHARACTERS, pull new data from the reader.
        switch (eventType) {
            case -1:
                eventType = START_DOCUMENT;
                break;
            case START_DOCUMENT:
                eventType = START_ELEMENT;
                break;
            case START_ELEMENT:
                charData = new char[4096];
                // No break here!
            case CHARACTERS:
                try {
                    charDataLength = reader.read(charData);
                }
                catch (IOException ex) {
                    throw new XMLStreamException(ex);
                }
                if (charDataLength == -1) {
                    charData = null;
                    eventType = END_ELEMENT;
                } else {
                    eventType = CHARACTERS;
                }
                break;
            case END_ELEMENT:
                eventType = END_DOCUMENT;
                break;
            default:
                throw new IllegalStateException();
        }
        return eventType;
    }
    
    public int getEventType() {
        return eventType;
    }

    public void require(int type, String namespaceURI, String localName) throws XMLStreamException {
        if (type != eventType
             || (namespaceURI != null && !namespaceURI.equals(getNamespaceURI()))
             || (localName != null && !namespaceURI.equals(getLocalName()))) {
            throw new XMLStreamException("Unexpected event type");
        }
    }
    
    public String getElementText() throws XMLStreamException {
        if (eventType == START_ELEMENT) {
            try {
                String result = IOUtils.toString(reader);
                eventType = END_ELEMENT;
                return result;
            }
            catch (IOException ex) {
                throw new XMLStreamException(ex);
            }
        } else {
            throw new XMLStreamException("Current event is not a START_ELEMENT");
        }
    }

    public int nextTag() throws XMLStreamException {
        // We don't have white space, comments or processing instructions
        throw new XMLStreamException("Current event is not white space");
    }

    public boolean hasNext() throws XMLStreamException {
        return eventType != END_DOCUMENT;
    }
    
    public void close() throws XMLStreamException {
        // Javadoc says that this method should not close the underlying input source,
        // but we need to close the reader somewhere.
        try {
            reader.close();
        }
        catch (IOException ex) {
            throw new XMLStreamException(ex);
        }
    }

    public NamespaceContext getNamespaceContext() {
        if (namespaceContext == null) {
            namespaceContext = new NamespaceContextImpl(Collections.singletonMap(wrapperElementName.getPrefix(), wrapperElementName.getNamespaceURI()));
        }
        return namespaceContext;
    }
    
    public String getNamespaceURI(String prefix) {
        String namespaceURI = getNamespaceContext().getNamespaceURI(prefix);
        // NamespaceContext#getNamespaceURI and XMLStreamReader#getNamespaceURI have slightly
        // different semantics for unbound prefixes.
        return namespaceURI.equals(XMLConstants.NULL_NS_URI) ? null : prefix;
    }

    public boolean isStartElement() { return eventType == START_ELEMENT; }
    public boolean isEndElement() { return eventType == END_ELEMENT; }
    public boolean isCharacters() { return eventType == CHARACTERS; }
    public boolean isWhiteSpace() { return false; }
    public boolean hasText() { return eventType == CHARACTERS; }
    public boolean hasName() { return eventType == START_ELEMENT || eventType == END_ELEMENT; }
    
    private void checkStartElement() {
        if (eventType != START_ELEMENT) {
            throw new IllegalStateException();
        }
    }
    
    public String getAttributeValue(String namespaceURI, String localName) {
        checkStartElement();
        return null;
    }

    public int getAttributeCount() {
        checkStartElement();
        return 0;
    }
    
    public QName getAttributeName(int index) {
        checkStartElement();
        throw new ArrayIndexOutOfBoundsException();
    }

    public String getAttributeLocalName(int index) {
        checkStartElement();
        throw new ArrayIndexOutOfBoundsException();
    }

    public String getAttributePrefix(int index) {
        checkStartElement();
        throw new ArrayIndexOutOfBoundsException();
    }

    public String getAttributeNamespace(int index) {
        checkStartElement();
        throw new ArrayIndexOutOfBoundsException();
    }

    public String getAttributeType(int index) {
        checkStartElement();
        throw new ArrayIndexOutOfBoundsException();
    }

    public String getAttributeValue(int index) {
        checkStartElement();
        throw new ArrayIndexOutOfBoundsException();
    }

    public boolean isAttributeSpecified(int index) {
        checkStartElement();
        throw new ArrayIndexOutOfBoundsException();
    }
    
    private void checkElement() {
        if (eventType != START_ELEMENT && eventType != END_ELEMENT) {
            throw new IllegalStateException();
        }
    }
    
    public QName getName() {
        return null;
    }

    public String getLocalName() {
        checkElement();
        return wrapperElementName.getLocalPart();
    }

    public String getPrefix() {
        return wrapperElementName.getPrefix();
    }

    public String getNamespaceURI() {
        checkElement();
        return wrapperElementName.getNamespaceURI();
    }
    
    public int getNamespaceCount() {
        checkElement();
        // There is one namespace declared on the wrapper element
        return 1;
    }

    public String getNamespacePrefix(int index) {
        checkElement();
        if (index == 0) {
            return wrapperElementName.getPrefix();
        } else {
            throw new IndexOutOfBoundsException();
        }
    }

    public String getNamespaceURI(int index) {
        checkElement();
        if (index == 0) {
            return wrapperElementName.getNamespaceURI();
        } else {
            throw new IndexOutOfBoundsException();
        }
    }
    
    private void checkCharacters() {
        if (eventType != CHARACTERS) {
            throw new IllegalStateException();
        }
    }
    
    public String getText() {
        checkCharacters();
        return new String(charData, 0, charDataLength);
    }

    public char[] getTextCharacters() {
        checkCharacters();
        return charData;
    }

    public int getTextStart() {
        checkCharacters();
        return 0;
    }

    public int getTextLength() {
        checkCharacters();
        return charDataLength;
    }

    public int getTextCharacters(int sourceStart, char[] target, int targetStart, int length) throws XMLStreamException {
        checkCharacters();
        int c = Math.min(charDataLength-sourceStart, length);
        System.arraycopy(charData, sourceStart, target, targetStart, c);
        return c;
    }

    public String getPIData() {
        throw new IllegalStateException();
    }

    public String getPITarget() {
        throw new IllegalStateException();
    }
}
