package fr.inria.gforge.spoon.transformation.mutation;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.mdkt.compiler.InMemoryJavaCompiler;

import spoon.Launcher;
import spoon.processing.Processor;
import spoon.reflect.code.CtExpression;
import spoon.reflect.code.CtStatement;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.visitor.Filter;
import spoon.reflect.visitor.filter.TypeFilter;

/** mutates and kills mutants of type T.
 * 
 *  @See {@link MutationTesterTest} for an example usage
 */
public class MutationTester<T> {

	/** the content of the Java source code file to be mutated */
	private String sourceCodeToBeMutated;
	/** responsible for killing the mutants */
	private TestDriver testDriver;
	/** mutation operator */
	private Processor mutator;
	
	/** the produced mutants */
	private final List<CtClass> mutants = new ArrayList<>();

	// public for testing
	public final List<T> mutantInstances = new ArrayList<>();

	public MutationTester(String src, TestDriver tester, Processor mutator) {
		this.sourceCodeToBeMutated = src;
		this.testDriver = tester;
		this.mutator = mutator;
	}

	/** returns a list of mutant classes */
	public void generateMutants() {
		Launcher l = new Launcher();
		l.addInputResource(sourceCodeToBeMutated);
		l.buildModel();

		CtClass origClass = (CtClass) l.getFactory().Package().getRootPackage()
				.getElements(new TypeFilter(CtClass.class)).get(0);

		// now we apply a transformation
		// we replace "+" and "*" by "-"
		List<CtElement> elementsToBeMutated = origClass.getElements(new Filter<CtElement>() {

			@Override
			public boolean matches(CtElement arg0) {
				return mutator.isToBeProcessed(arg0);
			}
		});
		
		for (CtElement e : elementsToBeMutated) {
			// this loop is the trickiest part
			// because we want one mutation after the other
			
			// cloning the AST element
			CtElement op = l.getFactory().Core().clone(e);
			
			// mutate the element
			mutator.process(op);
			
			// temporarily replacing the original AST node with the mutated element 
			replace(e,op);

			// creating a new class containing the mutating code
			CtClass klass = l.getFactory().Core()
					.clone(op.getParent(CtClass.class));
			// setting the package
			klass.setParent(origClass.getParent());

			// adding the new mutant to the list
			mutants.add(klass);

			// restoring the original code
			replace(op, e);
		}
	}

	public List<CtClass> getMutants() {
		return Collections.unmodifiableList(mutants);
	}
	
	private void replace(CtElement e, CtElement op) {
		if (e instanceof CtStatement  && op instanceof CtStatement) {
			e.replace(op);
			return;
		}
		if (e instanceof CtExpression && op instanceof CtExpression) {
			e.replace(op);
			return;
		}
		throw new IllegalArgumentException(e.getClass()+" "+op.getClass());
	}

	/** tries to kill all generated mutants, throws an AssertionError if one mutant is not killed */
	public void killMutants() throws Exception {
		
		List<Class<?>> compiledMutants = compileMutants(mutants);

		List<T> mutantInstances = instantiateMutants(compiledMutants);

		runTestsOnEachMutantInstance(mutantInstances);
		
	}

	/** applies the test driver of this mutation tester on each mutant instance */
	public void runTestsOnEachMutantInstance(List<T> mutantInstances) throws Exception {
		// now we run the mutants against the test class
		for (T t : mutantInstances) {
			try {
				testDriver.test(t);
				throw new MutantNotKilledException();
			} catch (AssertionError expected) {
				System.out.println("mutant killed!");
			}
		}
	}

	/** instantiate the mutant classes using the default zero-arg constructor */
	public List<T> instantiateMutants(List<Class<?>> compiledMutants)
			throws Exception {
		// we run each mutant one by one and check whether they are killed

		// instantiating the mutant classes
		for (Class mutantClass : compiledMutants) {
			mutantInstances.add((T) mutantClass.newInstance());
		}
		return mutantInstances;
	}

	/** compiles the mutants on the fly */
	public List<Class<?>> compileMutants(List<CtClass> mutants) throws Exception {
		List<Class<?>> compiledMutants = new ArrayList<>();
		for (CtClass mutantClass : mutants) {
			Class<?> klass = InMemoryJavaCompiler.newInstance().compile(
					mutantClass.getQualifiedName(), "package "
							+ mutantClass.getPackage().getQualifiedName() + ";"
							+ mutantClass);
			compiledMutants.add(klass);
		}
		return compiledMutants;
	}

}