/*
 *  Copyright 2013 Christopher Pheby
 *
 *  Licensed 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.jadira.reflection.access.asm;

import static org.objectweb.asm.Opcodes.AALOAD;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_SUPER;
import static org.objectweb.asm.Opcodes.ACC_VARARGS;
import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.ATHROW;
import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DLOAD;
import static org.objectweb.asm.Opcodes.DRETURN;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.FLOAD;
import static org.objectweb.asm.Opcodes.FRETURN;
import static org.objectweb.asm.Opcodes.F_APPEND;
import static org.objectweb.asm.Opcodes.F_SAME;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IRETURN;
import static org.objectweb.asm.Opcodes.ISTORE;
import static org.objectweb.asm.Opcodes.LLOAD;
import static org.objectweb.asm.Opcodes.LRETURN;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.POP;
import static org.objectweb.asm.Opcodes.PUTFIELD;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Opcodes.V1_7;
import static org.objectweb.asm.Type.BOOLEAN;
import static org.objectweb.asm.Type.BYTE;
import static org.objectweb.asm.Type.CHAR;
import static org.objectweb.asm.Type.DOUBLE;
import static org.objectweb.asm.Type.FLOAT;
import static org.objectweb.asm.Type.INT;
import static org.objectweb.asm.Type.LONG;
import static org.objectweb.asm.Type.SHORT;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;

import org.jadira.reflection.access.AbstractClassAccess;
import org.jadira.reflection.access.api.ClassAccess;
import org.jadira.reflection.access.api.FieldAccess;
import org.jadira.reflection.access.api.MethodAccess;
import org.jadira.reflection.access.classloader.AccessClassLoader;
import org.jadira.reflection.access.portable.PortableFieldAccess;
import org.jadira.reflection.core.misc.ClassUtils;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

/**
 * ClassAccess implementation which uses ASM to generate accessors
 * @param <C> The Class to be accessed
 */
public abstract class AsmClassAccess<C> extends AbstractClassAccess<C> implements ClassAccess<C> {

    private static final ConcurrentHashMap<Class<?>, AsmClassAccess<?>> CLASS_ACCESSES = new ConcurrentHashMap<Class<?>, AsmClassAccess<?>>();
    
	private static final String CLASS_ACCESS_NM = ClassAccess.class.getName().replace('.', '/');

	private static final String ASM_CLASS_ACCESS_NM = AsmClassAccess.class.getName().replace('.', '/');
    
	private boolean isNonStaticMemberClass;
		
	/**
	 * Constructor, intended for use by generated subclasses
	 * @param clazz The Class to be accessed
	 */
	protected AsmClassAccess(Class<C> clazz) {
		super(clazz);
	}

	/**
	 * Indicates if the class being accessed is a non-static member class
	 * @return True if the class is a non-static member class
	 */
	public boolean isNonStaticMemberClass() {
		return isNonStaticMemberClass;
	}
	
	/**
	 * Get a new instance that can access the given Class. If the ClassAccess for this class
	 * has not been obtained before, then the specific AsmClassAccess is created by generating
	 * a specialised subclass of this class and returning it. 
	 * @param clazz Class to be accessed
	 * @param <C> The type of class
	 * @return New AsmClassAccess instance
	 */
	public static <C> AsmClassAccess<C> get(Class<C> clazz) {

	    @SuppressWarnings("unchecked")
	    AsmClassAccess<C> access = (AsmClassAccess<C>) CLASS_ACCESSES.get(clazz);
        if (access != null) {
            return access;
        }
        	    
		Class<?> enclosingType = clazz.getEnclosingClass();

		final boolean isNonStaticMemberClass = determineNonStaticMemberClass(clazz, enclosingType);

		String clazzName = clazz.getName();
		
		Field[] fields = ClassUtils.collectInstanceFields(clazz, false, false, true);
		Method[] methods = ClassUtils.collectMethods(clazz);
		
		String accessClassName = constructAccessClassName(clazzName);

		Class<?> accessClass = null;

		AccessClassLoader loader = AccessClassLoader.get(clazz);
		synchronized (loader) {
			try {
				accessClass = loader.loadClass(accessClassName);
			} catch (ClassNotFoundException ignored) {

				String accessClassNm = accessClassName.replace('.', '/');
				String clazzNm = clazzName.replace('.', '/');
				String enclosingClassNm = determineEnclosingClassNm(clazz, enclosingType, isNonStaticMemberClass);

				String signatureString = "L" + ASM_CLASS_ACCESS_NM + "<L" + clazzNm + ";>;L" + CLASS_ACCESS_NM + "<L" + clazzNm + ";>;";

				ClassWriter cw = new ClassWriter(0);

//				TraceClassVisitor tcv = new TraceClassVisitor(cv, new PrintWriter(System.err));
//				CheckClassAdapter cw = new CheckClassAdapter(tcv);

				cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, accessClassNm, signatureString, ASM_CLASS_ACCESS_NM, null);

				enhanceForConstructor(cw, accessClassNm, clazzNm);

				if (isNonStaticMemberClass) {
					enhanceForNewInstanceInner(cw, clazzNm, enclosingClassNm);
				} else {
					enhanceForNewInstance(cw, clazzNm);
				}

				enhanceForGetValueObject(cw, accessClassNm, clazzNm, fields);
				enhanceForPutValueObject(cw, accessClassNm, clazzNm, fields);
				enhanceForGetValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.BOOLEAN_TYPE);
				enhanceForPutValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.BOOLEAN_TYPE);
				enhanceForGetValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.BYTE_TYPE);
				enhanceForPutValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.BYTE_TYPE);
				enhanceForGetValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.SHORT_TYPE);
				enhanceForPutValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.SHORT_TYPE);
				enhanceForGetValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.INT_TYPE);
				enhanceForPutValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.INT_TYPE);
				enhanceForGetValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.LONG_TYPE);
				enhanceForPutValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.LONG_TYPE);
				enhanceForGetValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.DOUBLE_TYPE);
				enhanceForPutValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.DOUBLE_TYPE);
				enhanceForGetValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.FLOAT_TYPE);
				enhanceForPutValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.FLOAT_TYPE);
				enhanceForGetValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.CHAR_TYPE);
				enhanceForPutValuePrimitive(cw, accessClassNm, clazzNm, fields, Type.CHAR_TYPE);
				enhanceForInvokeMethod(cw, accessClassNm, clazzNm, methods);

				cw.visitEnd();

				loader.registerClass(accessClassName, cw.toByteArray());

				try {
					accessClass = loader.findClass(accessClassName);
				} catch (ClassNotFoundException e) {
					throw new IllegalStateException("AccessClass unexpectedly could not be found", e);
				}
			}
		}
		
		try {
			@SuppressWarnings("unchecked")
			Constructor<AsmClassAccess<C>> c = (Constructor<AsmClassAccess<C>>) accessClass.getConstructor(new Class[] { Class.class });
			
			access = c.newInstance(clazz);
			access.isNonStaticMemberClass = isNonStaticMemberClass;
				        
			CLASS_ACCESSES.putIfAbsent(clazz, access);
			
			return access;
		} catch (Exception ex) {
		    throw new RuntimeException("Error constructing constructor access class: " + accessClassName + "{ " + ex.getMessage() + " }", ex);
		}
	}

	private static Label[] constructLabels(Field[] fields) {

		Label[] labels = new Label[fields.length];
		for (int i = 0, n = labels.length; i < n; i++) {
			labels[i] = new Label();
		}
		return labels;
	}

	private static Label[] constructLabels(Method[] methods) {

		Label[] labels = new Label[methods.length];
		for (int i = 0, n = labels.length; i < n; i++) {
			labels[i] = new Label();
		}
		return labels;
	}
	
	private static <C> String determineEnclosingClassNm(Class<C> clazz, Class<?> enclosingType, final boolean isNonStaticMemberClass) {

		final String enclosingClassNm;

		if (!isNonStaticMemberClass) {

			enclosingClassNm = null;
		} else {

			enclosingClassNm = enclosingType.getName().replace('.', '/');
		}
		return enclosingClassNm;
	}

	private static boolean determineNonStaticMemberClass(Class<?> clazz, Class<?> enclosingType) {
		final boolean isNonStaticMemberClass;
		if (enclosingType != null && clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())) {
			isNonStaticMemberClass = true;
		} else {
			isNonStaticMemberClass = false;
		}
		;
		return isNonStaticMemberClass;
	}

	private static String constructAccessClassName(String clazzName) {

		String accessClassName = clazzName + AsmClassAccess.class.getSimpleName();
		if (accessClassName.startsWith("java.")) {
			accessClassName = AsmClassAccess.class.getSimpleName().toLowerCase() + accessClassName;
		}

		return accessClassName;
	}

	private static void enhanceForConstructor(ClassVisitor cw, String accessClassNm, String clazzNm) {
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/Class;)V", "(L" + clazzNm + ";)V", null);
		mv.visitCode();
		mv.visitVarInsn(ALOAD, 0);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKESPECIAL, ASM_CLASS_ACCESS_NM, "<init>", "(Ljava/lang/Class;)V");
		mv.visitInsn(RETURN);
		mv.visitMaxs(2, 2);
		mv.visitEnd();
	}

	private static void enhanceForNewInstance(ClassVisitor cw, String classNm) {
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "newInstance", "()Ljava/lang/Object;", null, null);

		mv.visitCode();
		mv.visitTypeInsn(NEW, classNm);
		mv.visitInsn(DUP);
		mv.visitMethodInsn(INVOKESPECIAL, classNm, "<init>", "()V");
		mv.visitInsn(ARETURN);
		mv.visitMaxs(2, 1);
		mv.visitEnd();
	}

	private static void enhanceForNewInstanceInner(ClassVisitor cw, String classNm, String enclosingClassNm) {
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "newInstance", "()LLjava/lang/Object;", null, null);
		mv.visitCode();
		mv.visitTypeInsn(NEW, classNm);
		mv.visitInsn(DUP);
		mv.visitTypeInsn(NEW, enclosingClassNm);
		mv.visitInsn(DUP);
		mv.visitMethodInsn(INVOKESPECIAL, enclosingClassNm, "<init>", "()V");
		mv.visitInsn(DUP);
		mv.visitMethodInsn(INVOKEVIRTUAL, classNm, "getClass", "()Ljava/lang/Class;");
		mv.visitInsn(POP);
		mv.visitMethodInsn(INVOKESPECIAL, classNm, "<init>", "(L" + enclosingClassNm + ";)V");
		mv.visitInsn(ARETURN);
		mv.visitMaxs(4, 1);
		mv.visitEnd();
	}

	private static void enhanceForGetValueObject(ClassVisitor cw, String accessClassNm, String clazzNm, Field[] fields) {

		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getValue", "(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;", null, null);

		mv.visitCode();
		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, accessClassNm, "fieldNames", "[Ljava/lang/String;");
		mv.visitVarInsn(ALOAD, 2);
		mv.visitMethodInsn(INVOKESTATIC, "java/util/Arrays", "binarySearch", "([Ljava/lang/Object;Ljava/lang/Object;)I");
		mv.visitVarInsn(ISTORE, 3);
		mv.visitVarInsn(ILOAD, 3);

		final int maxStack;

		if (fields.length > 0) {
			maxStack = 5;
			Label[] labels = constructLabels(fields);

			Label defaultLabel = new Label();
			mv.visitTableSwitchInsn(0, labels.length - 1, defaultLabel, labels);

			for (int i = 0, n = labels.length; i < n; i++) {
				Field field = fields[i];
				mv.visitLabel(labels[i]);
				mv.visitFrame(F_SAME, 0, null, 0, null);
				mv.visitVarInsn(ALOAD, 1);
				mv.visitTypeInsn(CHECKCAST, clazzNm);
				mv.visitFieldInsn(GETFIELD, clazzNm, field.getName(), Type.getDescriptor(field.getType()));

				Type fieldType = Type.getType(field.getType());
				switch (fieldType.getSort()) {
				case Type.BOOLEAN:
					mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;");
					break;
				case Type.BYTE:
					mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;");
					break;
				case Type.CHAR:
					mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;");
					break;
				case Type.SHORT:
					mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;");
					break;
				case Type.INT:
					mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
					break;
				case Type.FLOAT:
					mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;");
					break;
				case Type.LONG:
					mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;");
					break;
				case Type.DOUBLE:
					mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;");
					break;
				}

				mv.visitInsn(ARETURN);
			}

			mv.visitLabel(defaultLabel);
			mv.visitFrame(F_SAME, 0, null, 0, null);
		} else {
			maxStack = 6;
		}
		enhanceForThrowingException(mv, IllegalArgumentException.class, "Field was not found", "Ljava/lang/Object;", ALOAD, 2);
		mv.visitMaxs(maxStack, 4);
		mv.visitEnd();
	}

	private static void enhanceForPutValueObject(ClassVisitor cw, String accessClassNm, String clazzNm, Field[] fields) {

		int maxStack = 6;
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "putValue", "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;)V", null, null);

		mv.visitCode();

		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, accessClassNm, "fieldNames", "[Ljava/lang/String;");
		mv.visitVarInsn(ALOAD, 2);
		mv.visitMethodInsn(INVOKESTATIC, "java/util/Arrays", "binarySearch", "([Ljava/lang/Object;Ljava/lang/Object;)I");
		mv.visitVarInsn(ISTORE, 4);
		mv.visitVarInsn(ILOAD, 4);

		if (fields.length > 0) {
			maxStack = 5;
			Label[] labels = constructLabels(fields);

			Label defaultLabel = new Label();
			mv.visitTableSwitchInsn(0, labels.length - 1, defaultLabel, labels);

			for (int i = 0, n = labels.length; i < n; i++) {
				Field field = fields[i];

				mv.visitLabel(labels[i]);
				mv.visitFrame(F_SAME, 0, null, 0, null);
				mv.visitVarInsn(ALOAD, 1);
				mv.visitTypeInsn(CHECKCAST, clazzNm);

				mv.visitVarInsn(ALOAD, 3);

				Type fieldType = Type.getType(field.getType());
				switch (fieldType.getSort()) {
				case Type.BOOLEAN:
					mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean");
					mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z");
					break;
				case Type.BYTE:
					mv.visitTypeInsn(CHECKCAST, "java/lang/Byte");
					mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B");
					break;
				case Type.CHAR:
					mv.visitTypeInsn(CHECKCAST, "java/lang/Character");
					mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C");
					break;
				case Type.SHORT:
					mv.visitTypeInsn(CHECKCAST, "java/lang/Short");
					mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S");
					break;
				case Type.INT:
					mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
					mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
					break;
				case Type.FLOAT:
					mv.visitTypeInsn(CHECKCAST, "java/lang/Float");
					mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F");
					break;
				case Type.LONG:
					mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
					mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J");
					break;
				case Type.DOUBLE:
					mv.visitTypeInsn(CHECKCAST, "java/lang/Double");
					mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D");
					break;
				case Type.ARRAY:
					mv.visitTypeInsn(CHECKCAST, fieldType.getDescriptor());
					break;
				case Type.OBJECT:
					mv.visitTypeInsn(CHECKCAST, fieldType.getInternalName());
					break;
				}

				mv.visitFieldInsn(PUTFIELD, clazzNm, field.getName(), fieldType.getDescriptor());
				mv.visitInsn(RETURN);
			}

			mv.visitLabel(defaultLabel);
			mv.visitFrame(F_SAME, 0, null, 0, null);
		} else {
			maxStack = 6;
		}
		enhanceForThrowingException(mv, IllegalArgumentException.class, "Field was not found", "Ljava/lang/Object;", ALOAD, 2);
		mv.visitMaxs(maxStack, 5);
		mv.visitEnd();
	}

	private static void enhanceForPutValuePrimitive(ClassVisitor cw, String accessClassNm, String clazzNm, Field[] fields, Type type) {

		final String methodName;
		final String typeNm = type.getDescriptor();
		final int instruction;

		switch (type.getSort()) {
		case BOOLEAN:
			methodName = "putBooleanValue";
			instruction = ILOAD;
			break;
		case BYTE:
			methodName = "putByteValue";
			instruction = ILOAD;
			break;
		case CHAR:
			methodName = "putCharValue";
			instruction = ILOAD;
			break;
		case SHORT:
			methodName = "putShortValue";
			instruction = ILOAD;
			break;
		case INT:
			methodName = "putIntValue";
			instruction = ILOAD;
			break;
		case FLOAT:
			methodName = "putFloatValue";
			instruction = FLOAD;
			break;
		case LONG:
			methodName = "putLongValue";
			instruction = LLOAD;
			break;
		case DOUBLE:
			methodName = "putDoubleValue";
			instruction = DLOAD;
			break;
		default:
			methodName = "put" + type.getInternalName().lastIndexOf('/') + "Value";
			instruction = ALOAD;
			break;
		}

		int offset = (instruction == LLOAD || instruction == DLOAD) ? 1 : 0;

		int maxStack = 6;
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, methodName, "(Ljava/lang/Object;Ljava/lang/String;" + typeNm + ")V", null, null);

		mv.visitCode();

		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, accessClassNm, "fieldNames", "[Ljava/lang/String;");
		mv.visitVarInsn(ALOAD, 2);
		mv.visitMethodInsn(INVOKESTATIC, "java/util/Arrays", "binarySearch", "([Ljava/lang/Object;Ljava/lang/Object;)I");

		mv.visitVarInsn(ISTORE, 4 + offset);
		mv.visitVarInsn(ILOAD, 4 + offset);

		if (fields.length > 0) {
			maxStack = 6;

			Label[] labels = new Label[fields.length];
			Label labelForInvalidTypes = new Label();
			boolean hasAnyBadTypeLabel = false;

			for (int i = 0, n = labels.length; i < n; i++) {
				if (Type.getType(fields[i].getType()).equals(type))
					labels[i] = new Label();
				else {
					labels[i] = labelForInvalidTypes;
					hasAnyBadTypeLabel = true;
				}
			}
			Label defaultLabel = new Label();
			mv.visitTableSwitchInsn(0, labels.length - 1, defaultLabel, labels);

			for (int i = 0, n = labels.length; i < n; i++) {
				if (!labels[i].equals(labelForInvalidTypes)) {
					Field field = fields[i];

					mv.visitLabel(labels[i]);
					mv.visitFrame(F_SAME, 0, null, 0, null);
					mv.visitVarInsn(ALOAD, 1);
					mv.visitTypeInsn(CHECKCAST, clazzNm);

					mv.visitVarInsn(instruction, 3);
					mv.visitFieldInsn(PUTFIELD, clazzNm, field.getName(), typeNm);
					mv.visitInsn(RETURN);
				}
			}

			if (hasAnyBadTypeLabel) {
				mv.visitLabel(labelForInvalidTypes);
				mv.visitFrame(F_SAME, 0, null, 0, null);
				enhanceForThrowingException(mv, IllegalArgumentException.class, type.getClassName(), typeNm, instruction, 3);
			}

			mv.visitLabel(defaultLabel);
			mv.visitFrame(F_SAME, 0, null, 0, null);
		}

		final int maxLocals = 5 + offset;

		enhanceForThrowingException(mv, IllegalArgumentException.class, "Field was not found", typeNm, instruction, 3);
		mv.visitMaxs(maxStack, maxLocals);
		mv.visitEnd();
	}

	private static void enhanceForGetValuePrimitive(ClassVisitor cw, String accessClassNm, String clazzNm, Field[] fields, Type type) {

		String methodName;
		final String typeNm = type.getDescriptor();
		final int instruction;

		switch (type.getSort()) {
		case Type.BOOLEAN:
			methodName = "getBooleanValue";
			instruction = IRETURN;
			break;
		case Type.BYTE:
			methodName = "getByteValue";
			instruction = IRETURN;
			break;
		case Type.CHAR:
			methodName = "getCharValue";
			instruction = IRETURN;
			break;
		case Type.SHORT:
			methodName = "getShortValue";
			instruction = IRETURN;
			break;
		case Type.INT:
			methodName = "getIntValue";
			instruction = IRETURN;
			break;
		case Type.FLOAT:
			methodName = "getFloatValue";
			instruction = FRETURN;
			break;
		case Type.LONG:
			methodName = "getLongValue";
			instruction = LRETURN;
			break;
		case Type.DOUBLE:
			methodName = "getDoubleValue";
			instruction = DRETURN;
			break;
		default:
			methodName = "getValue";
			instruction = ARETURN;
			break;
		}

		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, methodName, "(Ljava/lang/Object;Ljava/lang/String;)" + typeNm, null, null);

		mv.visitCode();
		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, accessClassNm, "fieldNames", "[Ljava/lang/String;");
		mv.visitVarInsn(ALOAD, 2);
		mv.visitMethodInsn(INVOKESTATIC, "java/util/Arrays", "binarySearch", "([Ljava/lang/Object;Ljava/lang/Object;)I");
		mv.visitVarInsn(ISTORE, 3);
		mv.visitVarInsn(ILOAD, 3);

		final int maxStack;

		if (fields.length > 0) {
			maxStack = 5;
			Label[] labels = constructLabels(fields);

			Label defaultLabel = new Label();
			mv.visitTableSwitchInsn(0, labels.length - 1, defaultLabel, labels);

			for (int i = 0, n = labels.length; i < n; i++) {
				Field field = fields[i];
				mv.visitLabel(labels[i]);
				mv.visitFrame(F_SAME, 0, null, 0, null);
				mv.visitVarInsn(ALOAD, 1);
				mv.visitTypeInsn(CHECKCAST, clazzNm);
				mv.visitFieldInsn(GETFIELD, clazzNm, field.getName(), typeNm);
				mv.visitInsn(instruction);
			}

			mv.visitLabel(defaultLabel);
			mv.visitFrame(F_SAME, 0, null, 0, null);
		} else {
			maxStack = 6;
		}
		enhanceForThrowingException(mv, IllegalArgumentException.class, "Field was not found", "Ljava/lang/Object;", ALOAD, 2);
		mv.visitMaxs(maxStack, 4);
		mv.visitEnd();
	}

	private static void enhanceForThrowingException(MethodVisitor mv, Class<? extends Exception> exceptionClass, String msg, String argType, int instruction, int slot) {

		String exceptionClassNm = Type.getInternalName(exceptionClass);
		mv.visitTypeInsn(NEW, exceptionClassNm);
		mv.visitInsn(DUP);
		mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
		mv.visitInsn(DUP);
		mv.visitLdcInsn(msg);
		mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V");
		mv.visitVarInsn(instruction, slot);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(" + argType + ")Ljava/lang/StringBuilder;");
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
		mv.visitMethodInsn(INVOKESPECIAL, exceptionClassNm, "<init>", "(Ljava/lang/String;)V");
		mv.visitInsn(ATHROW);
	}

	private static void enhanceForInvokeMethod(ClassVisitor cw, String accessClassNm, String clazzNm, Method[] methods) {

		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC  + ACC_VARARGS, "invokeMethod", "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;", null, null);              
        mv.visitCode();

        if (methods.length > 0) {
        	
            mv.visitVarInsn(ALOAD, 1);
            mv.visitTypeInsn(CHECKCAST, clazzNm);
            
            mv.visitVarInsn(ASTORE, 4);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitVarInsn(ALOAD, 2);
            mv.visitMethodInsn(INVOKESPECIAL, "org/jadira/reflection/access/asm/AsmClassAccess", "getMethodIndex", "(Ljava/lang/reflect/Method;)I");            
            mv.visitVarInsn(ISTORE, 5);
    		mv.visitVarInsn(ILOAD, 5);

            Label[] labels = constructLabels(methods);
			Label defaultLabel = new Label();

			mv.visitTableSwitchInsn(0, labels.length - 1, defaultLabel, labels);

			for (int i = 0; i < labels.length; i++) {

				Method method = methods[i];
				Class<?>[] parameterTypes = method.getParameterTypes();

				mv.visitLabel(labels[i]);

				if (i == 0) {
					mv.visitFrame(F_APPEND, 1, new Object[] { clazzNm }, 0, null);
				} else {
					mv.visitFrame(F_SAME, 0, null, 0, null);
				}
				mv.visitVarInsn(ALOAD, 4);

				StringBuilder methodDescriptor = new StringBuilder("(");
                for (int parameterIdx = 0; parameterIdx < parameterTypes.length; parameterIdx++) {
                	
                	Type type = Type.getType(parameterTypes[parameterIdx]);
                	
                	mv.visitVarInsn(ALOAD, 3);
                    mv.visitIntInsn(BIPUSH, parameterIdx);
                    mv.visitInsn(AALOAD);
                                
					switch (type.getSort()) {
					case Type.BOOLEAN:
						mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean");
						mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z");
						break;
					case Type.BYTE:
						mv.visitTypeInsn(CHECKCAST, "java/lang/Byte");
						mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B");
						break;
					case Type.CHAR:
						mv.visitTypeInsn(CHECKCAST, "java/lang/Character");
						mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C");
						break;
					case Type.SHORT:
						mv.visitTypeInsn(CHECKCAST, "java/lang/Short");
						mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S");
						break;
					case Type.INT:
						mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
						mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
						break;
					case Type.FLOAT:
						mv.visitTypeInsn(CHECKCAST, "java/lang/Float");
						mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F");
						break;
					case Type.LONG:
						mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
						mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J");
						break;
					case Type.DOUBLE:
						mv.visitTypeInsn(CHECKCAST, "java/lang/Double");
						mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D");
						break;
					case Type.ARRAY:
						mv.visitTypeInsn(CHECKCAST, type.getDescriptor());
						break;
					case Type.OBJECT:
						mv.visitTypeInsn(CHECKCAST, type.getInternalName());
						break;
					}
                    methodDescriptor.append(type.getDescriptor());
                }

                methodDescriptor.append(')');
                methodDescriptor.append(Type.getDescriptor(method.getReturnType()));
                
                mv.visitMethodInsn(INVOKEVIRTUAL, clazzNm, method.getName(), methodDescriptor.toString());

                switch (Type.getType(method.getReturnType()).getSort()) {
                case Type.VOID:
                        mv.visitInsn(ACONST_NULL);
                        break;
                case Type.BOOLEAN:
                        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;");
                        break;
                case Type.BYTE:
                        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;");
                        break;
                case Type.CHAR:
                        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;");
                        break;
                case Type.SHORT:
                        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;");
                        break;
                case Type.INT:
                        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
                        break;
                case Type.FLOAT:
                        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;");
                        break;
                case Type.LONG:
                        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;");
                        break;
                case Type.DOUBLE:
                        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;");
                        break;
                }

                mv.visitInsn(ARETURN);
            }

            mv.visitLabel(defaultLabel);
            mv.visitFrame(F_SAME, 0, null, 0, null);
        }
        
        enhanceForThrowingException(mv, IllegalArgumentException.class, "Method not found", "Ljava/lang/Object;", ALOAD, 2);
        
        mv.visitMaxs(8, 6);
        mv.visitEnd();		
	}
	
	@Override
	public abstract C newInstance();

	/**
	 * Retrieve the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @return The field value as an object
	 */
	public abstract Object getValue(C object, String fieldName);

	/**
	 * Update the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @param value The new value
	 */
	public abstract void putValue(C object, String fieldName, Object value);

	/**
	 * Retrieve the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @return The field value as a boolean
	 */
	public abstract boolean getBooleanValue(C object, String fieldName);

	/**
	 * Update the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @param value The new boolean value
	 */
	public abstract void putBooleanValue(C object, String fieldName, boolean value);

	/**
	 * Retrieve the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @return The field value as a byte
	 */
	public abstract byte getByteValue(C object, String fieldName);

	/**
	 * Update the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @param value The new byte value
	 */
	public abstract void putByteValue(C object, String fieldName, byte value);

	/**
	 * Retrieve the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @return The field value as a char
	 */
	public abstract char getCharValue(C object, String fieldName);

	/**
	 * Update the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @param value The new char value
	 */
	public abstract void putCharValue(C object, String fieldName, char value);

	/**
	 * Retrieve the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @return The field value as a short
	 */
	public abstract short getShortValue(C object, String fieldName);

	/**
	 * Update the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @param value The new short value
	 */
	public abstract void putShortValue(C object, String fieldName, short value);

	/**
	 * Retrieve the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @return The field value as a int
	 */
	public abstract int getIntValue(C object, String fieldName);

	/**
	 * Update the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @param value The new int value
	 */
	public abstract void putIntValue(C object, String fieldName, int value);

	/**
	 * Retrieve the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @return The field value as a long
	 */
	public abstract long getLongValue(C object, String fieldName);

	/**
	 * Update the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @param value The new long value
	 */
	public abstract void putLongValue(C object, String fieldName, long value);

	/**
	 * Retrieve the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @return The field value as a float
	 */
	public abstract float getFloatValue(C object, String fieldName);

	/**
	 * Update the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @param value The new float value
	 */
	public abstract void putFloatValue(C object, String fieldName, float value);

	/**
	 * Retrieve the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @return The field value as a double
	 */
	public abstract double getDoubleValue(C object, String fieldName);

	/**
	 * Update the value of the field for the given instance
	 * @param object The instance to access the field for
	 * @param fieldName Name of the field to access
	 * @param value The new double value
	 */
	public abstract void putDoubleValue(C object, String fieldName, double value);

	/**
	 * Invoke the given method on the given instance
	 * @param target The target object
	 * @param method The method to invoke
	 * @param args The parameters to pass
	 * @return The result of the invocation (null for a void method)
	 */
	public abstract Object invokeMethod(Object target, Method method, Object[] args);
	
	String foo = new String();
	
	public Object invokeMethod2(Object target, Method method, Object[] args) {
		return getMethodIndex(method);
	}
	
	private int getMethodIndex(Method m) {
		int idx = Arrays.binarySearch(getDeclaredMethodNames(), m.getName());
		if (getDeclaredMethodAccessors()[idx].method().equals(m)) {
			return idx;
		}
		int backTrack = idx;
		while (true) {
			if (getDeclaredMethodAccessors()[backTrack].method().equals(m)) {
				return idx;
			}
			if (!(getDeclaredMethodAccessors()[backTrack].method().getName()
					.equals(m.getName()))) {
				break;
			}
			backTrack = backTrack - 1;
		}
		while (true) {
			idx = idx + 1;
			if (getDeclaredMethodAccessors()[idx].method().equals(m)) {
				return idx;
			}
			if (!(getDeclaredMethodAccessors()[idx].method().getName().equals(m.getName()))) {
				break;
			}
		}
		return -1;
	}
	
	@Override
	protected MethodAccess<C> constructMethodAccess(Method method) {
		return AsmMethodAccess.get(this, method);
	}
	
	@Override
	protected FieldAccess<C> constructFieldAccess(Field field) {
		if ((field.getModifiers() & Modifier.PRIVATE) != 0) {
			return PortableFieldAccess.get(field);
		} else {
			return AsmFieldAccess.get(this, field);
		}
	}
	
	@Override
	protected <X> ClassAccess<X> constructClassAccess(Class<X> clazz) {
		return AsmClassAccess.get(clazz);
	}
}