/*
 * 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.xpath.functions.hof;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

import javax.xml.transform.SourceLocator;

import org.apache.xalan.res.XSLMessages;
import org.apache.xalan.templates.ElemFunction;
import org.apache.xalan.templates.ElemTemplateElement;
import org.apache.xalan.templates.XMLNSDecl;
import org.apache.xalan.transformer.TransformerImpl;
import org.apache.xalan.xslt.util.XslTransformEvaluationHelper;
import org.apache.xml.dtm.DTM;
import org.apache.xml.dtm.DTMCursorIterator;
import org.apache.xml.dtm.DTMManager;
import org.apache.xml.utils.QName;
import org.apache.xpath.XPath;
import org.apache.xpath.XPathStaticContext;
import org.apache.xpath.XPathContext;
import org.apache.xpath.compiler.FunctionTable;
import org.apache.xpath.compiler.Keywords;
import org.apache.xpath.composite.XPathNamedFunctionReference;
import org.apache.xpath.functions.Function;
import org.apache.xpath.functions.FunctionMultiArgs;
import org.apache.xpath.functions.WrongNumberArgsException;
import org.apache.xpath.objects.InlineFunctionParameter;
import org.apache.xpath.objects.ResultSequence;
import org.apache.xpath.objects.XMLNodeCursorImpl;
import org.apache.xpath.objects.XObject;
import org.apache.xpath.objects.XPathInlineFunction;
import org.apache.xpath.operations.Variable;
import org.apache.xpath.patterns.NodeTest;
import org.apache.xpath.res.XPATHErrorResources;

/**
 * Implementation of XPath 3.1 function fn:sort.
 * 
 * @author Mukul Gandhi <mukulg@apache.org>
 * 
 * @xsl.usage advanced
 */
public class FuncSort extends FunctionMultiArgs
{

    private static final long serialVersionUID = -7037457505611880167L;
    
    /**
     * Class constructor.
     */
    public FuncSort() {
 	   m_defined_arity = new Short[] { 1, 2, 3 };
    }
    
    /**
     * The number of arguments passed to the fn:sort function 
     * call.
     */
    private int numOfArgs = 0;

    /**
     * Evaluate the function. The function must return a valid object.
     * 
     * @param xctxt The current execution context.
     * @return A valid XObject.
     *
     * @throws javax.xml.transform.TransformerException
     */
    public XObject execute(XPathContext xctxt) throws javax.xml.transform.TransformerException
    {
    	
        XObject result = null;
        
        SourceLocator srcLocator = xctxt.getSAXLocator();
        
        String collationUri = xctxt.getDefaultCollation();
        
        if ((numOfArgs == 2) || (numOfArgs == 3)) {           
           XObject XObjArg1 = m_arg1.execute(xctxt);
            
           if ((XObjArg1 instanceof ResultSequence) && 
                                                (((ResultSequence)XObjArg1).size() == 0)) {
              collationUri = xctxt.getDefaultCollation();   
           }
           else {
              // The collation uri, is explicitly provided during the function call fn:sort               
              collationUri = XslTransformEvaluationHelper.getStrVal(XObjArg1); 
           }
        }
        
        XObject arg0Obj = m_arg0.execute(xctxt);              
        
        List<InpSeqItemWithSortKeyValue> inpSeqItemWithSortKeyValueList = new ArrayList<InpSeqItemWithSortKeyValue>();
        
        ResultSequence sortedResultSeq = new ResultSequence();
        
        TransformerImpl transformerImpl = null;
        
        ElemFunction elemFunction = null;
        
        if (arg0Obj instanceof XMLNodeCursorImpl) {
           DTMManager dtmMgr = (DTMManager)xctxt;
            
           XMLNodeCursorImpl xNodeSet = (XMLNodeCursorImpl)arg0Obj;           
           DTMCursorIterator sourceNodes = xNodeSet.iter();                      
           
           Map<QName, XObject> inlineFunctionVarMap = xctxt.getXPathVarMap();
           
           ElemTemplateElement elemTemplateElement = (ElemTemplateElement)xctxt.getNamespaceContext();
           List<XMLNSDecl> prefixTable = null;
           if (elemTemplateElement != null) {
               prefixTable = (List<XMLNSDecl>)elemTemplateElement.getPrefixTable();
           }

           int nextNode;         
           
           while ((nextNode = sourceNodes.nextNode()) != DTM.NULL) {
              XMLNodeCursorImpl xNodeSetItem = new XMLNodeCursorImpl(nextNode, dtmMgr);
              
              if (numOfArgs == 3) {
            	  if (m_arg2 instanceof Variable) {
            		  XObject arg2obj = ((Variable)m_arg2).execute(xctxt);
            		  
            		  if (arg2obj instanceof XPathInlineFunction) {
            			  XPathInlineFunction arg2InlineFunc = (XPathInlineFunction)arg2obj;

                		  List<InlineFunctionParameter> funcParamList = arg2InlineFunc.getFuncParamList();
                		  inlineFunctionVarMap.put(new QName((funcParamList.get(0)).getParamName()), xNodeSetItem);

                		  String sortKeyXPathStr = arg2InlineFunc.getFuncBodyXPathExprStr();

                		  if (prefixTable != null) {
                			  sortKeyXPathStr = XslTransformEvaluationHelper.replaceNsUrisWithPrefixesOnXPathStr(sortKeyXPathStr, 
                					                                                                             prefixTable);
                		  }

                		  XPath sortKeyXPathExpr = new XPath(sortKeyXPathStr, srcLocator, xctxt.getNamespaceContext(), 
                				                             XPath.SELECT, null);
                		  XObject sortKeyVal = sortKeyXPathExpr.execute(xctxt, nextNode, xctxt.getNamespaceContext());

                		  // Reset the function item argument reference value
                		  inlineFunctionVarMap.put(new QName((funcParamList.get(0)).getParamName()), null);

                		  inpSeqItemWithSortKeyValueList.add(new InpSeqItemWithSortKeyValue(xNodeSetItem, sortKeyVal)); 
            		  }
            		  else if (arg2obj instanceof XMLNodeCursorImpl) {            			              			  
            			  if (elemFunction == null) {
            				 XMLNodeCursorImpl xmlNodeCursorImpl = (XMLNodeCursorImpl)arg2obj;
                			 DTMCursorIterator dtmIter = xmlNodeCursorImpl.getContainedIter();
                			 
            			     transformerImpl = getTransformerImplFromXPathExpression((NodeTest)dtmIter);
            			     elemFunction = XslTransformEvaluationHelper.getElemFunctionFromNodeTestExpression(
            			    		                                                                    (NodeTest)dtmIter, transformerImpl, srcLocator);
            			  }                      	  
                      	  
                      	  ResultSequence argSeq = new ResultSequence(); 
                    	  argSeq.add(xNodeSetItem);
                    	   
                    	  XObject sortKeyVal = elemFunction.evaluateXslFunction(transformerImpl, argSeq);
                    	  inpSeqItemWithSortKeyValueList.add(new InpSeqItemWithSortKeyValue(xNodeSetItem, sortKeyVal));
            		  }
            	  }
            	  else if (m_arg2 instanceof XPathInlineFunction) {
            		  XPathInlineFunction arg2InlineFunc = (XPathInlineFunction)m_arg2;

            		  List<InlineFunctionParameter> funcParamList = arg2InlineFunc.getFuncParamList();
            		  inlineFunctionVarMap.put(new QName((funcParamList.get(0)).getParamName()), xNodeSetItem);

            		  String sortKeyXPathStr = arg2InlineFunc.getFuncBodyXPathExprStr();

            		  if (prefixTable != null) {
            			  sortKeyXPathStr = XslTransformEvaluationHelper.replaceNsUrisWithPrefixesOnXPathStr(sortKeyXPathStr, 
            					                                                                             prefixTable);
            		  }

            		  XPath sortKeyXPathExpr = new XPath(sortKeyXPathStr, srcLocator, xctxt.getNamespaceContext(), 
            				                             XPath.SELECT, null);
            		  XObject sortKeyVal = sortKeyXPathExpr.execute(xctxt, nextNode, xctxt.getNamespaceContext());

            		  // Reset the function item argument reference value
            		  inlineFunctionVarMap.put(new QName((funcParamList.get(0)).getParamName()), null);

            		  inpSeqItemWithSortKeyValueList.add(new InpSeqItemWithSortKeyValue(xNodeSetItem, sortKeyVal));
            	  }
            	  else if (m_arg2 instanceof XPathNamedFunctionReference) {
             		  XPathNamedFunctionReference namedFuncRef = (XPathNamedFunctionReference)m_arg2;
                      String funcNamespace = namedFuncRef.getFuncNamespace();
                      String funcLocalName = namedFuncRef.getFuncName();
                      int funcArity = 0;           
                	  if ((XPathStaticContext.XPATH_BUILT_IN_FUNCS_NS_URI).equals(funcNamespace) && 
                			  															        (Keywords.FUNC_CONCAT_STRING).equals(funcLocalName)) {
                		  funcArity = namedFuncRef.getConcatArity();
                	  }
                	  else {
                		  funcArity = namedFuncRef.getArity(); 
                	  }
                      
                      FunctionTable funcTable = xctxt.getFunctionTable();
                      
                      Object funcIdObj = null;
                      if (XPathStaticContext.XPATH_BUILT_IN_FUNCS_NS_URI.equals(funcNamespace)) {
                         funcIdObj = funcTable.getFunctionId(funcLocalName);
                      }
                      else if (XPathStaticContext.XPATH_BUILT_IN_MATH_FUNCS_NS_URI.equals(funcNamespace)) {
                         funcIdObj = funcTable.getFunctionIdForXPathBuiltinMathFuncs(funcLocalName);
                      }
                      else if (XPathStaticContext.XPATH_BUILT_IN_MAP_FUNCS_NS_URI.equals(funcNamespace)) {
                         funcIdObj = funcTable.getFunctionIdForXPathBuiltinMapFuncs(funcLocalName);
                      }
                      else if (XPathStaticContext.XPATH_BUILT_IN_ARRAY_FUNCS_NS_URI.equals(funcNamespace)) {
                         funcIdObj = funcTable.getFunctionIdForXPathBuiltinArrayFuncs(funcLocalName);
                      }
                      
                      if (funcIdObj != null) {
                    	  String funcIdStr = funcIdObj.toString();
                    	  Function function = funcTable.getFunction(Integer.valueOf(funcIdStr));
                    	  try {
                    		  function.setArg(xNodeSetItem, 0);
                    	  } catch (WrongNumberArgsException ex) {
                    		  String expandedFuncName = "{" + funcNamespace + ":" + funcLocalName + "}#" + funcArity;  
                    		  throw new javax.xml.transform.TransformerException("XPTY0004 : Wrong number of arguments provided, "
                    				                                          + "during function call " + expandedFuncName + ".", srcLocator);
                    	  }
                    	  XObject sortKeyVal = function.execute(xctxt);
                    	  inpSeqItemWithSortKeyValueList.add(new InpSeqItemWithSortKeyValue(xNodeSetItem, sortKeyVal));               
                      } 
             	   }            	  
            	   else if (m_arg2 instanceof NodeTest) {
            		   transformerImpl = getTransformerImplFromXPathExpression(m_arg2);
                       
                   	   elemFunction = XslTransformEvaluationHelper.getElemFunctionFromNodeTestExpression(
                   			                                                                      (NodeTest)m_arg2, transformerImpl, srcLocator);
                   	   
                   	   ResultSequence argSeq = new ResultSequence(); 
                   	   argSeq.add(xNodeSetItem);
                   	   
                   	   XObject sortKeyVal = elemFunction.evaluateXslFunction(transformerImpl, argSeq);
                   	   inpSeqItemWithSortKeyValueList.add(new InpSeqItemWithSortKeyValue(xNodeSetItem, sortKeyVal));
            	   }
               }
               else if ((numOfArgs == 1) || (numOfArgs == 2)) {
                  inpSeqItemWithSortKeyValueList.add(new InpSeqItemWithSortKeyValue(xNodeSetItem, xNodeSetItem));
               }
           }
           
           FnSortComparator fnSortComparator = new FnSortComparator(collationUri, srcLocator);
           
           inpSeqItemWithSortKeyValueList.sort(fnSortComparator);
           
           if (fnSortComparator.getErrMessage() != null) {
              throw new javax.xml.transform.TransformerException(fnSortComparator.getErrMessage(), srcLocator); 
           }
           
           for (int idx = 0; idx < inpSeqItemWithSortKeyValueList.size(); idx++) {
              InpSeqItemWithSortKeyValue inpSeqItemWithSortKeyValue = inpSeqItemWithSortKeyValueList.get(idx);
              sortedResultSeq.add(inpSeqItemWithSortKeyValue.getInpSeqItem()); 
           }
           
        } 
        else if (arg0Obj instanceof ResultSequence) {
           ResultSequence arg0ResultSeq = (ResultSequence)arg0Obj;
           
           Map<QName, XObject> inlineFunctionVarMap = xctxt.getXPathVarMap();
           
           ElemTemplateElement elemTemplateElement = (ElemTemplateElement)xctxt.getNamespaceContext();
           List<XMLNSDecl> prefixTable = null;
           if (elemTemplateElement != null) {
               prefixTable = (List<XMLNSDecl>)elemTemplateElement.getPrefixTable();
           }
           
           for (int idx = 0; idx < arg0ResultSeq.size(); idx++) {
              XObject inputSeqItem = arg0ResultSeq.item(idx);
              if (numOfArgs == 3) {
            	  if (m_arg2 instanceof XPathInlineFunction) {
	                 XPathInlineFunction arg2InlineFunc = (XPathInlineFunction)m_arg2;
	                 
	                 List<InlineFunctionParameter> funcParamList = arg2InlineFunc.getFuncParamList();
	                 inlineFunctionVarMap.put(new QName((funcParamList.get(0)).getParamName()), inputSeqItem);
	                 
	                 String sortKeyXPathStr = arg2InlineFunc.getFuncBodyXPathExprStr();
	                 
	                 if (prefixTable != null) {
	                     sortKeyXPathStr = XslTransformEvaluationHelper.replaceNsUrisWithPrefixesOnXPathStr(sortKeyXPathStr, 
	                                                                                                         prefixTable);
	                 }
	                 
	                 XPath sortKeyXPathExpr = new XPath(sortKeyXPathStr, srcLocator, xctxt.getNamespaceContext(), 
	                                                                                                     XPath.SELECT, null);
	                 XObject sortKeyVal = sortKeyXPathExpr.execute(xctxt, xctxt.getContextNode(), xctxt.getNamespaceContext());
	                 
	                 // Reset the function item argument reference value
	                 inlineFunctionVarMap.put(new QName((funcParamList.get(0)).getParamName()), null);
	                 
	                 inpSeqItemWithSortKeyValueList.add(new InpSeqItemWithSortKeyValue(inputSeqItem, sortKeyVal));
            	 }
            	 else if (m_arg2 instanceof XPathNamedFunctionReference) {
            		 XPathNamedFunctionReference namedFuncRef = (XPathNamedFunctionReference)m_arg2;
                     String funcNamespace = namedFuncRef.getFuncNamespace();
                     String funcLocalName = namedFuncRef.getFuncName();
                     int funcArity = 0;           
                     if ((XPathStaticContext.XPATH_BUILT_IN_FUNCS_NS_URI).equals(funcNamespace) && 
                    		                                                                   (Keywords.FUNC_CONCAT_STRING).equals(funcLocalName)) {
                    	 funcArity = namedFuncRef.getConcatArity();
                     }
                     else {
                    	 funcArity = namedFuncRef.getArity(); 
                     }
                     
                     FunctionTable funcTable = xctxt.getFunctionTable();
                     
                     Object funcIdObj = null;
                     if (XPathStaticContext.XPATH_BUILT_IN_FUNCS_NS_URI.equals(funcNamespace)) {
                        funcIdObj = funcTable.getFunctionId(funcLocalName);
                     }
                     else if (XPathStaticContext.XPATH_BUILT_IN_MATH_FUNCS_NS_URI.equals(funcNamespace)) {
                        funcIdObj = funcTable.getFunctionIdForXPathBuiltinMathFuncs(funcLocalName);
                     }
                     else if (XPathStaticContext.XPATH_BUILT_IN_MAP_FUNCS_NS_URI.equals(funcNamespace)) {
                        funcIdObj = funcTable.getFunctionIdForXPathBuiltinMapFuncs(funcLocalName);
                     }
                     else if (XPathStaticContext.XPATH_BUILT_IN_ARRAY_FUNCS_NS_URI.equals(funcNamespace)) {
                        funcIdObj = funcTable.getFunctionIdForXPathBuiltinArrayFuncs(funcLocalName);
                     }
                     
                     if (funcIdObj != null) {
                        String funcIdStr = funcIdObj.toString();
                        Function function = funcTable.getFunction(Integer.valueOf(funcIdStr));
            			try {
							function.setArg(inputSeqItem, 0);
						} catch (WrongNumberArgsException ex) {
							String expandedFuncName = "{" + funcNamespace + ":" + funcLocalName + "}#" + funcArity;  
 							throw new javax.xml.transform.TransformerException("XPTY0004 : Wrong number of arguments provided, "
 									                                           + "during function call " + expandedFuncName + ".", srcLocator);
						}
            			XObject sortKeyVal = function.execute(xctxt);
            			inpSeqItemWithSortKeyValueList.add(new InpSeqItemWithSortKeyValue(inputSeqItem, sortKeyVal));               
                     } 
            	 }
            	 else if (m_arg2 instanceof NodeTest) {
            		 transformerImpl = getTransformerImplFromXPathExpression(m_arg2);

            		 elemFunction = XslTransformEvaluationHelper.getElemFunctionFromNodeTestExpression(
            				                                                                    (NodeTest)m_arg2, transformerImpl, srcLocator);

            		 ResultSequence argSeq = new ResultSequence(); 
            		 argSeq.add(inputSeqItem);

            		 XObject sortKeyVal = elemFunction.evaluateXslFunction(transformerImpl, argSeq);
            		 inpSeqItemWithSortKeyValueList.add(new InpSeqItemWithSortKeyValue(inputSeqItem, sortKeyVal));
          	    }            	
              }
              else if ((numOfArgs == 2) || (numOfArgs == 1)) {
                 inpSeqItemWithSortKeyValueList.add(new InpSeqItemWithSortKeyValue(inputSeqItem, inputSeqItem));  
              }
           }
           
           FnSortComparator fnSortComparator = new FnSortComparator(collationUri, srcLocator);
           
           inpSeqItemWithSortKeyValueList.sort(fnSortComparator);
           
           if (fnSortComparator.getErrMessage() != null) {
              throw new javax.xml.transform.TransformerException(fnSortComparator.getErrMessage(), srcLocator); 
           }
               
           for (int idx = 0; idx < inpSeqItemWithSortKeyValueList.size(); idx++) {
              InpSeqItemWithSortKeyValue inpSeqItemWithSortKeyValue = inpSeqItemWithSortKeyValueList.get(idx);
              sortedResultSeq.add(inpSeqItemWithSortKeyValue.getInpSeqItem()); 
           }           
        }
        else {           
           sortedResultSeq.add(arg0Obj); 
        }
        
        result = sortedResultSeq; 
        
        return result;
    }
    
    /**
     * Check that the number of arguments passed to this function is correct.
     *
     * @param argNum The number of arguments that is being passed to the function.
     *
     * @throws WrongNumberArgsException
     */
    public void checkNumberArgs(int argNum) throws WrongNumberArgsException
    {
       if (!(argNum > 0 && argNum <= 3)) {
          reportWrongNumberArgs();
       }
       else {
          numOfArgs = argNum;   
       }
    }

    /**
     * Constructs and throws a WrongNumberArgException with the appropriate
     * message for this function object.
     *
     * @throws WrongNumberArgsException
     */
    protected void reportWrongNumberArgs() throws WrongNumberArgsException {
        throw new WrongNumberArgsException(XSLMessages.createXPATHMessage(
                                                                     XPATHErrorResources.ER_ONE_TWO_OR_THREE, 
                                                                     null));
    }
    
    /*
     * An object of this class, contains a pair of an input 
     * sequence xdm item and its corresponding sort key value.
     */
    class InpSeqItemWithSortKeyValue {
        
       private XObject inpSeqItem;
       
       private XObject inpSeqItemSortKeyVal;
       
       public InpSeqItemWithSortKeyValue(XObject inpSeqItem, 
                                                      XObject inpSeqItemSortKeyVal) {
          this.inpSeqItem = inpSeqItem;
          this.inpSeqItemSortKeyVal = inpSeqItemSortKeyVal; 
       }

       public XObject getInpSeqItem() {
          return inpSeqItem;
       }

       public void setInpSeqItem(XObject inpSeqItem) {
          this.inpSeqItem = inpSeqItem;
       }

       public XObject getInpSeqItemSortKeyVal() {
          return inpSeqItemSortKeyVal;
       }

       public void setInpSeqItemSortKeyVal(XObject inpSeqItemSortKeyVal) {
          this.inpSeqItemSortKeyVal = inpSeqItemSortKeyVal;
       }

    }
    
    /*
     * A class definition implementing sort comparator
     * for fn:sort function call.
     */
    private class FnSortComparator implements Comparator {
        
        private String collationUri;
        
        private SourceLocator srcLocator;
        
        private String errMesg = null;
        
        /*
         * The class constructor.
         */
        public FnSortComparator(String collationUri, SourceLocator srcLocator) {
           this.collationUri = collationUri; 
           this.srcLocator = srcLocator;    
        }

        @Override
        public int compare(Object obj1, Object obj2) {           
           int comparisonResult = 0;
           
           XObject sortKeyVal1 = ((InpSeqItemWithSortKeyValue)obj1).getInpSeqItemSortKeyVal();
           XObject sortKeyVal2 = ((InpSeqItemWithSortKeyValue)obj2).getInpSeqItemSortKeyVal();
           
           try {
              if (sortKeyVal1.vcLessThan(sortKeyVal2, null, collationUri, true)) {
                 comparisonResult = -1;  
              }
              else if (sortKeyVal1.vcGreaterThan(sortKeyVal2, null, collationUri, true)) {
                 comparisonResult = 1; 
              }
           }
           catch (javax.xml.transform.TransformerException ex) {
              errMesg = ex.getMessage();
           }
           
           return comparisonResult;           
        }
        
        public String getErrMessage() {
           return errMesg; 
        }
    }
    
}
