/**
 * 
 */
package de.dhbwstuttgart.bytecode.utilities;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.signature.SignatureVisitor;
import org.objectweb.asm.signature.SignatureWriter;

import de.dhbwstuttgart.bytecode.Exception.NotInCurrentPackageException;
import de.dhbwstuttgart.bytecode.descriptor.DescriptorToString;
import de.dhbwstuttgart.bytecode.descriptor.TypeToDescriptor;
import de.dhbwstuttgart.bytecode.signature.Signature;
import de.dhbwstuttgart.bytecode.signature.TypeToSignature;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.FormalParameter;
import de.dhbwstuttgart.syntaxtree.GenericDeclarationList;
import de.dhbwstuttgart.syntaxtree.GenericTypeVar;
import de.dhbwstuttgart.syntaxtree.Method;
import de.dhbwstuttgart.syntaxtree.ParameterList;
import de.dhbwstuttgart.syntaxtree.SourceFile;
import de.dhbwstuttgart.syntaxtree.statement.Expression;
import de.dhbwstuttgart.syntaxtree.statement.LambdaExpression;
import de.dhbwstuttgart.syntaxtree.statement.MethodCall;
import de.dhbwstuttgart.syntaxtree.type.GenericRefType;
import de.dhbwstuttgart.syntaxtree.type.RefTypeOrTPHOrWildcardOrGeneric;
import de.dhbwstuttgart.syntaxtree.type.TypePlaceholder;
import de.dhbwstuttgart.typeinference.result.ResultSet;
import javassist.NotFoundException;

/**
 * @author fayez
 *
 */
public class MethodCallHelper {
	private MethodCall methCall;
	private SourceFile sourceFile;
	private ResultSet resultSet;
	private String path;

	/**
	 * @param methCall
	 * @param sourceFile
	 * @param resultSet
	 * @param path TODO
	 */
	public MethodCallHelper(MethodCall methCall, SourceFile sourceFile, ResultSet resultSet, String path) {
		this.methCall = methCall;
		this.sourceFile = sourceFile;
		this.resultSet = resultSet;
		this.path = path;
	}

	public String getResolvedType(RefTypeOrTPHOrWildcardOrGeneric type) {
		return resultSet.resolveType(type).resolvedType.acceptTV(new TypeToDescriptor());
	}

	public boolean isInCurrPkg(String className) {
		for (ClassOrInterface cl : sourceFile.KlassenVektor) {
			if (className.equals(cl.getClassName().toString()))
				return true;
		}
		return false;
	}

	public String getSuperClass(String className) throws NotInCurrentPackageException {
		
		for (ClassOrInterface cl : sourceFile.getClasses()) {
			if (className.equals(cl.getClassName().toString())) {
				return cl.getSuperClass().getName().toString();
			}
		}
		throw new NotInCurrentPackageException("Class " + className + " is not in the current package.");
	}

	public ClassOrInterface getClassFromCurrPkg(String className) throws NotInCurrentPackageException {
		for (ClassOrInterface cl : sourceFile.KlassenVektor) {
			if (className.equals(cl.getClassName().toString()))
				return cl;
		}
		throw new NotInCurrentPackageException("Class of " + className + " is not in the current package.");
	}

	public String getDesc(String className) throws NotInCurrentPackageException, NotFoundException {
		String name = methCall.name;
		ClassOrInterface clazz = getClassFromCurrPkg(className);
		String retType = getResolvedType(methCall.getType());
		ArrayList<String> params = getTypes(methCall.arglist.getArguments());
		
		Map<String, String> genAndBoundsClass = getGenericsAndBounds(clazz.getGenerics());
		modifyGenAndBounds(genAndBoundsClass);
		for (Method m : clazz.getMethods()) {
			if (name.equals(m.getName()) && retType.equals(getResolvedType(m.getReturnType()))) {
				ArrayList<String> paramsOfM = getTypes(m.getParameterList());
				if(areEquals(params,paramsOfM)) {
					Map<String, String> genAndBoundsMethod = getGenericsAndBoundsMethod(m.getGenerics());
					modifyGenAndBounds(genAndBoundsMethod);
					boolean hasGen = hasGen(m, genAndBoundsClass);
					NormalMethod nm = new NormalMethod(m, (HashMap<String, String>) genAndBoundsClass,
							(HashMap<String, String>) genAndBoundsMethod, hasGen);
					return nm.accept(new DescriptorToString(resultSet));
				}
			}
		}

		throw new NotFoundException("Method " + name + " is not found");
	}

	private boolean areEquals(ArrayList<String> params, ArrayList<String> paramsOfM) {
		if(params.size() != paramsOfM.size())
			return false;
		
		for(String t : params) {
			for(String t2 : paramsOfM) {
				if(!t.equals(t2))
					return false;
			}
		}
		return true;
	}

	private ArrayList<String> getTypes(ParameterList parameterList) {
		Iterator<FormalParameter> itr = parameterList.iterator();
		ArrayList<String> typeList = new ArrayList<>();
		while (itr.hasNext()) {
			FormalParameter fp = itr.next();
			String t = getResolvedType(fp.getType());
			typeList.add(t);
		}
		
		return typeList;
	}

	private ArrayList<String> getTypes(List<Expression> arguments) {
		ArrayList<String> types = new ArrayList<>();
		for(int i = 0; i<arguments.size(); ++i) {
			String t = getResolvedType(arguments.get(i).getType());
			types.add(t);
		}
		return types;
	}

	private boolean hasGen(Method m, Map<String, String> genericsAndBounds) {
		String retType = resultSet.resolveType(m.getReturnType()).resolvedType.acceptTV(new TypeToSignature());
		/*Prüfe, ob die Rückgabe-Type der Methode eine Type-Variable ist*/
		boolean hasGenInParameterList = genericsAndBounds.containsKey(retType) || retType.contains("TPH ") || retType.contains("<");

		Map<String,RefTypeOrTPHOrWildcardOrGeneric> methodParamsAndTypes = new HashMap<>();
		Iterator<FormalParameter> itr = m.getParameterList().iterator();
		while(itr.hasNext()) {
			FormalParameter fp = itr.next();
			methodParamsAndTypes.put(fp.getName(), resultSet.resolveType(fp.getType()).resolvedType);
		}
		/*Wenn die Rückgabe-Type eine Typ-variable ist, erzeuge direkt die Signature, wenn nicht,
		 * prüfe, ob einer der Parameter Typ-Variable als Typ hat*/
		if(!hasGenInParameterList) {
			for(String paramName : methodParamsAndTypes.keySet()) {
				String typeOfParam = methodParamsAndTypes.get(paramName).acceptTV(new TypeToDescriptor());
				String sigOfParam = methodParamsAndTypes.get(paramName).acceptTV(new TypeToSignature());
				if(genericsAndBounds.containsKey(typeOfParam)||typeOfParam.contains("TPH ")||sigOfParam.contains("<")) {
					hasGenInParameterList = true;
					break;
				}
			}
		}
		return m.getGenerics().iterator().hasNext() || hasGenInParameterList;
	}

	private Map<String, String> getGenericsAndBoundsMethod(Iterable<? extends GenericTypeVar> generics) {
		Map<String, String> genAndBounds = new HashMap<>();
		Iterator<? extends GenericTypeVar> itr = generics.iterator();
		while (itr.hasNext()) {
			GenericTypeVar gtv = itr.next();
			getBoundsOfTypeVar(gtv, genAndBounds);
		}
		return genAndBounds;
	}

	private void modifyGenAndBounds(Map<String, String> genAndBoundsClass) {
		List<String> visited = new ArrayList<>(genAndBoundsClass.size());
		Map<String, String> toReplace = new HashMap<>();
		for (String tv : genAndBoundsClass.keySet()) {

			if (visited.contains(tv))
				continue;

			List<String> types = new LinkedList<>();
			String bound = genAndBoundsClass.get(tv);
			types.add(tv);
			visited.add(tv);
			boolean doReplace = false;
			while (genAndBoundsClass.keySet().contains(bound)) {
				doReplace = true;
				types.add(bound);
				visited.add(bound);
				bound = genAndBoundsClass.get(bound);
			}

			if (doReplace) {
				for (String tt : types) {
					toReplace.put(tt, bound);
				}
			}
		}

		for (String key : toReplace.keySet()) {
			genAndBoundsClass.replace(key, toReplace.get(key));
		}

	}

	private Map<String, String> getGenericsAndBounds(GenericDeclarationList generics) {
		Map<String, String> genAndBounds = new HashMap<>();
		Iterator<GenericTypeVar> itr = generics.iterator();
		while (itr.hasNext()) {
			GenericTypeVar gtv = itr.next();
			getBoundsOfTypeVar(gtv, genAndBounds);
		}
		return genAndBounds;
	}

	private void getBoundsOfTypeVar(GenericTypeVar g, Map<String, String> genAndBounds) {

		Iterator<? extends RefTypeOrTPHOrWildcardOrGeneric> bItr = g.getBounds().iterator();
		while (bItr.hasNext()) {
			RefTypeOrTPHOrWildcardOrGeneric b = bItr.next();
			String boundDesc = b.acceptTV(new TypeToDescriptor());
			genAndBounds.put(g.getName(), boundDesc);
		}
	}

	public String generateBCForFunN() {
		ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);

		SignatureWriter methSig = new SignatureWriter();

		int numberOfParams = 0;
		SignatureVisitor paramVisitor = methSig.visitParameterType();
		Iterator<Expression> itr1 = methCall.arglist.getArguments().iterator();
		String methDesc = "(";
		while(itr1.hasNext()) {
			numberOfParams++;
			// getBounds
			paramVisitor.visitTypeVariable("T" + numberOfParams);
			methDesc += "L" + Type.getInternalName(Object.class) + ";";
			itr1.next();
		}
		methDesc += ")L" + Type.getInternalName(Object.class) + ";";
		
		methSig.visitReturnType().visitTypeVariable("R");
		// ")"+lam.getReturn.getBounds
		Signature sig = new Signature(numberOfParams);
		String name = "Fun" + numberOfParams + "$$";
		classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC+Opcodes.ACC_INTERFACE + Opcodes.ACC_ABSTRACT, name, sig.toString(),
				Type.getInternalName(Object.class), null);
		MethodVisitor mvApply = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "apply", methDesc,
				methSig.toString(), null);
		mvApply.visitEnd();
		writeClassFile(classWriter.toByteArray(), name);
		return methDesc;
	}
	
	public String generateBCForFunN(String returnType, String[] paramTypes) {
		ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);

		SignatureWriter methSig = new SignatureWriter();

		int numberOfParams = 0;
		SignatureVisitor paramVisitor = methSig.visitParameterType();
		Iterator<Expression> itr1 = methCall.arglist.getArguments().iterator();
		String methDesc = "(";
		while(itr1.hasNext()) {
			numberOfParams++;
			// getBounds
			paramVisitor.visitTypeVariable("T" + numberOfParams);
			methDesc += "L" + paramTypes[numberOfParams-1] + ";";
			itr1.next();
		}
		methDesc += ")L" + returnType + ";";
		
		methSig.visitReturnType().visitTypeVariable("R");
		// ")"+lam.getReturn.getBounds
		Signature sig = new Signature(numberOfParams,returnType,paramTypes);
		String name = "Fun" + numberOfParams + "$$";
		classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC+Opcodes.ACC_INTERFACE + Opcodes.ACC_ABSTRACT, name, sig.toString(),
				Type.getInternalName(Object.class), null);
		MethodVisitor mvApply = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "apply", methDesc,
				methSig.toString(), null);
		mvApply.visitEnd();
		writeClassFile(classWriter.toByteArray(), name);
		return methDesc;
	}

	private void writeClassFile(byte[] bytecode, String name) {
		FileOutputStream output;
		try {
			System.out.println("generating " + name + ".class file...");
			output = new FileOutputStream(
					new File(path + name + ".class"));
			output.write(bytecode);
			output.close();
			System.out.println(name + ".class file generated");
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}
	
	/**
	 * @param methodCall
	 */
	public void createCheckCast(MethodCall methodCall, MethodVisitor mv) {
		String checkCast = getResolvedType(methodCall.getType());
		if(!checkCast.contains("TPH ")) {
			int pos = checkCast.length();
			if(checkCast.contains("<"))
				pos = checkCast.indexOf("<");
			mv.visitTypeInsn(Opcodes.CHECKCAST,checkCast.substring(0,pos));
		}
	}

}
