/*
 * Copyright 2010-2012 VMware and contributors
 *
 * 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.springsource.loaded;

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

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.springsource.loaded.Utils.ReturnType;


/**
 * Builder that creates the dispatcher. The dispatcher is the implementation of the interface extracted for a type which then
 * delegates to the executor. A new dispatcher (and executor) is built for each class reload.
 * 
 * @author Andy Clement
 * @since 0.5.0
 */
public class DispatcherBuilder {

	/**
	 * Factory method that builds the dispatcher for a specified reloadabletype.
	 * 
	 * @param rtype the reloadable type
	 * @param newVersionTypeDescriptor the descriptor of the new version (the executor will be generated according to this)
	 * @param versionstamp the suffix that should be appended to the generated dispatcher
	 * @return the bytecode for the new dispatcher
	 */
	public static byte[] createFor(ReloadableType rtype, IncrementalTypeDescriptor newVersionTypeDescriptor, String versionstamp) {
		ClassReader fileReader = new ClassReader(rtype.interfaceBytes);
		DispatcherBuilderVisitor dispatcherVisitor = new DispatcherBuilderVisitor(rtype, newVersionTypeDescriptor, versionstamp);
		fileReader.accept(dispatcherVisitor, 0);
		return dispatcherVisitor.getBytes();
	}

	/**
	 * Whilst visiting the interface, the implementation is created.
	 */
	static class DispatcherBuilderVisitor extends ClassVisitor implements Opcodes, Constants {

		private ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);

		private String classname;
		private String executorClassName;
		private String suffix;
		private ReloadableType rtype;
		private IncrementalTypeDescriptor typeDescriptor;

		public DispatcherBuilderVisitor(ReloadableType rtype, IncrementalTypeDescriptor typeDescriptor, String suffix) {
			super(ASM5);
			this.classname = rtype.getSlashedName();
			this.typeDescriptor = typeDescriptor;
			this.suffix = suffix;
			this.rtype = rtype;
			this.executorClassName = Utils.getExecutorName(classname, suffix);
		}

		public byte[] getBytes() {
			return cw.toByteArray();
		}

		public void visit(int version, int flags, String name, String signature, String superclassName, String[] interfaceNames) {
			String dispatcherName = Utils.getDispatcherName(classname, suffix);
			cw.visit(version, Opcodes.ACC_PUBLIC, dispatcherName, null, "java/lang/Object",
					new String[] { Utils.getInterfaceName(classname), "org/springsource/loaded/__DynamicallyDispatchable" });
			generateDefaultConstructor();
		}

		private void generateDefaultConstructor() {
			MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
			mv.visitCode();
			mv.visitVarInsn(ALOAD, 0);
			mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
			mv.visitInsn(RETURN);
			mv.visitMaxs(1, 1);
			mv.visitEnd();
		}

		private void generateClinitDispatcher() {
			MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, mStaticInitializerName, "()V", null, null);
			mv.visitCode();
			mv.visitMethodInsn(INVOKESTATIC, executorClassName, mStaticInitializerName, "()V", false);
			mv.visitInsn(RETURN);
			mv.visitMaxs(1, 1);
			mv.visitEnd();
		}

		public AnnotationVisitor visitAnnotation(String arg0, boolean arg1) {
			return null;
		}

		public void visitAttribute(Attribute arg0) {
		}

		public void visitEnd() {
		}

		public FieldVisitor visitField(int arg0, String arg1, String arg2, String arg3, Object arg4) {
			return null;
		}

		public void visitInnerClass(String arg0, String arg1, String arg2, int arg3) {
		}

		public MethodVisitor visitMethod(int flags, String name, String descriptor, String signature, String[] exceptions) {
			if (name.equals(mDynamicDispatchName)) {
				generateDynamicDispatchMethod(name, descriptor, signature, exceptions);
			} else if (!name.equals("<init>")) {
				generateRegularMethod(name, descriptor, signature, exceptions);
			}
			return null;
		}

		/**
		 * Generate the body of the dynamic dispatcher method. This method is responsible for calling all the methods that are added
		 * to a type after the first time it is defined.
		 */
		private void generateDynamicDispatchMethod(String name, String descriptor, String signature, String[] exceptions) {
			final int indexDispatcherInstance = 0;
			final int indexArgs = 1;
			final int indexTarget = 2;
			final int indexNameAndDescriptor = 3;

			// Should be generating the code for each additional method in
			// the executor (new version) that wasn't in the original. 
			MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC , name, descriptor, signature, exceptions);
			mv.visitCode();

			// Entries required here for all methods that exist in the new version but didn't exist in the original version
			// There should be no entries for catchers

			int maxStack = 0;
			// Basically generate a long if..else sequence for each method
			List<MethodMember> methods = new ArrayList<MethodMember>(typeDescriptor.getNewOrChangedMethods());

			// these are added because we may be calling through the dynamic dispatcher if calling from an invokeinterface - the invokeinterface
			// will call __execute on the interface, which is then implemented by the real class - but it may be that the
			// actual type implementing the interface already implements that method - if the dispatcher doesn't recognize
			// it then we may go bang

			//			System.out.println("Generating __execute in type " + classname);
			for (MethodMember m : typeDescriptor.getOriginal().getMethods()) {
				methods.add(m);
			}

			for (MethodMember method : methods) {
				if (MethodMember.isCatcher(method) || MethodMember.isSuperDispatcher(method)) { // for reason above, may also need to consider catchers here - what if an interface is changed to add a toString() method, for example
					continue;
					// would the implementation for a catcher call the super catcher?
				}
				//				System.out.println("Generating handler for " + method.name);
				String nameWithDescriptor = new StringBuilder(method.name).append(method.descriptor).toString();

				// 2. Load the input name+descriptor and compare it with this method:
				mv.visitVarInsn(ALOAD, 3);
				mv.visitLdcInsn(nameWithDescriptor);
				mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
				Label label = new Label();
				mv.visitJumpInsn(IFEQ, label); // means if false

				// 3. Generate the code that will call the method on the executor:
				if (!method.isStatic()) {
					mv.visitVarInsn(Opcodes.ALOAD, 2);
					mv.visitTypeInsn(CHECKCAST, classname);
				}
				String callDescriptor = method.isStatic() ? method.descriptor : Utils.insertExtraParameter(classname,
						method.descriptor);

				int pcount = Utils.getParameterCount(method.descriptor);
				if (pcount > maxStack) {
					pcount = maxStack;
				}

				// 4. Unpack parameter array to fit the descriptor for that method
				Utils.generateInstructionsToUnpackArrayAccordingToDescriptor(mv, method.descriptor, 1);

				ReturnType returnType = Utils.getReturnTypeDescriptor(method.descriptor);
				mv.visitMethodInsn(Opcodes.INVOKESTATIC, executorClassName, method.name, callDescriptor, false);
				if (returnType.isVoid()) {
					mv.visitInsn(ACONST_NULL);
				} else if (returnType.isPrimitive()) {
					Utils.insertBoxInsns(mv, returnType.descriptor);
				}
				mv.visitInsn(Opcodes.ARETURN);
				mv.visitLabel(label);
			}
			for (MethodMember ctor : typeDescriptor.getLatestTypeDescriptor().getConstructors()) {
				String nameWithDescriptor = new StringBuilder(ctor.name).append(ctor.descriptor).toString();

				// 2. Load the input name+descriptor and compare it with this method:
				//    if (nameAndDescriptor.equals(xxx)) {
				mv.visitVarInsn(ALOAD, indexNameAndDescriptor);
				mv.visitLdcInsn(nameWithDescriptor);
				mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
				Label label = new Label();
				mv.visitJumpInsn(IFEQ, label); // means if false

				// 3. Generate the code that will call the method on the executor:
				mv.visitVarInsn(Opcodes.ALOAD, 2);
				mv.visitTypeInsn(CHECKCAST, classname);
				String callDescriptor = Utils.insertExtraParameter(classname, ctor.descriptor);

				int pcount = Utils.getParameterCount(ctor.descriptor);
				if (pcount > maxStack) {
					pcount = maxStack;
				}

				// 4. Unpack parameter array to fit the descriptor for that method
				Utils.generateInstructionsToUnpackArrayAccordingToDescriptor(mv, ctor.descriptor, 1);

				//				ReturnType returnType = Utils.getReturnTypeDescriptor(method.descriptor);
				mv.visitMethodInsn(Opcodes.INVOKESTATIC, executorClassName, "___init___", callDescriptor, false);
				//				if (returnType.isVoid()) {
				mv.visitInsn(ACONST_NULL);
				//				} else if (returnType.isPrimitive()) {
				//					Utils.insertBoxInsns(mv, returnType.descriptor);
				//				}
				mv.visitInsn(Opcodes.ARETURN);
				mv.visitLabel(label);
			}

			// 5. Throw exception as dynamic dispatcher has been called for something it shouldn't have

			// At this point we failed to find it as a method we can dispatch to our executor, so we want
			// to pass it 'up' to our supertype.  We need to get the dispatcher for our superclass
			// and then call the __execute() on it, assuming that it will be able to handle this request.		

			// alternative 1: use the dispatcher for the superclass

			// Determine the supertype
			String slashedSupertypeName = rtype.getTypeDescriptor().getSupertypeName();

			// getDispatcher will give us the dispatcher for the supertype
			mv.visitFieldInsn(Opcodes.GETSTATIC, slashedSupertypeName, fReloadableTypeFieldName, lReloadableType);
			mv.visitMethodInsn(INVOKEVIRTUAL, tReloadableType, "getDispatcher",
					"()Lorg/springsource/loaded/__DynamicallyDispatchable;", false);

			// alternative 2: find the right dispatcher - i.e. who in the super hierarchy provides that nameAndDescriptor

			// now invoke the dynamic dispatch call on that dispatcher
			mv.visitVarInsn(ALOAD, indexArgs);
			mv.visitVarInsn(ALOAD, indexTarget);
			mv.visitVarInsn(ALOAD, indexNameAndDescriptor);
			mv.visitMethodInsn(INVOKEINTERFACE, tDynamicallyDispatchable, mDynamicDispatchName, mDynamicDispatchDescriptor, false);
			mv.visitInsn(ARETURN);

			//			mv.visitTypeInsn(NEW, "java/lang/IllegalStateException");
			//			mv.visitInsn(DUP);
			//			mv.visitVarInsn(ALOAD, 3);
			//			mv.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalStateException", "<init>", "(Ljava/lang/String;)V");
			//			mv.visitInsn(ATHROW);
			mv.visitMaxs(maxStack, 6);
			mv.visitEnd();
		}

		/**
		 * Called to generate the implementation of a normal method on the interface - a normal method is one that did exist when
		 * the type was first defined. Might be a catcher.
		 */
		private void generateRegularMethod(String name, String descriptor, String signature, String[] exceptions) {
			// The original descriptor is how it was defined on the original type and how it is defined in the executor class.
			// The original descriptor is this descriptor with the first parameter trimmed off. 
			boolean isClinit = name.equals("___clinit___");
			String originalDescriptor = isClinit ? descriptor : Utils.stripFirstParameter(descriptor);
			MethodMember method = null;

			// Detect if the name has been modified for clash avoidance reasons
			if (name.equals("___init___")) {
				// it is a ctor
				method = rtype.getConstructor(originalDescriptor);
			} else {
				if (isClinit) {
					generateClinitDispatcher();
					return;
				} else {
					// TODO need a better solution that these __
					if (name.startsWith("__") && !name.equals("__$swapInit")) { // __$swapInit is the groovy reset method
						// clash avoidance name
						method = rtype.getMethod(name.substring(2), originalDescriptor);
					} else {
						method = rtype.getMethod(name, originalDescriptor);
					}
				}
			}
			boolean isStatic = method.isStatic();

			MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, name, descriptor, signature, exceptions);
			mv.visitCode();
			// The input descriptor will include the extra initial parameter (the instance, or null for static methods)
			ReturnType returnTypeDescriptor = Utils.getReturnTypeDescriptor(descriptor);
			// For a static method the first parameter can be ignored
			int params = Utils.getParameterCount(descriptor);
			String callDescriptor = isStatic ? originalDescriptor : descriptor;
			Utils.createLoadsBasedOnDescriptor(mv, callDescriptor, isStatic ? 2 : 1);
			mv.visitMethodInsn(INVOKESTATIC, executorClassName, name, callDescriptor, false);
			Utils.addCorrectReturnInstruction(mv, returnTypeDescriptor, false);
			mv.visitMaxs(params, params + 1);
			mv.visitEnd();
		}

		public void visitOuterClass(String arg0, String arg1, String arg2) {
		}

		public void visitSource(String arg0, String arg1) {
		}

	}

}
