/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.processor.transformer;

import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.io.DocumentSource;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.util.XPath;
import org.orbeon.oxf.xml.XMLParsing;
import org.orbeon.oxf.xml.XMLReceiver;
import org.orbeon.oxf.processor.*;
import org.orbeon.oxf.processor.transformer.xslt.StringErrorListener;
import org.orbeon.oxf.processor.transformer.xslt.XSLTTransformer;
import org.orbeon.oxf.properties.PropertySet;
import org.orbeon.oxf.properties.PropertyStore;
import org.orbeon.oxf.xml.XMLConstants;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.saxon.Configuration;
import org.orbeon.saxon.query.DynamicQueryContext;
import org.orbeon.saxon.query.StaticQueryContext;
import org.orbeon.saxon.query.XQueryExpression;

import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXResult;
import java.util.Collections;
import java.util.Map;

/**
 * XQuery processor based on the Saxon engine.
 *
 * TODO: should work like the XSLT processor, and handle:
 *
 * - caching [BE CAREFUL WITH NOT CACHING TransformerURIResolver!]
 * - errors
 * - additional inputs
 * - etc.
 *
 * To get there, should maybe abstract what's in XSLT processor and derive from it here.
 */
public class SaxonXQueryProcessor extends ProcessorImpl {

    private static Logger logger = Logger.getLogger(SaxonXQueryProcessor.class);

    // This input determines attributes to set on the Configuration
    private static final String INPUT_ATTRIBUTES = "attributes";

    public SaxonXQueryProcessor() {
        addInputInfo(new ProcessorInputOutputInfo(INPUT_CONFIG));
        addInputInfo(new ProcessorInputOutputInfo(INPUT_ATTRIBUTES, XSLTTransformer.XSLT_PREFERENCES_CONFIG_NAMESPACE_URI));
        addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_DATA));
    }

    @Override
    public ProcessorOutput createOutput(String name) {
        final ProcessorOutput output = new ProcessorOutputImpl(SaxonXQueryProcessor.this, name) {
            public void readImpl(final PipelineContext pipelineContext, XMLReceiver xmlReceiver) {
                try {
                    final Document dataDocument = readInputAsDOM4J(pipelineContext, INPUT_DATA);

                    // Create XQuery configuration (depends on attributes input)
                    final URIResolver uriResolver = new TransformerURIResolver(SaxonXQueryProcessor.this, pipelineContext, INPUT_DATA, XMLParsing.ParserConfiguration.PLAIN);
                    // TODO: once caching is in place, make sure cached object does not contain a reference to the URIResolver
                    final Configuration configuration = XPath.newConfiguration();
                    {
                        configuration.setErrorListener(new StringErrorListener(logger));

                        // 2007-07-05 MK says: "fetching of query modules is done by the ModuleURIResolver in the
                        // static context, fetching of doc() is done by the URIResolver in the dynamic context; the
                        // URIResolver in the Configuration is just a fallback."
//                        config.setURIResolver(uriResolver);

                        // Read attributes
                        final Map<String, Boolean> attributesFromProperties;
                        {
                            // Read attributes input only if connected
                            if (getConnectedInputs().get(INPUT_ATTRIBUTES) != null) {
                                // Read input as an attribute Map and cache it
                                attributesFromProperties = readCacheInputAsObject(pipelineContext, getInputByName(INPUT_ATTRIBUTES), new CacheableInputReader<Map<String, Boolean>>() {
                                    public Map<String, Boolean> read(PipelineContext context, ProcessorInput input) {
                                        final Document preferencesDocument = readInputAsDOM4J(context, input);
                                        final PropertyStore propertyStore = new PropertyStore(preferencesDocument);
                                        final PropertySet propertySet = propertyStore.getGlobalPropertySet();
                                        return propertySet.getBooleanProperties();
                                    }
                                });
                            } else
                                attributesFromProperties = Collections.emptyMap();
                        }
                        // Set configuration attributes if any
                        for (Map.Entry<String, Boolean> entry: attributesFromProperties.entrySet())
                            configuration.setConfigurationProperty(entry.getKey(), entry.getValue());
                    }

                    // Create static context
                    final StaticQueryContext staticContext = new StaticQueryContext(configuration);

                    // Create XQuery expression (depends on config input and static context)
                    // TODO: caching of query must also depend on attributes input
                    XQueryExpression xqueryExpression = readCacheInputAsObject(pipelineContext, getInputByName(INPUT_CONFIG), new CacheableInputReader<XQueryExpression>() {
                        public XQueryExpression read(PipelineContext context, ProcessorInput input) {

                            try {

                                // Read XQuery into String
                                final Document xqueryDocument = readCacheInputAsDOM4J(pipelineContext, INPUT_CONFIG);
                                String xqueryBody;
                                if (XMLConstants.XS_STRING_QNAME.equals(Dom4jUtils.extractAttributeValueQName(xqueryDocument.getRootElement(), XMLConstants.XSI_TYPE_QNAME, false))) {
                                    // Content is text under an XML root element
                                    xqueryBody = xqueryDocument.getRootElement().getStringValue();
                                } else {
                                    // Content is XQuery embedded into XML
                                    xqueryBody = Dom4jUtils.domToString(xqueryDocument);
                                    xqueryBody = xqueryBody.substring(xqueryBody.indexOf(">") + 1);
                                    xqueryBody = xqueryBody.substring(0, xqueryBody.lastIndexOf("<"));
                                }

                                // 2007-07-05 MK says: "fetching of query modules is done by the ModuleURIResolver in the
                                // static context, fetching of doc() is done by the URIResolver in the dynamic context; the
                                // URIResolver in the Configuration is just a fallback."
                                // Clear URI resolver from static context as this must not end up in the cache
//                                staticContext.getConfiguration().setURIResolver(null);
                                return staticContext.compileQuery(xqueryBody);
                            } catch (Exception e) {
                                throw new OXFException(e);
                            }
                        }
                    });

                    // Create dynamic context and run query
                    final DynamicQueryContext dynamicContext =  new DynamicQueryContext(configuration);
                    dynamicContext.setContextItem(staticContext.buildDocument(new DocumentSource(dataDocument)));
                    dynamicContext.setURIResolver(uriResolver);
                    // TODO: use xqueryExpression.getStaticContext() when Saxon is upgraded
                    final SAXResult saxResult = new SAXResult(xmlReceiver);
                    saxResult.setLexicalHandler(xmlReceiver);

                    xqueryExpression.run(dynamicContext, saxResult, new java.util.Properties());

                } catch (Exception e) {
                    throw new OXFException(e);
                }
            }
        };
        addOutput(name, output);
        return output;
    }
}
