/*
 * Copyright 2011, 2012 Odysseus Software GmbH
 *
 * 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.synapse.commons.staxon.core.json;

import java.io.IOException;

import javax.xml.XMLConstants;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;

import org.apache.synapse.commons.staxon.core.base.AbstractXMLStreamReader;
import org.apache.synapse.commons.staxon.core.base.XMLStreamReaderScope;
import org.apache.synapse.commons.staxon.core.json.stream.JsonStreamSource;
import org.apache.synapse.commons.staxon.core.json.stream.JsonStreamSource.Value;
import org.apache.synapse.commons.staxon.core.json.stream.JsonStreamToken;

/**
 * JSON XML stream reader.
 * <p/>
 * <h4>Limitations</h4>
 * <ul>
 * <li>Mixed content (e.g. <code>&lt;alice&gt;bob&lt;edgar/&gt;&lt;/alice&gt;</code>) is not supported.</li>
 * </ul>
 * <p/>
 * <p>The reader may produce processing instructions
 * <code>&lt;?xml-multiple element-name?&gt;</code>
 * to indicate array starts (<code>'['</code>).</p>
 */
public class JsonXMLStreamReader extends AbstractXMLStreamReader<JsonXMLStreamReader.ScopeInfo> {
    static class ScopeInfo extends JsonXMLStreamScopeInfo {
        private String currentTagName;
    }

    private final JsonStreamSource source;
    private final boolean multiplePI;
    private final char namespaceSeparator;

    private final boolean xmlNilReadWriteEnabled;

    private boolean documentArray = false;

    /**
     * Create reader instance.
     *
     * @param source             stream source
     * @param multiplePI         whether to produce <code>&lt;xml-multiple?&gt;</code> PIs to signal array start
     * @param namespaceSeparator namespace prefix separator
     * @throws XMLStreamException
     */
    public JsonXMLStreamReader(JsonStreamSource source, boolean multiplePI, char namespaceSeparator) throws XMLStreamException {
        super(new ScopeInfo(), source);
        this.source = source;
        this.multiplePI = multiplePI;
        this.namespaceSeparator = namespaceSeparator;
        xmlNilReadWriteEnabled = false;
        initialize();
    }

    /**
     * Create reader instance.
     *
     * @param source             stream source
     * @param multiplePI         whether to produce <code>&lt;xml-multiple?&gt;</code> PIs to signal array start
     * @param namespaceSeparator namespace prefix separator
     * @param xmlNilReadWriteEnabled       Supports reading and writing of XML Nil elements as defined by http://www.w3.org/TR/xmlschema-1/#xsi_nil when the XML/JSON inputs contains nil/null values.
     * @throws XMLStreamException
     */
    public JsonXMLStreamReader(JsonStreamSource source, boolean multiplePI, char namespaceSeparator,
                               boolean xmlNilReadWriteEnabled) throws XMLStreamException {
        super(new ScopeInfo(), source);
        this.source = source;
        this.multiplePI = multiplePI;
        this.namespaceSeparator = namespaceSeparator;
        this.xmlNilReadWriteEnabled = xmlNilReadWriteEnabled;
        initialize();
    }

    private void readStartElementTag(String name) throws XMLStreamException {
        int separator = name.indexOf(namespaceSeparator);
        if (separator < 0) {
            readStartElementTag(XMLConstants.DEFAULT_NS_PREFIX, name, null, new ScopeInfo());
        } else {
            readStartElementTag(name.substring(0, separator), name.substring(separator + 1), null, new ScopeInfo());
        }
    }

    private void readAttrNsDecl(String name, String value) throws XMLStreamException {
        int separator = name.indexOf(namespaceSeparator);
        if (separator < 0) {
            if (XMLConstants.XMLNS_ATTRIBUTE.equals(name)) {
                readNsDecl(XMLConstants.DEFAULT_NS_PREFIX, value);
            } else {
                readAttr(XMLConstants.DEFAULT_NS_PREFIX, name, null, value);
            }
        } else {
            if (name.startsWith(XMLConstants.XMLNS_ATTRIBUTE) && separator == XMLConstants.XMLNS_ATTRIBUTE.length()) {
                readNsDecl(name.substring(separator + 1), value);
            } else {
                readAttr(name.substring(0, separator), name.substring(separator + 1), null, value);
            }
        }
    }

    private void readData(Value value, int type) throws XMLStreamException {
        readData(value.text, value.data, type);
    }

    private void consumeName(ScopeInfo info) throws XMLStreamException, IOException {
        String fieldName = source.name();
        if (fieldName.startsWith("@")) {
            fieldName = fieldName.substring(1);
            if (source.peek() == JsonStreamToken.VALUE) {
                readAttrNsDecl(fieldName, source.value().text);
            } else if (XMLConstants.XMLNS_ATTRIBUTE.equals(fieldName)) { // badgerfish
                source.startObject();
                while (source.peek() == JsonStreamToken.NAME) {
                    String prefix = source.name();
                    if ("$".equals(prefix)) {
                        readNsDecl(XMLConstants.DEFAULT_NS_PREFIX, source.value().text);
                    } else {
                        readNsDecl(prefix, source.value().text);
                    }
                }
                source.endObject();
            } else {
                throw new IllegalStateException("Expected attribute value");
            }
        } else if ("$".equals(fieldName)) {
            readData(source.value(), XMLStreamConstants.CHARACTERS);
        } else {
            info.currentTagName = fieldName;
        }
    }

    @Override
    protected boolean consume() throws XMLStreamException, IOException {
        XMLStreamReaderScope<ScopeInfo> scope = getScope();
        switch (source.peek()) {
            case NAME:
                consumeName(scope.getInfo());
                return consume();
            case START_ARRAY:
                source.startArray();
                if (scope.getInfo().isArray()) {
                    throw new IOException("Array start inside array");
                }
                if (scope.isRoot() && !isStartDocumentRead()) {
                    documentArray = true;
                } else {
                    if (scope.getInfo().currentTagName == null) {
                        throw new IOException("Array name missing");
                    }
                    scope.getInfo().startArray(scope.getInfo().currentTagName);
                }
                if (multiplePI) {
                    readPI(JsonXMLStreamConstants.MULTIPLE_PI_TARGET, scope.getInfo().currentTagName);
                }
                return consume();
            case START_OBJECT:
                source.startObject();
                if (scope.isRoot() && !isStartDocumentRead()) {
                    readStartDocument(null, null, null);
                } else {
                    if (scope.getInfo().isArray()) {
                        scope.getInfo().incArraySize();
                    }
                    if (scope.getInfo().currentTagName != null) {
                        readStartElementTag(scope.getInfo().currentTagName);
                    }
                }
                return consume();
            case END_OBJECT:
                source.endObject();
                if (scope.isRoot() && isStartDocumentRead()) {
                    readEndDocument();
                    return documentArray;
                } else {
                    readEndElementTag();
                    return true;
                }
            case VALUE:
                String name = scope.getInfo().currentTagName;
                if (scope.getInfo().isArray()) {
                    scope.getInfo().incArraySize();
                    name = scope.getInfo().getArrayName();
                }
                if (getScope().isRoot() && !isStartDocumentRead()) { // hack: allow to read simple value
                    readData(source.value(), XMLStreamConstants.CHARACTERS);
                } else {
                    readStartElementTag(name);
                    Value value = source.value();
                    if (value != JsonStreamSource.NULL) {
                        readData(value, XMLStreamConstants.CHARACTERS);
                    }
                    if (value == JsonStreamSource.NULL && xmlNilReadWriteEnabled) {
                        readAttrNsDecl("nil", "true");
                    }
                    readEndElementTag();
                }
                return true;
            case END_ARRAY:
                source.endArray();
                if (getScope().isRoot() && documentArray) {
                    return false;
                }
                if (!scope.getInfo().isArray()) {
                    throw new IllegalStateException("Array end without matching start");
                }
                scope.getInfo().endArray();
                return true;
            case NONE:
                return false;
            default:
                throw new IOException("Unexpected token: " + source.peek());
        }
    }

    /**
     * @return <code>true</code> iff the current event data is a number primitive
     */
    public boolean hasNumber() {
        return getEventData() instanceof Number;
    }

    /**
     * @return number primitive
     * @throws ClassCastException
     */
    public Number getNumber() {
        return (Number) getEventData();
    }

    /**
     * @return <code>true</code> iff the current event data is a boolean primitive
     */
    public boolean hasBoolean() {
        return getEventData() instanceof Boolean;
    }

    /**
     * @return boolean primitive
     * @throws ClassCastException
     */
    public Boolean getBoolean() {
        return (Boolean) getEventData();
    }

    @Override
    public void close() throws XMLStreamException {
        super.close();
        try {
            source.close();
        } catch (IOException e) {
            throw new XMLStreamException(e);
        }
    }
}
