package railo.runtime.interpreter.ref.func;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import railo.commons.lang.CFTypes;
import railo.commons.lang.StringUtil;
import railo.runtime.PageContext;
import railo.runtime.exp.ExpressionException;
import railo.runtime.exp.PageException;
import railo.runtime.functions.BIF;
import railo.runtime.interpreter.InterpreterException;
import railo.runtime.interpreter.ref.Ref;
import railo.runtime.interpreter.ref.RefSupport;
import railo.runtime.interpreter.ref.cast.Casting;
import railo.runtime.interpreter.ref.literal.LFunctionValue;
import railo.runtime.interpreter.ref.util.RefUtil;
import railo.runtime.op.Caster;
import railo.runtime.op.Constants;
import railo.runtime.type.FunctionValue;
import railo.runtime.type.FunctionValueImpl;
import railo.runtime.type.util.ArrayUtil;
import railo.runtime.type.util.UDFUtil;
import railo.transformer.library.function.FunctionLibFunction;
import railo.transformer.library.function.FunctionLibFunctionArg;

/**
 * a built In Function call
 *
 *
 */
public final class BIFCall extends RefSupport implements Ref {
		
	private Ref[] refArgs;
    private FunctionLibFunction flf;
	private Object obj;


	/**
	 * constructor of the class
	 * @param pc
	 * @param flf 
	 * @param refArgs 
	 */
	public BIFCall(FunctionLibFunction flf,Ref[] refArgs) {
		this.flf=flf;
		this.refArgs=refArgs;
	}
	public BIFCall(Object obj,FunctionLibFunction flf,Ref[] refArgs) {
		this.obj=obj;
		this.flf=flf;
		this.refArgs=refArgs;
	}
	
	@Override
    public Object getValue(PageContext pc) throws PageException {
        
        Object[] arguments = null;
        
        
        if(isDynamic()){
        	arguments = RefUtil.getValue(pc,refArgs);
        	if(flf.hasDefaultValues()){
        		List<Object> tmp=new ArrayList<Object>();
        		ArrayList<FunctionLibFunctionArg> args = flf.getArg();
        		Iterator<FunctionLibFunctionArg> it = args.iterator();
        		FunctionLibFunctionArg arg;
        		while(it.hasNext()){
        			arg=it.next();
        			if(arg.getDefaultValue()!=null)
        				tmp.add(new FunctionValueImpl(arg.getName(),arg.getDefaultValue()));
        		}
        		for(int i=0;i<arguments.length;i++){
        			tmp.add(arguments[i]);
        		}
        		arguments=tmp.toArray();
        	}
        	arguments=new Object[]{arguments};
        }
        else {
        	if(isNamed(pc,refArgs)){
        		FunctionValue[] fvalues=getFunctionValues(pc,refArgs);
        		String[] names = getNames(fvalues);
        		
        		ArrayList<FunctionLibFunctionArg> list = flf.getArg();
				Iterator<FunctionLibFunctionArg> it = list.iterator();
				arguments=new Object[list.size()];
				
				
				FunctionLibFunctionArg flfa;
				int index=0;
				VT vt;
				while(it.hasNext()) {
					flfa =it.next();
					vt = getMatchingValueAndType(flfa,fvalues,names);
					if(vt.index!=-1) 
						names[vt.index]=null;
					arguments[index++]=new Casting( vt.type, CFTypes.toShort(vt.type, false, CFTypes.TYPE_UNKNOW), vt.value).getValue(pc);	
				}
				
				for(int y=0;y<names.length;y++){
					if(names[y]!=null) {
						ExpressionException ee = new InterpreterException("argument ["+names[y]+"] is not allowed for function ["+flf.getName()+"]");
						UDFUtil.addFunctionDoc(ee, flf);
						throw ee;
					}
				}
        		
        	}
        	else {
        		arguments = RefUtil.getValue(pc,refArgs);
        	}
        }
        BIF bif=flf.getBIF();
        
        if(flf.getMemberChaining() && obj!=null) {
        	bif.invoke(pc, arguments);
        	return obj;
        }
        return Caster.castTo(pc,flf.getReturnTypeAsString(),bif.invoke(pc, arguments),false);
	}
	


    private VT getMatchingValueAndType(FunctionLibFunctionArg flfa, FunctionValue[] fvalues, String[] names) throws ExpressionException {
    	String flfan=flfa.getName();
		
		// first search if a argument match
		for(int i=0;i<names.length;i++){
			if(names[i]!=null && names[i].equalsIgnoreCase(flfan)) {
				return new VT(fvalues[i].getValue(),flfa.getTypeAsString(),i);
			}
		}
		
		// then check if a alias match
		String alias=flfa.getAlias();
		if(!StringUtil.isEmpty(alias)) {
			for(int i=0;i<names.length;i++){
				if(names[i]!=null && railo.runtime.type.util.ListUtil.listFindNoCase(alias, names[i])!=-1){
					return new VT(fvalues[i].getValue(),flfa.getTypeAsString(),i);
				}
			}
		}
		
		// if not required return the default value
		if(!flfa.getRequired()) {
			String defaultValue = flfa.getDefaultValue();
			String type=flfa.getTypeAsString().toLowerCase();
			
			if(defaultValue==null) {
				if(type.equals("boolean") || type.equals("bool")) 
					return new VT(Boolean.FALSE,type,-1);
				if(type.equals("number") || type.equals("numeric") || type.equals("double")) 
					return new VT(Constants.DOUBLE_ZERO,type,-1);
				return new VT(null,type,-1);
			}
			return new VT(defaultValue,type,-1);
			
		}
		ExpressionException ee = new InterpreterException("missing required argument ["+flfan+"] for function ["+flfa.getFunction().getName()+"]");
		UDFUtil.addFunctionDoc(ee, flfa.getFunction());
		throw ee;
	}

	private String[] getNames(FunctionValue[] fvalues) {
    	String[] names=new String[fvalues.length];
    	for(int i=0;i<fvalues.length;i++){
			names[i]=fvalues[i].getNameAsString();
		}
		return names;
	}

	private FunctionValue[] getFunctionValues(PageContext pc,Ref[] refArgs) throws PageException {
		FunctionValue[] fvalues=new FunctionValue[refArgs.length];
    	for(int i=0;i<refArgs.length;i++){
			fvalues[i]=(FunctionValue) ((LFunctionValue) ((Casting)refArgs[i]).getRef()).getValue(pc);
		}
		return fvalues;
	}

	private boolean isNamed(PageContext pc,Ref[] refArgs) throws PageException {
    	if(ArrayUtil.isEmpty(refArgs)) return false;
		Casting cast;
		int count=0;
		for(int i=0;i<refArgs.length;i++){
			if(refArgs[i] instanceof Casting){
				cast=(Casting) refArgs[i];
				if(cast.getRef() instanceof LFunctionValue && ((LFunctionValue)cast.getRef()).getValue(pc) instanceof FunctionValue) {
					count++;
				}
			}
		}
		if(count!=0 && count!=refArgs.length){
			ExpressionException ee = new InterpreterException("invalid argument for function "+flf.getName()+", you can not mix named and unnamed arguments");
			UDFUtil.addFunctionDoc(ee, flf);
			throw ee;
		}
		return count!=0;
	}

	private boolean isDynamic() {
        return flf.getArgType()==FunctionLibFunction.ARG_DYNAMIC;
    }
    
	@Override
    public String getTypeName() {
		return "built in function";
	}

}

class VT{

	Object value;
	String type;
	int index;

	public VT(Object value, String type, int index) {
		this.value=value;
		this.type=type;
		this.index=index;
	}
	
}
