/**
 * Copyright (C) 2010-2016 Gordon Fraser, Andrea Arcuri and EvoSuite
 * contributors
 *
 * This file is part of EvoSuite.
 *
 * EvoSuite is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3.0 of the License, or
 * (at your option) any later version.
 *
 * EvoSuite is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
 */
package org.evosuite.testcase;

import org.evosuite.Properties;
import org.evosuite.TimeController;
import org.evosuite.ga.ConstructionFailedException;
import org.evosuite.ga.FitnessFunction;
import org.evosuite.ga.SecondaryObjective;
import org.evosuite.testcase.statements.Statement;
import org.evosuite.testcase.variable.VariableReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * Remove all statements from a test case that do not contribute to the fitness
 * 
 * @author Gordon Fraser
 */
public class TestCaseMinimizer {

	private static final Logger logger = LoggerFactory.getLogger(TestCaseMinimizer.class);

	private final TestFitnessFunction fitnessFunction;

	/**
	 * Constructor
	 * 
	 * @param fitnessFunction
	 *            Fitness function with which to measure whether a statement is
	 *            necessary
	 */
	public TestCaseMinimizer(TestFitnessFunction fitnessFunction) {
		this.fitnessFunction = fitnessFunction;
	}

	/**
	 * Remove all unreferenced variables
	 * 
	 * @param t
	 *            The test case
	 * @return True if something was deleted
	 */
	public static boolean removeUnusedVariables(TestCase t) {
		List<Integer> to_delete = new ArrayList<Integer>();
		boolean has_deleted = false;

		int num = 0;
		for (Statement s : t) {
			VariableReference var = s.getReturnValue();
			if (!t.hasReferences(var)) {
				to_delete.add(num);
				has_deleted = true;
			}
			num++;
		}
		Collections.sort(to_delete, Collections.reverseOrder());
		for (Integer position : to_delete) {
			t.remove(position);
		}

		return has_deleted;
	}

	private static boolean isWorse(FitnessFunction<TestChromosome> fitness,
	        TestChromosome oldChromosome, TestChromosome newChromosome) {
		if (fitness.isMaximizationFunction()) {
			if (oldChromosome.getFitness(fitness) > fitness.getFitness(newChromosome))
				return true;
		} else {
			if (fitness.getFitness(newChromosome) > oldChromosome.getFitness(fitness))
				return true;
		}

		for (SecondaryObjective objective : TestChromosome.getSecondaryObjectives()) {
			if (objective.compareChromosomes(oldChromosome, newChromosome) < 0)
				return true;
		}

		return false;
	}
	
    private boolean isTimeoutReached() {
        return !TimeController.getInstance().isThereStillTimeInThisPhase();
    }
    
	/**
	 * Central minimization function. Loop and try to remove until all
	 * statements have been checked.
	 * 
	 * @param c
	 *            a {@link org.evosuite.testcase.TestChromosome} object.
	 */
	public void minimize(TestChromosome c) {
		if (!Properties.MINIMIZE) {
			return;
		}
		logger.info("Minimizing test case");


		double fitness = fitnessFunction.getFitness(c);
		if (isTimeoutReached()) {
			return;
		}

		logger.debug("Start fitness values: {}", fitness);
		assert ConstraintVerifier.verifyTest(c);
		
		if (isTimeoutReached()) {
			logger.debug("Timeout reached after verifying test");
			return;
		}
		
		boolean changed = true;

		while (changed) {
			changed = false;

			for (int i = c.test.size() - 1; i >= 0; i--) {
				if (isTimeoutReached()) {
					logger.debug("Timeout reached before minimizing statement {}", c.test.getStatement(i).getCode());
					return;
				}
				
				logger.debug("Deleting statement {}", c.test.getStatement(i).getCode());
				TestChromosome copy = (TestChromosome) c.clone();
				boolean modified;
				try {
					modified = TestFactory.getInstance().deleteStatementGracefully(c.test, i);
				} catch (ConstructionFailedException e) {
					modified = false;
				}
				
				if(!modified){
					c.setChanged(false);
					c.test = copy.test;
					logger.debug("Deleting failed");
					continue;
				}

				c.setChanged(true);

				if (isTimeoutReached()) {
					logger.debug("Keeping original version due to timeout");
					restoreTestCase(c, copy);
					return;
				}

				if (! isWorse(fitnessFunction, copy, c)) {
					logger.debug("Keeping shorter version");
					changed = true;
					break;
				} else {
					logger.debug("Keeping original version");
					restoreTestCase(c, copy);
				}

			}
		}

		//TODO: add back this check
		assert  (fitnessFunction.isMaximizationFunction() ?
				fitnessFunction.getFitness(c) >= fitness : fitnessFunction.getFitness(c) <= fitness)
				:
				"Minimization worsened " + fitnessFunction.getClass().getName()+" fitness from "+fitness+
						" to "+fitnessFunction.getFitness(c)+" on test "+c.getTestCase().toCode();


		if (Properties.MINIMIZE_VALUES) {
			logger.info("Minimizing values of test case");
			ValueMinimizer minimizer = new ValueMinimizer();
			minimizer.minimize(c, fitnessFunction);
		}

		assert ConstraintVerifier.verifyTest(c);

		if (logger.isDebugEnabled()) {
			logger.debug("Minimized test case: ");
			logger.debug(c.test.toCode());
		}

	}

	private static void restoreTestCase(TestChromosome c, TestChromosome copy) {
		c.test = copy.test;
		c.copyCachedResults(copy);
		//c.setFitness(copy.getFitness());
		c.setFitnessValues(copy.getFitnessValues());
		c.setPreviousFitnessValues(copy.getPreviousFitnessValues());
		c.setChanged(false);
	}

}
