package de.dhbwstuttgart.bytecode;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import de.dhbwstuttgart.bytecode.Exception.BytecodeGeneratorError;
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.bytecode.signature.TypeToString;
import de.dhbwstuttgart.bytecode.genericsGeneratorTypes.GenericGenratorResultForSourceFile;
import de.dhbwstuttgart.bytecode.genericsGeneratorTypes.GenericsGeneratorResult;
import de.dhbwstuttgart.bytecode.genericsGeneratorTypes.GenericsGeneratorResultForClass;
import de.dhbwstuttgart.bytecode.utilities.MethodUtility;
import de.dhbwstuttgart.bytecode.utilities.NormalConstructor;
import de.dhbwstuttgart.bytecode.utilities.NormalMethod;
import de.dhbwstuttgart.bytecode.utilities.Resolver;
import de.dhbwstuttgart.exceptions.NotImplementedException;
import de.dhbwstuttgart.parser.SyntaxTreeGenerator.AssignToLocal;
import de.dhbwstuttgart.syntaxtree.ASTVisitor;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.Constructor;
import de.dhbwstuttgart.syntaxtree.Field;
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.ArgumentList;
import de.dhbwstuttgart.syntaxtree.statement.Assign;
import de.dhbwstuttgart.syntaxtree.statement.AssignToField;
import de.dhbwstuttgart.syntaxtree.statement.BinaryExpr;
import de.dhbwstuttgart.syntaxtree.statement.Block;
import de.dhbwstuttgart.syntaxtree.statement.CastExpr;
import de.dhbwstuttgart.syntaxtree.statement.DoStmt;
import de.dhbwstuttgart.syntaxtree.statement.EmptyStmt;
import de.dhbwstuttgart.syntaxtree.statement.ExpressionReceiver;
import de.dhbwstuttgart.syntaxtree.statement.FieldVar;
import de.dhbwstuttgart.syntaxtree.statement.ForStmt;
import de.dhbwstuttgart.syntaxtree.statement.IfStmt;
import de.dhbwstuttgart.syntaxtree.statement.InstanceOf;
import de.dhbwstuttgart.syntaxtree.statement.LambdaExpression;
import de.dhbwstuttgart.syntaxtree.statement.Literal;
import de.dhbwstuttgart.syntaxtree.statement.LocalVar;
import de.dhbwstuttgart.syntaxtree.statement.LocalVarDecl;
import de.dhbwstuttgart.syntaxtree.statement.MethodCall;
import de.dhbwstuttgart.syntaxtree.statement.NewArray;
import de.dhbwstuttgart.syntaxtree.statement.NewClass;
import de.dhbwstuttgart.syntaxtree.statement.Return;
import de.dhbwstuttgart.syntaxtree.statement.ReturnVoid;
import de.dhbwstuttgart.syntaxtree.statement.StaticClassName;
import de.dhbwstuttgart.syntaxtree.statement.Super;
import de.dhbwstuttgart.syntaxtree.statement.SuperCall;
import de.dhbwstuttgart.syntaxtree.statement.This;
import de.dhbwstuttgart.syntaxtree.statement.UnaryExpr;
import de.dhbwstuttgart.syntaxtree.statement.WhileStmt;
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 BytecodeGen implements ASTVisitor {

	ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);

	String type;

	public static RefTypeOrTPHOrWildcardOrGeneric THISTYPE = null;
	private String className;
	private String pkgName;
	private boolean isInterface;
	private Collection<ResultSet> listOfResultSets;
	private ResultSet resultSet;
	private SourceFile sf;
	private String path;

	private Optional<Constructor> fieldInitializations;

	private int indexOfFirstParam = 0;

	private String superClass;

	private List<String> tphsClass;

	// stores parameter, local vars and the next index on the local variable table,
	// which use for aload_i, astore_i,...
	HashMap<String, Integer> paramsAndLocals = new HashMap<>();
	// stores generics and their bounds of class
	HashMap<String, String> genericsAndBounds = new HashMap<>();

	private int constructorPos = 0;

	HashMap<String, RefTypeOrTPHOrWildcardOrGeneric> methodParamsAndTypes = new HashMap<>();
	byte[] bytecode;
	HashMap<String, byte[]> classFiles;

	private final ArrayList<String> methodNameAndParamsT = new ArrayList<>();
	private final ArrayList<String> fieldNameAndParamsT = new ArrayList<>();

	private final ArrayList<String> fieldNameSignature = new ArrayList<>();

	private List<GenericGenratorResultForSourceFile> simplifyResultsForAllSourceFiles;
	private GenericsGeneratorResultForClass generatedGenerics;

	private Resolver resolver;

	public BytecodeGen(HashMap<String, byte[]> classFiles, Collection<ResultSet> listOfResultSets, List<GenericGenratorResultForSourceFile> simplifyResultsForAllSourceFiles, SourceFile sf,
			String path) {
		this.classFiles = classFiles;
		this.listOfResultSets = listOfResultSets;
		this.simplifyResultsForAllSourceFiles = simplifyResultsForAllSourceFiles;
		this.sf = sf;
		this.path = path;
		this.pkgName = sf.getPkgName();
	}

	@Override
	public void visit(SourceFile sourceFile) {
		for (ClassOrInterface cl : sourceFile.getClasses()) {
			System.out.println("in Class: " + cl.getClassName().toString());
			BytecodeGen classGen = new BytecodeGen(classFiles, listOfResultSets, simplifyResultsForAllSourceFiles, sf, path);
			cl.accept(classGen);
			classGen.writeClass(cl.getClassName().toString());
		}
	}

	/**
	 * Associates the bytecode of the class that was build with the classWriter
	 * {@link #cw} with the class name in the map {@link #classFiles}
	 * 
	 * @param name name of the class with which the bytecode is to be associated
	 */
	private void writeClass(String name) {
		bytecode = cw.toByteArray();
		classFiles.put(name, bytecode);

	}

	public HashMap<String, byte[]> getClassFiles() {
		return classFiles;
	}

	@Override
	public void visit(ClassOrInterface classOrInterface) {

		className = classOrInterface.getClassName().toString();

		cw.visitSource(className + ".jav", null);

		isInterface = (classOrInterface.getModifiers() & 512) == 512;

		int acc = isInterface ? classOrInterface.getModifiers() + Opcodes.ACC_ABSTRACT
				: classOrInterface.getModifiers() + Opcodes.ACC_SUPER;

		fieldInitializations = classOrInterface.getfieldInitializations();

//		resultSet = listOfResultSets.get(0);
		boolean isVisited = false;
		List<ResultSet> listOfResultSetsList = new ArrayList<>(listOfResultSets);
		generatedGenerics = simplifyResultsForAllSourceFiles.stream().map(sr->sr.getSimplifyResultsByName(pkgName, className)).findFirst().get();
		for (int i = 0; i < listOfResultSetsList.size(); i++) {
		//for (ResultSet rs : listOfResultSets) {
			superClass = classOrInterface.getSuperClass().acceptTV(new TypeToDescriptor());
			resultSet = listOfResultSetsList.get(i);
			resolver = new Resolver(resultSet);
//			tphExtractor.setResultSet(resultSet);
			

			// Nur einmal ausführen!!
			if (!isVisited) {

				String sig = null;
				/*
				 * if class has generics then creates signature Signature looks like:
				 * <E:Ljava/...>Superclass
				 */
				if (classOrInterface.getGenerics().iterator().hasNext() || classOrInterface.getSuperClass().acceptTV(new TypeToSignature()).contains("<")
						|| !generatedGenerics.getClassConstraints().isEmpty()) {
					
					List<GenericsGeneratorResult> consClass = generatedGenerics.getClassConstraints();
//					
					Signature signature = new Signature(classOrInterface, genericsAndBounds, consClass);
					sig = signature.createSignatureForClassOrInterface();
					System.out.println("Signature: => " + sig);
				}

				cw.visit(Opcodes.V1_8, acc, classOrInterface.getClassName().toString(), sig,
						classOrInterface.getSuperClass().acceptTV(new TypeToDescriptor()), null);

				isVisited = true;
			}

			for (Field f : classOrInterface.getFieldDecl()) {
				f.accept(this);
			}

			for (Constructor c : classOrInterface.getConstructors()) {
				c.accept(this);
			}

			for (Method m : classOrInterface.getMethods()) {
				m.accept(this);
			}

		}

	}

	@Override
	public void visit(Constructor field) {
		// stores generics and their bounds of method
		HashMap<String, String> genericsAndBoundsMethod = new HashMap<>();

		field.getParameterList().accept(this);

		String id = MethodUtility.createID(resolver, field);

		if (methodNameAndParamsT.contains(id)) {
			return;
		}
		methodNameAndParamsT.add(id);
		System.out.println("Constructor: " + field.name + " , paramsType: " + id);

		String desc = null;
		boolean hasGen = false;

		for (String paramName : methodParamsAndTypes.keySet()) {
			String typeOfParam = methodParamsAndTypes.get(paramName).acceptTV(new TypeToSignature());
			System.out.println(typeOfParam);
			if (genericsAndBounds.containsKey(typeOfParam) || typeOfParam.contains("$") || typeOfParam.contains("<")) {
				hasGen = true;
				break;
			}
		}
		String sig = null;
		if (hasGen) {
			List<GenericsGeneratorResult> constraints = generatedGenerics.getClassConstraints();
			Signature signature = new Signature(genericsAndBounds,
					methodParamsAndTypes, resultSet, constraints);

			sig = signature.createSignatureForConstructor(field);
		}
		if (field.getParameterList().iterator().hasNext())
			System.out.println(field.getParameterList().iterator().next().getType().acceptTV(new TypeToDescriptor()));

		NormalConstructor constructor = new NormalConstructor(field, genericsAndBounds, hasGen);
		desc = constructor.accept(new DescriptorToString(resultSet));
		System.out.println("Constructor: " + field.getName() + " Sig: " + sig + " Desc: " + desc);
		MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", desc, sig, null);
		mv.visitCode();

		Block block = fieldInitializations.get().block;

		constructorPos += 1;

		BytecodeGenMethod gen = new BytecodeGenMethod(className, superClass, resultSet, field, mv, paramsAndLocals, cw,
				genericsAndBoundsMethod, genericsAndBounds, isInterface, classFiles, sf, path, block, constructorPos);
		if (!field.getParameterList().iterator().hasNext()
				&& !(field.block.statements.get(field.block.statements.size() - 1) instanceof ReturnVoid)) {
			mv.visitInsn(Opcodes.RETURN);
		}
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}

	@Override
	public void visit(Method method) {
		// TODO: check if the method is static => if static then the first param will be
		// stored in pos 0
		// else it will be stored in pos 1 and this will be stored in pos 0
		String retType = resultSet.resolveType(method.getReturnType()).resolvedType.acceptTV(new TypeToDescriptor());
		
		String id = MethodUtility.createID(resolver, method);

		if (methodNameAndParamsT.contains(id)) {
			return;
		}
		methodNameAndParamsT.add(id);
		System.out.println("Method: " + method.name + " , paramsType: " + id);
		// stores generics and their bounds of method
		HashMap<String, String> genericsAndBoundsMethod = new HashMap<>();
		
		method.getParameterList().accept(this);
		
		String methDesc = null;

		// Method getModifiers() ?
		int acc = isInterface ? Opcodes.ACC_ABSTRACT : method.modifier;
		System.out.println(acc);

		/* Prüfe, ob die Rückgabe-Type der Methode eine Type-Variable ist */
		boolean hasGenInParameterList = genericsAndBounds.containsKey(retType) || retType.contains("TPH ")
				|| resultSet.resolveType(method.getReturnType()).resolvedType.acceptTV(new TypeToSignature())
						.contains("<");
		/*
		 * 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;
				}
			}
		}
		// TODO: Test if the return-type or any of the parameter is a parameterized
		// type. (VP)
		// then create the descriptor with the new syntax.

		String sig = null;
		/*
		 * method.getGenerics: <....> RT method(..)
		 */
		boolean hasGen = method.getGenerics().iterator().hasNext() || hasGenInParameterList;
		/* if method has generics or return type is TPH, create signature */
		// zwite operand muss weggelassen werden
		if (hasGen || resultSet.resolveType(method.getReturnType()).resolvedType.acceptTV(new TypeToString())
				.equals("TPH")) {
			
			List<GenericsGeneratorResult> constraints = generatedGenerics.getMethodConstraintsByID(id);
			List<GenericsGeneratorResult> classConstraints = generatedGenerics.getClassConstraints();
			Signature signature = new Signature(genericsAndBoundsMethod, genericsAndBounds,
					methodParamsAndTypes, resultSet, constraints,classConstraints);
			sig = signature.createSignatureForMethod(method);
		}
		System.out.println(method.getName() + " ==> " + sig);
		NormalMethod meth = new NormalMethod(method, genericsAndBounds, genericsAndBoundsMethod, hasGen);
		methDesc = meth.accept(new DescriptorToString(resultSet));

		MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + acc, method.getName(), methDesc, sig, null);

		mv.visitCode();
		BytecodeGenMethod gen = new BytecodeGenMethod(className, superClass, resultSet, method, mv, paramsAndLocals, cw,
				genericsAndBoundsMethod, genericsAndBounds, isInterface, classFiles, sf, path);

		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}

	@Override
	public void visit(ParameterList formalParameters) {
		paramsAndLocals = new HashMap<>();
		methodParamsAndTypes = new HashMap<>();
		Iterator<FormalParameter> itr = formalParameters.iterator();
		int i = 1;
		while (itr.hasNext()) {
			FormalParameter fp = itr.next();
			paramsAndLocals.put(fp.getName(), i);
			methodParamsAndTypes.put(fp.getName(), resultSet.resolveType(fp.getType()).resolvedType);
			fp.accept(this);
			i++;
		}
	}

	@Override
	public void visit(FormalParameter formalParameter) {
		formalParameter.getType().accept(this);
	}

	@Override
	public void visit(RefType refType) {
		type = "L" + refType.toString() + ";";
	}

	@Override
	public void visit(SuperWildcardType superWildcardType) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(TypePlaceholder typePlaceholder) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(ExtendsWildcardType extendsWildcardType) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(GenericRefType genericRefType) {
		// TODO Auto-generated method stub

	}

	// ??
	@Override
	public void visit(FieldVar fieldVar) {
		System.out.println("In FieldVar ---");
//		cw.newField(fieldVar.receiver.toString(), fieldVar.fieldVarName.toString(), fieldVar.getType().toString());
		FieldVisitor fv = cw.visitField(Opcodes.ACC_PRIVATE, fieldVar.fieldVarName, "L" + fieldVar.getType() + ";",
				null, null);
		fv.visitEnd();
	}

	@Override
	public void visit(Field field) {
		System.out.println("In Field ---");
		String des = "L";
		if (resultSet.resolveType(field.getType()).resolvedType instanceof TypePlaceholder) {
			des += Type.getInternalName(Object.class);
		} else {
			des += resultSet.resolveType(field.getType()).resolvedType.acceptTV(new TypeToDescriptor());
		}
		des += ";";
		System.out.println(des);
		String sig = resultSet.resolveType(field.getType()).resolvedType.acceptTV(new TypeToSignature(generatedGenerics.getClassConstraints()));
		System.out.println(sig);
		if (sig.charAt(sig.length() - 1) != (";").charAt(0)) {
			sig += ";";
		}
		String nameAndDesc = field.getName() + "%%" + des;
		String nameAndSig = field.getName() + "%%" + sig;
		if (fieldNameAndParamsT.contains(nameAndDesc)) {
			if (fieldNameSignature.contains(nameAndSig)) {
				return;
			}
			throw new BytecodeGeneratorError("Bytecode generation aborted due to duplicate field name&signature");
		}
		fieldNameAndParamsT.add(nameAndDesc);
		fieldNameSignature.add(nameAndSig);

		cw.visitField(field.modifier, field.getName(), des, sig, null);
	}

	@Override
	public void visit(LambdaExpression lambdaExpression) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(Assign assign) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(BinaryExpr binary) {

	}

	@Override
	public void visit(Block block) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(CastExpr castExpr) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(EmptyStmt emptyStmt) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(ForStmt forStmt) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(IfStmt ifStmt) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(InstanceOf instanceOf) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(LocalVar localVar) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(LocalVarDecl localVarDecl) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(MethodCall methodCall) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(NewClass methodCall) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(NewArray newArray) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(Return aReturn) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(ReturnVoid aReturn) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(StaticClassName staticClassName) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(Super aSuper) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(This aThis) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(WhileStmt whileStmt) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(DoStmt whileStmt) {
		// TODO Auto-generated method stub

	}

	// ???
	@Override
	public void visit(Literal literal) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(ArgumentList argumentList) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(GenericTypeVar genericTypeVar) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(GenericDeclarationList genericTypeVars) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(AssignToField assignLeftSide) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(AssignToLocal assignLeftSide) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(SuperCall superCall) {

	}

	@Override
	public void visit(ExpressionReceiver expressionReceiver) {
		// TODO Auto-generated method stub

	}

	@Override
	public void visit(UnaryExpr unaryExpr) {
		throw new NotImplementedException();
	}

}
