package org.etsi.ttcn.extfunc.core;



import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import org.elvior.ttcn.tritci.TciProvider;
import org.etsi.common.ByteHelper;
import org.etsi.common.plugins.IXfPlugin;
import org.etsi.ttcn.codec.core.TciCDWrapperFactory;
import org.etsi.ttcn.tci.BooleanValue;
import org.etsi.ttcn.tci.CharstringValue;
import org.etsi.ttcn.tci.FloatValue;
import org.etsi.ttcn.tci.IntegerValue;
import org.etsi.ttcn.tci.OctetstringValue;
import org.etsi.ttcn.tci.Value;
import org.etsi.ttcn.tool.elvior.codec.TciCDWrapper;
import org.etsi.ttcn.tri.TriFunctionId;
import org.etsi.ttcn.tri.TriParameter;
import org.etsi.ttcn.tri.TriParameterList;
import org.etsi.ttcn.tri.TriParameterPassingMode;

public abstract class ExternalFunction implements IXfPlugin, InvocationHandler {

    public ExternalFunction() {
    	TciCDWrapperFactory.getInstance().setImpl(new TciCDWrapper());
    	
    	// Find available methods
    	Method[] methods = this.getClass().getDeclaredMethods();
    	for(int i=0; i < methods.length; i++) {
    		this.methods.put(methods[i].getName(), methods[i]);
    	}
	}

	private Map<String, Method> methods = new HashMap<String, Method>();
	
	@Override
	public void initPlugin() {
		XfFactory.getInstance().registerXf(this.getPluginName(), this.getClass());
	}

    private Value simpleDecode(Class<?> c, byte[] encoded) {
    	switch(c.getSimpleName()) {
    	case "IntegerValue":
    		IntegerValue iv = (IntegerValue)TciProvider.getInstance().getTciCDRequired().getInteger().newInstance();
            iv.setInteger(ByteHelper.byteArrayToInt(encoded));
            return iv;
    	case "FloatValue": 
    		FloatValue fv = (FloatValue)TciProvider.getInstance().getTciCDRequired().getFloat().newInstance();
            fv.setFloat(ByteHelper.byteArrayToInt(encoded)); // FIXME Should be byteArrayToFloat???
    	case "OctetstringValue":
	    	OctetstringValue ov = (OctetstringValue)TciProvider.getInstance().getTciCDRequired().getOctetstring().newInstance();
	        ov.setLength(encoded.length);
	        for(int i=0; i < encoded.length; i++) {
	            ov.setOctet(i, encoded[i]);
	        }
    	case "CharstringValue":
    		CharstringValue cv = TciCDWrapperFactory.getTciCDInstance().getCharstringValue();
            cv.setString(new String(encoded));
    	case "BooleanValue":
    		assert(false); // TODO
    		return null;
    	case "RecordOfValue": //fx_isValidPolygonalRegion
    	case "RecordValue": //fx_isLocationInsideCircularRegion
    		
	    }

    	return null;    	
    }

    private byte[] simpleEncode(Class<?> c, Value v) {
    	if(IntegerValue.class.isInstance(v)) {
    		IntegerValue res = (IntegerValue)v;    		
    		return ByteHelper.intToByteArray(res.getInteger(), Integer.SIZE / Byte.SIZE);
    	}

    	if(FloatValue.class.isInstance(v)) {
    		FloatValue res = (FloatValue)v;    		
    		return ByteHelper.floatToByteArray(res.getFloat());
    	}
    	
    	if(OctetstringValue.class.isInstance(v)) {
    		OctetstringValue res = (OctetstringValue)v;
            byte[] encoded = new byte[res.getLength()];
            for(int i=0; i < encoded.length; i++) {
            	encoded[i] = (byte) res.getOctet(i);
            } 
            return encoded; 
    	}
    	
    	if(CharstringValue.class.isInstance(v)) {
    		assert(false); // TODO
    		return null;
    	}
    	
    	if(BooleanValue.class.isInstance(v)) {
    		BooleanValue res = (BooleanValue)v; 
            return new byte[] { (byte)((res.getBoolean() == true) ? 0x01 : 0x00) };
    	}    	
    	
    	return null;    	
    }
    
	public void exec(TriFunctionId functionId, TriParameterList parameterList, TriParameter returnValue) {
		
    	String functionName = functionId.getFunctionName();
    	String[] parts = functionName.split("\\.");
    	functionName = parts[parts.length - 1];
		
    	Method xf = methods.get("exec_" + functionName);
    	if(xf != null) {
    		Object[] args = new Object[2];
    		args[0] = parameterList;
    		args[1] = returnValue;
    		try {
				xf.invoke(this, args);
				parameterList = (TriParameterList)args[0];
				returnValue = (TriParameter)args[1];
			} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
				e.printStackTrace();
			}    		
    		return;
    	}
    	
    	xf = methods.get(functionName);
    	
    	if(xf == null) {
    		System.err.println("ERROR: Unknown external function");
    		return;
    	}
    	
		Class<?>[] params = xf.getParameterTypes();		
		
		if(params.length != parameterList.size()) {
    		System.err.println("ERROR: Incorrect number of parameters");
    		return;
    	}
		
		Object[] args = new Object[params.length];
		for(int i=0; i < params.length; i++) {
			args[i] = simpleDecode(params[i], parameterList.get(i).getEncodedParameter());
		}
    	
    	// Invoke the external function
		try {
			Object result = xf.invoke(this, args);

			// return value
			byte[] res = simpleEncode(result.getClass(), (Value)result);
			returnValue.setParameterPassingMode(TriParameterPassingMode.TRI_OUT);
	        returnValue.setNumberOfBits(res.length * 8); // TODO: improve
	        returnValue.setEncodedParameter(res);
	        
	        // (in)out parameters
	        // TODO
	        
		} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
			e.printStackTrace();
		}
	}
	
	public Object getProxy(Class<?> iface) {
				
		Class<?>[] xf_ifaces = this.getClass().getInterfaces();
    	Class<?>[] ifaces = new Class[xf_ifaces.length + 1];
    	for(int i=0; i < xf_ifaces.length; i++) {
    		ifaces[i] = xf_ifaces[i];
    	}
    	ifaces[ifaces.length -1] = iface;
    	
    	Object instance = Proxy.newProxyInstance(
			this.getClass().getClassLoader(),
			ifaces,
			this
		);

    	return instance;
	}
	
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    	System.err.println("Calling: " + method.getName());
    	Class<?>[] params = null;
    	if(args != null) {
    		params = new Class<?>[args.length];
        	for(int i=0; i < args.length; i++) {
        		params[i] = args[i].getClass();
        	}
    	}
        Method m = this.getClass().getMethod(method.getName(), params);
        return m.invoke(this, args);
	}

}
