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

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.Modifier;

import us.msu.cse.repair.core.util.Helper;

public class FieldVarDetector {
	List<ModificationPoint> modificationPoints;

	Map<String, ITypeBinding> declaredClasses;

	Set<String> dependences;

	Map<String, Map<String, VarInfo>> declaredFieldMap;
	Map<String, Map<String, VarInfo>> inheritedFieldMap;
	Map<String, Map<String, VarInfo>> outerFieldMap;

	URLClassLoader classLoader;

	public FieldVarDetector(List<ModificationPoint> modificationPoints, Map<String, ITypeBinding> declaredClasses,
			Set<String> dependences) throws MalformedURLException {
		this.modificationPoints = modificationPoints;
		this.declaredClasses = declaredClasses;

		declaredFieldMap = new HashMap<String, Map<String, VarInfo>>();
		inheritedFieldMap = new HashMap<String, Map<String, VarInfo>>();
		outerFieldMap = new HashMap<String, Map<String, VarInfo>>();

		if (dependences != null) {
			URL[] urls = Helper.getURLs(dependences);
			classLoader = new URLClassLoader(urls);
		} else
			classLoader = new URLClassLoader(new URL[0]);
	}

	public void detect() throws ClassNotFoundException, IOException {
		for (ModificationPoint mp : modificationPoints)
			detectVisibleFields(mp);
		classLoader.close();
	}

	void detectVisibleFields(ModificationPoint mp) throws ClassNotFoundException {
		String className = mp.getLCNode().getClassName();

		detectVisibleFields(className);

		Map<String, VarInfo> declaredFields = declaredFieldMap.get(className);
		Map<String, VarInfo> inheritedFields = inheritedFieldMap.get(className);
		Map<String, VarInfo> outerFields = outerFieldMap.get(className);

		if (mp.isInStaticMethod()) {
			declaredFields = getStaticFields(declaredFields);
			inheritedFields = getStaticFields(inheritedFields);
		}

		mp.setDeclaredFields(declaredFields);
		mp.setInheritedFields(inheritedFields);
		mp.setOuterFields(outerFields);
	}

	void detectVisibleFields(String className) throws ClassNotFoundException {
		if (declaredFieldMap.containsKey(className))
			return;

		Map<String, VarInfo> fields;
		String superClassName = null, outerClassName = null;
		boolean isStaticClass;

		if (declaredClasses.containsKey(className)) {
			ITypeBinding tb = declaredClasses.get(className);
			IVariableBinding[] vbs = tb.getDeclaredFields();
			fields = Helper.getVarInfos(vbs);

			ITypeBinding superClass = tb.getSuperclass();
			if (superClass != null && !superClass.isInterface())
				superClassName = superClass.getBinaryName();

			ITypeBinding outerClass = tb.getDeclaringClass();
			if (outerClass != null && !outerClass.isInterface())
				outerClassName = outerClass.getBinaryName();

			isStaticClass = Modifier.isStatic(tb.getModifiers());
		} else {
			Class<?> target = classLoader.loadClass(className);
			Field[] fs = target.getDeclaredFields();
			fields = Helper.getVarInfos(fs);

			Class<?> superClass = target.getSuperclass();
			if (superClass != null && !superClass.isInterface())
				superClassName = superClass.getName();

			Class<?> outerClass = target.getDeclaringClass();
			if (outerClass != null && !outerClass.isInterface())
				outerClassName = outerClass.getName();

			isStaticClass = Modifier.isStatic(target.getModifiers());
		}
		declaredFieldMap.put(className, fields);

		Map<String, VarInfo> inheritedFields = new HashMap<String, VarInfo>();
		if (superClassName != null) {
			detectVisibleFields(superClassName);

			Map<String, VarInfo> declaredFieldsOfSuper = declaredFieldMap.get(superClassName);
			Map<String, VarInfo> inheritedFieldsOfSuper = inheritedFieldMap.get(superClassName);

			collectInheritedFields(inheritedFieldsOfSuper, inheritedFields, className, superClassName);
			collectInheritedFields(declaredFieldsOfSuper, inheritedFields, className, superClassName);
		}

		HashMap<String, VarInfo> outerFields = new HashMap<String, VarInfo>();
		if (outerClassName != null) {
			detectVisibleFields(outerClassName);

			Map<String, VarInfo> declaredFieldsOfOuter = declaredFieldMap.get(outerClassName);
			Map<String, VarInfo> inheritedFieldsOfOuter = inheritedFieldMap.get(outerClassName);
			Map<String, VarInfo> outerFieldsOfOuter = outerFieldMap.get(outerClassName);

			collectOuterFields(outerFieldsOfOuter, outerFields, isStaticClass);
			collectOuterFields(inheritedFieldsOfOuter, outerFields, isStaticClass);
			collectOuterFields(declaredFieldsOfOuter, outerFields, isStaticClass);

		}

		inheritedFieldMap.put(className, inheritedFields);
		outerFieldMap.put(className, outerFields);
	}

	Map<String, VarInfo> getStaticFields(Map<String, VarInfo> fields) {
		Map<String, VarInfo> staticFields = new HashMap<String, VarInfo>();

		for (Map.Entry<String, VarInfo> entry : fields.entrySet()) {
			if (Helper.isStaticVar(entry.getValue()))
				staticFields.put(entry.getKey(), entry.getValue());
		}

		return staticFields;
	}

	void collectOuterFields(Map<String, VarInfo> map, Map<String, VarInfo> outerFields, boolean isStaticClass) {

		if (!isStaticClass) {
			for (Map.Entry<String, VarInfo> entry : map.entrySet())
				outerFields.put(entry.getKey(), entry.getValue());
		} else {
			for (Map.Entry<String, VarInfo> entry : map.entrySet()) {
				if (Helper.isStaticVar(entry.getValue()))
					outerFields.put(entry.getKey(), entry.getValue());
			}
		}
	}

	void collectInheritedFields(Map<String, VarInfo> map, Map<String, VarInfo> inheritedFields, String className,
			String superClassName) {
		for (Map.Entry<String, VarInfo> entry : map.entrySet()) {
			boolean flag1 = Helper.isPublicVar(entry.getValue());
			boolean flag2 = Helper.isProtectedVar(entry.getValue());
			boolean flag3 = Helper.isPackagePrivateVar(entry.getValue())
					&& Helper.isInSamePackage(className, superClassName);

			if (flag1 || flag2 || flag3)
				inheritedFields.put(entry.getKey(), entry.getValue());
		}
	}
}
