package org.exist.cocoon;

import org.apache.cocoon.ProcessingException;

import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.excalibur.source.Source;
import org.apache.cocoon.transformation.AbstractSAXTransformer;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.environment.Response;
import org.apache.cocoon.environment.http.HttpEnvironment;
import org.apache.cocoon.environment.Context;
import org.apache.cocoon.xml.EmbeddedXMLPipe;

import org.xml.sax.Attributes;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.SAXException;
import org.xml.sax.ContentHandler;

import java.io.File;
import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;

import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.Database;
import org.xmldb.api.base.ResourceSet;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.XMLResource;

import org.exist.xmldb.XQueryService;
import org.exist.xquery.functions.request.RequestModule;
import org.exist.source.CocoonSource;
import org.exist.cocoon.CocoonRequestWrapper;
import org.exist.cocoon.CocoonResponseWrapper;
import org.exist.storage.serializers.Serializer;
import org.exist.storage.serializers.EXistOutputKeys;
import org.exist.xmldb.CollectionImpl;

public class XQueryTransformer extends AbstractSAXTransformer {
    public final static String DRIVER = "org.exist.xmldb.DatabaseImpl";

    private final static String XQUERY_TAG = "xquery";
    private final static String XQUERY_ATTR_SRC = "src";
    private final static String PARAMETER_TAG = "parameter";
    private final static String PARAMETER_ATTR_NAME = "name";
    private final static String PARAMETER_ATTR_VALUE = "value";
    private final static String VALUE_TAG = "value";

    private final static String CONF_COLLECTION = "collection";
    private final static String CONF_USER = "user";
    private final static String CONF_PASSWORD = "password";
    private final static String CONF_CREATE_SESSION = "create-session";
    private final static String CONF_EXPAND_XINCLUDES = "expand-xincludes";

    private Source inputSource = null;
    private Map optionalParameters = new HashMap();
    private String currentParam = null;
    private List currentValues = null;
    private boolean insideValueTag = false;
    private String baseURI = null;

    private String[] configArr = new String[] {CONF_COLLECTION, 
                                               CONF_USER, 
                                               CONF_PASSWORD,
                                               CONF_CREATE_SESSION,
                                               CONF_EXPAND_XINCLUDES};

    private Map configMap = new HashMap();

    public void configure (Configuration configuration) throws ConfigurationException {
        this.configMap.put(CONF_COLLECTION, "xmldb:exist:///db");
        this.configMap.put(CONF_USER, "guest");
        this.configMap.put(CONF_PASSWORD, "guest");
        this.configMap.put(CONF_CREATE_SESSION, new Boolean(false));
        this.configMap.put(CONF_EXPAND_XINCLUDES, new Boolean(false));
        for (int i=0; i<configArr.length; i++) {
            Configuration child = configuration.getChild(configArr[i]);
            Object value = null;
            if (configArr[i].equals(CONF_CREATE_SESSION) || configArr[i].equals(CONF_EXPAND_XINCLUDES)) {
                value = new Boolean(child.getValueAsBoolean(
                        ((Boolean)configMap.get(configArr[i])).booleanValue()));
            } else {
                value = child.getValue((String)configMap.get(configArr[i]));
            }
            this.configMap.put(configArr[i], value);
        }
        super.configure(configuration);
        this.defaultNamespaceURI = "http://www.exist.org/xquery/transform";
    }

    public void setup(SourceResolver resolver, 
                      Map objectModel, 
                      String src, 
                      Parameters params) 
        throws ProcessingException, SAXException, IOException
    {
        Context context = ObjectModelHelper.getContext(objectModel);
        String dbHome = context.getRealPath("WEB-INF");
        try {
            Class driver = Class.forName(DRIVER);
            Database database = (Database)driver.newInstance();
            database.setProperty("create-database", "true");
            database.setProperty("configuration", dbHome + File.separatorChar + "conf.xml");
            DatabaseManager.registerDatabase(database);
        } catch(Exception e) {
            throw new ProcessingException("Failed to initialize database driver: " + e.getMessage(), e);
        }
        
        String servletPath = request.getServletPath();
        String pathInfo = request.getPathInfo();
        StringBuffer baseURIBuffer = new StringBuffer(servletPath);
        if (pathInfo != null) baseURIBuffer.append(pathInfo);
        int p = baseURIBuffer.lastIndexOf("/");
        if (p > -1)  baseURIBuffer.delete(p,baseURIBuffer.length());            
        baseURI = context.getRealPath(baseURIBuffer.toString());
        super.setup(resolver, objectModel, src, params);
    }

    public void startTransformingElement( String uri, 
                                          String name, 
                                          String raw, 
                                          Attributes attr )
           throws ProcessingException, IOException, SAXException 
    {
        if(XQUERY_TAG.equals(name)) {
            currentParam = null;
            currentValues = null;
            String xqFilename = attr.getValue(XQUERY_ATTR_SRC);
            this.inputSource = resolver.resolveURI(xqFilename);
        } else if(PARAMETER_TAG.equals(name)) {
            currentParam = attr.getValue(PARAMETER_ATTR_NAME);
            String paramValue = attr.getValue(PARAMETER_ATTR_VALUE);
            if (paramValue != null) {
                optionalParameters.put(currentParam, paramValue);
            } else {
                currentValues = new ArrayList();
            }
        } else if(VALUE_TAG.equals(name)) {
            insideValueTag = true;
        } else {
            super.startTransformingElement(uri, name, raw, attr);
        }
        
    }

    public void endTransformingElement( String uri, 
                                        String name, 
                                        String raw )
            throws ProcessingException, IOException, SAXException {
        if(XQUERY_TAG.equals(name)) {
            try {
                Collection collection = DatabaseManager.getCollection(
                        (String)configMap.get(CONF_COLLECTION), 
                        (String)configMap.get(CONF_USER),
                        (String)configMap.get(CONF_PASSWORD));
                if (collection == null) {
                    if (getLogger().isErrorEnabled())
                        getLogger().error(
                                "Collection " + configMap.get(CONF_COLLECTION) + " not found");
                    throw new ProcessingException("Collection " + configMap.get(CONF_COLLECTION)
                            + " not found");
                }
                XQueryService service = (XQueryService) collection.getService(
                        "XQueryService", "1.0");
                service.setProperty(Serializer.GENERATE_DOC_EVENTS, "false");
                service.setProperty(EXistOutputKeys.EXPAND_XINCLUDES,
                        ((Boolean)configMap.get(CONF_EXPAND_XINCLUDES)).booleanValue() ? "yes" : "no");
                service.setProperty("base-uri", baseURI);
                service.setNamespace(RequestModule.PREFIX, RequestModule.NAMESPACE_URI);
                service.setModuleLoadPath(baseURI);
                if(!((CollectionImpl)collection).isRemoteCollection()) {
                    HttpServletRequest httpRequest = (HttpServletRequest) objectModel
                            .get(HttpEnvironment.HTTP_REQUEST_OBJECT);
                    service.declareVariable(RequestModule.PREFIX + ":request",
                            new CocoonRequestWrapper(request, httpRequest));
                    service.declareVariable(RequestModule.PREFIX + ":response",
                            new CocoonResponseWrapper(response));
                }
                declareParameters(service);
                
                ResourceSet result = service.execute(new CocoonSource(inputSource, true));
                XMLResource resource;
                for (long i = 0; i < result.getSize(); i++) {
                    resource = (XMLResource) result.getResource(i);
                    resource.getContentAsSAX(new EmbeddedXMLPipe(this.contentHandler));
                }
            } catch (XMLDBException e) {
                throw new ProcessingException("XMLDBException occurred: "
                        + e.getMessage(), e);
            }
        } else if(PARAMETER_TAG.equals(name)) {
            if (currentValues != null) {
                if (currentValues.size() == 1) {
                    optionalParameters.put(currentParam, (String)currentValues.get(0));
                } else if (currentValues.size() > 1) {
                    optionalParameters.put(currentParam, currentValues);
                } else {
                    optionalParameters.put(currentParam, "");
                }
            }
            currentParam = null;
            currentValues = null;
        } else if(VALUE_TAG.equals(name)) {
            insideValueTag = false;
        } else {
            super.endTransformingElement(uri, name, raw);
        }
    }

    public void characters(char[] p0, int p1, int p2)
            throws SAXException {
        if (currentParam != null && insideValueTag) {
            String value = String.copyValueOf(p0, p1, p2);
            currentValues.add(value);
        } else {
            super.characters(p0, p1, p2);
        }
    }

    private void declareParameters(XQueryService service) throws XMLDBException {
        for(Iterator i = optionalParameters.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry entry = (Map.Entry)i.next();
            service.declareVariable((String)entry.getKey(), entry.getValue());
        }
    }
}
