/* Woodstox Lite ("wool") XML processor
 *
 * Copyright (c) 2006- 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.fasterxml.aalto.stax;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.nio.ByteBuffer;

import javax.xml.stream.*;
import javax.xml.stream.util.XMLEventAllocator;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamSource;

import org.codehaus.stax2.XMLEventReader2;
import org.codehaus.stax2.XMLStreamReader2;
import org.codehaus.stax2.io.Stax2ByteArraySource;
import org.codehaus.stax2.io.Stax2CharArraySource;
import org.codehaus.stax2.io.Stax2Source;
import org.codehaus.stax2.ri.Stax2FilteredStreamReader;
import org.codehaus.stax2.ri.Stax2ReaderAdapter;
import org.codehaus.stax2.ri.evt.Stax2EventReaderAdapter;
import org.codehaus.stax2.ri.evt.Stax2FilteredEventReader;
import org.xml.sax.InputSource;

import com.fasterxml.aalto.*;
import com.fasterxml.aalto.async.AsyncByteArrayScanner;
import com.fasterxml.aalto.async.AsyncByteBufferScanner;
import com.fasterxml.aalto.async.AsyncStreamReaderImpl;
import com.fasterxml.aalto.dom.DOMReaderImpl;
import com.fasterxml.aalto.evt.EventAllocatorImpl;
import com.fasterxml.aalto.evt.EventReaderImpl;
import com.fasterxml.aalto.impl.IoStreamException;
import com.fasterxml.aalto.in.*;
import com.fasterxml.aalto.util.URLUtil;

/**
 * Aalto implementation of basic Stax factory (both
 * {@link javax.xml.stream.XMLInputFactory} and {@link org.codehaus.stax2.XMLInputFactory2})
 * as well as API for producing non-blocking (async) parsers
 * (that is, {@link AsyncXMLInputFactory}).
 *
 * @author Tatu Saloranta
 */
public final class InputFactoryImpl
    extends AsyncXMLInputFactory
{
    /**
     * This is the currently active configuration that will be used
     * for readers created by this factory.
     */
    final ReaderConfig _config;

    // // // StAX - mandated objects:

    protected XMLEventAllocator _allocator = null;

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

    public InputFactoryImpl() {
        _config = new ReaderConfig();
    }

    /*
    /**********************************************************************
    /* Stax, XMLInputFactory: filtered reader factory methods
    /**********************************************************************
     */

    // // // Filtered reader factory methods

    @Override
    public XMLEventReader createFilteredReader(XMLEventReader reader, EventFilter filter)
    {
        return new Stax2FilteredEventReader(Stax2EventReaderAdapter.wrapIfNecessary(reader), filter);
    }

    @Override
    public XMLStreamReader createFilteredReader(XMLStreamReader reader, StreamFilter filter)
        throws XMLStreamException
    {
         Stax2FilteredStreamReader fr = new Stax2FilteredStreamReader(reader, filter);
        /* As per Stax 1.0 TCK, apparently the filtered
         * reader is expected to be automatically forwarded to the first
         * acceptable event. This is different from the way RI works, but
         * since specs don't say anything about filtered readers, let's
         * consider TCK to be "more formal" for now, and implement that
         * behavior.
         */
        if (!filter.accept(fr)) { // START_DOCUMENT ok?
            // Ok, nope, this should do the trick:
            fr.next();
        }
        return fr;
    }

    /*
    /**********************************************************************
    /* Stax, XMLInputFactory: XMLEventReader factory methods
    /**********************************************************************
     */

    @Override
    public XMLEventReader createXMLEventReader(InputStream in) throws XMLStreamException {
        return createXMLEventReader(in, null);
    }

    @Override
    public XMLEventReader createXMLEventReader(InputStream in, String enc) throws XMLStreamException {
        return constructER(constructSR(in, enc, true));
    }

    @Override
    public XMLEventReader createXMLEventReader(Reader r) throws XMLStreamException {
        return createXMLEventReader(null, r);
    }

    @Override
    public XMLEventReader createXMLEventReader(javax.xml.transform.Source source) throws XMLStreamException {
        return constructER(constructSR(source, true));
    }

    @Override
    public XMLEventReader createXMLEventReader(String systemId, InputStream in)
        throws XMLStreamException
    {
        return constructER(constructSR(systemId, in, true));
    }

    @Override
    public XMLEventReader createXMLEventReader(String systemId, Reader r)
        throws XMLStreamException
    {
        return constructER(constructSR(systemId, r, true));
    }

    @Override
    public XMLEventReader createXMLEventReader(XMLStreamReader sr)
        throws XMLStreamException
    {
        return constructER(Stax2ReaderAdapter.wrapIfNecessary(sr));
    }

    /*
    /**********************************************************************
    /* Stax, XMLInputFactory: XMLStreamReader factory methods
    /**********************************************************************
     */

    @Override
    public XMLStreamReader createXMLStreamReader(InputStream in)
        throws XMLStreamException
    {
        return constructSR(in, null, false);
    }
    
    @Override
    public XMLStreamReader createXMLStreamReader(InputStream in, String enc)
        throws XMLStreamException
    {
        return constructSR(in, enc, false);
    }

    @Override
    public XMLStreamReader createXMLStreamReader(Reader r)
        throws XMLStreamException
    {
        return constructSR(null, r, false);
    }

    @Override
    public XMLStreamReader createXMLStreamReader(String systemId, Reader r)
        throws XMLStreamException
    {
        return constructSR(systemId, r, false);
    }

    @Override
    public XMLStreamReader createXMLStreamReader(javax.xml.transform.Source src)
        throws XMLStreamException
    {
        return constructSR(src, false);
    }

    @Override
    public XMLStreamReader createXMLStreamReader(String systemId, InputStream in)
        throws XMLStreamException
    {
        return constructSR(systemId, in, false);
    }

    /*
    /**********************************************************************
    /* Stax, XMLInputFactory; generic accessors/mutators
    /**********************************************************************
     */

    @Override
    public Object getProperty(String name)
    {
        // false -> is mandatory, unrecognized will throw IllegalArgumentException
        return _config.getProperty(name, true);
    }

    @Override
    public void setProperty(String propName, Object value)
    {
        _config.setProperty(propName, value);
    } 

    @Override
    public XMLEventAllocator getEventAllocator() {
        return _allocator;
    }
    
    @Override
    public XMLReporter getXMLReporter() {
        return _config.getXMLReporter();
    }

    @Override
    public XMLResolver getXMLResolver() {
        return _config.getXMLResolver();
    }

    @Override
    public boolean isPropertySupported(String name) {
        return _config.isPropertySupported(name);
    }

    @Override
    public void setEventAllocator(XMLEventAllocator allocator) {
        _allocator = allocator;
    }

    @Override
    public void setXMLReporter(XMLReporter r) {
        _config.setXMLReporter(r);
    }

    @Override
    public void setXMLResolver(XMLResolver r) {
        _config.setXMLResolver(r);
    }

    /*
    /**********************************************************************
    /* Stax2 implementation; additional factory methods
    /**********************************************************************
     */

    @Override
    public XMLEventReader2 createXMLEventReader(URL src)
        throws XMLStreamException
    {
        return constructER(constructSR(src, true));
    }

    @Override
    public XMLEventReader2 createXMLEventReader(File f)
        throws XMLStreamException
    {
        return constructER(constructSR(f, true));
    }

    @Override
    public XMLStreamReader2 createXMLStreamReader(URL src)
        throws XMLStreamException
    {
        return constructSR(src, false);
    }

    /**
     * Convenience factory method that allows for parsing a document
     * stored in the specified file.
     */
    @Override
    public XMLStreamReader2 createXMLStreamReader(File f)
        throws XMLStreamException
    {
        return constructSR(f, false);
    }

    // // // StAX2 "Profile" mutators

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

    @Override
    public void configureForConvenience()
    {
        _config.configureForConvenience();
    }

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

    @Override
    public void configureForLowMemUsage()
    {
        _config.configureForLowMemUsage();
    }

    @Override
    public void configureForRoundTripping()
    {
        _config.configureForRoundTripping();
    }

    /*
    /**********************************************************************
    /* Non-blocking reader factories (AsyncXMLInputFactory)
    /**********************************************************************
     */

    @Override
    public AsyncXMLStreamReader<AsyncByteArrayFeeder> createAsyncForByteArray()
    {
        // TODO: pass system and/or public ids?
        ReaderConfig cfg = getNonSharedConfig(null, null, null, false, false);
        cfg.setActualEncoding("UTF-8");
        return new AsyncStreamReaderImpl<AsyncByteArrayFeeder>(new AsyncByteArrayScanner(cfg));
    }

    @Override
    public AsyncXMLStreamReader<AsyncByteArrayFeeder> createAsyncFor(byte[] input) throws XMLStreamException
    {
        return createAsyncFor(input, 0, input.length);
    }

    @Override
    public AsyncXMLStreamReader<AsyncByteArrayFeeder> createAsyncFor(byte[] input, int offset, int length)
        throws XMLStreamException
    {
         ReaderConfig cfg = getNonSharedConfig(null, null, null, false, false);
         cfg.setActualEncoding("UTF-8");
         AsyncByteArrayScanner scanner = new AsyncByteArrayScanner(cfg);
         scanner.feedInput(input, offset, length);
         return new AsyncStreamReaderImpl<AsyncByteArrayFeeder>(scanner);
    }

    @Override
    public AsyncXMLStreamReader<AsyncByteBufferFeeder> createAsyncForByteBuffer() {
        ReaderConfig cfg = getNonSharedConfig(null, null, null, false, false);
        cfg.setActualEncoding("UTF-8");
        return new AsyncStreamReaderImpl<AsyncByteBufferFeeder>(new AsyncByteBufferScanner(cfg));
    }

    @Override
    public AsyncXMLStreamReader<AsyncByteBufferFeeder> createAsyncFor(ByteBuffer input) throws XMLStreamException
    {
        ReaderConfig cfg = getNonSharedConfig(null, null, null, false, false);
        cfg.setActualEncoding("UTF-8");
        AsyncByteBufferScanner scanner = new AsyncByteBufferScanner(cfg);
        scanner.feedInput(input);
        return new AsyncStreamReaderImpl<AsyncByteBufferFeeder>(scanner);
    }

    /*
    /**********************************************************************
    /* Internal/package methods
    /**********************************************************************
     */

    /**
     * Method called when a non-shared copy of the current configuration
     * is needed. This is usually done when a new reader is constructed.
     */
    public ReaderConfig getNonSharedConfig(String systemId, String publicId,
            String extEncoding, boolean forEventReader, boolean forceAutoClose)
    {
        ReaderConfig cfg = _config.createNonShared(publicId, systemId, extEncoding);
        if (forEventReader) {
            /* No point in lazy parsing for event readers: no more efficient
             * (and possible less) since all data is needed, always; and
             * exceptions also get lazily thrown after the fact.
             */
            cfg.doParseLazily(false);
        }
        if (forceAutoClose) {
            cfg.doAutoCloseInput(true);
        }
        return cfg;
    }

    protected XMLStreamReader2 constructSR(InputStream in, String enc,
            boolean forEventReader)
        throws XMLStreamException
    {
        ReaderConfig cfg = getNonSharedConfig(null, null, enc, forEventReader, false);
        return StreamReaderImpl.construct(ByteSourceBootstrapper.construct(cfg, in));
    }

    protected XMLStreamReader2 constructSR(String systemId, Reader r,
            boolean forEventReader)
        throws XMLStreamException
    {
        ReaderConfig cfg = getNonSharedConfig(null, systemId, null, forEventReader, false);
        return StreamReaderImpl.construct(CharSourceBootstrapper.construct(cfg, r));
    }

    protected XMLStreamReader2 constructSR(String systemId, InputStream in,
            boolean forEventReader)
        throws XMLStreamException
    {
        ReaderConfig cfg = getNonSharedConfig(null, systemId, null, forEventReader, false);
        return StreamReaderImpl.construct(ByteSourceBootstrapper.construct(cfg, in));
    }

    @SuppressWarnings("resource")
    protected XMLStreamReader2 constructSR(javax.xml.transform.Source src,
            boolean forEventReader)
        throws XMLStreamException
    {
        if (src instanceof Stax2Source) {
            return constructSR2((Stax2Source) src, forEventReader);
        }

        Reader r = null;
        InputStream in = null;
        String pubId = null;
        String sysId = null;
        String encoding = null;
        boolean autoCloseInput;

        if (src instanceof StreamSource) {
            StreamSource ss = (StreamSource) src;
            sysId = ss.getSystemId();
            pubId = ss.getPublicId();
            in = ss.getInputStream();
            if (in == null) {
                r = ss.getReader();
            }
            /* Caller still has access to stream/reader (except if we only
             * get system-id); no need to force auto-close here
             */
            autoCloseInput = false;
        } else if (src instanceof SAXSource) {
            SAXSource ss = (SAXSource) src;
            // Not a complete implementation, but maybe it's enough?
            sysId = ss.getSystemId();
            InputSource isrc = ss.getInputSource();
            if (isrc != null) {
                sysId = isrc.getSystemId();
                pubId = isrc.getPublicId();
                encoding = isrc.getEncoding();
                in = isrc.getByteStream();
                if (in == null) {
                    r = isrc.getCharacterStream();
                }
            }
            /* Caller still has access to stream/reader (except if we only
             * get system-id); no need to force auto-close here
             */
            autoCloseInput = false;
        } else if (src instanceof DOMSource) {
            autoCloseInput = false; // shouldn't matter
            ReaderConfig cfg = getNonSharedConfig(pubId, sysId, encoding, forEventReader, autoCloseInput);
            return DOMReaderImpl.createFrom((DOMSource) src, cfg);
        } else {
            throw new IllegalArgumentException("Can not instantiate StAX reader for XML source type "+src.getClass()+" (unrecognized type)");
        }
        if (in != null) {
            ReaderConfig cfg = getNonSharedConfig(pubId, sysId, encoding, forEventReader, autoCloseInput);
            return StreamReaderImpl.construct(ByteSourceBootstrapper.construct(cfg, in));
        }
        if (r != null) {
            ReaderConfig cfg = getNonSharedConfig(pubId, sysId, encoding, forEventReader, autoCloseInput);
            return StreamReaderImpl.construct(CharSourceBootstrapper.construct(cfg, r));
        }
        if (sysId != null && sysId.length() > 0) {
            /* If we must construct URL from system id, caller will not have
             * access to resulting stream, need to force auto-closing.
             */
            autoCloseInput = true;
            ReaderConfig cfg = getNonSharedConfig(pubId, sysId, encoding, forEventReader, autoCloseInput);
            try {
                URL url = URLUtil.urlFromSystemId(sysId);
                in = URLUtil.inputStreamFromURL(url);
                return StreamReaderImpl.construct(ByteSourceBootstrapper.construct(cfg, in));
            } catch (IOException ioe) {
                throw new IoStreamException(ioe);
            }
        }
        throw new XMLStreamException("Can not create Stax reader for the Source passed -- neither reader, input stream nor system id was accessible; can not use other types of sources (like embedded SAX streams)");
    }

    protected XMLStreamReader2 constructSR2(Stax2Source ss, boolean forEventReader)
        throws XMLStreamException
    {
        /* Caller has no access to these input sources, so we must force
         * auto-close ('true' after 'forEventReader')
         */
        ReaderConfig cfg = getNonSharedConfig(ss.getPublicId(), ss.getSystemId(), ss.getEncoding(), forEventReader, true);

        // Byte arrays can be accessed VERY efficiently...
        if (ss instanceof Stax2ByteArraySource) {
            Stax2ByteArraySource bs = (Stax2ByteArraySource) ss;
            return StreamReaderImpl.construct(ByteSourceBootstrapper.construct
                                              (cfg, bs.getBuffer(), bs.getBufferStart(), bs.getBufferLength()));
        }
        if (ss instanceof Stax2CharArraySource) {
            Stax2CharArraySource cs = (Stax2CharArraySource) ss;
            return StreamReaderImpl.construct(CharSourceBootstrapper.construct
                                              (cfg, cs.getBuffer(), cs.getBufferStart(), cs.getBufferLength()));
        }
        
        /* Ok, and this is the default, if we don't know a better
         * type-specific method:
         */
        try {
            InputStream in = ss.constructInputStream();
            if (in != null) {
                return StreamReaderImpl.construct(ByteSourceBootstrapper.construct(cfg, in));
            }
            Reader r = ss.constructReader();
            if (r != null) {
                return StreamReaderImpl.construct(CharSourceBootstrapper.construct(cfg, r));
            }
        } catch (IOException ioe) {
            throw new IoStreamException(ioe);
        }

        throw new IllegalArgumentException("Can not create stream reader for given Stax2Source: neither InputStream nor Reader available");
    }

    protected XMLStreamReader2 constructSR(URL src, boolean forEventReader)
        throws XMLStreamException
    {
        InputStream in;
        try {
            in = URLUtil.inputStreamFromURL(src);
        } catch (IOException ioe) {
            throw new IoStreamException(ioe);
        }
        // Construct from URL? Must auto-close:
        ReaderConfig cfg = getNonSharedConfig(URLUtil.urlToSystemId(src), null, null, forEventReader, true);
        return StreamReaderImpl.construct(ByteSourceBootstrapper.construct(cfg, in));
    }

    protected XMLStreamReader2 constructSR(File f, boolean forEventReader)
        throws XMLStreamException
    {
        try {
            InputStream in = new FileInputStream(f);
            String systemId = URLUtil.fileToSystemId(f);
        // Construct from File? Must auto-close:
            ReaderConfig cfg = getNonSharedConfig(systemId, null, null, forEventReader, true);
            return StreamReaderImpl.construct(ByteSourceBootstrapper.construct(cfg, in));
        } catch (IOException ioe) {
            throw new IoStreamException(ioe);
        }
    }

    public XMLEventReader2 constructER(XMLStreamReader2 sr) {
        return new EventReaderImpl(createEventAllocator(), sr);
    }

    protected XMLEventAllocator createEventAllocator() 
    {
        // Explicitly set allocate?
        if (_allocator != null) {
            return _allocator.newInstance();
        }
        // Complete or fast one?
        return _config.willPreserveLocation() ?
            EventAllocatorImpl.getDefaultInstance()
            : EventAllocatorImpl.getFastInstance();
    }
}

