/**
 *  Copyright (c) 2005-2010, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  WSO2 Inc. licenses this file to you 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.json;

import org.apache.synapse.commons.staxon.core.json.JsonXMLConfig;
import org.apache.synapse.commons.staxon.core.json.JsonXMLConfigBuilder;
import org.apache.synapse.commons.staxon.core.json.JsonXMLInputFactory;

import org.apache.axiom.om.OMDataSource;
import org.apache.axiom.om.OMException;
import org.apache.axiom.om.OMOutputFormat;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;

final class JsonDataSourceImpl implements OMDataSource {
    public static final String END_OBJECT = "}";
    public static final String END_ARRAY = "}}";
    public static final String EMPTY_OBJECT = "{}";
    public static final String WRAPPER_OBJECT = "{" + Constants.K_OBJECT + ":";
    public static final String WRAPPER_ARRAY = "{" + Constants.K_ARRAY + ":{" + Constants.K_ARRAY_ELEM + ":";

    private byte[] stream;
    private int offset = 0;
    private int length = 0;
    private boolean isObject = true;

    private static final JsonXMLConfig xmlConfig = new JsonXMLConfigBuilder()
            .multiplePI(true)
            .autoArray(true)
            .autoPrimitive(true)
            .namespaceDeclarations(false)
            .build();

    private static final JsonXMLInputFactory xmlInputFactory = new JsonXMLInputFactory(xmlConfig);

    /**
     * Configuration used to produce XML that has no processing instructions in it.
     */
    private static final JsonXMLConfig xmlConfigNoPIs = new JsonXMLConfigBuilder()
            .multiplePI(false)
            .autoArray(true)
            .autoPrimitive(true)
            .namespaceDeclarations(false)
            .build();

    private static final JsonXMLInputFactory xmlInputFactoryNoPIs = new JsonXMLInputFactory(xmlConfigNoPIs);

    private static final Log logger = LogFactory.getLog(JsonDataSourceImpl.class.getName());

    public JsonDataSourceImpl(InputStream inputStream) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        int c;
        try {
            while (((c = inputStream.read()) != -1) && Character.isWhitespace((char) c)) {
            }
            ++length;
            if (c == '{') {
                stream.write(WRAPPER_OBJECT.getBytes());
                offset = WRAPPER_OBJECT.length();
            } else if (c == '[') {
                stream.write(WRAPPER_ARRAY.getBytes());
                isObject = false;
                offset = WRAPPER_ARRAY.length();
            } else {
                logger.error("Could not create a JSON data source from the input stream. Found '"
                        + ((char) c) + "' at the start of the input stream.");
                this.stream = EMPTY_OBJECT.getBytes();
                return;
            }
            stream.write(c);
            while (((c = inputStream.read()) != -1)) {
                stream.write(c);
                ++length;
            }
            if (isObject) {
                stream.write(END_OBJECT.getBytes());
            } else {
                stream.write(END_ARRAY.getBytes());
            }
            stream.flush();
            this.stream = stream.toByteArray();
            if (logger.isDebugEnabled()) {
                logger.debug("Built JSON Data Source from the incoming stream.");
            }
        } catch (IOException e) {
            logger.error("Could not create a JSON data source from the input stream. "
                    + e.getLocalizedMessage());
            this.stream = EMPTY_OBJECT.getBytes();
            isObject = true;
        }
    }

    public void serialize(OutputStream output, OMOutputFormat format) throws XMLStreamException {
        try {
            output.write(stream, offset, length);
        } catch (IOException e) {
            throw new OMException("Could not serialize payload. " + e.getLocalizedMessage());
        }
    }

    public void serialize(Writer writer, OMOutputFormat format) throws XMLStreamException {
        try {
            IOUtils.copy(new ByteArrayInputStream(stream, offset, length), writer);
        } catch (IOException e) {
            throw new OMException("Could not serialize payload. " + e.getLocalizedMessage());
        }
    }

    public void serialize(XMLStreamWriter xmlWriter) throws XMLStreamException {
        XMLStreamReader reader = getReader();
        xmlWriter.writeStartDocument();
        while (reader.hasNext()) {
            int x = reader.next();
            switch (x) {
                case XMLStreamConstants.START_ELEMENT:
                    xmlWriter.writeStartElement(reader.getPrefix(), reader.getLocalName(),
                            reader.getNamespaceURI());
                    int namespaceCount = reader.getNamespaceCount();
                    for (int i = namespaceCount - 1; i >= 0; i--) {
                        xmlWriter.writeNamespace(reader.getNamespacePrefix(i),
                                reader.getNamespaceURI(i));
                    }
                    int attributeCount = reader.getAttributeCount();
                    for (int i = 0; i < attributeCount; i++) {
                        xmlWriter.writeAttribute(reader.getAttributePrefix(i),
                                reader.getAttributeNamespace(i),
                                reader.getAttributeLocalName(i),
                                reader.getAttributeValue(i));
                    }
                    break;
                case XMLStreamConstants.START_DOCUMENT:
                    break;
                case XMLStreamConstants.CHARACTERS:
                    xmlWriter.writeCharacters(reader.getText());
                    break;
                case XMLStreamConstants.CDATA:
                    xmlWriter.writeCData(reader.getText());
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    xmlWriter.writeEndElement();
                    break;
                case XMLStreamConstants.END_DOCUMENT:
                    xmlWriter.writeEndDocument();
                    break;
                case XMLStreamConstants.SPACE:
                    break;
                case XMLStreamConstants.COMMENT:
                    xmlWriter.writeComment(reader.getText());
                    break;
                case XMLStreamConstants.DTD:
                    xmlWriter.writeDTD(reader.getText());
                    break;
                case XMLStreamConstants.PROCESSING_INSTRUCTION:
                    xmlWriter
                            .writeProcessingInstruction(reader.getPITarget(), reader.getPIData());
                    break;
                case XMLStreamConstants.ENTITY_REFERENCE:
                    xmlWriter.writeEntityRef(reader.getLocalName());
                    break;
                default:
                    throw new OMException();
            }
        }
        xmlWriter.writeEndDocument();
        xmlWriter.flush();
        xmlWriter.close();
    }

    public XMLStreamReader getReader() throws XMLStreamException {
        return new JsonReaderDelegate(xmlInputFactory.createXMLStreamReader(new ByteArrayInputStream(stream)), false);
    }
}
