package antipatternsrecovery.metrics;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Vector;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;

import antipatternsrecovery.parsingElements.FieldAccessVisitor;
import antipatternsrecovery.parsingElements.MethodInvocationsVisitor;
import antipatternsrecovery.parsingElements.Parser;
import antipatternsrecovery.parsingElements.VariableDeclarationVisitor;


/*
 * # method pairs sharing attributes or having method calls among them / # method pairs in the class
 */
public class ConnectivityMetric {

	public double computeConnectivityMetric(ICompilationUnit pClass) {
		double nominator=0.0;
		double denominator=0.0;

		if(pClass.exists()) {

			IType type=pClass.getType(pClass.getElementName().substring(0, pClass.getElementName().length()-5));
			Vector<Vector<IMethod>> alreadyAnalyzed=new Vector<Vector<IMethod>>(); 

			try {
				for(IMethod method: type.getMethods()) {
					for (IMethod secondMethod: type.getMethods()) {
						if(! method.equals(secondMethod)) {
							if((this.hasCallsAmongThem(method, secondMethod)) || this.hasSharedAttributes(method, secondMethod))
								nominator++;
						}
					}
				}

				if(type.getMethods().length > 0) {
					denominator=type.getMethods().length*2;//this.combination(type.getMethods().length, 2);
				}

			} catch (JavaModelException e) {
				e.printStackTrace();
			}
		}
		if(denominator != 0.0) {
			return nominator/denominator;
		}
		else return 0.0;
	}

	private boolean hasSharedAttributes(IMethod pFirstMethod, IMethod pSecondMethod) {
		try {
			ArrayList<String> classVariables = (ArrayList<String>) this.getVariablesInClass(pFirstMethod.getCompilationUnit().getSource());

			for(String variable: classVariables) {
				boolean variableUsedByFirstMethod = this.useInstanceVariable(pFirstMethod.getSource(), variable);
				boolean variableUsedBySecondMethod = this.useInstanceVariable(pSecondMethod.getSource(), variable);

				if(variableUsedByFirstMethod && variableUsedBySecondMethod) 
					return true;
			}

		} catch (JavaModelException e) {
			e.printStackTrace();
		}


		return false;
	}

	private boolean useInstanceVariable(String pMethod, String pInstanceVariable) {
		String methodToParse = "public class A {" + pMethod + "}";

		FieldAccessVisitor fav = new FieldAccessVisitor();

		CompilationUnit unit = null;

		Parser parser = new Parser();
		unit = parser.createParser(methodToParse, true);

		unit.accept(fav);

		for(FieldAccess fa: fav.getFields()) {
			if(fa.toString().contains("this."+pInstanceVariable))
				return true;
			else if (fa.toString().contains(pInstanceVariable))
				return true;
		}

		return false;
	}

	private Collection<String> getVariablesInClass(String pClassCode) {
		ArrayList<String> variables = new ArrayList<String>();
		CompilationUnit unit = null;
		VariableDeclarationVisitor variableDeclarationVisitor = new VariableDeclarationVisitor();

		Parser parser = new Parser();
		unit = parser.createParser(pClassCode, true);

		unit.accept(variableDeclarationVisitor);

		for(VariableDeclarationFragment variableDeclaration: variableDeclarationVisitor.getFields()) {
			variables.add(variableDeclaration.toString());
		}

		return variables;
	}


	private boolean hasCallsAmongThem(IMethod pFirstMethod, IMethod pSecondMethod) {
		MethodInvocationsVisitor methodInvocationVisitor=new MethodInvocationsVisitor();

		CompilationUnit unit = null;

		try {
			Parser parser = new Parser();
			String methodToParse = "public class A {" + pFirstMethod.getSource() + "}";
			unit = parser.createParser(methodToParse, true);

			unit.accept(methodInvocationVisitor);

			for(MethodInvocation methodInvocation: methodInvocationVisitor.getMethods()) {
				if(methodInvocation.toString().contains("this." + pSecondMethod.getElementName()))
					return true;
				else if(methodInvocation.toString().contains(pSecondMethod.getElementName()))
					return true;
			}

		} catch (JavaModelException e) {
			e.printStackTrace();
		}

		// Check if the first method calls the second one
		try {
			Parser parser = new Parser();
			unit = parser.createParser(pSecondMethod.getSource(), true);

			unit.accept(methodInvocationVisitor);

			for(MethodInvocation methodInvocation: methodInvocationVisitor.getMethods()) {
				if(methodInvocation.toString().contains("this." + pFirstMethod.getElementName()))
					return true;
				else if(methodInvocation.toString().contains(pFirstMethod.getElementName()))
					return true;
			}

		} catch (JavaModelException e) {
			e.printStackTrace();
		}

		return false;
	}

	private double combination(int n, int k) {
		BigInteger factorN = this.factor(n); 
		BigInteger factorNk = this.factor(n-k);

		BigInteger result = factorN.divide(factorNk);

		return result.doubleValue();
	}

	private BigInteger factor(int pToCalculate) {
		BigInteger fatt = new BigInteger("1");
		BigInteger k = new BigInteger("1");
		BigInteger j = new BigInteger("1");
		long cont ;
		long n;
		n = pToCalculate;

		for(cont = 1; cont <= n; cont ++){
			fatt = fatt.multiply(k);
			k = k.add(j);
		}

		return fatt;
	}
}
