package de.dhbwstuttgart.bytecode.signature;

import java.util.*;

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

import de.dhbwstuttgart.bytecode.constraint.TPHConstraint;
import de.dhbwstuttgart.bytecode.descriptor.TypeToDescriptor;
import de.dhbwstuttgart.bytecode.genericsGeneratorTypes.GenericsGeneratorResult;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.Constructor;
import de.dhbwstuttgart.syntaxtree.GenericTypeVar;
import de.dhbwstuttgart.syntaxtree.Method;
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.typeinference.result.ResultSet;

public class Signature {
	private static final char SUPER_CHAR = '-';
	private static final char EXTENDS_CHAR = '+';
	private static final String SPECIAL_CHAR_FOR_FUN = "$$";
	private static final String SPECIAL_CHAR = "$";
	private ClassOrInterface classOrInterface;
	private HashMap<String, String> genericsAndBounds;
	private HashMap<String, String> genericsAndBoundsMethod;
	private final SignatureWriter sw = new SignatureWriter();;
	private Constructor constructor;
	private Method method;
	private HashMap<String, RefTypeOrTPHOrWildcardOrGeneric> methodParamsAndTypes;
	private ResultSet resultSet;
	private Map<TPHConstraint, Set<String>> methodConstraints;
	private List<GenericsGeneratorResult> consClass;
	private List<GenericsGeneratorResult> constraints;

//	public Signature(ClassOrInterface classOrInterface, HashMap<String, String> genericsAndBounds,
//			List<TPHConstraint> consClass) {
//		this.classOrInterface = classOrInterface;
//		this.genericsAndBounds = genericsAndBounds;
//		this.consClass = consClass;
//		sw = new SignatureWriter();
//		createSignatureForClassOrInterface();
//	}

	public Signature(HashMap<String, String> genericsAndBounds,
			HashMap<String, RefTypeOrTPHOrWildcardOrGeneric> methodParamsAndTypes, ResultSet resultSet,
					 List<GenericsGeneratorResult> constraints) {
		//this.constructor = constructor;
		this.genericsAndBounds = genericsAndBounds;
		this.methodParamsAndTypes = methodParamsAndTypes;
		this.resultSet = resultSet;
		this.constraints = constraints;
	}

	public Signature(int numberOfParams) {
		createSignatureForFunN(numberOfParams);
	}

	public Signature(int numberOfParams, String to, String[] paramTypes) {
		createSignatureForFunN(numberOfParams, to, paramTypes);
	}

	public Signature(ClassOrInterface classOrInterface, HashMap<String, String> genericsAndBounds,
			List<GenericsGeneratorResult> consClass) {
		this.classOrInterface = classOrInterface;
		this.genericsAndBounds = genericsAndBounds;
		this.consClass = consClass;
	}

	public Signature(HashMap<String, String> genericsAndBoundsMethod,
			HashMap<String, String> genericsAndBounds,
			HashMap<String, RefTypeOrTPHOrWildcardOrGeneric> methodParamsAndTypes, ResultSet resultSet,
			List<GenericsGeneratorResult> constraints,List<GenericsGeneratorResult> consClass) {
		this.genericsAndBoundsMethod = genericsAndBoundsMethod;
		this.genericsAndBounds = genericsAndBounds;
		this.methodParamsAndTypes = methodParamsAndTypes;
		this.resultSet = resultSet;
		this.constraints = constraints;
		this.consClass = consClass;
	}

	public String createSignatureForConstructor(Constructor constructor) {
		visitParams();
		sw.visitReturnType().visitBaseType('V');
		return sw.toString();
	}

	public String createSignatureForMethod(Method method) {
		defineGenerics(method);

		String ret = resultSet.resolveType(method.getReturnType()).resolvedType.acceptTV(new TypeToSignature());

		visitParams();
		visitReturnType(method, ret);
		return sw.toString();
	}

	/**
	 * @param method
	 * @param ret
	 */
	private void visitReturnType(Method method, String ret) {
		if (ret.equals("V")) {
			sw.visitReturnType().visitBaseType('V');
		} else {
			RefTypeOrTPHOrWildcardOrGeneric returnType = method.getReturnType();
			SignatureVisitor sv = sw.visitReturnType();
			doVisitParamsOrReturn(returnType, sv);
		}
	}

	/**
	 * 
	 */
	private void visitParams() {
		for (String paramName : methodParamsAndTypes.keySet()) {
			RefTypeOrTPHOrWildcardOrGeneric t = methodParamsAndTypes.get(paramName);
			SignatureVisitor sv = sw.visitParameterType();
			doVisitParamsOrReturn(t, sv);
		}
	}

	/**
	 *
	 * @param method
	 */
	private void defineGenerics(Method method) {
		method.getGenerics().forEach(g -> {
			visitTypeVarsAndTheirBounds(g, genericsAndBoundsMethod);
		});
		defineGenericsFromConstraints(constraints,genericsAndBoundsMethod);
	}

	private void createSignatureForFunN(int numberOfParams, String to, String[] paramTypes) {
		defineTypeVariablesForParametersOfFunN(numberOfParams);

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

	}

	private void createSignatureForFunN(int numberOfParams) {

		defineTypeVariablesForParametersOfFunN(numberOfParams);

		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();
	}

	private void defineTypeVariablesForParametersOfFunN(int numberOfParams) {
		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();
		}
	}

	/**
	 * Visits parameter type or return type with {@link SignatureVisitor} to create
	 * the method signature
	 * 
	 * @param t  type of parameter or return type
	 * @param sv true if t is type of parameter
	 */
	private void doVisitParamsOrReturn(RefTypeOrTPHOrWildcardOrGeneric t, SignatureVisitor sv) {
		String type = t.acceptTV(new TypeToString());

		switch (type) {
		case "RT":
			String sig = t.acceptTV(new TypeToSignature(constraints));
			sv.visitClassType(sig.substring(1, sig.length()));
			break;
		case "GRT":
			GenericRefType g = (GenericRefType) t;
			sv.visitTypeVariable(g.acceptTV(new TypeToSignature(constraints)));
			break;
		case "TPH":
			RefTypeOrTPHOrWildcardOrGeneric r = resultSet.resolveType(t).resolvedType;
			// der Fall wenn der Typ 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(constraints));
			if (r instanceof GenericRefType) {
				sv.visitTypeVariable(sig2);
			} else if (!(r instanceof TypePlaceholder)) {
				if (sig2.contains(SPECIAL_CHAR_FOR_FUN)) {
					sv.visitInterface().visitClassType(sig2.substring(1));
				} 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 realName = sig2.substring(1, sig2.length() - 1);
				String toVisit = realName+SPECIAL_CHAR;
				if(!genericsAndBounds.containsKey(toVisit)) {
					Optional<GenericsGeneratorResult> equalTPH = getEqualTPHFromClassConstraints(consClass, realName);
					if(equalTPH.isPresent()){
						toVisit = equalTPH.get().getConstraint().getLeft() + SPECIAL_CHAR;
					} else {
						toVisit = getEqualTPH(constraints, realName) + SPECIAL_CHAR;
					}
				}
				sv.visitTypeVariable(toVisit);
			}

			break;

		case "SWC":
			SuperWildcardType swc = (SuperWildcardType) t;
			String sigInner = swc.getInnerType().acceptTV(new TypeToSignature(constraints));
			int length = sigInner.length();
			visitWildCard(sv, sigInner, length, swc.getInnerType(), SUPER_CHAR);

			break;

		case "EWC":
			ExtendsWildcardType ewc = (ExtendsWildcardType) t;
			String esigInner = ewc.getInnerType().acceptTV(new TypeToSignature(constraints));
			int lengthEWCSig = esigInner.length();
			visitWildCard(sv, esigInner, lengthEWCSig, ewc.getInnerType(), EXTENDS_CHAR);

			break;
		default:
//			if (!sv)
//				sv.visitBaseType('V');
			break;
		}
	}

	private void visitWildCard(SignatureVisitor sv, String sigInner, int length, RefTypeOrTPHOrWildcardOrGeneric innerType, char superOrExtendsChar) {
		if (innerType instanceof TypePlaceholder) {
			sv.visitTypeArgument(superOrExtendsChar).visitTypeVariable(sigInner.substring(1, length));
		} else if (innerType instanceof RefType) {
			checkInnerSignatureOfWildCard(sv, sigInner, length, superOrExtendsChar);
		} else {
			sv.visitTypeArgument(superOrExtendsChar).visitTypeVariable(sigInner.substring(1));
		}
	}

	private void checkInnerSignatureOfWildCard(SignatureVisitor sv, String sigInner, int length, char superOrExtendsChar) {
		if (sigInner.contains(SPECIAL_CHAR_FOR_FUN)) {
			sv.visitTypeArgument(superOrExtendsChar).visitInterface().visitClassType(sigInner.substring(1, length));
		} else {
			sv.visitTypeArgument(superOrExtendsChar).visitClassType(sigInner.substring(1, length));
		}
	}

	private Optional<GenericsGeneratorResult> getEqualTPHFromClassConstraints(List<GenericsGeneratorResult> consClass, String tph) {
		return consClass.stream()
				.filter(c -> c.getConstraint().getLeft().equals(tph) || c.getEqualsTPHs().contains(tph))
				.findFirst();
	}

	private String getEqualTPH(List<GenericsGeneratorResult> constraints2, String tph) {
		return constraints2.stream()
				.filter(c -> c.getConstraint().getLeft().equals(tph) || c.getEqualsTPHs().contains(tph))
				.findFirst().get().getConstraint().getLeft();
	}

	/**
	 * Creates signature for class or interface with {@link SignatureWriter}
	 * Signature looks like: <typevaliables (K:Ljava/lang/Object
	 * "Bounds")>superclass
	 */
	public String createSignatureForClassOrInterface() {
		defineTypeVariablesForClassOrInterface();

		defineGenericsFromConstraints(consClass,genericsAndBounds);

		String sClass = classOrInterface.getSuperClass().acceptTV(new TypeToSignature());
		sw.visitSuperclass().visitClassType(sClass.substring(1, sClass.length() - 1));
		sw.visitEnd();
		return sw.toString();
	}

	private void defineTypeVariablesForClassOrInterface() {
		Iterator<GenericTypeVar> itr = classOrInterface.getGenerics().iterator();

		while (itr.hasNext()) {
			GenericTypeVar g = itr.next();
			visitTypeVarsAndTheirBounds(g, genericsAndBounds);
		}
	}

	/**
	 * @param genericsAndBounds2 
	 * 
	 */
	private void defineGenericsFromConstraints(List<GenericsGeneratorResult> constraints, HashMap<String,String> genericsAndBounds2) {
		constraints.forEach(c -> {
			String typeVariable = c.getConstraint().getLeft() + SPECIAL_CHAR;
			sw.visitFormalTypeParameter(typeVariable);

			String bound = c.getConstraint().getRight();
			bound = checkBound(bound);
			genericsAndBounds2.put(typeVariable, bound);
		});
	}

	/**
	 * @param bound
	 * @return
	 */
	private String checkBound(String bound) {
		if (bound.equals(Type.getInternalName(Object.class))) {
			visitClassBound(bound);
		} else {
			bound += SPECIAL_CHAR;
			sw.visitClassBound().visitTypeVariable(bound);
		}
		return bound;
	}

	/**
	 * @param bound
	 */
	private void visitClassBound(String bound) {
		sw.visitClassBound().visitClassType(bound);
		sw.visitClassBound().visitEnd();
	}

	/**
	 * Get bounds of type variable
	 * 
	 * @param g            type variable
	 * @param genAndBounds
	 */
	private void visitTypeVarsAndTheirBounds(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());
			// Ensure that <...> extends java.lang.Object OR ...
			if (b instanceof GenericRefType) {
				sw.visitClassBound().visitTypeVariable(boundDesc);
			} else {
				visitClassBound(boundDesc);
			}
			genAndBounds.put(g.getName(), boundDesc);
		}
	}

}
