/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 org.apache.ode.bpel.elang.xquery10.runtime;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.transform.TransformerFactory;
import javax.xml.xpath.XPathConstants;
import javax.xml.xquery.XQConnection;
import javax.xml.xquery.XQConstants;
import javax.xml.xquery.XQDataSource;
import javax.xml.xquery.XQException;
import javax.xml.xquery.XQItem;
import javax.xml.xquery.XQItemType;
import javax.xml.xquery.XQPreparedExpression;
import javax.xml.xquery.XQResultSequence;
import javax.xml.xquery.XQSequence;
import javax.xml.xquery.XQSequenceType;
import javax.xml.xquery.XQStaticContext;

import net.sf.saxon.Configuration;
import net.sf.saxon.om.Item;
import net.sf.saxon.om.Validation;
//import net.sf.saxon.trans.DynamicError;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.value.DurationValue;
import net.sf.saxon.value.Value;
import net.sf.saxon.xqj.SaxonXQConnection;
import net.sf.saxon.xqj.SaxonXQDataSource;
import net.sf.saxon.xqj.SaxonXQItem;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.bpel.common.FaultException;
import org.apache.ode.bpel.elang.xpath20.compiler.Constants;
import org.apache.ode.bpel.elang.xpath20.compiler.WrappedResolverException;
import org.apache.ode.bpel.elang.xpath20.runtime.JaxpFunctionResolver;
import org.apache.ode.bpel.elang.xpath20.runtime.JaxpVariableResolver;
import org.apache.ode.bpel.elang.xquery10.compiler.XQuery10BpelFunctions;
import org.apache.ode.bpel.elang.xquery10.o.OXQuery10ExpressionBPEL20;
import org.apache.ode.bpel.explang.ConfigurationException;
import org.apache.ode.bpel.explang.EvaluationContext;
import org.apache.ode.bpel.explang.EvaluationException;
import org.apache.ode.bpel.explang.ExpressionLanguageRuntime;
import org.apache.ode.bpel.o.OExpression;
import org.apache.ode.utils.DOMUtils;
import org.apache.ode.utils.ISO8601DateParser;
import org.apache.ode.utils.NSContext;
import org.apache.ode.utils.xsd.Duration;
import org.apache.ode.utils.xsl.XslTransformHandler;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;


/**
 * XQuery 1.0 Expression Language run-time subsytem. Saxon implementation.
 */
@SuppressWarnings("deprecation")
public class XQuery10ExpressionRuntime implements ExpressionLanguageRuntime {
    static final short NODE_TYPE = 1;
    static final short NODESET_TYPE = 2;
    static final short STRING_TYPE = 3;
    static final short BOOLEAN_TYPE = 4;
    static final short NUMBER_TYPE = 5;

    /** Class-level logger. */
    private static final Log __log = LogFactory.getLog(XQuery10ExpressionRuntime.class);

    /**
     * Creates a new XQuery10ExpressionRuntime object.
     */
    public XQuery10ExpressionRuntime() {
    }

    /**
     * Initialize XSL Transformer
     *
     * @param properties properties 
     *
     * @throws ConfigurationException ConfigurationException 
     */
    public void initialize(Map properties) throws ConfigurationException {
        TransformerFactory trsf = new net.sf.saxon.TransformerFactoryImpl();
        XslTransformHandler.getInstance().setTransformerFactory(trsf);
    }

    /**
     * 
     * @see org.apache.ode.bpel.explang.ExpressionLanguageRuntime#evaluateAsString(org.apache.ode.bpel.o.OExpression,
     *      org.apache.ode.bpel.explang.EvaluationContext)
     */
    public String evaluateAsString(OExpression cexp, EvaluationContext ctx)
        throws FaultException, EvaluationException {
        return (String) evaluate(cexp, ctx, XPathConstants.STRING);
    }

    /**
     * 
     * @see org.apache.ode.bpel.explang.ExpressionLanguageRuntime#evaluateAsBoolean(org.apache.ode.bpel.o.OExpression,
     *      org.apache.ode.bpel.explang.EvaluationContext)
     */
    public boolean evaluateAsBoolean(OExpression cexp, EvaluationContext ctx)
        throws FaultException, EvaluationException {
        return (Boolean) evaluate(cexp, ctx, XPathConstants.BOOLEAN);
    }

    /**
     * Evaluate expression and return a number
     *
     * @param cexp cexp 
     * @param ctx ctx 
     *
     * @return type
     *
     * @throws FaultException FaultException 
     * @throws EvaluationException EvaluationException 
     */
    public Number evaluateAsNumber(OExpression cexp, EvaluationContext ctx)
        throws FaultException, EvaluationException {
        return (Number) evaluate(cexp, ctx, XPathConstants.NUMBER);
    }

    /**
     * 
     * @see org.apache.ode.bpel.explang.ExpressionLanguageRuntime#evaluate(org.apache.ode.bpel.o.OExpression,
     *      org.apache.ode.bpel.explang.EvaluationContext)
     */
    public List evaluate(OExpression cexp, EvaluationContext ctx)
        throws FaultException, EvaluationException {
        List result;
        Object someRes = evaluate(cexp, ctx, XPathConstants.NODESET);

        if (someRes instanceof List) {
            result = (List) someRes;
            if (__log.isDebugEnabled()) {
                __log.debug("Returned list of size " + result.size());
            }

            if ((result.size() == 1) && !(result.get(0) instanceof Node)) {
                // Dealing with a Java class
                Object simpleType = result.get(0);

                // Dates get a separate treatment as we don't want to call toString on them
                String textVal;

                if (simpleType instanceof Date) {
                    textVal = ISO8601DateParser.format((Date) simpleType);
                } else if (simpleType instanceof DurationValue) {
                    textVal = ((DurationValue) simpleType).getStringValue();
                } else {
                    textVal = simpleType.toString();
                }

                // Wrapping in a document
                Document d = DOMUtils.newDocument();

                // Giving our node a parent just in case it's an LValue expression
                Element wrapper = d.createElement("wrapper");
                Text text = d.createTextNode(textVal);
                wrapper.appendChild(text);
                d.appendChild(wrapper);
                result = Collections.singletonList(text);
            }
        } else if (someRes instanceof NodeList) {
            NodeList retVal = (NodeList) someRes;
            if (__log.isDebugEnabled()) {
                __log.debug("Returned node list of size " + retVal.getLength());
            }
            result = new ArrayList(retVal.getLength());

            for (int m = 0; m < retVal.getLength(); ++m) {
                Node val = retVal.item(m);

                if (val.getNodeType() == Node.DOCUMENT_NODE) {
                    val = ((Document) val).getDocumentElement();
                }

                result.add(val);
            }
        } else {
            result = null;
        }

        return result;
    }

    /**
     * Evaluate expression and return a node
     *
     * @param cexp cexp 
     * @param ctx ctx 
     *
     * @return type
     *
     * @throws FaultException FaultException 
     * @throws EvaluationException EvaluationException 
     */
    public Node evaluateNode(OExpression cexp, EvaluationContext ctx)
        throws FaultException, EvaluationException {
        List retVal = evaluate(cexp, ctx);

        if (retVal.size() == 0) {
            throw new FaultException(cexp.getOwner().constants.qnSelectionFailure,
                "No results for expression: " + cexp);
        }

        if (retVal.size() > 1) {
            throw new FaultException(cexp.getOwner().constants.qnSelectionFailure,
                "Multiple results for expression: " + cexp);
        }

        return (Node) retVal.get(0);
    }

    /**
     * Evaluate expression and return a date
     *
     * @param cexp cexp 
     * @param context context 
     *
     * @return type
     *
     * @throws FaultException FaultException 
     * @throws EvaluationException EvaluationException 
     */
    public Calendar evaluateAsDate(OExpression cexp, EvaluationContext context)
        throws FaultException, EvaluationException {
        List literal = DOMUtils.toList(evaluate(cexp, context,
                    XPathConstants.NODESET));

        if (literal.size() == 0) {
            throw new FaultException(cexp.getOwner().constants.qnSelectionFailure,
                "No results for expression: " + cexp);
        }

        if (literal.size() > 1) {
            throw new FaultException(cexp.getOwner().constants.qnSelectionFailure,
                "Multiple results for expression: " + cexp);
        }

        Object date = literal.get(0);

        if (date instanceof Calendar) {
            return (Calendar) date;
        }

        if (date instanceof Date) {
            Calendar cal = Calendar.getInstance();
            cal.setTime((Date) date);

            return cal;
        }

        if (date instanceof Element) {
            date = ((Element) date).getTextContent();
        }

        try {
            return ISO8601DateParser.parseCal(date.toString());
        } catch (Exception ex) {
            String errmsg = "Invalid date: " + literal;
            __log.error(errmsg, ex);
            throw new FaultException(cexp.getOwner().constants.qnInvalidExpressionValue,
                errmsg);
        }
    }

    /**
     * Evaluate expression and return duration
     *
     * @param cexp cexp 
     * @param context context 
     *
     * @return type
     *
     * @throws FaultException FaultException 
     * @throws EvaluationException EvaluationException 
     */
    public Duration evaluateAsDuration(OExpression cexp,
        EvaluationContext context) throws FaultException, EvaluationException {
        String literal = this.evaluateAsString(cexp, context);

        try {
            return new Duration(literal);
        } catch (Exception ex) {
            String errmsg = "Invalid duration: " + literal;
            __log.error(errmsg, ex);
            throw new FaultException(cexp.getOwner().constants.qnInvalidExpressionValue,
                errmsg);
        }
    }

    /**
     * Evaluate expression and return opaque type
     *
     * @param cexp cexp 
     * @param ctx ctx 
     * @param type type 
     *
     * @return type
     *
     * @throws FaultException FaultException 
     * @throws EvaluationException EvaluationException 
     */
    private Object evaluate(OExpression cexp, EvaluationContext ctx, QName type)
        throws FaultException, EvaluationException {
        try {
            OXQuery10ExpressionBPEL20 oxquery10 = ((OXQuery10ExpressionBPEL20) cexp);

            XQDataSource xqds = new SaxonXQDataSource();
            XQConnection xqconn = xqds.getConnection();

            Configuration configuration = ((SaxonXQConnection) xqconn).getConfiguration();
            //configuration.setAllNodesUntyped(true);
            configuration.setHostLanguage(Configuration.XQUERY);

            XQStaticContext staticEnv = xqconn.getStaticContext();

            NSContext nsContext = oxquery10.namespaceCtx;
            Set<String> prefixes = nsContext.getPrefixes();
            for (String prefix : prefixes) {
                String uri = nsContext.getNamespaceURI(prefix);
                staticEnv.declareNamespace(prefix, uri);
            }

            configuration.setSchemaValidationMode(Validation.SKIP);
            xqconn.setStaticContext(staticEnv);

            // Prepare expression, for starters
            String xquery = oxquery10.xquery.replaceFirst(
                    Constants.XQUERY_FUNCTION_HANDLER_COMPILER,
                    Constants.XQUERY_FUNCTION_HANDLER_RUNTIME);
            XQPreparedExpression exp = xqconn.prepareExpression(xquery);

            JaxpFunctionResolver funcResolver = new JaxpFunctionResolver(ctx,
                    oxquery10);
            JaxpVariableResolver variableResolver = new JaxpVariableResolver(ctx,
                    oxquery10, configuration);
            // Bind external variables to runtime values
            for (QName variable : exp.getAllUnboundExternalVariables()) {
            	// Evaluate referenced variable
                Object value = variableResolver.resolveVariable(variable);
                
                 if (value instanceof Value) {
                     SaxonXQConnection saxonConn = (SaxonXQConnection) xqconn;
                     try {
                         Item item = ((Value) value).asItem();
                         if (item == null) {
                             exp.bindSequence(variable, xqconn.createSequence(Collections.EMPTY_LIST.iterator()));
                         } else {
                             XQItem item2 = new SaxonXQItem(item, saxonConn);
                             exp.bindItem(variable, item2);
                         }
                     } catch (XPathException e) {
                         __log.warn("", e);
                     }
                 } else {
                     
                     if (value instanceof Date) {
                         Date d = (Date) value;
                         value = org.apache.ode.utils.ISO8601DateParser.format(d);
                     }
 
                     // Figure out type of variable
                     XQSequenceType xqType = getItemType(xqconn, value);
 
                     // Saxon doesn't like binding sequences to variables
                     if (value instanceof Node) {
                         // a node is a node-list, but the inverse isn't true.
                         // so, if the value is truly a node, leave it alone.
                     } else if (value instanceof NodeList) {
                         // So extract the first item from the node list
                         NodeList nodeList = (NodeList) value;
                         ArrayList nodeArray = new ArrayList();
                         for (int i = 0; i < nodeList.getLength(); i++) {
                             nodeArray.add(nodeList.item(i));
                         }
                         value = xqconn.createSequence(nodeArray.iterator());
                     }
 
                     // Bind value with external variable
                     if (value != null && xqType != null) {
                         if (value instanceof XQSequence) {
                             exp.bindSequence(variable, (XQSequence) value);
                         } else {
                             if (xqType instanceof XQItemType) {
                                 exp.bindObject(variable, value, (XQItemType) xqType);
                             }
                         }
                     }
                  }
            }

            // Set context node
            Node contextNode = (ctx.getRootNode() == null)
	            ? DOMUtils.newDocument() : ctx.getRootNode();
            contextNode.setUserData(XQuery10BpelFunctions.USER_DATA_KEY_FUNCTION_RESOLVER,
                funcResolver, null);
            exp.bindItem(XQConstants.CONTEXT_ITEM,
                xqconn.createItemFromNode(contextNode, xqconn.createNodeType()));

            // Execute query
            XQResultSequence result = exp.executeQuery();

            // Cast Saxon result to Java result
            Object evalResult = getResultValue(type, result);

            if ((evalResult != null) && __log.isDebugEnabled()) {
                __log.debug("Expression " + cexp.toString() +
                    " generated result " + evalResult + " - type=" +
                    evalResult.getClass().getName());

                if (ctx.getRootNode() != null) {
                    __log.debug("Was using context node " +
                        DOMUtils.domToString(ctx.getRootNode()));
                }
            }

            return evalResult;
        } catch (XQException xqe) {
            // Extracting the real cause from all this wrapping isn't a simple task
            Throwable cause = (xqe.getCause() != null) ? xqe.getCause() : xqe;

            /*
            if (cause instanceof DynamicError) {
                Throwable th = ((DynamicError) cause).getException();

                if (th != null) {
                    cause = th;

                    if (cause.getCause() != null) {
                        cause = cause.getCause();
                    }
                }
            }
            */

            throw new EvaluationException(
                "Error while executing an XQuery expression: " + cause.toString(), cause);
        } catch (WrappedResolverException wre) {
            __log.debug("Could not evaluate expression because of ", wre);
            throw (FaultException) wre.getCause();
        }
    }

    /**
     * Return opaque object embedded in XQuery Item
     *
     * @param item item 
     *
     * @return type
     *
     * @throws XQException XQException 
     */
    private Object getItemValue(XQItem item) throws XQException {
        XQItemType itemType = item.getItemType();
        Object itemValue = null;

        switch (itemType.getBaseType()) {
        case XQItemType.XQBASETYPE_BOOLEAN:
            itemValue = item.getBoolean();

            break;

        case XQItemType.XQBASETYPE_DECIMAL:
            itemValue = item.getDouble();

            break;

        case XQItemType.XQBASETYPE_BYTE:
            itemValue = item.getByte();

            break;

        case XQItemType.XQBASETYPE_FLOAT:
            itemValue = item.getFloat();

            break;

        case XQItemType.XQBASETYPE_INT:
        case XQItemType.XQBASETYPE_INTEGER:
            itemValue = item.getInt();

            break;

        case XQItemType.XQBASETYPE_LONG:
            itemValue = item.getLong();

            break;

        case XQItemType.XQBASETYPE_ANYTYPE:
            itemValue = item.getNode();

            break;

        case XQItemType.XQBASETYPE_ANYURI:
            itemValue = item.getNodeUri();

            break;

        case XQItemType.XQBASETYPE_SHORT:
            itemValue = item.getShort();

            break;

        case XQItemType.XQBASETYPE_STRING:
        case XQItemType.XQBASETYPE_ANYATOMICTYPE:
            itemValue = item.getAtomicValue();

            break;
            
        }

        return itemValue;
    }
    
    /**
     * Return XQuery type corresponding to given value
     *
     * @param xqconn XQuery connection 
     * @param value value 
     *
     * @return type
     *
     * @throws XQException XQException 
     */
    private XQSequenceType getItemType(XQConnection xqconn, Object value) throws XQException {
        XQSequenceType xqType = null;
        if (value instanceof Long) {
            xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_LONG);
        } else if (value instanceof String) {
            xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_STRING);
        } else if (value instanceof Boolean) {
        	xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_BOOLEAN);
        } else if (value instanceof Date) {
        	xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_DATETIME);
        } else if (value instanceof BigDecimal) {
        	xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_DECIMAL);
        } else if (value instanceof Float) {
        	xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_FLOAT);
        } else if (value instanceof URI) {
        	xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_ANYURI);
        } else if (value instanceof QName) {
        	xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_QNAME);
        } else if (value instanceof BigInteger) {
        	xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_INT);
        } else if (value instanceof Integer) {
        	xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_INTEGER);
        } else if (value instanceof Double) {
        	xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_DOUBLE);
        } else if (value instanceof Byte) {
        	xqType = xqconn.createAtomicType(XQItemType.XQBASETYPE_BYTE);
        } else if (value instanceof Node) {
        	xqType = xqconn.createNodeType();
        } else if (value instanceof NodeList || value instanceof XQSequence) {
            XQItemType xqItemType = xqconn.createNodeType();
            xqType = xqconn.createSequenceType(xqItemType, XQSequenceType.OCC_ZERO_OR_MORE);
        }
        return xqType;
    }
    
    /**
     * Cast XQuery sequence into an opaque list 
     *
     * @param type type 
     * @param result result 
     *
     * @return value
     *
     * @throws XQException XQException 
     */
    private Object getResultValue(QName type, XQResultSequence result) throws XQException {
    	Document document = DOMUtils.newDocument();
    	Object resultValue = null;
        if (XPathConstants.NODESET.equals(type)) {
            List list = new ArrayList();

            while (result.next()) {
                Object itemValue = getItemValue(result.getItem());
                if (itemValue instanceof Document) {
                	itemValue = DOMUtils.cloneNode(document, ((Document) itemValue).getDocumentElement());
                } else if (itemValue instanceof Node) {
                	itemValue = DOMUtils.cloneNode(document, (Node) itemValue);
                }

                if (itemValue != null) {
                    list.add(itemValue);
                }
            }

            resultValue = list;
        } else if (XPathConstants.NODE.equals(type)) {
        	XQItem item = null;
    		if (result.count() > 0) {
    			result.first();
    			if (result.isOnItem()) {
    				item = result.getItem();
    			}
    		}
    		if (item != null) {
	            resultValue = getItemValue(item);
	            if (resultValue instanceof Node) {
	            	resultValue = DOMUtils.cloneNode(document, (Node) resultValue); 
	            }
    		}
        } else if (XPathConstants.STRING.equals(type)) {
        	resultValue = result.getSequenceAsString(new Properties());
        } else if (XPathConstants.NUMBER.equals(type)) {
        	resultValue = result.getSequenceAsString(new Properties());
    		resultValue = Integer.parseInt((String) resultValue);
        } else if (XPathConstants.BOOLEAN.equals(type)) {
        	resultValue = result.getSequenceAsString(new Properties());
    		resultValue = Boolean.parseBoolean((String) resultValue);
        }
    	return resultValue;
    }
}
