package metamutator;

import com.google.common.collect.Sets;
import spoon.processing.AbstractProcessor;
import spoon.reflect.code.BinaryOperatorKind;
import spoon.reflect.code.CtBinaryOperator;
import spoon.reflect.code.CtCodeSnippetExpression;
import spoon.reflect.code.CtExpression;
import spoon.reflect.declaration.CtAnonymousExecutable;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtConstructor;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtField;
import spoon.reflect.reference.CtTypeReference;

import java.util.EnumSet;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * inserts a mutation hotspot for each binary operator
 */
public class LogicalExpressionMetaMutator extends
		AbstractProcessor<CtBinaryOperator<Boolean>> {

	public static final String PREFIX =  "_binaryLogicalOperatorHotSpot";
	private static int index = 0;
	private static final int procId = 1;

	private static final EnumSet<BinaryOperatorKind> LOGICAL_OPERATORS = EnumSet
			.of(BinaryOperatorKind.AND, BinaryOperatorKind.OR);
	private static final EnumSet<BinaryOperatorKind> COMPARISON_OPERATORS = EnumSet
			.of(BinaryOperatorKind.EQ, BinaryOperatorKind.GE,
					BinaryOperatorKind.GT, BinaryOperatorKind.LE,
					BinaryOperatorKind.LT, BinaryOperatorKind.NE);
	private static final EnumSet<BinaryOperatorKind> REDUCED_COMPARISON_OPERATORS = EnumSet
			.of(BinaryOperatorKind.EQ, BinaryOperatorKind.NE);

	private Set<CtElement> hostSpots = Sets.newHashSet();

	@Override
	public boolean isToBeProcessed(CtBinaryOperator<Boolean> element) {
		// if (element.getParent(CtAnonymousExecutable.class)!=null) {
		// System.out.println(element.getParent(CtAnonymousExecutable.class));
		// }
	
		try {
			Selector.getTopLevelClass(element);
		} catch (NullPointerException e) {
			return false;
		}

		// not in constructors because we use static fields
		if (element.getParent(CtConstructor.class) != null) {
			return false;
		}

		// not in fields declaration because we use static fields
		if (element.getParent(CtField.class) != null) {
			return false;
		}
		
		return (LOGICAL_OPERATORS.contains(element.getKind()) || COMPARISON_OPERATORS
				.contains(element.getKind()))
				&& (element.getParent(CtAnonymousExecutable.class) == null) // not
																			// in
																			// static
																			// block
		;
	}

	public void process(CtBinaryOperator<Boolean> binaryOperator) {
		BinaryOperatorKind kind = binaryOperator.getKind();
		
		if (LOGICAL_OPERATORS.contains(kind)) {
			mutateOperator(binaryOperator, LOGICAL_OPERATORS);
		} else if (COMPARISON_OPERATORS.contains(kind)) {
			if (isNumber(binaryOperator.getLeftHandOperand())
			 && isNumber(binaryOperator.getRightHandOperand()))
			{
				mutateOperator(binaryOperator, COMPARISON_OPERATORS);
			}
			 else {
				 EnumSet<BinaryOperatorKind> clone = REDUCED_COMPARISON_OPERATORS.clone();
				 clone.add(kind);
				 mutateOperator(binaryOperator, clone);
			 }
		}
	}

	private boolean isNumber(CtExpression<?> operand) {
	
		if (operand.getType().toString().equals(CtTypeReference.NULL_TYPE_NAME))
			return false;
		
		if (operand.toString().contains(".class"))
			return false;
				
		return operand.getType().getSimpleName().equals("int")
			|| operand.getType().getSimpleName().equals("long")
			|| operand.getType().getSimpleName().equals("byte")
			|| operand.getType().getSimpleName().equals("char")
		|| operand.getType().getSimpleName().equals("float")
		|| operand.getType().getSimpleName().equals("double")
		|| operand.getType().isSubtypeOf(getFactory().Type().createReference(Number.class));
	}

/**
	 * Converts "a op b" bean op one of "<", "<=", "==", ">=", "!=" to:
	 *    (  (op(1, 0, "<")  && (a < b))
	 *    || (op(1, 1, "<=") && (a <= b))
	 *    || (op(1, 2, "==") && (a == b))
	 *    || (op(1, 3, ">=") && (a >= b))
	 *    || (op(1, 4, ">")  && (a > b))
	 *    )
	 *	 */
	private void mutateOperator(final CtBinaryOperator<Boolean> expression, EnumSet<BinaryOperatorKind> operators) {
		
		if (!operators.contains(expression.getKind())) {
			throw new IllegalArgumentException("not consistent ");
		}

		if (alreadyInHotsSpot(expression)
				|| expression.toString().contains(".is(\"")) {
			System.out
					.println(String
							.format("Expression '%s' ignored because it is included in previous hot spot",
									expression));
			return;
		}

		int thisIndex = ++index;

		BinaryOperatorKind originalKind = expression.getKind();
		String newExpression = operators
				.stream()
				.map(kind -> {
					expression.setKind(kind);
					return String.format("("+ PREFIX + "%s.is(%s) && (%s))",
							thisIndex, kind.getDeclaringClass().getName()+"."+kind.name(), expression);
				}).collect(Collectors.joining(" || "));

		CtCodeSnippetExpression<Boolean> codeSnippet = getFactory().Core()
				.createCodeSnippetExpression();
		codeSnippet.setValue('(' + newExpression + ')');

		expression.replace(codeSnippet);
		expression.replace(expression);
		Selector.generateSelector(expression, originalKind, thisIndex, operators, PREFIX);

		hostSpots.add(expression);

	}

	/**
	 * Check if this sub expression was already inside an uppermost expression
	 * that was processed has a hot spot. This version does not allowed
	 * conflicting hot spots
	 * 
	 * @param element
	 *            the current expression to test
	 * @return true if this expression is descendant of an already processed
	 *         expression
	 */
	private boolean alreadyInHotsSpot(CtElement element) {
		CtElement parent = element.getParent();
		while (!isTopLevel(parent) && parent != null) {
			if (hostSpots.contains(parent))
				return true;

			parent = parent.getParent();
		}

		return false;
	}

	private boolean isTopLevel(CtElement parent) {
		return parent instanceof CtClass && ((CtClass) parent).isTopLevel();
	}
}