/**
 * 
 */
package soottocfg.cfg.optimization.dataflow;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import soottocfg.cfg.Program;
import soottocfg.cfg.SourceLocation;
import soottocfg.cfg.expression.Expression;
import soottocfg.cfg.expression.IdentifierExpression;
import soottocfg.cfg.expression.literal.IntegerLiteral;
import soottocfg.cfg.method.CfgBlock;
import soottocfg.cfg.method.CfgEdge;
import soottocfg.cfg.method.Method;
import soottocfg.cfg.statement.AssignStatement;
import soottocfg.cfg.statement.Statement;
import soottocfg.cfg.type.IntType;
import soottocfg.cfg.type.Type;
import soottocfg.cfg.util.DataFlowUtils;
import soottocfg.cfg.util.DataFlowUtils.ReachingDefinitions;
import soottocfg.cfg.variable.Variable;
import soottocfg.soot.util.SootTranslationHelpers;

/**
 * @author schaef
 *         Implements copy propagation algorithm from
 *         Apple's Modern Compiler Construction book page 359
 */
public class CopyPropagator {

	private static boolean cfgEquals(Expression s1, Expression s2) {
		// TODO: equals currently broken
		return s1.toString().equals(s2.toString());
	}

	private static boolean cfgEquals(Statement s1, Statement s2) {
		// TODO: equals currently broken
		return s1.toString().equals(s2.toString());
	}

	public static void main(String[] args) {
		Method m = createExampleProgram();
		System.err.println(m);

		System.err.println(copyPropagate(m));

		System.err.println(m);
	}

	/**
	 * Copy propagation of local variables. Returns true
	 * if anything has changed and false otherwise.
	 * 
	 * @param m
	 *            The method to which copy propagation should be applied.
	 * @return True, if anything changed. Otherwise false.
	 */
	public static boolean copyPropagate(Method m) {

		ReachingDefinitions rdefs = DataFlowUtils.computeReachingDefinitions(m);
		boolean changes = false;
		for (CfgBlock b : m.vertexSet()) {
			if (progateForEdgeLabels(m, b, rdefs)) {
				changes = true;
			}
			List<Statement> newStmts = new LinkedList<Statement>();
			for (Statement s : b.getStatements()) {
				Map<Variable, Variable> subsitutions = createSubstitutionMap(s.getUseVariables(), rdefs.in.get(s));
				Statement newStmt = s.substitute(subsitutions);
				changes = !cfgEquals(newStmt, s) ? true : changes;				
				newStmts.add(newStmt);
			}
			if (changes) {
				b.setStatements(newStmts);
			}
		}
		return changes;
	}

	private static boolean progateForEdgeLabels(Method m, CfgBlock b, ReachingDefinitions rdefs) {
		boolean changes = false;
		if (!b.getStatements().isEmpty()) {
			Statement lastStmt = b.getStatements().get(b.getStatements().size() - 1);
			Set<Statement> defStmts = rdefs.out.get(lastStmt);
			for (CfgEdge e : m.outgoingEdgesOf(b)) {
				if (e.getLabel().isPresent()) {
					Expression expr = e.getLabel().get();
					Map<Variable, Variable> subsitutions = createSubstitutionMap(expr.getUseVariables(), defStmts);
					if (!subsitutions.isEmpty()) {
						Expression newExpr = expr.substitute(subsitutions);						
						if (!cfgEquals(expr, newExpr)) {
							e.setLabel(newExpr);
							changes = true;
						}
					}
				}
			}
		}
		return changes;
	}

	private static Map<Variable, Variable> createSubstitutionMap(Set<Variable> useVars,
			Set<Statement> reachingStatements) {
		Map<Variable, Variable> subsitutions = new HashMap<Variable, Variable>();
		for (Variable v : useVars) {
			Set<Statement> defStmts = getDefStatementsForVar(v, reachingStatements);
			if (defStmts.size() == 1) {
				Statement stmt = defStmts.iterator().next();
				if (stmt instanceof AssignStatement
						&& ((AssignStatement) stmt).getRight() instanceof IdentifierExpression) {
					IdentifierExpression rhsId = (IdentifierExpression) ((AssignStatement) stmt).getRight();
					subsitutions.put(v, rhsId.getVariable());
				}
			}
		}
		return subsitutions;
	}

	/**
	 * Returns the subset of statements that def the variable v.
	 * This can be assignments, calls, or pulls.
	 * 
	 * @param v
	 * @param reachingStatements
	 * @return
	 */
	private static Set<Statement> getDefStatementsForVar(Variable v, Set<Statement> reachingStatements) {
		Set<Statement> res = new HashSet<Statement>();
		for (Statement s : reachingStatements) {
			if (s.getDefVariables().contains(v)) {
				res.add(s);
			}
		}
		return res;
	}

	/**
	 * Creates example program from Listing 17.3 on page 356
	 * 
	 * @return
	 */
	private static Method createExampleProgram() {
		Program p = new Program();
		SootTranslationHelpers.initialize(p);
		SourceLocation loc = SourceLocation.ANALYSIS;
		Method m = Method.createMethodForTestingOnly(p, "test", new LinkedList<Variable>(), new LinkedList<Type>(),
				loc);

		Variable a = new Variable("a", IntType.instance());
		Variable b = new Variable("b", IntType.instance());
		Variable c = new Variable("c", IntType.instance());
		m.addLocalVariable(a);
		m.addLocalVariable(b);
		m.addLocalVariable(c);
		CfgBlock l0 = new CfgBlock(m);

		l0.addStatement(new AssignStatement(loc, a.mkExp(loc), new IntegerLiteral(loc, 5)));
		l0.addStatement(new AssignStatement(loc, b.mkExp(loc), a.mkExp(loc)));
		l0.addStatement(new AssignStatement(loc, c.mkExp(loc), b.mkExp(loc)));

		return m;
	}
}
