package metamutator;

import com.google.common.collect.Sets;
import spoon.Launcher;
import spoon.processing.AbstractProcessor;
import spoon.reflect.code.CtAssert;
import spoon.reflect.code.CtAssignment;
import spoon.reflect.code.CtBlock;
import spoon.reflect.code.CtCFlowBreak;
import spoon.reflect.code.CtCodeSnippetExpression;
import spoon.reflect.code.CtCodeSnippetStatement;
import spoon.reflect.code.CtConstructorCall;
import spoon.reflect.code.CtContinue;
import spoon.reflect.code.CtDo;
import spoon.reflect.code.CtFor;
import spoon.reflect.code.CtForEach;
import spoon.reflect.code.CtIf;
import spoon.reflect.code.CtInvocation;
import spoon.reflect.code.CtLiteral;
import spoon.reflect.code.CtLocalVariable;
import spoon.reflect.code.CtLoop;
import spoon.reflect.code.CtNewClass;
import spoon.reflect.code.CtOperatorAssignment;
import spoon.reflect.code.CtReturn;
import spoon.reflect.code.CtStatement;
import spoon.reflect.code.CtSwitch;
import spoon.reflect.code.CtSynchronized;
import spoon.reflect.code.CtThrow;
import spoon.reflect.code.CtUnaryOperator;
import spoon.reflect.code.CtWhile;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtEnum;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.factory.CoreFactory;
import spoon.reflect.visitor.Filter;
import spoon.reflect.visitor.filter.ReturnOrThrowFilter;
import spoon.reflect.visitor.filter.TypeFilter;
import spoon.support.reflect.code.CtLiteralImpl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class StatementDeletionMetaMutator 
extends AbstractProcessor<CtStatement> {

	public static final String PREFIX =  "_StatementDeletionMutatorHotSpot";
	private static final int procId = 6;
	private int selectorIndex = 0;
	
	public enum ACTIVABLE {
		// NO CHANGE
		ENABLED,
		// Absolute Value	
		DISABLED
	};
	
	private static final EnumSet<ACTIVABLE> ActivableSet = EnumSet
			.of(ACTIVABLE.ENABLED, ACTIVABLE.DISABLED);
	

	private Set<CtElement> hotSpots = Sets.newHashSet();
	
	//break? si boucle infini
	//ctcflowbreak ? kesako?
	//invocation?	private static final List<Class> MODIFIABLE_STATEMENTS = new ArrayList<Class>(
	private static final List<Class> MODIFIABLE_STATEMENTS = new ArrayList<Class>(
			Arrays.asList(CtAssert.class, CtContinue.class, CtDo.class, CtFor.class, CtForEach.class,
					CtIf.class, CtLoop.class, CtSwitch.class, CtThrow.class, CtWhile.class)
			);
	
	
	//break? si boucle infini
	//ctcflowbreak ? kesako?
	private static final List<Class> UNMODIFIABLE_STATEMENTS = new ArrayList<Class>(
			Arrays.asList(CtAssignment.class, CtBlock.class, CtCFlowBreak.class, CtClass.class,
					CtCodeSnippetStatement.class, CtConstructorCall.class, CtEnum.class, CtInvocation.class,
					CtLocalVariable.class, CtNewClass.class, CtOperatorAssignment.class, CtReturn.class, 
					CtSynchronized.class, CtUnaryOperator.class)
			);
	
	
	
	//Templates of returned expressions, used when a function have a return in a statement which can be deleted.
	private static final Map<Class, CtLiteral> PrimitiveTemplateExpressions;
	    static {
			CoreFactory f = new Launcher().getFactory().Core();
	        HashMap<Class, CtLiteral> map = new HashMap<Class, CtLiteral>();
	        map.put(byte.class, f.createLiteral().setValue((byte) 0));
	        map.put(short.class, f.createLiteral().setValue((short) 0));
	        map.put(int.class, f.createLiteral().setValue(0));
	        map.put(long.class, f.createLiteral().setValue(0L));
	        map.put(float.class, f.createLiteral().setValue(0.0f));
	        map.put(double.class,f.createLiteral().setValue(0.0d));
	        map.put(boolean.class,f.createLiteral().setValue(false));
	        map.put(char.class,f.createLiteral().setValue('\u0000'));
	        map.put(void.class,null);
	        PrimitiveTemplateExpressions = Collections.unmodifiableMap(map);
	    }

	
	@Override
	public boolean isToBeProcessed(CtStatement element) {
		for(Class<?> c : MODIFIABLE_STATEMENTS){
			if(c.isInstance(element)){
				return true;
			}
		}
		return false;
	}
	
	
	@Override
	public void process(CtStatement element) {
		//System.out.println("process");
		mutateOperator(element);
	}
	
	
	private void mutateOperator(final CtStatement expression) {
		
		
		/*if (alreadyInHotsSpot(expression)) {
			System.out
					.println(String
							.format("Expression '%s' ignored because it is included in previous hot spot",
									expression));
			return;
		}*/
		int thisIndex = ++selectorIndex;
		
		ACTIVABLE kind = ACTIVABLE.ENABLED;
		String expressionContent =  String.format("("+ PREFIX + "%s.is(%s))",
				thisIndex, kind.getClass().getCanonicalName()+"."+kind.name());
		
		//create IfChoice with right condition
		CtIf ifChoice = getFactory().Core().createIf();
		CtCodeSnippetExpression expIf = getFactory().Code().createCodeSnippetExpression(expressionContent);
		ifChoice.setCondition(expIf);
			
		
		
		//create block from a clone of expression
		CtStatement exp2 = getFactory().Core().clone(expression);
		CtBlock thenBlock = getFactory().Code().createCtBlock(exp2);
		
		//set if and replace the expression with the new if
		ifChoice.setThenStatement(thenBlock);
		expression.replace(ifChoice);
		
		//to be sure
		ifChoice.getParent().updateAllParentsBelow();
		
		//if there are return or throws, set else with value of return.
		Filter<CtCFlowBreak> filterReturn = new ReturnOrThrowFilter();
		if(!thenBlock.getElements(filterReturn).isEmpty()){
			SetElseStatementWithReturn(ifChoice);
		}

		
		//to avoid to delete assignement in statement, we assign a default value to all local variable.
		Filter<CtLocalVariable> filterLocalVariable =  new TypeFilter<CtLocalVariable>(CtLocalVariable.class);
		CtMethod method = ifChoice.getParent(CtMethod.class);
		if(method != null && !method.getElements(filterLocalVariable).isEmpty()){
			for(CtLocalVariable var : method.getElements(filterLocalVariable)){
				if(var.getAssignment() == null){
					//create right side expression from template.
					Class classOfAssignment = var.getType().getActualClass();
					CtLiteral rightHand = null;
					
					//Particular case of ForEach (int x : numbers) can't be (int x = 0 : numbers)
					if(var.getParent() instanceof CtForEach){
						continue;
					}
					if(PrimitiveTemplateExpressions.containsKey(classOfAssignment)){
						CtLiteral templateExpression = PrimitiveTemplateExpressions.get(classOfAssignment);
						rightHand = getFactory().Core().clone(templateExpression);
					}else{
						rightHand = getFactory().createLiteral().setValue(null);
					}
					var.setAssignment(rightHand);
				}
			}
		}
		
		Selector.generateSelector(expression, ACTIVABLE.ENABLED, thisIndex, ActivableSet, PREFIX);
		//hotSpots.add(expression);

	}
	
	
	
	
	/*
	 * set else statement with return. 
	 * This work only when the IfStatement contain a return.
	 */
	private void SetElseStatementWithReturn(CtIf ifStatement){
		//search the first parent method
		CtMethod parentMethod = ifStatement.getParent(CtMethod.class);
		//we go out of while with null of a CtMethod. If null, return without method in parents...?
		if(parentMethod == null){
			return;
		}

		
		//create returned expression from template.
		CtLiteral returnedExpression = null;
		Class classOfReturn = parentMethod.getType().getActualClass();
		
		if(PrimitiveTemplateExpressions.containsKey(classOfReturn)){
			CtLiteral templateExpression = PrimitiveTemplateExpressions.get(classOfReturn);
			returnedExpression = getFactory().Core().clone(templateExpression);
		}else{
			returnedExpression = new CtLiteralImpl().setValue(null);
		}
		
		
		CtReturn theReturn = getFactory().Core().createReturn();
		theReturn.setReturnedExpression(returnedExpression);
		CtBlock elseBlock = ifStatement.getElseStatement();
		if(elseBlock == null){
			elseBlock = getFactory().Core().createBlock();
		}
		elseBlock.addStatement(theReturn);
		ifStatement.setElseStatement(elseBlock);
	}
	
	
	
	private boolean alreadyInHotsSpot(CtElement element) {
		CtElement parent = element.getParent();
		while (!isTopLevel(parent) && parent != null) {
			if (hotSpots.contains(parent))
				return true;

			parent = parent.getParent();
		}

		return false;
	}

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