package us.msu.cse.repair.core.parser.ingredient;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.core.dom.ASTMatcher;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.Type;

import us.msu.cse.repair.core.parser.LCNode;
import us.msu.cse.repair.core.parser.MethodInfo;
import us.msu.cse.repair.core.parser.ModificationPoint;
import us.msu.cse.repair.core.parser.SeedStatement;
import us.msu.cse.repair.core.parser.SeedStatementInfo;
import us.msu.cse.repair.core.parser.StatementInfoExtractor;
import us.msu.cse.repair.core.parser.VarInfo;
import us.msu.cse.repair.core.util.Helper;
import us.msu.cse.repair.core.util.visitors.ReturnStatementASTVisitor;
import us.msu.cse.repair.core.util.visitors.ClassInstanceAndArrayCreationVisitor;
import us.msu.cse.repair.core.util.visitors.ReturnOrThrowStatementASTVisitor;

public class IngredientUtil {

	public static boolean doSuperVarMatch(Map<String, String> superVarMatchMap, Map<String, VarInfo> superVars,
			Map<String, VarInfo> inheritedFields) {
		List<Map<String, VarInfo>> varScopes = new ArrayList<Map<String, VarInfo>>();
		varScopes.add(inheritedFields);
		return doMatch1(superVarMatchMap, superVars, varScopes);
	}

	public static boolean doThisVarMatch(Map<String, String> thisVarMatchMap, Map<String, VarInfo> thisVars,
			Map<String, VarInfo> declaredFields, Map<String, VarInfo> inheritedFields) {

		List<Map<String, VarInfo>> varScopes = new ArrayList<Map<String, VarInfo>>();

		varScopes.add(declaredFields);
		varScopes.add(inheritedFields);

		return doMatch1(thisVarMatchMap, thisVars, varScopes);
	}

	public static boolean doVarMatch(Map<String, String> varMatchMap, Map<String, VarInfo> vars,
			Map<String, VarInfo> localVars, Map<String, VarInfo> declaredFields, Map<String, VarInfo> inheritedFields,
			Map<String, VarInfo> outerFields) {
		List<Map<String, VarInfo>> varScopes = new ArrayList<Map<String, VarInfo>>();

		varScopes.add(localVars);
		varScopes.add(declaredFields);
		varScopes.add(inheritedFields);
		varScopes.add(outerFields);

		return doMatch1(varMatchMap, vars, varScopes);
	}

	static boolean doMatch1(Map<String, String> matchMap, Map<String, VarInfo> vars,
			List<Map<String, VarInfo>> varScopes) {
		Iterator<Map.Entry<String, VarInfo>> iterator = vars.entrySet().iterator();
		while (iterator.hasNext()) {
			Map.Entry<String, VarInfo> varEntry = iterator.next();
			if (!doMatch1(matchMap, varEntry, varScopes))
				return false;
			else
				iterator.remove();
		}

		return true;

	}

	static boolean doMatch1(Map<String, String> matchMap, Map.Entry<String, VarInfo> varEntry,
			List<Map<String, VarInfo>> varScopes) {

		Map.Entry<String, VarInfo> maxEntry = null;
		Map<String, VarInfo> maxScope = null;
		int maxDegree = 0;

		for (Map<String, VarInfo> scope : varScopes) {
			for (Map.Entry<String, VarInfo> entry : scope.entrySet()) {
				int degree = getVarMatchDegree(varEntry, entry);
				if (degree > maxDegree) {
					maxDegree = degree;
					maxEntry = entry;
					maxScope = scope;
				}
			}
		}

		if (maxDegree > 0) {
			String name = varEntry.getKey();
			String newName = maxEntry.getKey();
			matchMap.put(name, newName);
			maxScope.remove(newName);
			return true;
		} else
			return false;
	}

	static int getVarMatchDegree(Map.Entry<String, VarInfo> entry1, Map.Entry<String, VarInfo> entry2) {
		String name1 = entry1.getKey();
		VarInfo vi1 = entry1.getValue();

		String name2 = entry2.getKey();
		VarInfo vi2 = entry2.getValue();

		if (!name1.equals(name2) && vi1.isStronglyTypeMatched(vi2))
			return 2;
		else if (!name1.equals(name2) && vi1.isWeaklyTypeMatched(vi2))
			return 1;
		else
			return 0;
	}

	public static void findSuperVars(Map<String, VarInfo> superVars, Map<String, VarInfo> inheritedFields) {
		List<Map<String, VarInfo>> varScopes = new ArrayList<Map<String, VarInfo>>();
		varScopes.add(inheritedFields);

		findInVarMaps(superVars, varScopes);
	}

	public static void findThisVars(Map<String, VarInfo> thisVars, Map<String, VarInfo> declaredFields,
			Map<String, VarInfo> inheritedFields) {
		List<Map<String, VarInfo>> varScopes = new ArrayList<Map<String, VarInfo>>();
		varScopes.add(declaredFields);
		varScopes.add(inheritedFields);

		findInVarMaps(thisVars, varScopes);
	}

	public static void findVars(Map<String, VarInfo> vars, Map<String, VarInfo> localVars,
			Map<String, VarInfo> declaredFields, Map<String, VarInfo> inheritedFields,
			Map<String, VarInfo> outerFields) {
		List<Map<String, VarInfo>> varScopes = new ArrayList<Map<String, VarInfo>>();
		varScopes.add(localVars);
		varScopes.add(declaredFields);
		varScopes.add(inheritedFields);
		varScopes.add(outerFields);

		findInVarMaps(vars, varScopes);
	}

	static void findInVarMaps(Map<String, VarInfo> vars, List<Map<String, VarInfo>> varScopes) {
		Iterator<Map.Entry<String, VarInfo>> iterator = vars.entrySet().iterator();
		while (iterator.hasNext()) {
			Map.Entry<String, VarInfo> entry = iterator.next();
			String name = entry.getKey();
			VarInfo vi1 = entry.getValue();

			for (int i = 0; i < varScopes.size(); i++) {
				Map<String, VarInfo> map = varScopes.get(i);
				if (map.containsKey(name)) {
					VarInfo vi2 = map.get(name);
					if (vi1.isStronglyTypeMatched(vi2) || vi1.isWeaklyTypeMatched(vi2)) {
						iterator.remove();
						map.remove(name);
						break;
					}
				}
			}
		}
	}

	public static boolean doSuperMethodMatch(Map<String, String> superMethodMatchMap,
			Map<String, MethodInfo> superMethods, Map<String, MethodInfo> inheritedMethods) {
		List<Map<String, MethodInfo>> methodScopes = new ArrayList<Map<String, MethodInfo>>();
		methodScopes.add(inheritedMethods);
		return doMatch2(superMethodMatchMap, superMethods, methodScopes);
	}

	public static boolean doThisMethodMatch(Map<String, String> thisMethodMatchMap, Map<String, MethodInfo> thisMethods,
			Map<String, MethodInfo> declaredMethods, Map<String, MethodInfo> inheritedMethods) {

		List<Map<String, MethodInfo>> methodScopes = new ArrayList<Map<String, MethodInfo>>();

		methodScopes.add(declaredMethods);
		methodScopes.add(inheritedMethods);

		return doMatch2(thisMethodMatchMap, thisMethods, methodScopes);
	}

	public static boolean doMethodMatch(Map<String, String> methodMatchMap, Map<String, MethodInfo> methods,
			Map<String, MethodInfo> declaredMethods, Map<String, MethodInfo> inheritedMethods,
			Map<String, MethodInfo> outerMethods) {
		List<Map<String, MethodInfo>> methodScopes = new ArrayList<Map<String, MethodInfo>>();

		methodScopes.add(declaredMethods);
		methodScopes.add(inheritedMethods);
		methodScopes.add(outerMethods);

		return doMatch2(methodMatchMap, methods, methodScopes);
	}

	static boolean doMatch2(Map<String, String> matchMap, Map<String, MethodInfo> methods,
			List<Map<String, MethodInfo>> methodScopes) {
		Iterator<Map.Entry<String, MethodInfo>> iterator = methods.entrySet().iterator();

		while (iterator.hasNext()) {
			Map.Entry<String, MethodInfo> varEntry = iterator.next();
			if (!doMatch2(matchMap, varEntry, methodScopes))
				return false;
			else
				iterator.remove();
		}

		return true;

	}

	static boolean doMatch2(Map<String, String> matchMap, Map.Entry<String, MethodInfo> varEntry,
			List<Map<String, MethodInfo>> methodScopes) {

		Map.Entry<String, MethodInfo> maxEntry = null;
		Map<String, MethodInfo> maxScope = null;
		int maxDegree = 0;

		for (Map<String, MethodInfo> scope : methodScopes) {
			for (Map.Entry<String, MethodInfo> entry : scope.entrySet()) {
				int degree = getMethodMatchDegree(varEntry, entry);
				if (degree > maxDegree) {
					maxDegree = degree;
					maxEntry = entry;
					maxScope = scope;
				}
			}
		}

		if (maxDegree > 0) {
			String key = varEntry.getKey();
			String newKey = maxEntry.getKey();
			matchMap.put(key, Helper.getMethodName(newKey));
			maxScope.remove(newKey);
			return true;
		} else
			return false;

	}

	static int getMethodMatchDegree(Map.Entry<String, MethodInfo> entry1, Map.Entry<String, MethodInfo> entry2) {
		MethodInfo mi1 = entry1.getValue();
		String ps1 = mi1.getParameterTypeNames();

		MethodInfo mi2 = entry2.getValue();
		String ps2 = mi2.getParameterTypeNames();

		if (ps1.equals(ps2) && mi1.isStronglyReturnTypeMatched(mi2))
			return 2;
		else if (ps1.equals(ps2) && mi1.isWeaklyReturnTypeMatched(mi2))
			return 1;
		else
			return 0;
	}

	public static void findSuperMethods(Map<String, MethodInfo> superMethods,
			Map<String, MethodInfo> inheritedMethods) {
		List<Map<String, MethodInfo>> methodScopes = new ArrayList<Map<String, MethodInfo>>();
		methodScopes.add(inheritedMethods);

		findInMethodMaps(superMethods, methodScopes);
	}

	public static void findThisMethods(Map<String, MethodInfo> thisMethods, Map<String, MethodInfo> declaredMethods,
			Map<String, MethodInfo> inheritedMethods) {
		List<Map<String, MethodInfo>> methodScopes = new ArrayList<Map<String, MethodInfo>>();
		methodScopes.add(declaredMethods);
		methodScopes.add(inheritedMethods);

		findInMethodMaps(thisMethods, methodScopes);
	}

	public static void findMethods(Map<String, MethodInfo> methods, Map<String, MethodInfo> declaredMethods,
			Map<String, MethodInfo> inheritedMethods, Map<String, MethodInfo> outerMethods) {
		List<Map<String, MethodInfo>> methodScopes = new ArrayList<Map<String, MethodInfo>>();
		methodScopes.add(declaredMethods);
		methodScopes.add(inheritedMethods);
		methodScopes.add(outerMethods);

		findInMethodMaps(methods, methodScopes);
	}

	static void findInMethodMaps(Map<String, MethodInfo> methods, List<Map<String, MethodInfo>> methodScopes) {
		Iterator<Map.Entry<String, MethodInfo>> iterator = methods.entrySet().iterator();
		while (iterator.hasNext()) {
			Map.Entry<String, MethodInfo> entry = iterator.next();
			String name = entry.getKey();
			MethodInfo mi1 = entry.getValue();

			for (int i = 0; i < methodScopes.size(); i++) {
				Map<String, MethodInfo> map = methodScopes.get(i);
				if (map.containsKey(name)) {
					MethodInfo mi2 = map.get(name);

					if (mi1.isStronglyReturnTypeMatched(mi2) || mi1.isWeaklyReturnTypeMatched(mi2)) {
						iterator.remove();
						map.remove(name);
					}
					break;
				}
			}
		}
	}

	public static boolean isInMethodScope(Statement seed, ModificationPoint mp, StatementInfoExtractor sie) {
		Map<String, MethodInfo> methods = sie.getMethods();
		Map<String, MethodInfo> thisMethods = sie.getThisMethods();
		Map<String, MethodInfo> superMethods = sie.getSuperMethods();

		if (mp.isInStaticMethod() && (thisMethods.size() > 0 || superMethods.size() > 0))
			return false;
		else if (!findThisMethods(thisMethods, mp))
			return false;
		else if (!findSuperMethods(superMethods, mp))
			return false;
		else if (!findMethods(methods, mp))
			return false;
		else
			return true;

	}

	public static boolean isInVarScope(Statement seed, ModificationPoint mp, StatementInfoExtractor sie) {
		Map<String, VarInfo> vars = sie.getVars();
		Map<String, VarInfo> thisVars = sie.getThisVars();
		Map<String, VarInfo> superVars = sie.getSuperVars();

		if (mp.isInStaticMethod() && (thisVars.size() > 0 || superVars.size() > 0))
			return false;
		else if (!findThisVars(thisVars, mp))
			return false;
		else if (!findSuperVars(superVars, mp))
			return false;
		else if (!findVars(vars, mp))
			return false;
		else
			return true;
	}

	static boolean findThisMethods(Map<String, MethodInfo> thisMethods, ModificationPoint mp) {
		Map<String, MethodInfo> declaredMethods = mp.getDeclaredMethods();
		Map<String, MethodInfo> inheritedMethods = mp.getInheritedMethods();

		List<Map<String, MethodInfo>> methodScopes = new ArrayList<Map<String, MethodInfo>>();

		methodScopes.add(declaredMethods);
		methodScopes.add(inheritedMethods);

		return findInMethodMaps2(thisMethods, methodScopes);
	}

	static boolean findSuperMethods(Map<String, MethodInfo> superMethods, ModificationPoint mp) {
		Map<String, MethodInfo> inheritedMethods = mp.getInheritedMethods();

		List<Map<String, MethodInfo>> methodScopes = new ArrayList<Map<String, MethodInfo>>();
		methodScopes.add(inheritedMethods);

		return findInMethodMaps2(superMethods, methodScopes);
	}

	static boolean findMethods(Map<String, MethodInfo> methods, ModificationPoint mp) {
		Map<String, MethodInfo> declaredMethods = mp.getDeclaredMethods();
		Map<String, MethodInfo> inheritedMethods = mp.getInheritedMethods();
		Map<String, MethodInfo> outerMethods = mp.getOuterMethods();

		List<Map<String, MethodInfo>> methodScopes = new ArrayList<Map<String, MethodInfo>>();
		methodScopes.add(declaredMethods);
		methodScopes.add(inheritedMethods);
		methodScopes.add(outerMethods);

		return findInMethodMaps2(methods, methodScopes);
	}

	static boolean findInMethodMaps2(Map<String, MethodInfo> methods, List<Map<String, MethodInfo>> methodScopes) {
		for (Map.Entry<String, MethodInfo> entry : methods.entrySet()) {
			String name = entry.getKey();
			MethodInfo mi1 = entry.getValue();

			int i = 0;
			for (i = 0; i < methodScopes.size(); i++) {
				Map<String, MethodInfo> map = methodScopes.get(i);

				if (map.containsKey(name)) {
					MethodInfo mi2 = map.get(name);
					if (mi1.isStronglyReturnTypeMatched(mi2) || mi1.isWeaklyReturnTypeMatched(mi2))
						break;
					else
						return false;
				}
			}
			if (i == methodScopes.size())
				return false;
		}
		return true;
	}

	static boolean findThisVars(Map<String, VarInfo> thisVars, ModificationPoint mp) {
		Map<String, VarInfo> declaredFields = mp.getDeclaredFields();
		Map<String, VarInfo> inheritedFields = mp.getInheritedFields();

		List<Map<String, VarInfo>> varScopes = new ArrayList<Map<String, VarInfo>>();

		varScopes.add(declaredFields);
		varScopes.add(inheritedFields);

		return findInVarMaps2(thisVars, varScopes);

	}

	static boolean findSuperVars(Map<String, VarInfo> superVars, ModificationPoint mp) {
		Map<String, VarInfo> inheritedFields = mp.getInheritedFields();
		List<Map<String, VarInfo>> varScopes = new ArrayList<Map<String, VarInfo>>();

		varScopes.add(inheritedFields);
		return findInVarMaps2(superVars, varScopes);
	}

	static boolean findVars(Map<String, VarInfo> vars, ModificationPoint mp) {
		Map<String, VarInfo> localVars = mp.getLocalVars();
		Map<String, VarInfo> declaredFields = mp.getDeclaredFields();
		Map<String, VarInfo> inheritedFields = mp.getInheritedFields();
		Map<String, VarInfo> outerFields = mp.getOuterFields();

		List<Map<String, VarInfo>> varScopes = new ArrayList<Map<String, VarInfo>>();

		varScopes.add(localVars);
		varScopes.add(declaredFields);
		varScopes.add(inheritedFields);
		varScopes.add(outerFields);

		return findInVarMaps2(vars, varScopes);
	}

	static boolean findInVarMaps2(Map<String, VarInfo> vars, List<Map<String, VarInfo>> varScopes) {
		for (Map.Entry<String, VarInfo> entry : vars.entrySet()) {
			String name = entry.getKey();
			VarInfo vi1 = entry.getValue();

			int i = 0;
			for (i = 0; i < varScopes.size(); i++) {
				Map<String, VarInfo> map = varScopes.get(i);
				if (map.containsKey(name)) {
					VarInfo vi2 = map.get(name);
					if (vi1.isStronglyTypeMatched(vi2) || vi1.isWeaklyTypeMatched(vi2))
						break;
				}
			}
			if (i == varScopes.size())
				return false;
		}

		return true;
	}

	public static boolean isInIngredientMode(SeedStatementInfo seedStatementInfo, ModificationPoint mp,
			IngredientMode ingredientMode) {
		if (ingredientMode == IngredientMode.File) {
			List<String> paths = seedStatementInfo.getSourceFilePaths();
			String sourceFilePath = mp.getSourceFilePath();

			for (String path : paths) {
				if (sourceFilePath.equals(path))
					return true;
			}
			return false;

		} else if (ingredientMode == IngredientMode.Package) {
			String className = mp.getLCNode().getClassName();
			List<LCNode> lcNodes = seedStatementInfo.getLCNodes();

			for (LCNode lcNode : lcNodes) {
				String name = lcNode.getClassName();

				if (Helper.isInSamePackage(name, className))
					return true;
			}
			return false;

		} else
			return true;
	}

	public static boolean isReturnThrowCompatible(Statement seed, ModificationPoint mp) {
		ReturnOrThrowStatementASTVisitor visitor = new ReturnOrThrowStatementASTVisitor();
		seed.accept(visitor);
		List<Statement> rsStatements = visitor.getReturnThrowStatements();

		if (rsStatements.isEmpty())
			return true;

		MethodDeclaration md = Helper.getMethodDeclaration(mp.getStatement());
		if (md == null)
			return false;

		Type returnType = md.getReturnType2();
		ITypeBinding methodReturnTypeBinding = (returnType != null ? returnType.resolveBinding() : null);

		List<ITypeBinding> methodThrowTypeBindings = new ArrayList<ITypeBinding>();
		for (Object type : md.thrownExceptionTypes())
			methodThrowTypeBindings.add(((Type) type).resolveBinding());

		for (Statement statement : rsStatements) {
			if (statement instanceof ReturnStatement) {

				if (methodReturnTypeBinding == null)
					return false;

				ReturnStatement rs = (ReturnStatement) statement;
				if (rs.getExpression() == null) {
					if (!methodReturnTypeBinding.getQualifiedName().equals("void"))
						return false;
				} else {
					ITypeBinding tb = rs.getExpression().resolveTypeBinding();
					if (!tb.isAssignmentCompatible(methodReturnTypeBinding))
						return false;
				}
			} else {
				ThrowStatement ts = (ThrowStatement) statement;
				ITypeBinding tb = ts.getExpression().resolveTypeBinding();

				if (Helper.isRuntimeException(tb))
					continue;

				boolean isMatched = false;
				for (ITypeBinding mehodThrowTypeBinding : methodThrowTypeBindings) {
					if (tb.isAssignmentCompatible(mehodThrowTypeBinding)) {
						isMatched = true;
						break;
					}
				}
				if (!isMatched)
					return false;
			}
		}

		return true;

	}

	public static boolean isReturnCompatible(Statement seed, ModificationPoint mp) {
		MethodDeclaration md = Helper.getMethodDeclaration(mp.getStatement());
		ITypeBinding methodReturnTypeBinding = md.getReturnType2().resolveBinding();

		ReturnStatementASTVisitor visitor = new ReturnStatementASTVisitor();
		seed.accept(visitor);

		List<ReturnStatement> returnStatements = visitor.getReturnStatements();

		for (ReturnStatement rs : returnStatements) {
			if (rs.getExpression() == null) {
				if (!methodReturnTypeBinding.getQualifiedName().equals("void"))
					return false;
			} else {
				ITypeBinding tb = rs.getExpression().resolveTypeBinding();
				if (!tb.isAssignmentCompatible(methodReturnTypeBinding))
					return false;
			}
		}

		return true;
	}

	public static boolean isSelfInIngredientMode(ModificationPoint mp,
			Map.Entry<SeedStatement, SeedStatementInfo> entry, IngredientMode ingredientMode) {
		Statement statement = mp.getStatement();
		Statement seed = entry.getKey().getStatement();

		if (!statement.subtreeMatch(new ASTMatcher(true), seed))
			return false;

		int count = 0;
		if (ingredientMode == IngredientMode.File) {
			List<String> paths = entry.getValue().getSourceFilePaths();
			String sourceFilePath = mp.getSourceFilePath();

			for (String path : paths) {
				if (sourceFilePath.equals(path))
					count++;
			}

		} else if (ingredientMode == IngredientMode.Package) {
			String className = mp.getLCNode().getClassName();
			List<LCNode> lcNodes = entry.getValue().getLCNodes();

			for (LCNode lcNode : lcNodes) {
				String name = lcNode.getClassName();

				if (Helper.isInSamePackage(name, className))
					count++;
			}

		} else
			count = entry.getValue().getSourceFilePaths().size();

		return count == 1 ? true : false;
	}

	public static boolean isNewInScope(Statement seed, ModificationPoint mp) {
		Statement statement = mp.getStatement();

		Set<String> visibleTypeDecls = Helper.getVisibleTypeDeclarations(statement);

		ITypeBinding tb = Helper.getAbstractTypeDeclaration(statement).resolveBinding();
		if (tb == null)
			return false;
		String clsName = tb.getBinaryName();

		ClassInstanceAndArrayCreationVisitor ciacVisitor = new ClassInstanceAndArrayCreationVisitor();
		seed.accept(ciacVisitor);
		Set<String> classes = ciacVisitor.getClasses();

		for (String cls : classes) {
			if (!cls.contains("$")) {
				String pk = null;
				int index = cls.lastIndexOf(".");
				if (index != -1)
					pk = cls.substring(0, index);
				boolean flag1 = visibleTypeDecls.contains(cls);
				boolean flag2 = (pk != null) && visibleTypeDecls.contains(pk);

				if (!flag1 && !flag2)
					return false;
			} else { /*
				int index = cls.lastIndexOf("$");
				String pc = cls.substring(0, index);
				if (!pc.equals(clsName))
					return false;
				*/
				
				int index = cls.lastIndexOf("$");
				String pc = cls.substring(0, index);
				String qc = cls.substring(index + 1);
				
				boolean flag = false;
				for (ITypeBinding innerType : tb.getDeclaredTypes()) {
					if (innerType.getName().equals(qc))
						flag = true;
				}
				
				if (!pc.equals(clsName) && !flag)
					return false; 
			}

		}

		return true;
	}
}
