/* Woodstox XML processor
 *
 * Copyright (c) 2004- Tatu Saloranta, tatu.saloranta@iki.fi
 *
 * Licensed under the License specified in the file LICENSE which is
 * included with the source code.
 * You may not use this file except in compliance with the License.
 *
 * 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 com.ctc.wstx.stax;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Result;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamResult;

import aQute.bnd.annotation.spi.ServiceProvider;
import org.codehaus.stax2.XMLOutputFactory2;
import org.codehaus.stax2.XMLStreamWriter2;
import org.codehaus.stax2.io.Stax2Result;
import org.codehaus.stax2.ri.Stax2EventWriterImpl;
import org.codehaus.stax2.ri.Stax2WriterAdapter;

import com.ctc.wstx.api.WriterConfig;
import com.ctc.wstx.api.WstxOutputProperties;
import com.ctc.wstx.cfg.OutputConfigFlags;
import com.ctc.wstx.dom.WstxDOMWrappingWriter;
import com.ctc.wstx.exc.WstxIOException;
import com.ctc.wstx.io.CharsetNames;
import com.ctc.wstx.io.UTF8Writer;
import com.ctc.wstx.sw.AsciiXmlWriter;
import com.ctc.wstx.sw.BufferingXmlWriter;
import com.ctc.wstx.sw.ISOLatin1XmlWriter;
import com.ctc.wstx.sw.NonNsStreamWriter;
import com.ctc.wstx.sw.RepairingNsStreamWriter;
import com.ctc.wstx.sw.SimpleNsStreamWriter;
import com.ctc.wstx.sw.XmlWriter;
import com.ctc.wstx.util.URLUtil;

/**
 * Implementation of {@link XMLOutputFactory} for Wstx.
 *<p>
 * TODO:
 *<ul>
 * <li>Implement outputter that creates SAX events (DOM-backed
 *   writer exists as of Woodstox 3.2)
 *  </li>
 *</ul>
 */
@ServiceProvider(XMLOutputFactory.class)
public class WstxOutputFactory
    extends XMLOutputFactory2
    implements OutputConfigFlags
{
    /*
    ///////////////////////////////////////////////////////////
    // Actual storage of configuration settings
    ///////////////////////////////////////////////////////////
     */

    protected final WriterConfig mConfig;

    /*
    ///////////////////////////////////////////////////////////
    // Life-cycle
    ///////////////////////////////////////////////////////////
     */

    public WstxOutputFactory() {
        mConfig = WriterConfig.createFullDefaults();
    }

    /*
    ///////////////////////////////////////////////////////////
    // XMLOutputFactory API
    ///////////////////////////////////////////////////////////
     */

    @Override
    public XMLEventWriter createXMLEventWriter(OutputStream out)
        throws XMLStreamException
    {
        return createXMLEventWriter(out, null);
    }

    @Override
    public XMLEventWriter createXMLEventWriter(OutputStream out, String enc)
         throws XMLStreamException
    {
       if (out == null) {
           throw new IllegalArgumentException("Null OutputStream is not a valid argument");
       }
       return new Stax2EventWriterImpl(createSW(out, null, enc, false));
    }

    @Override
    public XMLEventWriter createXMLEventWriter(javax.xml.transform.Result result)
         throws XMLStreamException
    {
        return new Stax2EventWriterImpl(createSW(result));
    }

    @Override
    public XMLEventWriter createXMLEventWriter(Writer w)
        throws XMLStreamException
    {
        if (w == null) {
            throw new IllegalArgumentException("Null Writer is not a valid argument");
        }
        return new Stax2EventWriterImpl(createSW(null, w, null, false));
    }

    @Override
    public XMLStreamWriter createXMLStreamWriter(OutputStream out)
        throws XMLStreamException
    {
        return createXMLStreamWriter(out, null);
    }

    @Override
    public XMLStreamWriter createXMLStreamWriter(OutputStream out, String enc)
        throws XMLStreamException
    {
        if (out == null) {
            throw new IllegalArgumentException("Null OutputStream is not a valid argument");
        }
        return createSW(out, null, enc, false);
    }

    @Override
    public XMLStreamWriter createXMLStreamWriter(javax.xml.transform.Result result)
        throws XMLStreamException
    {
        return createSW(result);
    }

    @Override
    public XMLStreamWriter createXMLStreamWriter(Writer w)
        throws XMLStreamException
    {
        if (w == null) {
            throw new IllegalArgumentException("Null Writer is not a valid argument");
        }
        return createSW(null, w, null, false);
    }
    
    @Override
    public Object getProperty(String name) {
        return mConfig.getProperty(name);
    }
    
    @Override
    public boolean isPropertySupported(String name) {
        return mConfig.isPropertySupported(name);
    }
    
    @Override
    public void setProperty(String name, Object value)
    {
        mConfig.setProperty(name, value);
    }

    /*
    ///////////////////////////////////////////////////////////
    // Stax2 extensions
    ///////////////////////////////////////////////////////////
     */

    // // // Stax2 additional (encoding-aware) factory methods

    @Override
    public XMLEventWriter createXMLEventWriter(Writer w, String enc)
        throws XMLStreamException
    {
        return new Stax2EventWriterImpl(createSW(null, w, enc, false));
    }

    @Override
    public XMLEventWriter createXMLEventWriter(XMLStreamWriter sw)
        throws XMLStreamException
    {
        XMLStreamWriter2 sw2 = Stax2WriterAdapter.wrapIfNecessary(sw);
        return new Stax2EventWriterImpl(sw2);
    }

    @Override
    public XMLStreamWriter2 createXMLStreamWriter(Writer w, String enc)
        throws XMLStreamException
    {
        return createSW(null, w, enc, false);
    }

    // // // Stax2 "Profile" mutators

    @Override
    public void configureForXmlConformance() {
        mConfig.configureForXmlConformance();
    }

    @Override
    public void configureForRobustness() {
        mConfig.configureForRobustness();
    }

    @Override
    public void configureForSpeed() {
        mConfig.configureForSpeed();
    }

    /*
    ///////////////////////////////////////////////////////////
    // Woodstox-specific configuration access
    ///////////////////////////////////////////////////////////
     */

    public WriterConfig getConfig() {
        return mConfig;
    }

    /*
    ///////////////////////////////////////////////////////////
    // Internal methods:
    ///////////////////////////////////////////////////////////
     */

    /**
     * Bottleneck factory method used internally; needs to take care of passing
     * proper settings to stream writer.
     *
     * @param requireAutoClose Whether this result will always require
     *   auto-close be enabled (true); or only if application has
     *   requested it (false)
     */
    @SuppressWarnings("resource")
    private XMLStreamWriter2 createSW(OutputStream out, Writer w, String enc,
                                      boolean requireAutoClose)
        throws XMLStreamException
    {
        /* Need to ensure that the configuration object is not shared
         * any more; otherwise later changes via factory could be
         * visible half-way through output...
         */
        WriterConfig cfg = mConfig.createNonShared();
        XmlWriter xw;

        boolean autoCloseOutput = requireAutoClose || mConfig.willAutoCloseOutput();

        if (w == null) {
            if (enc == null) {
                enc = WstxOutputProperties.DEFAULT_OUTPUT_ENCODING;
            } else {
                /* Canonical ones are interned, so we may have
                 * normalized encoding already...
                 */
                if (enc != CharsetNames.CS_UTF8
                    && enc != CharsetNames.CS_ISO_LATIN1
                    && enc != CharsetNames.CS_US_ASCII) {
                    enc = CharsetNames.normalize(enc);
                }
            }

            try {
                if (enc == CharsetNames.CS_UTF8) {
                    w = new UTF8Writer(cfg, out, autoCloseOutput);
                    xw = new BufferingXmlWriter(w, cfg, enc, autoCloseOutput, out, 16);
                } else if (enc == CharsetNames.CS_ISO_LATIN1) {
                    xw = new ISOLatin1XmlWriter(out, cfg, autoCloseOutput);
                } else if (enc == CharsetNames.CS_US_ASCII) {
                    xw = new AsciiXmlWriter(out, cfg, autoCloseOutput);
                } else {
                    w = new OutputStreamWriter(out, enc);
                    xw = new BufferingXmlWriter(w, cfg, enc, autoCloseOutput, out, -1);
                }
            } catch (IOException ex) {
                throw new XMLStreamException(ex);
            }
        } else {
            // we may still be able to figure out the encoding:
            if (enc == null) {
                enc = CharsetNames.findEncodingFor(w);
            }
            try {
                xw = new BufferingXmlWriter(w, cfg, enc, autoCloseOutput, null, -1);
            } catch (IOException ex) {
                throw new XMLStreamException(ex);
            }
        }

        return createSW(enc, cfg, xw);
    }

    /**
     * Called by {code createSW(OutputStream, Writer, String, boolean)} after all of the
     * necessary configuration logic is complete.
     */
    protected XMLStreamWriter2 createSW(String enc, WriterConfig cfg, XmlWriter xw) {
        if (cfg.willSupportNamespaces()) {
            if (cfg.automaticNamespacesEnabled()) {
                return new RepairingNsStreamWriter(xw, enc, cfg);
            }
            return new SimpleNsStreamWriter(xw, enc, cfg);
        }
        return new NonNsStreamWriter(xw, enc, cfg);
    }

    @SuppressWarnings("resource")
    private XMLStreamWriter2 createSW(Result res)
        throws XMLStreamException
    {
        OutputStream out = null;
        Writer w = null;
        String encoding = null;
        boolean requireAutoClose;
        String sysId = null;

        if (res instanceof Stax2Result) {
            Stax2Result sr = (Stax2Result) res;
            try {
                out = sr.constructOutputStream();
                if (out == null) {
                    w = sr.constructWriter();
                }
            } catch (IOException ioe) {
                throw new WstxIOException(ioe);
            }
            // yes, it's required since caller has no access to stream/writer:
            requireAutoClose = true;
        } else if (res instanceof StreamResult) {
            StreamResult sr = (StreamResult) res;
            out = sr.getOutputStream();
            sysId = sr.getSystemId();
            if (out == null) {
                w = sr.getWriter();
            }
            /* Caller owns it, only auto-close if requested to do so:
             * (except that for system-id-only, it'll still be required,
             * see code below)
             */
            requireAutoClose = false;
        } else if (res instanceof SAXResult) {
            SAXResult sr = (SAXResult) res;
            sysId = sr.getSystemId();
            if (sysId == null || sysId.length() == 0) {
                throw new XMLStreamException("Can not create a stream writer for a SAXResult that does not have System Id (support for using SAX input source not implemented)");
            }
            requireAutoClose = true;
        } else if (res instanceof DOMResult) {
            return WstxDOMWrappingWriter.createFrom(mConfig.createNonShared(), (DOMResult) res);
        } else {
            throw new IllegalArgumentException("Can not instantiate a writer for XML result type "+res.getClass()+" (unrecognized type)");
        }

        if (out != null) {
            return createSW(out, null, encoding, requireAutoClose);
        }
        if (w != null) {
            return createSW(null, w, encoding, requireAutoClose);
        }
        if (sysId != null && sysId.length() > 0) {
            /* 26-Dec-2008, TSa: If we must construct URL from system id,
             *   it means caller will not have access to resulting
             *   stream, thus we will force auto-closing.
             */
            requireAutoClose = true;
            try {
                out = URLUtil.outputStreamFromURL(URLUtil.urlFromSystemId(sysId));
            } catch (IOException ioe) {
                throw new WstxIOException(ioe);
            }
            return createSW(out, null, encoding, requireAutoClose);
        }
        throw new XMLStreamException("Can not create Stax writer for passed-in Result -- neither writer, output stream or system id was accessible");
    }
}
