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

import de.dhbwstuttgart.bytecode.Exception.NotInCurrentPackageException;
import de.dhbwstuttgart.bytecode.descriptor.DescriptorToString;
import de.dhbwstuttgart.bytecode.descriptor.TypeToDescriptor;
import de.dhbwstuttgart.bytecode.signature.TypeToSignature;
import de.dhbwstuttgart.syntaxtree.*;
import de.dhbwstuttgart.syntaxtree.statement.Expression;
import de.dhbwstuttgart.syntaxtree.statement.MethodCall;
import de.dhbwstuttgart.syntaxtree.type.RefTypeOrTPHOrWildcardOrGeneric;
import de.dhbwstuttgart.typeinference.result.ResultSet;
import javassist.NotFoundException;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.util.*;

/**
 * @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 void generateBCForFunN(String methodDescriptor) {
		ByteCodeForFunNGenerator.generateBCForFunN(methCall.arglist,methodDescriptor,path);
	}

	public String getDescriptorOfApplyMethod(String methodCallType) {
		return new DescriptorToString().createDescForFunN(methCall.arglist, methodCallType);
	}
	
	/**
	 * @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));
		}
	}

}
