package edu.iastate.cs.egroum.aug;

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

import edu.iastate.cs.egroum.utils.JavaASTUtil;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.ArrayAccess;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SuperFieldAccess;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;

public class EGroumBuildingContext {
	public static HashMap<String, HashMap<String, String>> typeFieldType = new HashMap<>();
	public static HashMap<String, HashMap<String, HashSet<String>>> typeMethodExceptions = new HashMap<>();
	public static HashMap<String, HashSet<String>> exceptionHierarchy = new HashMap<>();
	
	private MethodDeclaration method;
	private String type = "this", superType = "Object";
	protected boolean interprocedural;
	private Stack<ArrayList<EGroumActionNode>> stkTrys = new Stack<>(), stkInnerTrys = new Stack<>();
	private Stack<HashMap<String, String>> localVariables = new Stack<>(), localVariableTypes = new Stack<>();
	private HashMap<String, String> fieldTypes = new HashMap<>();
	
	public EGroumBuildingContext(boolean interprocedural) {
		this.interprocedural = interprocedural;
	}
	
	public EGroumBuildingContext(EGroumBuildingContext context) {
		this.interprocedural = context.interprocedural;
	}

	public void setMethod(MethodDeclaration method) {
		this.method = method;
		ASTNode p = this.method.getParent();
		if (p != null) {
			if (p instanceof AbstractTypeDeclaration) {
				AbstractTypeDeclaration atd = (AbstractTypeDeclaration) p;
				this.type = atd.getName().getIdentifier();
			}
			if (p instanceof TypeDeclaration) {
				buildFieldType((TypeDeclaration) p);
				TypeDeclaration td = (TypeDeclaration) p;
				if (td.getSuperclassType() != null)
					this.superType = JavaASTUtil.getSimpleType(td.getSuperclassType());
			}
			else if (p instanceof EnumDeclaration)
				buildFieldType((EnumDeclaration) p);
		}
	}
	
	public String getType() {
		return type;
	}

	public String getSuperType() {
		return superType;
	}

	private void buildFieldType(EnumDeclaration ed) {
		for (int i = 0; i < ed.bodyDeclarations().size(); i++) {
			if (ed.bodyDeclarations().get(i) instanceof FieldDeclaration) {
				FieldDeclaration f = (FieldDeclaration) ed.bodyDeclarations().get(i);
				String type = JavaASTUtil.getSimpleType(f.getType());
				for (int j = 0; j < f.fragments().size(); j++) {
					VariableDeclarationFragment vdf = (VariableDeclarationFragment) f.fragments().get(j);
					String dimensions = "";
					for (int k = 0; k < vdf.getExtraDimensions(); k++)
						dimensions += "[]";
					buildFieldType(vdf.getName().getIdentifier(), type + dimensions);
				}
			}
		}
		ASTNode p = ed.getParent();
		if (p != null) {
			if (p instanceof TypeDeclaration)
				buildFieldType((TypeDeclaration) p);
			else if (p instanceof EnumDeclaration)
				buildFieldType((EnumDeclaration) p);
		}
	}

	private void buildFieldType(TypeDeclaration td) {
		for (FieldDeclaration f : td.getFields()) {
			String type = JavaASTUtil.getSimpleType(f.getType());
			for (int i = 0; i < f.fragments().size(); i++) {
				VariableDeclarationFragment vdf = (VariableDeclarationFragment) f.fragments().get(i);
				String dimensions = "";
				for (int j = 0; j < vdf.getExtraDimensions(); j++)
					dimensions += "[]";
				buildFieldType(vdf.getName().getIdentifier(), type + dimensions);
			}
		}
		ASTNode p = td.getParent();
		if (p != null) {
			if (p instanceof TypeDeclaration)
				buildFieldType((TypeDeclaration) p);
			else if (p instanceof EnumDeclaration)
				buildFieldType((EnumDeclaration) p);
		}
	}

	private void buildFieldType(String name, String type) {
		if (!this.fieldTypes.containsKey(name))
			this.fieldTypes.put(name, type);
	}

	public void addMethodTry(EGroumActionNode node) {
		/*for (int i = 0; i < stkTrys.size(); i++)
			stkTrys.get(i).add(node);*/
		if (!stkTrys.isEmpty())
			stkTrys.peek().add(node);
	}

	public void pushTry() {
		stkTrys.push(new ArrayList<EGroumActionNode>());
		stkInnerTrys.push(new ArrayList<EGroumActionNode>());
	}
	
	public ArrayList<EGroumActionNode> popTry() {
		ArrayList<EGroumActionNode> methods = stkTrys.pop();
		if (!stkInnerTrys.isEmpty())
			stkInnerTrys.peek().addAll(methods);
		return methods;
	}

	public ArrayList<EGroumActionNode> popInnerTry() {
		ArrayList<EGroumActionNode> methods = stkInnerTrys.pop();
		if (!stkInnerTrys.isEmpty())
			stkInnerTrys.peek().addAll(methods);
		return methods;
	}

	public HashSet<EGroumActionNode> getTrys(String catchExceptionType, ArrayList<EGroumActionNode> triedMethods) {
		HashSet<EGroumActionNode> trys = new HashSet<>();
		for (EGroumActionNode node : triedMethods)
			if (node.exceptionTypes != null) {
				for (String type : node.exceptionTypes)
					if (isSuperType(catchExceptionType, type)) {
						trys.add(node);
						break;
					}
			}
		return trys;
	}

	private boolean isSuperType(String type, String otherType) {
		if (type == null) 
			return false;
		if (type.equals(otherType))
			return true;
		HashSet<String> subs = exceptionHierarchy.get(type);
		if (subs != null) {
			if (subs.contains(otherType))
				return true;
			for (String sub : subs)
				if (isSuperType(sub, otherType))
					return true;
		}
		return false;
	}

	public String getKey(ArrayAccess astNode) {
		String name = null;
		Expression a = astNode.getArray();
		if (a instanceof ArrayAccess)
			name = getKey((ArrayAccess) astNode.getArray());
		else if (a instanceof FieldAccess) {
			name = a.toString();
		} 
		else if (a instanceof QualifiedName)
			name = ((QualifiedName) a).getFullyQualifiedName();
		else if (a instanceof SimpleName) {
			name = ((SimpleName) a).getIdentifier();
			String[] info = getLocalVariableInfo(name);
			if (info != null)
				name = info[0];
		} else if (a instanceof SuperFieldAccess) {
			name = a.toString();
		}
		if (astNode.getIndex() instanceof NumberLiteral)
			name += "[" + ((NumberLiteral) (astNode.getIndex())).getToken()
					+ "]";
		else
			name += "[int]";
		return name;
	}

	public String[] getLocalVariableInfo(String identifier) {
		for (int i = localVariables.size() - 1; i >= 0; i--) {
			HashMap<String, String> variables = this.localVariables.get(i);
			if (variables.containsKey(identifier))
				return new String[]{variables.get(identifier), this.localVariableTypes.get(i).get(identifier)};
		}
		return null;
	}

	public void addScope() {
		this.localVariables.push(new HashMap<String, String>());
		this.localVariableTypes.push(new HashMap<String, String>());
	}

	public void removeScope() {
		this.localVariables.pop();
		this.localVariableTypes.pop();
	}

	public void addLocalVariable(String identifier, String key, String type) {
		this.localVariables.peek().put(identifier, key);
		this.localVariableTypes.peek().put(identifier, type);
	}

	public String getFieldType(SimpleName node) {
		if (node.resolveTypeBinding() != null) {
			return node.resolveTypeBinding().getTypeDeclaration().getName();
		}
		String name = node.getIdentifier();
		String type = this.fieldTypes.get(name);
		if (type == null) {
			buildSuperFieldTypes();
			type = this.fieldTypes.get(name);
		}
		return type;
	}

	public void buildSuperFieldTypes() {
		ASTNode p = this.method.getParent();
		if (p != null && p instanceof TypeDeclaration)
			buildSuperFieldTypes((TypeDeclaration) p);
	}

	private void buildSuperFieldTypes(TypeDeclaration td) {
		if (td.getSuperclassType() != null) {
			String stype = JavaASTUtil.getSimpleType(td.getSuperclassType());
			buildSuperFieldTypes(stype);
		}
		ASTNode p = td.getParent();
		if (p != null && p instanceof TypeDeclaration)
			buildSuperFieldTypes((TypeDeclaration) p);
	}

	private void buildSuperFieldTypes(String stype) {
		ArrayList<String> qns = getQualifiedTypes(stype);
		for (String qn : qns) {
			HashMap<String, String> superFieldType = typeFieldType.get(qn);
			if (superFieldType != null) {
				for (String name : superFieldType.keySet())
					buildFieldType(name, superFieldType.get(name));
				break;
			}
		}
	}

	private ArrayList<String> getQualifiedTypes(String stype) {
		ArrayList<String> qts = new ArrayList<>();
		int index = stype.indexOf('.');
		String qual = null;
		if (index > -1) {
			if (Character.isLowerCase(stype.charAt(0))) {
				qts.add(stype);
				return qts;
			}
			qual = stype.substring(0, index);
		}
		CompilationUnit cu = (CompilationUnit) this.method.getRoot();
		for (int i = 0; i < cu.imports().size(); i++) {
			ImportDeclaration id = (ImportDeclaration) cu.imports().get(i);
			if (id.isOnDemand()) {
				qts.add(id.getName().getFullyQualifiedName() + "." + stype);
			} else if (!id.isStatic()) {
				String qn = id.getName().getFullyQualifiedName();
				if (qn.endsWith("." + stype)) {
					qts.add(qn);
					return qts;
				}
				if (qual != null && qn.endsWith("." + qual)) {
					qts.add(qn + stype.substring(qual.length()));
					return qts;
				}
			}
		}
		String pkg = "";
		if (cu.getPackage() != null)
			pkg = cu.getPackage().getName().getFullyQualifiedName();
		qts.add(0, pkg + "." + stype);
		return qts;
	}

	public static HashSet<String> getExceptions(String type, String method) {
		HashMap<String, HashSet<String>> methodExceptions = typeMethodExceptions.get(type);
		if (methodExceptions != null)
			return methodExceptions.get(method);
		return null;
	}

	public static void buildExceptionHierarchy() {
		HashSet<String> exceptionTypes = new HashSet<>();
		buildExceptionHierarchy("Throwable", exceptionTypes);
		for (String type : new HashSet<String>(exceptionHierarchy.keySet()))
			if (!exceptionTypes.contains(type))
				exceptionHierarchy.remove(type);
	}

	private static void buildExceptionHierarchy(String type, HashSet<String> exceptionTypes) {
		if (exceptionTypes.contains(type))
			return;
		exceptionTypes.add(type);
		HashSet<String> subs = exceptionHierarchy.get(type);
		if (subs != null)
			for (String sub : subs)
				buildExceptionHierarchy(sub, exceptionTypes);
	}
}
