package railo.transformer.bytecode.reflection;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.collections.map.ReferenceMap;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;

import railo.commons.io.IOUtil;
import railo.commons.io.res.Resource;
import railo.commons.io.res.ResourceProvider;
import railo.commons.io.res.ResourcesImpl;
import railo.commons.io.res.util.ResourceUtil;
import railo.commons.lang.ClassUtil;
import railo.commons.lang.ExtendableClassLoader;
import railo.commons.lang.PhysicalClassLoader;
import railo.commons.lang.StringUtil;
import railo.runtime.PageContext;
import railo.runtime.functions.arrays.ArrayNew;
import railo.runtime.op.Caster;
import railo.runtime.type.util.ArrayUtil;
import railo.transformer.bytecode.util.ASMConstants;
import railo.transformer.bytecode.util.ASMUtil;
import railo.transformer.bytecode.util.Types;

public class ASMProxyFactory {

	public static final Type ASM_METHOD=Type.getType(ASMMethod.class);
	public static final Type CLASS404=Type.getType(ClassNotFoundException.class);
	public static final Type CLASS_UTIL=Type.getType(ClassUtil.class);
	
	
	
	//private static final org.objectweb.asm.commons.Method CONSTRUCTOR = 
    //	new org.objectweb.asm.commons.Method("<init>",Types.VOID,new Type[]{Types.CLASS_LOADER,Types.CLASS});
	private static final org.objectweb.asm.commons.Method CONSTRUCTOR = 
    	new org.objectweb.asm.commons.Method("<init>",Types.VOID,new Type[]{Types.CLASS,Types.CLASS_ARRAY});

	private static final org.objectweb.asm.commons.Method LOAD_CLASS = new org.objectweb.asm.commons.Method(
			"loadClass",
			Types.CLASS,
			new Type[]{Types.STRING});
	

	// public static Class loadClass(String className, Class defaultValue) {
	private static final org.objectweb.asm.commons.Method LOAD_CLASS_EL = new org.objectweb.asm.commons.Method(
			"loadClass",
			Types.CLASS,
			new Type[]{Types.STRING,Types.CLASS});
	

	// public String getName();
	private static final org.objectweb.asm.commons.Method GET_NAME = new org.objectweb.asm.commons.Method(
			"getName",
			Types.STRING,
			new Type[]{});
	
	// public int getModifiers();
	private static final org.objectweb.asm.commons.Method GET_MODIFIERS = new org.objectweb.asm.commons.Method(
			"getModifiers",
			Types.INT_VALUE,
			new Type[]{});

	// public Class getReturnType();
	private static final org.objectweb.asm.commons.Method GET_RETURN_TYPE_AS_STRING = new org.objectweb.asm.commons.Method(
			"getReturnTypeAsString",
			Types.STRING,
			new Type[]{});

	
	
	
	private static final org.objectweb.asm.commons.Method INVOKE = new org.objectweb.asm.commons.Method(
			"invoke",
			Types.OBJECT,
			new Type[]{Types.OBJECT,Types.OBJECT_ARRAY});
	
	
	// primitive to reference type
	private static final org.objectweb.asm.commons.Method BOOL_VALUE_OF = new org.objectweb.asm.commons.Method("valueOf",Types.BOOLEAN,new Type[]{Types.BOOLEAN_VALUE});
	private static final org.objectweb.asm.commons.Method SHORT_VALUE_OF = new org.objectweb.asm.commons.Method("valueOf",Types.SHORT,new Type[]{Types.SHORT_VALUE});
	private static final org.objectweb.asm.commons.Method INT_VALUE_OF = new org.objectweb.asm.commons.Method("valueOf",Types.INTEGER,new Type[]{Types.INT_VALUE});
	private static final org.objectweb.asm.commons.Method LONG_VALUE_OF = new org.objectweb.asm.commons.Method("valueOf",Types.LONG,new Type[]{Types.LONG_VALUE});
	private static final org.objectweb.asm.commons.Method FLT_VALUE_OF = new org.objectweb.asm.commons.Method("valueOf",Types.FLOAT,new Type[]{Types.FLOAT_VALUE});
	private static final org.objectweb.asm.commons.Method DBL_VALUE_OF = new org.objectweb.asm.commons.Method("valueOf",Types.DOUBLE,new Type[]{Types.DOUBLE_VALUE});
	private static final org.objectweb.asm.commons.Method CHR_VALUE_OF = new org.objectweb.asm.commons.Method("valueOf",Types.CHARACTER,new Type[]{Types.CHARACTER});
	private static final org.objectweb.asm.commons.Method BYT_VALUE_OF = new org.objectweb.asm.commons.Method("valueOf",Types.BYTE,new Type[]{Types.BYTE_VALUE});
	
	// reference type to primitive
	private static final org.objectweb.asm.commons.Method BOOL_VALUE = new org.objectweb.asm.commons.Method("booleanValue",Types.BOOLEAN_VALUE,new Type[]{});
	private static final org.objectweb.asm.commons.Method SHORT_VALUE = new org.objectweb.asm.commons.Method("shortValue",Types.SHORT_VALUE,new Type[]{});
	private static final org.objectweb.asm.commons.Method INT_VALUE = new org.objectweb.asm.commons.Method("intValue",Types.INT_VALUE,new Type[]{});
	private static final org.objectweb.asm.commons.Method LONG_VALUE = new org.objectweb.asm.commons.Method("longValue",Types.LONG_VALUE,new Type[]{});
	private static final org.objectweb.asm.commons.Method FLT_VALUE = new org.objectweb.asm.commons.Method("floatValue",Types.FLOAT_VALUE,new Type[]{});
	private static final org.objectweb.asm.commons.Method DBL_VALUE = new org.objectweb.asm.commons.Method("doubleValue",Types.DOUBLE_VALUE,new Type[]{});
	private static final org.objectweb.asm.commons.Method CHR_VALUE = new org.objectweb.asm.commons.Method("charValue",Types.CHAR,new Type[]{});
	private static final org.objectweb.asm.commons.Method BYT_VALUE = new org.objectweb.asm.commons.Method("byteValue",Types.BYTE_VALUE,new Type[]{});
	
	private static final org.objectweb.asm.commons.Method ASM_METHOD_CONSTRUCTOR = new org.objectweb.asm.commons.Method(
			"<init>",
			Types.VOID,
			new Type[]{Types.CLASS,Types.CLASS_ARRAY}
    		);
	
	
	private static final Map<String,ASMMethod>methods=new ReferenceMap();

	public static void main(String[] args) throws Throwable {
		ResourceProvider frp = ResourcesImpl.getFileResourceProvider();
		Resource root = frp.getResource("/Users/mic/Projects/Railo/webroot/WEB-INF/railo/cfclasses/wrappers/");
		root.mkdir();
		PhysicalClassLoader pcl = new PhysicalClassLoader(root);
		//PhysicalClassLoader pcl = (PhysicalClassLoader)ThreadLocalPageContext.getConfig().getRPCClassLoader(false);

		ASMProxyFactory.getClass(pcl, root, ArrayNew.class);
		
		
		ASMMethod method = ASMProxyFactory.getMethod(pcl, root, ArrayNew.class, "call", new Class[]{PageContext.class});
		//print.e(method.invoke(null, new Object[]{null}));
		
		
		
		
	}
	
	public static ASMClass getClass(ExtendableClassLoader pcl,Resource classRoot,Class clazz) throws IOException, InstantiationException, IllegalAccessException, IllegalArgumentException, SecurityException, InvocationTargetException, NoSuchMethodException{
		Type type = Type.getType(clazz); 

	    // Fields
	    Field[] fields = clazz.getFields();
	    for(int i=0;i<fields.length;i++){
	    	if(Modifier.isPrivate(fields[i].getModifiers())) continue;
	    	createField(type,fields[i]);
	    }
	    
	    // Methods
	    Method[] methods = clazz.getMethods();
	    Map<String,ASMMethod> amethods=new HashMap<String, ASMMethod>();
	    for(int i=0;i<methods.length;i++){
	    	if(Modifier.isPrivate(methods[i].getModifiers())) continue;
	    	amethods.put(methods[i].getName(), getMethod(pcl,classRoot,type,clazz,methods[i]));
	    }
	    
	    return new ASMClass(clazz.getName(),amethods);
	    
	}
	
	private static void createField(Type type, Field field) {
		// TODO Auto-generated method stub
		
	}

	public static ASMMethod getMethod(ExtendableClassLoader pcl, Resource classRoot, Class clazz, String methodName, Class[] parameters) throws IOException, InstantiationException, IllegalAccessException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
		String className = createMethodName(clazz,methodName,parameters);
		
		// check if already in memory cache
		ASMMethod asmm = methods.get(className);
		if(asmm!=null){
			//print.e("use loaded from memory");
			return asmm;
		}
		
		// try to load existing ASM Class
		Class<?> asmClass;
		try {
			asmClass = pcl.loadClass(className);
			//print.e("use existing class");
		}
		catch (ClassNotFoundException cnfe) {
			Type type = Type.getType(clazz);
			Method method = clazz.getMethod(methodName, parameters);
			byte[] barr = _createMethod(type, clazz, method, classRoot, className);
			asmClass=pcl.loadClass(className, barr);
			//print.e("create class");
		}
		asmm = newInstance(asmClass,clazz,parameters);
		//methods.put(className, asmm);
		return asmm;
	}

	private static ASMMethod getMethod(ExtendableClassLoader pcl, Resource classRoot, Type type,Class clazz, Method method) throws IOException, InstantiationException, IllegalAccessException, IllegalArgumentException, SecurityException, InvocationTargetException, NoSuchMethodException {
		String className = createMethodName(clazz,method.getName(),method.getParameterTypes());
		
		// check if already in memory cache
		ASMMethod asmm = methods.get(className);
		if(asmm!=null)return asmm;
		
		// try to load existing ASM Class
		Class<?> asmClass;
		try {
			asmClass = pcl.loadClass(className);
		}
		catch (ClassNotFoundException cnfe) {
			byte[] barr = _createMethod(type, clazz, method, classRoot, className);
			asmClass=pcl.loadClass(className, barr);
		}
		
		asmm = newInstance(asmClass,clazz,method.getParameterTypes());
		methods.put(className, asmm);
		return asmm;
	}

	private static ASMMethod newInstance(Class<?> asmClass, Class<?> decClass, Class[] params) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, SecurityException, NoSuchMethodException {
		Constructor<ASMMethod> constr = (Constructor<ASMMethod>) asmClass.getConstructor(
				new Class[]{
					Class.class,
					Class[].class
				}
		);
		return constr.newInstance(new Object[]{
				decClass,
				params
			});
		 
		//return (ASMMethod) asmClass.newInstance();
	}
	
	
	private static String createMethodName(Class clazz,String methodName,Class[] paramTypes) {
		StringBuilder sb = new StringBuilder("method.")
		.append(clazz.getName())
		.append(methodName);
		
		paramNames(sb,paramTypes);
		
		return sb.toString();
	}

	private static byte[] _createMethod(Type type,Class clazz, Method method,Resource classRoot, String className) throws IOException {
		Class<?> rtn = method.getReturnType();
	    Type rtnType = Type.getType(rtn);
	    
		className=className.replace('.',File.separatorChar);
		ClassWriter cw = ASMUtil.getClassWriter();
	    cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, className, null, ASM_METHOD.getInternalName(), null);
		

// CONSTRUCTOR

	    GeneratorAdapter adapter = new GeneratorAdapter(Opcodes.ACC_PUBLIC,CONSTRUCTOR,null,null,cw);
	    
	    Label begin = new Label();
        adapter.visitLabel(begin);
		adapter.loadThis();
        
	    adapter.visitVarInsn(Opcodes.ALOAD, 1);
		adapter.visitVarInsn(Opcodes.ALOAD, 2);
        
		adapter.invokeConstructor(ASM_METHOD, CONSTRUCTOR);
		adapter.visitInsn(Opcodes.RETURN);
		
		Label end = new Label();
		adapter.visitLabel(end);
		
		adapter.endMethod();
		
	/*
	 
	    GeneratorAdapter adapter = new GeneratorAdapter(Opcodes.ACC_PUBLIC,CONSTRUCTOR,null,null,cw);
	    
	    Label begin = new Label();
        adapter.visitLabel(begin);
		adapter.loadThis();
        
	    // clazz
        adapter.visitVarInsn(Opcodes.ALOAD, 2);
        
        // parameterTypes 
        Class<?>[] params = method.getParameterTypes();
        Type[] paramTypes = new Type[params.length];
	    ArrayVisitor av=new ArrayVisitor();
	    av.visitBegin(adapter, Types.CLASS, params.length);
	    for(int i=0;i<params.length;i++){
	    	paramTypes[i]=Type.getType(params[i]);
	    	av.visitBeginItem(adapter, i);
	    		loadClass(adapter,params[i]);
	    	av.visitEndItem(adapter);
	    }
	    av.visitEnd();
	    
		adapter.invokeConstructor(ASM_METHOD, ASM_METHOD_CONSTRUCTOR);
		adapter.visitInsn(Opcodes.RETURN);
		
		Label end = new Label();
		adapter.visitLabel(end);
		
		adapter.endMethod();
	 */
		
		
		
	// METHOD getName();
		adapter = new GeneratorAdapter(Opcodes.ACC_PUBLIC , GET_NAME, null, null, cw);
		adapter.push(method.getName());
		adapter.visitInsn(Opcodes.ARETURN);
        adapter.endMethod();
		
    // METHOD getModifiers();
		adapter = new GeneratorAdapter(Opcodes.ACC_PUBLIC , GET_MODIFIERS, null, null, cw);
		adapter.push(method.getModifiers());
		adapter.visitInsn(Opcodes.IRETURN);
        adapter.endMethod();
        

		
    // METHOD getReturnType();
        adapter = new GeneratorAdapter(Opcodes.ACC_PUBLIC , GET_RETURN_TYPE_AS_STRING, null, null, cw);
		
		adapter.push(method.getReturnType().getName());
		adapter.visitInsn(Opcodes.ARETURN);
        
		adapter.endMethod();
        
		
        
        
	// METHOD INVOKE
		boolean isStatic = Modifier.isStatic(method.getModifiers());
		adapter = new GeneratorAdapter(Opcodes.ACC_PUBLIC , INVOKE, null, null, cw);
        Label start=adapter.newLabel();
        adapter.visitLabel(start);
        
        // load Object
        if(!isStatic) {
        	adapter.visitVarInsn(Opcodes.ALOAD, 1);
        	adapter.checkCast(type);
		}
        	
        // load params
        Class<?>[] params = method.getParameterTypes();

        Type[] paramTypes = new Type[params.length];
	    for(int i=0;i<params.length;i++){
	    	paramTypes[i]=Type.getType(params[i]);
	    }

        for(int i=0;i<params.length;i++){
        	adapter.visitVarInsn(Opcodes.ALOAD, 2);
        	adapter.push(i);
        	//adapter.visitInsn(Opcodes.ICONST_0);
        	adapter.visitInsn(Opcodes.AALOAD);
        	
        	adapter.checkCast(toReferenceType(params[i],paramTypes[i]));
        	
        	// cast
        	if(params[i]==boolean.class) adapter.invokeVirtual(Types.BOOLEAN, BOOL_VALUE);
        	else if(params[i]==short.class) adapter.invokeVirtual(Types.SHORT, SHORT_VALUE);
        	else if(params[i]==int.class) adapter.invokeVirtual(Types.INTEGER, INT_VALUE);
        	else if(params[i]==float.class) adapter.invokeVirtual(Types.FLOAT, FLT_VALUE);
        	else if(params[i]==long.class) adapter.invokeVirtual(Types.LONG, LONG_VALUE);
        	else if(params[i]==double.class) adapter.invokeVirtual(Types.DOUBLE, DBL_VALUE);
        	else if(params[i]==char.class) adapter.invokeVirtual(Types.CHARACTER, CHR_VALUE);
        	else if(params[i]==byte.class) adapter.invokeVirtual(Types.BYTE, BYT_VALUE);
        	//else adapter.checkCast(paramTypes[i]);
        	
        }
        
        
        // call method
    	final org.objectweb.asm.commons.Method m = new org.objectweb.asm.commons.Method(method.getName(),rtnType,paramTypes);
    	if(isStatic)adapter.invokeStatic(type, m);
    	else adapter.invokeVirtual(type, m);
         
    	
    	// return
    	if(rtn==void.class) ASMConstants.NULL(adapter);
    	
    	
    	// cast result to object
    	if(rtn==boolean.class) adapter.invokeStatic(Types.BOOLEAN, BOOL_VALUE_OF);
    	else if(rtn==short.class) adapter.invokeStatic(Types.SHORT, SHORT_VALUE_OF);
    	else if(rtn==int.class) adapter.invokeStatic(Types.INTEGER, INT_VALUE_OF);
    	else if(rtn==long.class) adapter.invokeStatic(Types.LONG, LONG_VALUE_OF);
    	else if(rtn==float.class) adapter.invokeStatic(Types.FLOAT, FLT_VALUE_OF);
    	else if(rtn==double.class) adapter.invokeStatic(Types.DOUBLE, DBL_VALUE_OF);
    	else if(rtn==char.class) adapter.invokeStatic(Types.CHARACTER, CHR_VALUE_OF);
    	else if(rtn==byte.class) adapter.invokeStatic(Types.BYTE, BYT_VALUE_OF);
    	
    	adapter.visitInsn(Opcodes.ARETURN);
        
        adapter.endMethod();
		
        
		
        if(classRoot!=null) {
        	Resource classFile=classRoot.getRealResource(className+".class");
        	return store(cw.toByteArray(),classFile);
        }
        return cw.toByteArray();
	}
	

	private static Type toReferenceType(Class<?> clazz, Type defaultValue) {
		if(int.class==clazz) return Types.INTEGER;
		else if(long.class==clazz) return Types.LONG;
		else if(char.class==clazz) return Types.CHARACTER;
		else if(byte.class==clazz) return Types.BYTE;
		else if(float.class==clazz) return Types.FLOAT;
		else if(double.class==clazz) return Types.DOUBLE;
		else if(boolean.class==clazz) return Types.BOOLEAN;
		else if(short.class==clazz) return Types.SHORT;
		return defaultValue;
	}

	private static void loadClass(GeneratorAdapter adapter, Class<?> clazz) {
		if(void.class==clazz) adapter.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Void", "TYPE", "Ljava/lang/Class;");
		
		// primitive types
		else if(int.class==clazz) adapter.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Integer", "TYPE", "Ljava/lang/Class;");
		else if(long.class==clazz) adapter.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Long", "TYPE", "Ljava/lang/Class;");
		else if(char.class==clazz) adapter.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Character", "TYPE", "Ljava/lang/Class;");
		else if(byte.class==clazz) adapter.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Byte", "TYPE", "Ljava/lang/Class;");
		else if(float.class==clazz) adapter.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Float", "TYPE", "Ljava/lang/Class;");
		else if(double.class==clazz) adapter.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Double", "TYPE", "Ljava/lang/Class;");
		else if(boolean.class==clazz) adapter.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Boolean", "TYPE", "Ljava/lang/Class;");
		else if(short.class==clazz) adapter.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Short", "TYPE", "Ljava/lang/Class;");
		
		// TODO ref types
		
		else {
		    adapter.visitVarInsn(Opcodes.ALOAD, 1);
	        adapter.push(clazz.getName());
			adapter.invokeVirtual(Types.CLASS_LOADER,LOAD_CLASS );
		}
	}

	private static void paramNames(StringBuilder sb, Class<?>[] params) {
		if(ArrayUtil.isEmpty(params)) return;
		
		for(int i=0;i<params.length;i++){
			sb.append('$');
			if(params[i].isArray())
				sb.append(StringUtil.replace(Caster.toClassName(params[i]).replace('.', '_'),"[]","_arr",false));
			else
				sb.append(params[i].getName().replace('.', '_'));
		}
	}

	private static byte[] store(byte[] barr,Resource classFile) throws IOException {
		// create class file
        ResourceUtil.touch(classFile);
        //print.e(classFile);
        IOUtil.copy(new ByteArrayInputStream(barr), classFile,true);
       return barr;
	}
	/*private void store(ClassWriter cw) {
		// create class file
        byte[] barr = cw.toByteArray();
    	
        try {
        	ResourceUtil.touch(classFile);
	        IOUtil.copy(new ByteArrayInputStream(barr), classFile,true);
	        
	        cl = (PhysicalClassLoader) mapping.getConfig().getRPCClassLoader(true);
	        Class<?> clazz = cl.loadClass(className, barr);
	        return newInstance(clazz, config,cfc);
        }
        catch(Throwable t) {
        	throw Caster.toPageException(t);
        }
	}*/
	
}
