package de.dhbwstuttgart.bytecode.signature;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;

import org.objectweb.asm.Type;
import org.objectweb.asm.signature.SignatureVisitor;
import org.objectweb.asm.signature.SignatureWriter;

import de.dhbwstuttgart.bytecode.constraint.ExtendsConstraint;
import de.dhbwstuttgart.bytecode.constraint.TPHConstraint;
import de.dhbwstuttgart.bytecode.constraint.TPHConstraint.Relation;
import de.dhbwstuttgart.bytecode.descriptor.TypeToDescriptor;
import de.dhbwstuttgart.bytecode.utilities.Simplify;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.Constructor;
import de.dhbwstuttgart.syntaxtree.GenericTypeVar;
import de.dhbwstuttgart.syntaxtree.Method;
import de.dhbwstuttgart.syntaxtree.statement.LambdaExpression;
import de.dhbwstuttgart.syntaxtree.type.ExtendsWildcardType;
import de.dhbwstuttgart.syntaxtree.type.GenericRefType;
import de.dhbwstuttgart.syntaxtree.type.RefType;
import de.dhbwstuttgart.syntaxtree.type.RefTypeOrTPHOrWildcardOrGeneric;
import de.dhbwstuttgart.syntaxtree.type.SuperWildcardType;
import de.dhbwstuttgart.syntaxtree.type.TypePlaceholder;
import de.dhbwstuttgart.syntaxtree.type.WildcardType;
import de.dhbwstuttgart.typeinference.result.GenericInsertPair;
import de.dhbwstuttgart.typeinference.result.ResolvedType;
import de.dhbwstuttgart.typeinference.result.ResultSet;

public class Signature {
	private ClassOrInterface classOrInterface;
	private HashMap<String, String> genericsAndBounds;
	private HashMap<String, String> genericsAndBoundsMethod;
	private SignatureWriter sw;
	private Constructor constructor;
	private Method method;
	private HashMap<String,RefTypeOrTPHOrWildcardOrGeneric> methodParamsAndTypes;
	private ResultSet resultSet;
	private ArrayList<GenericInsertPair> commonPairs;
	private HashMap<TPHConstraint,HashSet<String>> methodConstraints;
	private ArrayList<String> tphsClass;
	private ArrayList<TPHConstraint> consClass;
	
	public Signature(ClassOrInterface classOrInterface, HashMap<String, String> genericsAndBounds,
			ArrayList<GenericInsertPair> commonPairs, ArrayList<String> tphsClass, ArrayList<TPHConstraint> consClass) {
		this.classOrInterface = classOrInterface;
		this.genericsAndBounds = genericsAndBounds;
		this.commonPairs = commonPairs;
		this.tphsClass = tphsClass;
		this.consClass = consClass;
		sw = new SignatureWriter();
		createSignatureForClassOrInterface();
	}

	public Signature(Constructor constructor, HashMap<String, String> genericsAndBounds, 
			HashMap<String,RefTypeOrTPHOrWildcardOrGeneric> methodParamsAndTypes,ResultSet resultSet,
			HashMap<TPHConstraint,HashSet<String>> methodConstraints) {
		this.constructor = constructor;
		this.genericsAndBounds = genericsAndBounds;
		this.methodParamsAndTypes = methodParamsAndTypes;
		this.resultSet = resultSet;
		this.methodConstraints = methodConstraints;
		sw = new SignatureWriter();
		createSignatureForConsOrMethod(this.constructor,true);
	}

	public Signature(Method method, HashMap<String, String> genericsAndBoundsMethod,HashMap<String, String> genericsAndBounds,
			HashMap<String, RefTypeOrTPHOrWildcardOrGeneric> methodParamsAndTypes, ResultSet resultSet, 
			HashMap<TPHConstraint,HashSet<String>> methodConstraints) {
		this.method = method;
		this.genericsAndBoundsMethod = genericsAndBoundsMethod;
		this.genericsAndBounds = genericsAndBounds;
		this.methodParamsAndTypes = methodParamsAndTypes;
		this.resultSet = resultSet;
		this.methodConstraints = methodConstraints;
		sw = new SignatureWriter();
		createSignatureForConsOrMethod(this.method,false);
	}
	
	public Signature(int numberOfParams) {
		sw = new SignatureWriter();
		createSignatureForFunN(numberOfParams);
	}

	private void createSignatureForFunN(int numberOfParams) {
		
//		sw.visitClassBound().visitEnd();
		for(int i = 0;i<numberOfParams;i++) {
			int j = i+1;
			sw.visitFormalTypeParameter("T"+ j);
			// getBounds von Params
			sw.visitClassBound().visitClassType(Type.getInternalName(Object.class));
			sw.visitClassBound().visitEnd();
		}

		sw.visitFormalTypeParameter("R");
		// getBounds vom Return-Type
		sw.visitClassBound().visitClassType(Type.getInternalName(Object.class));
		sw.visitClassBound().visitEnd();
		// TODO: prüfe ob Return-Type = void,
		sw.visitSuperclass().visitClassType(Type.getInternalName(Object.class));;
		sw.visitEnd();
	}

	/**
	 * Creates signature for a method or constructor with @see {@link SignatureWriter}
	 * Signature looks like: 
	 * <typevaliables (K:Ljava/lang/Object "Bounds")>(params L.. OR T.. Or basistape)ReturnType
	 * 
	 * @param method method or constructor
	 * @param isConstructor true if constructor
	 */
	private void createSignatureForConsOrMethod(Method method, boolean isConstructor) {
		Iterator<? extends GenericTypeVar> itr = method.getGenerics().iterator();
		// visits all formal type parameter and visits their bounds <T:...;B:...;...>
		while(itr.hasNext()) {
			System.out.println("HAS GENERICS!!");
			GenericTypeVar g = itr.next();
			getBoundsOfTypeVar(g,genericsAndBoundsMethod);
		}
		
		// Wenn die RückgabeType eine TPH ist, wird als generic behandelt
		// z.B: Type = TPH K => wird eine Formal Type Parameter K$ erzeugt und Bound = Object
		if(!isConstructor) {
			String ret = resultSet.resolveType(method.getReturnType()).resolvedType.acceptTV(new TypeToSignature());
			ArrayList<TPHConstraint> allConsBeforeSimplify = new ArrayList<>();
			
			HashMap<TPHConstraint,HashSet<String>> allConstraints = new HashMap<>();
			
			if(ret.contains("<")) {
				allConsBeforeSimplify = getAllConstraints((RefType) resultSet.resolveType(method.getReturnType()).resolvedType);
			}
			 
			for(String paramName : methodParamsAndTypes.keySet()) {
				RefTypeOrTPHOrWildcardOrGeneric t = methodParamsAndTypes.get(paramName);
				String pT = t.acceptTV(new TypeToSignature()); 
				
				if(pT.contains("<"))
					allConsBeforeSimplify = getAllConstraints((RefType) t);
			}
			boolean doSimplify = false;
			if(!allConsBeforeSimplify.isEmpty()) {
				addConstraintsToMap(allConstraints,allConsBeforeSimplify);
				doSimplify = true;
			}
			createTypeVars(allConstraints, doSimplify);
			
			if(!ret.equals("V")) {
				if(ret.contains("$") && !ret.contains("$$") && !ret.contains("<")) {
					if(genericsAndBounds.containsKey(ret)) {
						genericsAndBoundsMethod.put(ret.substring(1), genericsAndBounds.get(ret.substring(1)));
					}
				}
				
			}
		}
		
		// visit each method-parameter to create the signature 
		for(String paramName : methodParamsAndTypes.keySet()) {
			RefTypeOrTPHOrWildcardOrGeneric t = methodParamsAndTypes.get(paramName);
			// parameter type deswegen ist true
			doVisitParamsOrReturn(t,true);
		}
		if(isConstructor || 
				resultSet.resolveType(method.getReturnType()).resolvedType.acceptTV(new TypeToSignature()).equals("V")) {
			sw.visitReturnType().visitBaseType('V');
		}else {
			RefTypeOrTPHOrWildcardOrGeneric returnType = method.getReturnType();
			// return type deswegen ist false
			doVisitParamsOrReturn(returnType, false);
		}
//		sw.visitEnd();
	}
	
	private void addConstraintsToMap(HashMap<TPHConstraint, HashSet<String>> allConstraints,
			ArrayList<TPHConstraint> allConsBeforeSimplify) {
		for(TPHConstraint tphCons : allConsBeforeSimplify) {
			allConstraints.put(tphCons, null);
		}
	}

	private String getEqualTPH(HashMap<TPHConstraint, HashSet<String>> methodConstraints, String tph) {
		for(TPHConstraint cons : methodConstraints.keySet()) {
			if(methodConstraints.get(cons) != null) {
				if(methodConstraints.get(cons).contains(tph)) {
					return cons.getLeft();
				}
			}
		}
		return tph;
	}

	private void createTypeVars(HashMap<TPHConstraint, HashSet<String>> allConstraints, boolean doSimplify) {
		allConstraints.putAll(methodConstraints);
		
		HashMap<TPHConstraint, HashSet<String>> simplifiedConstraints;
		if(doSimplify) {
			simplifiedConstraints = Simplify.simplifyContraints(allConstraints);
		}else {
			simplifiedConstraints = allConstraints;
		}
		
		for(TPHConstraint cons : simplifiedConstraints.keySet()) {
			// need subst.
			String sub = cons.getLeft()+"$";
			if(!genericsAndBoundsMethod.containsKey(sub) && !genericsAndBounds.containsKey(sub)) {
				sw.visitFormalTypeParameter(sub);
				String bound = cons.getRight();
				if(bound.equals(Type.getInternalName(Object.class))) {
					sw.visitClassBound().visitClassType(Type.getInternalName(Object.class));
					sw.visitClassBound().visitEnd();
					genericsAndBoundsMethod.put(sub, bound);
				} else {
					String boundName = bound+"$";
					sw.visitClassBound().visitTypeVariable(boundName);
					genericsAndBoundsMethod.put(sub, boundName);
				}
			}
			
		}
	}
	
	private ArrayList<TPHConstraint> getAllConstraints(RefType ref) {
		final ArrayList<TPHConstraint> res = new ArrayList<>();
		for(RefTypeOrTPHOrWildcardOrGeneric p : ref.getParaList()) {
			ResolvedType resType;
			if(p instanceof WildcardType) {
				resType = resultSet.resolveType(((WildcardType) p).getInnerType());
			}else {
				resType = resultSet.resolveType(p);
			}
			
			RefTypeOrTPHOrWildcardOrGeneric resolved = resType.resolvedType;
			if(resolved instanceof TypePlaceholder) {
				//resType.getAdditionalGenerics().forEach(ag ->{
				resultSet.genIns.forEach(ag ->{
					TPHConstraint constr = new ExtendsConstraint(ag.getLeft().getName(), ag.getRight().getName(), Relation.EXTENDS);
					if(!contains(res,constr)) {
						res.add(constr);
					}
				});
			}
			if(resolved instanceof WildcardType) {
				WildcardType resWC = (WildcardType) resolved;
				ResolvedType resType2 = resultSet.resolveType(resWC.getInnerType());
				if(resType2.resolvedType instanceof TypePlaceholder) {
					//resType2.getAdditionalGenerics().forEach(ag ->{
					resultSet.genIns.forEach(ag ->{
						TPHConstraint constr = new ExtendsConstraint(ag.getLeft().getName(), ag.getRight().getName(), Relation.EXTENDS);
						if(!contains(res,constr)) {
							res.add(constr);
						}
				});
				}
			}
				
		}
		System.out.println("RES GIP === " + res.size());
		return res;
	}
	private boolean contains(ArrayList<TPHConstraint> constraints, TPHConstraint constr) {
		for(int i=0; i<constraints.size();++i) {
			TPHConstraint p = constraints.get(i);
			if(constr.getLeft().equals(p.getLeft()) && constr.getRight().equals(p.getRight()))
				return true;
		}
		return false;
	}

	/**
	 * Visits parameter type or return type with {@link SignatureVisitor} to create
	 * the method signature
	 * @param t type of parameter or return type
	 * @param isParameterType true if t is type of parameter
	 */
	private void doVisitParamsOrReturn(RefTypeOrTPHOrWildcardOrGeneric t, boolean isParameterType) {
		String type = t.acceptTV(new TypeToString());
		SignatureVisitor sv;
		if(isParameterType) {
			 sv = sw.visitParameterType();
		}
		else {
			sv = sw.visitReturnType();
		}
		switch (type) {
		case "RT":
			String sig = t.acceptTV(new TypeToSignature());
			sv.visitClassType(sig.substring(1, sig.length()));
			break;
		case "GRT":
			GenericRefType g = (GenericRefType) t;
//			sv.visitTypeVariable(g.acceptTV(new TypeToSignature()).substring(1));
			sv.visitTypeVariable(g.acceptTV(new TypeToSignature()));
			break;
		case "TPH":
			RefTypeOrTPHOrWildcardOrGeneric r = resultSet.resolveType(t).resolvedType;
			// der Fall wenn die Type eine Interface ist, muss betrachtet werden
			// Deswegen muss in ResutSet noch enthalten werden, ob die Type eine
			// Interface oder eine Klasse ist.
			
			// das braucht man nicht es reicht: sv.visitTypeVariable(r.acceptTV(new TypeToSignature())
			// 
			String sig2 = r.acceptTV(new TypeToSignature());
			if(r instanceof GenericRefType) {
				sv.visitTypeVariable(sig2);
			}else if(!(r instanceof TypePlaceholder)) {
				if(sig2.contains("$$")) {
					System.out.println(" Signature FUN$$: "+r);
					sv.visitInterface().visitClassType(sig2.substring(1, sig2.length()));
				} else {
					// Kann zwischen GenericRefType und RefType nicht unterscheiden
					// Deswegen wird immer geprüft, ob der Name in Generic Maps liegt
					String n = sig2.substring(1, sig2.length()-1);
//					if(genericsAndBoundsMethod.containsKey(n) || genericsAndBounds.containsKey(n)) {
//						sv.visitTypeVariable(n);
//					} else {
						sv.visitClassType(n);
						sv.visitEnd();
//					}
//					sv.visitClassType(n);
				}
				
			} else {
				String eqTPH = getEqualTPH(methodConstraints, sig2.substring(1, sig2.length()-1))+"$";
				System.out.println(r.getClass()+" Signature TPH: "+r.acceptTV(new TypeToSignature()));
				sv.visitTypeVariable(eqTPH);
			}
			
			break;
			
		case "SWC":
			System.out.println("SWC---Signature");
			SuperWildcardType swc = (SuperWildcardType) t;
			String sigInner = swc.getInnerType().acceptTV(new TypeToSignature());
			if(swc.getInnerType() instanceof TypePlaceholder) {
				sv.visitTypeArgument('-').visitTypeVariable(sigInner.substring(1, sigInner.length()));
			} else if(swc.getInnerType() instanceof RefType) {
				if(sigInner.contains("$$")) {
					sv.visitTypeArgument('-').visitInterface().visitClassType(sigInner.substring(1,sigInner.length()));
				}else {
					sv.visitTypeArgument('-').visitClassType(sigInner.substring(1,sigInner.length()));
				}
			}else {
				sv.visitTypeArgument('-').visitTypeVariable(sigInner.substring(1));
			}
			
			break;
			
		case "EWC":
			System.out.println("EWC---Signature");
			ExtendsWildcardType ewc = (ExtendsWildcardType) t;
			String esigInner = ewc.getInnerType().acceptTV(new TypeToSignature());
			System.out.println(esigInner);
			if(ewc.getInnerType() instanceof TypePlaceholder) {
				sv.visitTypeArgument('+').visitTypeVariable(esigInner.substring(1, esigInner.length()));
			} else if(ewc.getInnerType() instanceof RefType) {
				if(esigInner.contains("$$")) {
					sv.visitTypeArgument('+').visitInterface().visitClassType(esigInner.substring(1,esigInner.length()));
				}else {
//					sv.visitClassType(esigInner.substring(1,esigInner.length()));
					sv.visitTypeArgument('+').visitClassType(esigInner.substring(1,esigInner.length()));
				}
			}else {
				sv.visitTypeArgument('+').visitTypeVariable(esigInner.substring(1));
			}
			
			break;
		default:
			if(!isParameterType)
				sv.visitBaseType('V');
			break;
		}
	}
	/**
	 * Creates signature for class or interface with {@link SignatureWriter}
	 * Signature looks like: 
	 * <typevaliables (K:Ljava/lang/Object "Bounds")>superclass
	 */
	private void createSignatureForClassOrInterface() {
		Iterator<GenericTypeVar> itr = classOrInterface.getGenerics().iterator();

		while(itr.hasNext()) {
			GenericTypeVar g = itr.next();
			getBoundsOfTypeVar(g,genericsAndBounds);
		}
		
		if(!consClass.isEmpty()) {
			ArrayList<String> types = new ArrayList<>();
			ArrayList<String> superTypes = new ArrayList<>();
			
			for(TPHConstraint cons : consClass) {
				types.add(cons.getLeft());
				superTypes.add(cons.getRight());
			}
			
			for(TPHConstraint cons : consClass) {
				String t = cons.getLeft()+"$";
				String bound = cons.getRight()+"$";
				sw.visitFormalTypeParameter(t);
				sw.visitClassBound().visitTypeVariable(bound);
				genericsAndBounds.put(t, bound);
			}
			
			for(TPHConstraint cons : consClass) {
				if(!types.contains(cons.getRight()) && !genericsAndBounds.keySet().contains(cons.getRight()+"$")) {
					String t = cons.getRight()+"$";
					String bound = Type.getInternalName(Object.class);
					sw.visitFormalTypeParameter(t);
					sw.visitClassBound().visitClassType(bound);
					genericsAndBounds.put(t, bound);
					sw.visitClassBound().visitEnd();
				}
			}
			
		}
		/*if(!commonPairs.isEmpty()) {
			ArrayList<TypePlaceholder> types = new ArrayList<>();
			ArrayList<TypePlaceholder> superTypes = new ArrayList<>();
			
			for(GenericInsertPair p : commonPairs) {
				types.add(p.TA1);
				superTypes.add(p.TA2);
			}
			
			for(GenericInsertPair p : commonPairs) {
				String t = p.TA1.getName()+"$";
				String bound = p.TA2.getName()+"$";
				sw.visitFormalTypeParameter(t);
				sw.visitClassBound().visitTypeVariable(bound);
				genericsAndBounds.put(t, bound);
			}
			
			for(GenericInsertPair p : commonPairs) {
				if(!types.contains(p.TA2)) {
					String t = p.TA2.getName()+"$";
					String bound = Type.getInternalName(Object.class);
					sw.visitFormalTypeParameter(t);
					sw.visitClassBound().visitClassType(bound);
					genericsAndBounds.put(t, bound);
					sw.visitClassBound().visitEnd();
				}
			}
		}
		for(TypePlaceholder t : tphsClass) {
			String n = t.getName()+"$";
			String bound = Type.getInternalName(Object.class);
			sw.visitFormalTypeParameter(n);
			sw.visitClassBound().visitClassType(bound);
			genericsAndBounds.put(n, bound);
			sw.visitClassBound().visitEnd();
		}*/
		String sClass = classOrInterface.getSuperClass().acceptTV(new TypeToSignature());
		sw.visitSuperclass().visitClassType(sClass.substring(1, sClass.length()-1));
		sw.visitEnd();
	}
	/**
	 * Get bounds of type variable
	 * @param g type variable
	 * @param genAndBounds 
	 */
	private void getBoundsOfTypeVar(GenericTypeVar g, HashMap<String, String> genAndBounds) {
		sw.visitFormalTypeParameter(g.getName());
		
		Iterator<? extends RefTypeOrTPHOrWildcardOrGeneric> bItr = g.getBounds().iterator(); 
		while(bItr.hasNext()) {
			RefTypeOrTPHOrWildcardOrGeneric b =bItr.next();
			String boundDesc = b.acceptTV(new TypeToDescriptor());
//			System.out.println("GetBounds: " + boundDesc);
			// Ensure that <...> extends java.lang.Object OR ...
			if(b instanceof GenericRefType) {
				sw.visitClassBound().visitTypeVariable(boundDesc);
			} else {
				sw.visitClassBound().visitClassType(boundDesc);
				sw.visitClassBound().visitEnd();
			}
			genAndBounds.put(g.getName(), boundDesc);
		}
//		sw.visitClassBound().visitEnd();
	}
	
	public String toString() {
		return sw.toString();
	}
	
}
