package de.dhbwstuttgart.syntaxtree.factory;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import java.util.*;

import com.google.common.collect.Lists;
import de.dhbwstuttgart.bytecode.JavaTXSignatureAttribute;
import de.dhbwstuttgart.parser.NullToken;
import de.dhbwstuttgart.parser.scope.JavaClassName;
import de.dhbwstuttgart.syntaxtree.*;
import de.dhbwstuttgart.syntaxtree.Field;
import de.dhbwstuttgart.syntaxtree.Method;
import de.dhbwstuttgart.syntaxtree.type.*;
import de.dhbwstuttgart.syntaxtree.type.Void;
import de.dhbwstuttgart.syntaxtree.statement.Block;
import de.dhbwstuttgart.syntaxtree.statement.Statement;
import de.dhbwstuttgart.syntaxtree.type.WildcardType;
import de.dhbwstuttgart.util.Pair;
import org.antlr.v4.runtime.Token;
import org.apache.commons.io.IOUtils;
import org.objectweb.asm.*;
import org.objectweb.asm.signature.SignatureReader;
import org.objectweb.asm.signature.SignatureVisitor;

/**
 * Anmerkung: Die ASTFactory Methoden, welche ASTBäume aus java.lang.Class Objekten generieren, können davon ausgehen, dass alle Imports und Typnamen korrekt sind und müssen diese nicht überprüfen.
 */
public class ASTFactory {

	private static final HashMap<java.lang.Class, ClassOrInterface> cache = new HashMap<>();

	public static ClassOrInterface createClass(java.lang.Class jreClass) {
		if (cache.containsKey(jreClass))
			return cache.get(jreClass);

		// TODO Inner classes

		var methodSignatures = new HashMap<Pair<String, String>, String>();
		String classSignature = null;

		// Load class with asm to figure out if there's a JavaTX signature
		try {
			var path = jreClass.getName().replace('.', '/') + ".class";
			var classLoader = jreClass.getClassLoader();
			if (classLoader != null && new File(path).exists()) {
				var bytes = IOUtils.toByteArray(Objects.requireNonNull(classLoader.getResourceAsStream(path)));
				var classReader = new ClassReader(bytes);
				var classVisitor = new ClassVisitor(Opcodes.ASM7) {
					String classSignature;

					@Override
					public void visitAttribute(Attribute attribute) {
						if (attribute.type.equals("JavaTXSignature")) {
							classSignature = ((JavaTXSignatureAttribute) attribute).signature;
						}
						super.visitAttribute(attribute);
					}

					@Override
					public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
						classSignature = signature;
						super.visit(version, access, name, signature, superName, interfaces);
					}

					@Override
					public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {

						methodSignatures.put(new Pair<>(name, descriptor), signature);
						return new MethodVisitor(Opcodes.ASM7) {
							@Override
							public void visitAttribute(Attribute attribute) {
								if (attribute.type.equals("JavaTXSignature")) {
									methodSignatures.put(new Pair<>(name, descriptor), ((JavaTXSignatureAttribute) attribute).signature);
								}
								super.visitAttribute(attribute);
							}
						};
					}
				};

				classReader.accept(classVisitor, new Attribute[] { new JavaTXSignatureAttribute() }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
				classSignature = classVisitor.classSignature;
			}
		} catch (IOException e) {
			// Skip
		}
		JavaClassName name = new JavaClassName(jreClass.getName());
		List<Method> methoden = new ArrayList<>();
		List<de.dhbwstuttgart.syntaxtree.Constructor> konstruktoren = new ArrayList<>();
		for (java.lang.reflect.Constructor constructor : jreClass.getConstructors()) {
			var signature = methodSignatures.get(new Pair<>(constructor.getName(), org.objectweb.asm.Type.getConstructorDescriptor(constructor)));
			createConstructor(constructor, signature, jreClass).map(c -> konstruktoren.add(c));
		}

		Set<java.lang.reflect.Method> allMethods = new HashSet<>(Arrays.asList(jreClass.getMethods()));
		if (jreClass.isInterface())
			allMethods.addAll(Arrays.asList(Object.class.getMethods())); // For some reason interfaces don't inherit from Object

		Set<java.lang.reflect.Method> allDeclaredMethods = new HashSet<>(Arrays.asList(jreClass.getDeclaredMethods()));
		Set<java.lang.reflect.Method> allInheritedMethods = new HashSet<>(allMethods);
		allInheritedMethods.removeAll(allDeclaredMethods);
		for (java.lang.reflect.Method method : allDeclaredMethods) {
			var signature = methodSignatures.get(new Pair<>(method.getName(), org.objectweb.asm.Type.getMethodDescriptor(method)));
			if (jreClass.getSuperclass()==null) {
				methoden.add(createMethod(method, signature, jreClass, false, false));
			}
			else {
				Boolean isImplemented = false;
				isImplemented = Arrays.stream(jreClass.getInterfaces()).
									   reduce(false, 
											  (x,y) -> {
												  try { 
													  y.getDeclaredMethod(method.getName(), method.getParameterTypes());
													  return true;
												  }
												  catch (java.lang.NoSuchMethodException e) {
													  return false;
												  }},
											  (x,y) -> (x || y)
											 );
					
			    if (isImplemented) {
			    	methoden.add(createMethod(method, signature, jreClass, false, true));
			    }
			    else {
			    	if (Modifier.isAbstract(jreClass.getSuperclass().getModifiers())) {	
			    		try {
			    			jreClass.getSuperclass().getDeclaredMethod(method.getName(), method.getParameterTypes());
			    			methoden.add(createMethod(method, signature, jreClass, false, true));
			    		}
			    		catch (java.lang.NoSuchMethodException e) { 
								methoden.add(createMethod(method, signature, jreClass, false, false));	
			    		}
			    	}
			    	else {
			    		methoden.add(createMethod(method, signature, jreClass, false, false));
			    	}
			    	
			    }
		}}
		for (java.lang.reflect.Method method : allInheritedMethods) {
			var signature = methodSignatures.get(new Pair<>(method.getName(), org.objectweb.asm.Type.getMethodDescriptor(method)));
			methoden.add(createMethod(method, signature, jreClass, true, false));
		}
		List<Field> felder = new ArrayList<>();
		for (java.lang.reflect.Field field : jreClass.getDeclaredFields()) {
			felder.add(createField(field, name));
		}
		int modifier = jreClass.getModifiers();
		boolean isInterface = jreClass.isInterface();
		List<Annotation> aLA;
		boolean isFunctionalInterface =
                !(aLA = Arrays.asList(jreClass.getAnnotations())).isEmpty() &&
                        aLA.get(0) instanceof FunctionalInterface;
		// see: https://stackoverflow.com/questions/9934774/getting-generic-parameter-from-supertype-class
		ParameterizedType parameterSuperClass = null;
		Type tempSuperClass = jreClass.getGenericSuperclass();
		if (tempSuperClass instanceof ParameterizedType)
			parameterSuperClass = (ParameterizedType) tempSuperClass;
		java.lang.Class superjreClass = jreClass.getSuperclass();
		RefType superClass;
		if (parameterSuperClass != null) {
			superClass = (RefType) createType(parameterSuperClass);
		} else if (superjreClass != null) {
			superClass = (RefType) createType(superjreClass);
		} else {// Jede Klasse und jedes Interface erbt von Object: (auch Object selbst!)
			superClass = (RefType) createType(java.lang.Object.class);
		}
		List<RefType> implementedInterfaces = new ArrayList<>();
		for (Type jreInterface : jreClass.getGenericInterfaces()) {
			implementedInterfaces.add((RefType) createType(jreInterface));
		}
		List<RefType> permittedSubtypes = null;
		if (jreClass.isSealed()) {
			permittedSubtypes = new ArrayList<>();
			for (Class subclass : jreClass.getPermittedSubclasses()) {
				permittedSubtypes.add((RefType) createType(subclass));
			}
		}

		GenericDeclarationList genericDeclarationList = createGenerics(jreClass.getTypeParameters(), jreClass, null, null);
		Token offset = new NullToken(); // Braucht keinen Offset, da diese Klasse nicht aus einem Quellcode geparst wurde

		var cinf = new ClassOrInterface(modifier, name, felder, Optional.empty() /* eingefuegt PL 2018-11-24 */, Optional.empty(), methoden, konstruktoren, genericDeclarationList, superClass, isInterface, isFunctionalInterface, implementedInterfaces, permittedSubtypes, offset, null);
		cache.put(jreClass, cinf);
		return cinf;
	}

	private static Field createField(java.lang.reflect.Field field, JavaClassName jreClass) {
		return new Field(field.getName(), createType(field.getGenericType()), field.getModifiers(), new NullToken());
	}

	// private static RefType createType(Class classType) {
	// return createClass(classType).getType();
	// }

	private static Optional<de.dhbwstuttgart.syntaxtree.Constructor> createConstructor(Constructor constructor, String signature, Class inClass) {
		String name = constructor.getName();
		RefTypeOrTPHOrWildcardOrGeneric returnType = createType(inClass);
		Parameter[] jreParams = constructor.getParameters();
		Type[] jreGenericParams = constructor.getGenericParameterTypes();
		List<Pattern> params = new ArrayList<>();
		int i = 0;
		for (Type jreParam : jreGenericParams) {
			if (jreParam == null)
				continue;
			RefTypeOrTPHOrWildcardOrGeneric paramType = createType(jreParam);
			params.add(new FormalParameter(jreParams[i].getName(), paramType, new NullToken()));
			i++;
		}
		ParameterList parameterList = new ParameterList(params, new NullToken());
		Block block = new Block(new ArrayList<Statement>(), new NullToken());
		GenericDeclarationList gtvDeclarations = createGenerics(constructor.getTypeParameters(), inClass, constructor.getName(), signature);
		Token offset = new NullToken();
		int modifier = constructor.getModifiers();

		if (inClass.equals(java.lang.Object.class)) {
			return Optional.empty();
		}

		return Optional.of(new de.dhbwstuttgart.syntaxtree.Constructor(modifier, name, returnType, parameterList, block, gtvDeclarations, offset /* , new ArrayList<>() geloescht PL 2018-11-24 */));
	}

	public static Method createMethod(java.lang.reflect.Method jreMethod, String signature, java.lang.Class inClass, Boolean isInherited, Boolean isImplemented) {
		String name = jreMethod.getName();
		RefTypeOrTPHOrWildcardOrGeneric returnType;
		Type jreRetType;
		if (jreMethod.getGenericReturnType() != null) {
			jreRetType = jreMethod.getGenericReturnType();
		} else {
			jreRetType = jreMethod.getReturnType();
		}
		returnType = createType(jreRetType);
		Parameter[] jreParams = jreMethod.getParameters();
		Type[] jreGenericParams = jreMethod.getGenericParameterTypes();
		List<Pattern> params = new ArrayList<>();
		int i = 0;
		for (Type jreParam : jreGenericParams) {
			if (jreParam == null)
				continue;
			RefTypeOrTPHOrWildcardOrGeneric paramType = createType(jreParam);
			params.add(new FormalParameter(jreParams[i].getName(), paramType, new NullToken()));
			i++;
		}
		ParameterList parameterList = new ParameterList(params, new NullToken());
		Block block = new Block(new ArrayList<Statement>(), new NullToken());
		GenericDeclarationList gtvDeclarations = createGenerics(jreMethod.getTypeParameters(), inClass, jreMethod.getName(), signature);
		Token offset = new NullToken();

		return new Method(jreMethod.getModifiers(), name, returnType, parameterList, block, gtvDeclarations, offset, isInherited, isImplemented);
	}

	public static GenericDeclarationList createGenerics(TypeVariable[] typeParameters, Class context, String methodName, String signature) {
		if (signature == null) {
			List<de.dhbwstuttgart.syntaxtree.GenericTypeVar> gtvs = new ArrayList<>();
			for (TypeVariable jreTV : typeParameters) {
				de.dhbwstuttgart.syntaxtree.GenericTypeVar gtv = createGeneric(jreTV, jreTV.getName(), context, methodName);
				gtvs.add(gtv);
			}
			return new GenericDeclarationList(gtvs, new NullToken());
		} else {
			var res = createGenerics(signature);
			return res;
		}
	}

	public static GenericDeclarationList createGenerics(String signature) {
		if (signature == null)
			return new GenericDeclarationList(new ArrayList<>(), new NullToken());

		var gtvs = new ArrayList<GenericTypeVar>();
		var signatureVisitor = new SignatureVisitor(Opcodes.ASM7) {
			List<RefTypeOrTPHOrWildcardOrGeneric> bounds = new ArrayList<>();
			final Stack<RefTypeOrTPHOrWildcardOrGeneric> bound = new Stack<>();
			final Stack<RefType> classTypes = new Stack<>();

			// All hail the mighty visitor pattern
			final SignatureVisitor doNothing = new SignatureVisitor(Opcodes.ASM7) {
			};

			char wildcard = '=';

			@Override
			public SignatureVisitor visitSuperclass() {
				return doNothing;
			}

			@Override
			public SignatureVisitor visitParameterType() {
				return doNothing;
			}

			@Override
			public SignatureVisitor visitReturnType() {
				return doNothing;
			}

			@Override
			public SignatureVisitor visitExceptionType() {
				return doNothing;
			}

			@Override
			public void visitFormalTypeParameter(String name) {
				bounds = new ArrayList<>();
				gtvs.add(new GenericTypeVar(name, bounds, new NullToken(), new NullToken()));
			}

			@Override
			public void visitTypeVariable(String name) {
				var refType = new GenericRefType(name, new NullToken());
				if (classTypes.isEmpty()) {
					((List<RefTypeOrTPHOrWildcardOrGeneric>) gtvs.get(gtvs.size() - 1).getBounds()).add(refType);
				} else {
					pushType(refType);
				}
			}

			@Override
			public void visitClassType(String name) {
				var refType = new RefType(new JavaClassName(name.replaceAll("/", ".")), new ArrayList<>(), new NullToken());
				classTypes.push(refType);
				pushType(refType);
			}

			void pushType(RefTypeOrTPHOrWildcardOrGeneric refType) {
				if (wildcard == SignatureVisitor.SUPER) {
					bound.push(new SuperWildcardType(refType, new NullToken()));
				} else if (wildcard == SignatureVisitor.EXTENDS) {
					bound.push(new ExtendsWildcardType(refType, new NullToken()));
				} else {
					bound.push(refType);
				}
			}

			@Override
			public SignatureVisitor visitTypeArgument(char wildcard) {
				this.wildcard = wildcard;
				return this;
			}

			boolean equals(RefTypeOrTPHOrWildcardOrGeneric a, RefTypeOrTPHOrWildcardOrGeneric b) {
				if (b instanceof SuperWildcardType wc)
					return equals(a, wc.getInnerType());
				else if (b instanceof ExtendsWildcardType wc)
					return equals(a, wc.getInnerType());
				return a == b;
			}

			@Override
			public void visitEnd() {
				wildcard = '=';
				var classType = (RefType) classTypes.pop();
				if (!classTypes.isEmpty()) {
					var next = classTypes.peek();
					var par = bound.pop();
					var toAdd = new ArrayList<RefTypeOrTPHOrWildcardOrGeneric>();
					while (!equals(next, par)) {
						toAdd.add(par);
						par = bound.pop();
					}
					var element = par;
					if (par instanceof WildcardType wc) {
						element = wc.getInnerType();
					}
					Collections.reverse(toAdd);
					((RefType) element).getParaList().addAll(toAdd);

					bound.push(par);
				} else {
					if (bound.peek() != classType) {
						classType.getParaList().add(bound.pop());
						bounds.add(classType);
					} else {
						bounds.add(bound.pop());
					}
				}
			}
		};

		var sr = new SignatureReader(signature);
		sr.accept(signatureVisitor);

		return new GenericDeclarationList(gtvs, new NullToken());
	}

	private static RefTypeOrTPHOrWildcardOrGeneric createType(java.lang.reflect.Type type) {
		if (type == null || type.getTypeName().equals("void")) {
			return new Void(new NullToken());
		} else if (type.getTypeName().equals("int")) {
			return new RefType(new JavaClassName("java.lang.Integer"), new ArrayList<>(), new NullToken(), true);
		} else if (type.getTypeName().equals("byte")) {
			return new RefType(new JavaClassName("java.lang.Byte"), new ArrayList<>(), new NullToken(), true);
		} else if (type.getTypeName().equals("boolean")) {
			return new RefType(new JavaClassName("java.lang.Boolean"), new ArrayList<>(), new NullToken(), true);
		} else if (type.getTypeName().equals("char")) {
			return new RefType(new JavaClassName("java.lang.Character"), new ArrayList<>(), new NullToken(), true);
		} else if (type.getTypeName().equals("short")) {
			return new RefType(new JavaClassName("java.lang.Short"), new ArrayList<>(), new NullToken(), true);
		} else if (type.getTypeName().equals("float")) {
			return new RefType(new JavaClassName("java.lang.Float"), new ArrayList<>(), new NullToken(), true);
		} else if (type.getTypeName().equals("double")) {
			return new RefType(new JavaClassName("java.lang.Double"), new ArrayList<>(), new NullToken(), true);
		} else if (type.getTypeName().equals("long")) {
			return new RefType(new JavaClassName("java.lang.Long"), new ArrayList<>(), new NullToken(), true);
		} else {
			if (type instanceof TypeVariable) {
				// GTVDeclarationContext via "(TypeVariable) type).getGenericDeclaration()"
				return new GenericRefType(type.getTypeName(), new NullToken());
			}
			List<RefTypeOrTPHOrWildcardOrGeneric> params = new ArrayList<>();
			if (type instanceof ParameterizedType) {
				for (Type t : ((ParameterizedType) type).getActualTypeArguments()) {
					params.add(createType(t));
				}
			}
			String name = type.getTypeName();
			if (name.contains("<")) { // Komischer fix. Type von Generischen Typen kann die Generics im Namen enthalten Type<A>
				// Diese entfernen:
				name = name.split("<")[0];
			}
			if (type instanceof java.lang.reflect.WildcardType) {
				java.lang.reflect.WildcardType wildcardType = (java.lang.reflect.WildcardType) type;
				if (wildcardType.getLowerBounds().length > 0) {
					return new SuperWildcardType(createType(wildcardType.getLowerBounds()[0]), new NullToken());
				} else if (wildcardType.getUpperBounds().length > 0) {
					return new ExtendsWildcardType(createType(wildcardType.getUpperBounds()[0]), new NullToken());
				} else {// Es handelt sich um den '?'-Typ:
					return new ExtendsWildcardType(createObjectType(), new NullToken());
				}
			} else {
				RefType ret = new RefType(new JavaClassName(name), params, new NullToken());
				return ret;
			}
		}
	}

	public static de.dhbwstuttgart.syntaxtree.GenericTypeVar createGeneric(TypeVariable jreTypeVar, String jreTVName, Class context, String parentMethod) {
		JavaClassName parentClass = new JavaClassName(context.getName());
		List<RefTypeOrTPHOrWildcardOrGeneric> genericBounds = new ArrayList<>();
		java.lang.reflect.Type[] bounds = jreTypeVar.getBounds();
		if (bounds.length > 0) {
			for (java.lang.reflect.Type bound : bounds) {
				genericBounds.add(createType(bound));
			}
		}
		return new de.dhbwstuttgart.syntaxtree.GenericTypeVar(jreTVName, genericBounds, new NullToken(), new NullToken());
	}

	public static ClassOrInterface createObjectClass() {
		return createClass(Object.class);
	}

	public static RefType createObjectType() {
		return new RefType(createClass(Object.class).getClassName(), new NullToken());
	}

	/*
	 * public Constructor createEmptyConstructor(Class parent){ Block block = new Block(); block.setType(new de.dhbwstuttgart.syntaxtree.type.Void(block, 0)); block.statements.add(new SuperCall(block));
	 * 
	 * return ASTFactory.createConstructor(parent, new ParameterList(), block); }
	 * 
	 * public static Constructor createConstructor(Class superClass, ParameterList paralist, Block block){ block.parserPostProcessing(superClass);
	 * 
	 * Method method = ASTFactory.createMethod("<init>", paralist, block, superClass); method.setType(new de.dhbwstuttgart.syntaxtree.type.Void(block, 0));
	 * 
	 * return new Constructor(method, superClass); }
	 * 
	 * public static Class createClass(String className, RefType type, Modifiers modifiers, Menge supertypeGenPara, SourceFile parent) { // TODO bytecode createClass //String name, RefType superClass, Modifiers modifiers, Menge<String> supertypeGenPara Class generatedClass = new Class(className, type, modifiers, supertypeGenPara); generatedClass.addField(ASTFactory.createEmptyConstructor(generatedClass));
	 * 
	 * generatedClass.parserPostProcessing(parent);
	 * 
	 * return generatedClass; }
	 * 
	 * public static Class createObject(){ return createClass(java.lang.Object.class); }
	 * 
	 * public static Class createInterface(String className, RefType superClass, Modifiers modifiers, Menge supertypeGenPara, SourceFile parent){ Class generatedClass = new Class(new JavaClassName(className), new ArrayList<Method>(), new ArrayList<Field>(), modifiers, true, superClass, new ArrayList<RefType>(), new GenericDeclarationList(), -1); generatedClass.parserPostProcessing(parent); return generatedClass; }
	 * 
	 * public static RefType createObjectType(){ return createObjectClass().getType(); }
	 */
}