package gr.uom.java.ast.decomposition.cfg.mapping;

import java.util.List;
import java.util.Set;

import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;

import gr.uom.java.ast.decomposition.AbstractMethodFragment;
import gr.uom.java.ast.decomposition.AbstractStatement;
import gr.uom.java.ast.decomposition.cfg.AbstractVariable;
import gr.uom.java.ast.decomposition.cfg.CompositeVariable;
import gr.uom.java.ast.decomposition.cfg.PDGBlockNode;
import gr.uom.java.ast.decomposition.cfg.PDGControlDependence;
import gr.uom.java.ast.decomposition.cfg.PDGNode;
import gr.uom.java.ast.decomposition.cfg.PlainVariable;
import gr.uom.java.ast.decomposition.cfg.mapping.precondition.ExpressionPreconditionViolation;
import gr.uom.java.ast.decomposition.cfg.mapping.precondition.PreconditionViolation;
import gr.uom.java.ast.decomposition.cfg.mapping.precondition.PreconditionViolationType;
import gr.uom.java.ast.decomposition.matching.ASTNodeDifference;
import gr.uom.java.ast.decomposition.matching.ASTNodeMatcher;
import gr.uom.java.ast.decomposition.matching.Difference;
import gr.uom.java.ast.decomposition.matching.DifferenceType;

public class PDGNodeMapping extends IdBasedMapping {
	private PDGNode nodeG1;
	private PDGNode nodeG2;
	private ASTNodeMatcher matcher;
	private List<ASTNodeDifference> nodeDifferences;
	private List<AbstractMethodFragment> additionallyMatchedFragments1;
	private List<AbstractMethodFragment> additionallyMatchedFragments2;
	private PDGNodeMapping symmetricalIfNodePair;
	private boolean symmetricalIfElse;
	private volatile int hashCode = 0;
	
	public PDGNodeMapping(PDGNode nodeG1, PDGNode nodeG2, ASTNodeMatcher matcher) {
		super(nodeG1.getId(), nodeG2.getId());
		this.nodeG1 = nodeG1;
		this.nodeG2 = nodeG2;
		this.matcher = matcher;
		this.nodeDifferences = matcher.getDifferences();
		this.additionallyMatchedFragments1 = matcher.getAdditionallyMatchedFragments1();
		this.additionallyMatchedFragments2 = matcher.getAdditionallyMatchedFragments2();
	}
	
	public PDGNode getNodeG1() {
		return nodeG1;
	}

	public PDGNode getNodeG2() {
		return nodeG2;
	}

	public List<ASTNodeDifference> getNodeDifferences() {
		return nodeDifferences;
	}

	public boolean isAdvancedMatch() {
		return additionallyMatchedFragments1.size() > 0 || additionallyMatchedFragments2.size() > 0 || symmetricalIfElse;
	}

	public List<AbstractMethodFragment> getAdditionallyMatchedFragments1() {
		return additionallyMatchedFragments1;
	}

	public List<AbstractMethodFragment> getAdditionallyMatchedFragments2() {
		return additionallyMatchedFragments2;
	}

	public boolean containsAdditionallyMatchedFragment1(PDGNode node) {
		for(AbstractMethodFragment fragment : additionallyMatchedFragments1) {
			if(fragment instanceof AbstractStatement) {
				AbstractStatement statement = (AbstractStatement)fragment;
				if(statement.getStatement().equals(node.getASTStatement())) {
					return true;
				}
			}
		}
		return false;
	}

	public boolean containsAdditionallyMatchedFragment2(PDGNode node) {
		for(AbstractMethodFragment fragment : additionallyMatchedFragments2) {
			if(fragment instanceof AbstractStatement) {
				AbstractStatement statement = (AbstractStatement)fragment;
				if(statement.getStatement().equals(node.getASTStatement())) {
					return true;
				}
			}
		}
		return false;
	}

	public ITypeRoot getTypeRoot1() {
		return matcher.getTypeRoot1();
	}

	public ITypeRoot getTypeRoot2() {
		return matcher.getTypeRoot2();
	}

	public PDGNodeMapping getSymmetricalIfNodePair() {
		return symmetricalIfNodePair;
	}

	public void setSymmetricalIfNodePair(PDGNodeMapping symmetricalIfNodePair) {
		this.symmetricalIfNodePair = symmetricalIfNodePair;
	}

	public boolean isSymmetricalIfElse() {
		return symmetricalIfElse;
	}

	public void setSymmetricalIfElse(boolean symmetricalIfElse) {
		this.symmetricalIfElse = symmetricalIfElse;
	}

	public boolean isFalseControlDependent() {
		PDGControlDependence controlDependence1 = nodeG1.getIncomingControlDependence();
		PDGControlDependence controlDependence2 = nodeG2.getIncomingControlDependence();
		if(controlDependence1 != null && controlDependence2 != null)
			return controlDependence1.isFalseControlDependence() && controlDependence2.isFalseControlDependence();
		if(nodeG1 instanceof PDGBlockNode && nodeG2 instanceof PDGBlockNode)
			return isNestedUnderElse((PDGBlockNode)nodeG1) && isNestedUnderElse((PDGBlockNode)nodeG2);
		return false;
	}

	public boolean isNode1FalseControlDependent() {
		PDGControlDependence controlDependence1 = nodeG1.getIncomingControlDependence();
		if(controlDependence1 != null)
			return controlDependence1.isFalseControlDependence();
		if(nodeG1 instanceof PDGBlockNode)
			return isNestedUnderElse((PDGBlockNode)nodeG1);
		return false;
	}

	public boolean isNode2FalseControlDependent() {
		PDGControlDependence controlDependence2 = nodeG2.getIncomingControlDependence();
		if(controlDependence2 != null)
			return controlDependence2.isFalseControlDependence();
		if(nodeG2 instanceof PDGBlockNode)
			return isNestedUnderElse((PDGBlockNode)nodeG2);
		return false;
	}

	private boolean isNestedUnderElse(PDGBlockNode blockNode) {
		Statement statement = blockNode.getASTStatement();
		if(statement.getParent() instanceof Block) {
			Block block = (Block)statement.getParent();
			if(block.getParent() instanceof IfStatement) {
				IfStatement ifStatement = (IfStatement)block.getParent();
				if(ifStatement.getElseStatement() != null && ifStatement.getElseStatement().equals(block))
					return true;
			}
		}
		else if(statement.getParent() instanceof IfStatement) {
			IfStatement ifStatement = (IfStatement)statement.getParent();
			if(ifStatement.getElseStatement() != null && ifStatement.getElseStatement().equals(statement))
				return true;
		}
		return false;
	}

	public boolean matchingVariableDifference(AbstractVariable variable1, AbstractVariable variable2) {
		if(variable1.getClass() == variable2.getClass()) {
			String rightPartVariable1 = null;
			String rightPartVariable2 = null;
			if(variable1 instanceof CompositeVariable) {
				CompositeVariable comp1 = (CompositeVariable)variable1;
				CompositeVariable comp2 = (CompositeVariable)variable2;
				rightPartVariable1 = comp1.getRightPart().toString();
				rightPartVariable2 = comp2.getRightPart().toString();
			}
			boolean equalRightPart = false;
			if(rightPartVariable1 != null && rightPartVariable2 != null) {
				equalRightPart = rightPartVariable1.equals(rightPartVariable2);
			}
			else {
				equalRightPart = true;
			}
			for(ASTNodeDifference nodeDifference : nodeDifferences)
			{
				List<Difference> differences = nodeDifference.getDifferences();
				for(Difference difference : differences) {
					if(equalRightPart && difference.getFirstValue().equals(variable1.getVariableName()) &&
							difference.getSecondValue().equals(variable2.getVariableName())) {
						return true;
					}
					else if(difference.getType().equals(DifferenceType.SUBCLASS_TYPE_MISMATCH) &&
							difference.getFirstValue().equals(variable1.getVariableType()) &&
							difference.getSecondValue().equals(variable2.getVariableType())) {
						return true;
					}
				}
			}
		}
		return false;
	}

	public boolean isVoidMethodCallDifferenceCoveringEntireStatement() {
		boolean expression1IsVoidMethodCallDifference = false;
		boolean expression2IsVoidMethodCallDifference = false;
		for(ASTNodeDifference difference : nodeDifferences) {
			Expression expr1 = ASTNodeDifference.getParentExpressionOfMethodNameOrTypeName(difference.getExpression1().getExpression());
			Expression expr2 = ASTNodeDifference.getParentExpressionOfMethodNameOrTypeName(difference.getExpression2().getExpression());
			for(PreconditionViolation violation : getPreconditionViolations()) {
				if(violation instanceof ExpressionPreconditionViolation && violation.getType().equals(PreconditionViolationType.EXPRESSION_DIFFERENCE_IS_VOID_METHOD_CALL)) {
					ExpressionPreconditionViolation expressionViolation = (ExpressionPreconditionViolation)violation;
					Expression expression = ASTNodeDifference.getParentExpressionOfMethodNameOrTypeName(expressionViolation.getExpression().getExpression());
					if(expression.equals(expr1)) {
						if(expr1.getParent() instanceof ExpressionStatement) {
							expression1IsVoidMethodCallDifference = true;
						}
					}
					if(expression.equals(expr2)) {
						if(expr2.getParent() instanceof ExpressionStatement) {
							expression2IsVoidMethodCallDifference = true;
						}
					}
				}
			}
		}
		return expression1IsVoidMethodCallDifference && expression2IsVoidMethodCallDifference;
	}

	public boolean declaresInconsistentlyRenamedVariable(Set<VariableBindingPair> renamedVariables) {
		VariableBindingPair variableBindingPair = declaresVariableWithVariableNameMismatch();
		if(variableBindingPair != null) {
			if(renamedVariables.contains(variableBindingPair))
				return false;
			else
				return true;
		}
		return false;
	}

	private VariableBindingPair declaresVariableWithVariableNameMismatch() {
		for(ASTNodeDifference difference : nodeDifferences) {
			if(difference.containsDifferenceType(DifferenceType.VARIABLE_NAME_MISMATCH)) {
				Expression expr1 = ASTNodeDifference.getParentExpressionOfMethodNameOrTypeName(difference.getExpression1().getExpression());
				Expression expr2 = ASTNodeDifference.getParentExpressionOfMethodNameOrTypeName(difference.getExpression2().getExpression());
				if(expr1 instanceof SimpleName && expr2 instanceof SimpleName) {
					SimpleName simpleName1 = (SimpleName)expr1;
					SimpleName simpleName2 = (SimpleName)expr2;
					IBinding binding1 = simpleName1.resolveBinding();
					IBinding binding2 = simpleName2.resolveBinding();
					if(binding1 != null && binding1.getKind() == IBinding.VARIABLE && binding2 != null && binding2.getKind() == IBinding.VARIABLE) {
						IVariableBinding variableBinding1 = (IVariableBinding)binding1;
						IVariableBinding variableBinding2 = (IVariableBinding)binding2;
						PlainVariable variable1 = new PlainVariable(variableBinding1);
						PlainVariable variable2 = new PlainVariable(variableBinding2);
						if(nodeG1.declaresLocalVariable(variable1) && nodeG2.declaresLocalVariable(variable2)) {
							return new VariableBindingPair(variableBinding1, variableBinding2);
						}
					}
				}
			}
		}
		return null;
	}

	public boolean equals(Object o) {
		if(this == o)
			return true;
		if(o instanceof PDGNodeMapping) {
			PDGNodeMapping mapping = (PDGNodeMapping)o;
			return this.nodeG1.equals(mapping.nodeG1) &&
					this.nodeG2.equals(mapping.nodeG2);
		}
		return false;
	}

	public int hashCode() {
		if(hashCode == 0) {
			int result = 17;
			result = 37*result + nodeG1.hashCode();
			result = 37*result + nodeG2.hashCode();
			hashCode = result;
		}
		return hashCode;
	}

	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(nodeG1);
		sb.append(nodeG2);
		for(ASTNodeDifference nodeDifference : nodeDifferences)
		{
			sb.append(nodeDifference.toString());
		}
		return sb.toString();
	}
}
