package hu.swankey.ammo.common.methods;

import hu.swankey.ammo.common.yggdrasil.YUnit;
import hu.swankey.ammo.common.yggdrasil.basics.Container;
import hu.swankey.ammo.common.yggdrasil.basics.Field;
import hu.swankey.ammo.common.yggdrasil.basics.SoftLink;
import hu.swankey.ammo.common.yggdrasil.basics.YObject;
import hu.swankey.ammo.common.yggdrasil.basics.Field.FieldClass;
import hu.swankey.ammo.common.yggdrasil.basics.YObject.YClass;
import hu.swankey.ammo.common.yggdrasil.definition.ElementDefinition;
import hu.swankey.ammo.common.yggdrasil.definition.MethodDefinition;
import hu.swankey.ammo.common.yggdrasil.script.AmmoScriptException;
import hu.swankey.ammo.common.yggdrasil.script.AmmoScriptRuntimeException;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public abstract class Method extends YUnit {
	
	public static final int PARAMETER_INCOMPATIBLE = -1;
	public static final int PARAMETER_NOT_FOUND = -2;

	private YClass returnType;
    private final HashMap<String, YClass> parametertypes = new HashMap<String, YClass>();
    private final HashMap<String, YObject> defaultvalues = new HashMap<String, YObject>();
    public abstract YObject run(Map<String, YObject> parameters, Map<String, YObject> envrionment, YObject thisElement);
    private final Map<String, YClass> unmodifiedSignature = Collections.unmodifiableMap(parametertypes);
    
    private MethodDefinition methodDef;
    
    public Method(String name) {
    	super(name);
    }
    
    public static MethodType yclass(){
    	return MethodType.singleton();
    }
    
    
    public void addParameter(String name, YClass type, YObject defaultvalue){
    	
    	if (name == null)
    		throw new RuntimeException("Parameter name cannot be null.");
    	
    	if (type == null)
    		throw new RuntimeException("Parameter type cannot be null.");
    	
    	parametertypes.put(name, type);
    	
    	if (defaultvalue != null)
    		defaultvalues.put(name, defaultvalue);
    }
    
    public void removeParameters(){
    	parametertypes.clear();
    	defaultvalues.clear();
    }
    
    public Map<String, YClass> getSignature(){
    	return unmodifiedSignature;
    }
    
    
    public void setReturnType(YClass newReturnType){
    	returnType = newReturnType;
    }
    
    public YClass getReturnType(){
    	
//    	if (returnType == null)
//    		return YVoid.yclass();
//    	else
//    		return returnType;
    	
    	return returnType;
    }
    
	@Override
	public void setWrapperElement(ElementDefinition wrapper) {
		this.methodDef = (MethodDefinition)wrapper;
	}    
    
	@Override
	public boolean hasWrapperElement() {
		return methodDef != null;
	}        
    
	@Override
	public MethodDefinition getWrapperElement() {
		if (methodDef == null)
			methodDef = MethodDefinition.yclass().create(this);
		return methodDef;
	}    


//    protected void compute(Complex location, Map<String, Element> params, Map<String, Element> envrionment) throws AmmoScriptException {
//
//    	if (params == null || params.size() == 0)
//    		return;
//
//        for (String paramName: params.keySet())
//        	params.put(paramName, computeParam(params.get(paramName), location, envrionment ));
//    }
    
    protected YObject compute(Map<String, YObject> params, Map<String, YObject> env, YObject thisElement, String key) throws AmmoScriptException {
    	
    	YObject param = params.get(key);
    	if (param == null)
    		param = env.get(key);
    	
    	if (param == null)
    		throw new RuntimeException("Variable not found: " + key);
        
        return compute(param, env, thisElement);
    }
    
    protected YObject compute(YObject element, Map<String, YObject> env, YObject thisElement){
    	
    	if (element instanceof SoftLink) {
    		Container location = (thisElement instanceof Container ? (Container)thisElement : thisElement.getParent());
    		
    		YObject computed = ((SoftLink)element).getFinalTarget(location, env);
    		if (computed == null)
    			throw new AmmoScriptRuntimeException("Broken link: " + element.getPathString()
    					+ "\n value: " + ((SoftLink)element).getValue()
    					+ "\n location: " + thisElement.getPathString()
    					+ "\n envrionment: " + env );
    		return computed;
    	}
        
        if (element instanceof MethodDefinition)
        	return ((MethodDefinition) element).getWrapped().run(null, env, thisElement);

        return element;
    }


    protected void checkParams(Map<String, YObject> parameters) throws AmmoScriptException {

        if (parameters == null)
            return;

//        if (paramTypes == null || parameters.size() > paramTypes.size())
//            throw new AmmoScriptException("Too many parameters for function \"" + getName() + "\":" + details(parameters));
//
//        if (parameters == null || parameters.size() < paramTypes.size())
//            throw new AmmoScriptException("Not enought parameters for function \"" + getName() + "\":" + details(parameters));

//        for (int i = 0; i < parameters.size(); i++) {
//            IElement element = parameters.get(i);
//            if (!paramTypes.get(i).isTypeOf(element))
//                throw new AmmoScriptException("Type mismatch in function \"" + getName() + "\"" + details(parameters));
//        }
        
        for(String paramName: parametertypes.keySet() ){
        	
        	YObject value = parameters.get(paramName);
        	if (value == null) {
        		
        		if (!defaultvalues.containsKey(paramName))
        			throw new AmmoScriptException("Missing parameter: '" + paramName + "'");
        		
        	} else {
        		
        		YClass wantedType = parametertypes.get(paramName);        		
        		if (!wantedType.isTypeOf(value))
        			throw new AmmoScriptException("Type mismatch at parameter '" + paramName + "': "
        					+ wantedType.getName() +" expected, but " + value.getYClass().getName() + " found.");
        	}
        		
        }
    }

    
    public int calculateDistance( Map<String, YClass> signature ){
    	

    	
    	int distSum = 0;
    	
		for (String paramName: parametertypes.keySet()) {
			
			int dist = PARAMETER_NOT_FOUND;
			
			if (signature.containsKey(paramName)) {
				
				YClass expectedType = parametertypes.get(paramName);
				YClass foundedType = signature.get(paramName);
				
				dist = foundedType.distanceFromSuperior( expectedType );				
			} 
			
			if (dist == PARAMETER_INCOMPATIBLE
					|| (dist == PARAMETER_NOT_FOUND && !defaultvalues.containsKey(paramName) ))
				return PARAMETER_INCOMPATIBLE;
			
			distSum += dist;
		}
		
		return distSum;
		
    }
    
    public String toString(){
    	String str = getName() + "(";
	
    	for(String paramName: parametertypes.keySet()) {
    		str += paramName + ":" + parametertypes.get(paramName).getName();
    		
    		if (defaultvalues.containsKey(paramName))
    			str += "(default: " + defaultvalues.get(paramName) + ")";
    			
    		str += " ";
    	}
	
    	str += ");";
	
    	return str;
    }
    
    public static class MethodType extends FieldClass {

        private static final String CLASS_NAME = "Method";
        private static final String DEFAULT_VALUE = "";
        private static MethodType SINGLETON; 

    	private static MethodType singleton(){
    		if (SINGLETON == null) {
    			SINGLETON = new MethodType(CLASS_NAME);
    			SINGLETON.addSuperior(Field.yclass());
    		}
    		return SINGLETON;
    	}

        protected MethodType(String name) {
            super(name);
        }



        @Override
        public Object getDefaultValue() {
            return DEFAULT_VALUE;
        }



        /** Get the name of the type */
        @Override
        public String getName() {
            return CLASS_NAME;
        }

    }
    
}
