package org.xbib.elasticsearch.common.xcontent.xml;

import com.ctc.wstx.stax.WstxInputFactory;
import com.ctc.wstx.stax.WstxOutputFactory;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.FastStringReader;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentType;
import org.xbib.elasticsearch.common.xcontent.XmlXContentBuilder;
import org.elasticsearch.common.xcontent.XContentGenerator;
import org.elasticsearch.common.xcontent.XContentParser;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;

/**
 * A XML based content implementation using Jackson XML dataformat
 *
 */
public class XmlXContent implements XContent {

    /*
     *  This is a bit tricky. We want Woodstox. Woodstox is stax2-api, and can indent XML. We package it in the plugin.
     *  But how can we force to load Woodstox instead of internal com.sun JDK streaming XML parser?
     *
     *  1) Elasticsearch runs the JVM in a "parent" classpath and a plugin in a separate "child" classpath.
     *  2) the Java XML API under XMLInputFactory.newInstance() / XMLOutputFactory.newInstance()
     *  uses a mixture of META-INF/services and system property configuration.
     *  Both work only on Elasticsearch parent classloader. They can not use resources inside a plugin.
     *  So, XML factory lookup does only work on the ES "lib" folder.
     *  3) com.fasterxml.jackson.dataformat.xml.XmlFactory creates internal XMLStreamWriter instances to create an
     *  indenting XML stream in the generator. We need XMLStreamWriter2 for indentation.
     *  But XmlFactory uses the ES parent class loader because it's JDK code in javax.xml.stream that tries to load it.
     *  Therefore, XML indentation crashes with:
     *  java.lang.UnsupportedOperationException: Not implemented
     *  at org.codehaus.stax2.ri.Stax2WriterAdapter.writeRaw(Stax2WriterAdapter.java:380)
     *  because it only sees the Stax API implementation of the JDK, not Woodstox of the plugin.
     *  4) We can work around it by:
     *     a) setting system properties in this static initializer (as early as possible)
     *     b) use direct initialization of WstxInputFactory / WstxOutputFactory, no javax.xml.stream class loading
     */
    static {
        System.setProperty("javax.xml.stream.XMLInputFactory", WstxInputFactory.class.getName());
        System.setProperty("javax.xml.stream.XMLOutputFactory", WstxOutputFactory.class.getName());

        XMLInputFactory inputFactory = new WstxInputFactory(); // do not use  XMLInputFactory.newInstance()
        inputFactory.setProperty("javax.xml.stream.isNamespaceAware", Boolean.TRUE);
        inputFactory.setProperty("javax.xml.stream.isValidating", Boolean.FALSE);
        inputFactory.setProperty("javax.xml.stream.isCoalescing", Boolean.TRUE);
        inputFactory.setProperty("javax.xml.stream.isReplacingEntityReferences", Boolean.FALSE);
        inputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", Boolean.FALSE);

        XMLOutputFactory outputFactory = new WstxOutputFactory(); // do not use  XMLOutputFactory.newInstance()
        outputFactory.setProperty("javax.xml.stream.isRepairingNamespaces", Boolean.TRUE);

        xmlFactory = new XmlFactory(inputFactory, outputFactory);

        xmlXContent = new XmlXContent();
    }

    public static XmlXContentBuilder contentBuilder() throws IOException {
        return XmlXContentBuilder.builder(xmlXContent);
    }

    public static XmlXContentBuilder contentBuilder(XmlXParams params) throws IOException {
        XmlXContentBuilder builder = XmlXContentBuilder.builder(xmlXContent);
        ((XmlXContentGenerator) builder.generator()).setParams(params);
        return builder;
    }

    private final static XmlFactory xmlFactory;

    private final static XmlXContent xmlXContent;

    private XmlXContent() {

    }

    @Override
    public XContentType type() {
        //return XmlXContentType.XML;
        return null;
    }

    public static XmlXContent xmlXContent() {
        return xmlXContent;
    }

    protected static XmlFactory xmlFactory() {
        return xmlFactory;
    }

    @Override
    public byte streamSeparator() {
        throw new UnsupportedOperationException("xml does not support stream parsing...");
    }

    @Override
    public XContentGenerator createGenerator(OutputStream os) throws IOException {
        return new XmlXContentGenerator(xmlFactory.createGenerator(os, JsonEncoding.UTF8));
    }

    @Override
    public XContentGenerator createGenerator(OutputStream os, String[] filters) throws IOException {
        // ignore filters (for now)
        return new XmlXContentGenerator(xmlFactory.createGenerator(os, JsonEncoding.UTF8));
    }

    @Override
    public XContentParser createParser(String content) throws IOException {
        return new XmlXContentParser(xmlFactory.createParser(new FastStringReader(content)));
    }

    @Override
    public XContentParser createParser(InputStream is) throws IOException {
        return new XmlXContentParser(xmlFactory.createParser(is));
    }

    @Override
    public XContentParser createParser(byte[] data) throws IOException {
        return new XmlXContentParser(xmlFactory.createParser(data));
    }

    @Override
    public XContentParser createParser(byte[] data, int offset, int length) throws IOException {
        return new XmlXContentParser(xmlFactory.createParser(data, offset, length));
    }

    @Override
    public XContentParser createParser(BytesReference bytes) throws IOException {
        if (bytes.hasArray()) {
            return createParser(bytes.array(), bytes.arrayOffset(), bytes.length());
        }
        return createParser(bytes.streamInput());
    }

    @Override
    public XContentParser createParser(Reader reader) throws IOException {
        return new XmlXContentParser(xmlFactory.createParser(reader));
    }
}