/**
 * 
 */
package soottocfg.soot.util;

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

import com.google.common.base.Verify;

import soot.Local;
import soot.RefType;
import soot.SootField;
import soot.SootMethod;
import soot.Unit;
import soot.VoidType;
import soot.jimple.ParameterRef;
import soottocfg.cfg.SourceLocation;
import soottocfg.cfg.expression.Expression;
import soottocfg.cfg.expression.IdentifierExpression;
import soottocfg.cfg.method.CfgBlock;
import soottocfg.cfg.method.CfgEdge;
import soottocfg.cfg.method.Method;
import soottocfg.cfg.type.Type;
import soottocfg.cfg.variable.Variable;

/**
 * @author schaef
 *
 */
public class MethodInfo {

	private final String sourceFileName;

	public static final String returnVariableName = "$ret_";
	public static final String exceptionVariableName = "$ex_";
	private static final String thisVariableName = "$this_";
	private static final String constructorOutVarName = "$co_";

	private final SootMethod sootMethod;
	private final Method cfgMethod;

	private CfgBlock source = null, sink = null;
	private Map<Unit, CfgBlock> unitToBlockMap = new HashMap<Unit, CfgBlock>();
	private Map<Local, Variable> localsMap = new HashMap<Local, Variable>();

	private Variable thisVariable;
	private Variable exceptionVariable;
	private List<Variable> returnVariables;

	private final Set<Variable> freshLocals = new LinkedHashSet<Variable>();

	private SourceLocation methodLoc = null;

	public MethodInfo(SootMethod sm, String sourceFileName) {
		sootMethod = sm;
		cfgMethod = SootTranslationHelpers.v().lookupOrCreateMethod(sootMethod);
		methodLoc = SootTranslationHelpers.v().getSourceLocation(sm);
		sink = new CfgBlock(getMethod());
		getMethod().setSink(sink);
		this.sourceFileName = sourceFileName;

		this.returnVariables = new LinkedList<Variable>();
		
		this.exceptionVariable = new Variable(exceptionVariableName, SootTranslationHelpers.v().getMemoryModel()
				.lookupType(RefType.v("java.lang.Throwable")));
		this.returnVariables.add(this.exceptionVariable); 
		
		if (!(sm.getReturnType() instanceof VoidType)) {
			// create a return variable if the method does not
			// return void.		
			this.returnVariables.add(new Variable(returnVariableName,
					SootTranslationHelpers.v().getMemoryModel().lookupType(sm.getReturnType())));
		} 

		if (sm.isConstructor()) {
			int i=0;
			for (SootField sf : SootTranslationHelpers.findNonStaticFieldsRecursively(sm.getDeclaringClass())) {
				this.returnVariables.add(new Variable(constructorOutVarName+(++i), SootTranslationHelpers.v().getMemoryModel().lookupType(sf.getType())));
			}
		}
		
		// If the method is not static, create a this-variable which is
		// passed as the first parameter to the method.
		if (!sm.isStatic()) {
			this.thisVariable = new Variable(thisVariableName,
					SootTranslationHelpers.v().getMemoryModel().lookupType(sm.getDeclaringClass().getType()));
		}
	}

	public Method getMethod() {
		return cfgMethod;
	}

	/**
	 * Called after the method has been translated. Looks up the corresponding
	 * Method object in Program and fills in all the information. Should only be
	 * called once per method.
	 */
	public void finalizeAndAddToProgram() {
		Method m = cfgMethod;
		Collection<Variable> locals = new LinkedHashSet<Variable>();
		locals.addAll(this.localsMap.values());
		locals.addAll(this.freshLocals);
		m.initialize(this.thisVariable, this.returnVariables, locals, source, false);
		// System.out.println("Initialized method " + m);
		CfgBlock uniqueSink = m.findOrCreateUniqueSink();
		if (sink != uniqueSink) {
			System.err.println("Something strange with the CFG. More than one sink found for " + m.getMethodName());
		}
		if (m.inDegreeOf(sink) == 0) {
			System.err.println("Method " + sootMethod.getSignature() + " has no sink! Ignoring.");
			for (CfgEdge e : new HashSet<CfgEdge>(m.edgeSet())) {
				m.removeEdge(e);
			}
			for (CfgBlock b : new HashSet<CfgBlock>(m.vertexSet())) {
				if (!b.equals(sink) && !b.equals(source)) {
					m.removeVertex(b);
				}
			}
			m.addEdge(source, sink);
		}
		sink = uniqueSink;
	}

	public String getSourceFileName() {
		return this.sourceFileName;
	}

	public IdentifierExpression getReturnVariable() {
		// TODO this is a hack that assumes that we only use that if there
		// is a single return variable.
		Verify.verify(this.returnVariables.size() >= 2);
		return new IdentifierExpression(methodLoc, this.returnVariables.get(1));
	}

	public IdentifierExpression getExceptionVariable() {
		// TODO this is a hack that assumes that we only use that if there
		// is a single return variable.
		Verify.verify(this.returnVariables.size() >= 1);
		return new IdentifierExpression(methodLoc, this.returnVariables.get(0));
	}

	public Variable getOutVariable(int i) {
		Verify.verify(this.returnVariables.size() >= 2 && i>=0 && i<this.returnVariables.size(), "Index bound 0<="+i+"<"+this.returnVariables.size());
		return this.returnVariables.get(i);
	}

	public List<Variable> getOutVariables() {
		return this.returnVariables;
	}
	
	// public Expression getExceptionVariable() {
	// return new IdentifierExpression(this.exceptionalReturnVariable);
	// }

	public Expression getThisVariable() {
		return new IdentifierExpression(methodLoc, this.thisVariable);
	}

	public Expression lookupParameterRef(ParameterRef arg0) {
		int offset = thisVariable == null ? 0 : 1;
		// offset += Options.v().passCallerIdIntoMethods() ? 1 : 0;
		return new IdentifierExpression(methodLoc, cfgMethod.getInParam(arg0.getIndex() + offset));
	}

	public Variable lookupLocalVariable(Local arg0) {
		if (!localsMap.containsKey(arg0)) {
			localsMap.put(arg0, createLocalVariable(arg0));
		}
		return localsMap.get(arg0);
	}

	public Expression lookupLocal(Local arg0) {
		return new IdentifierExpression(methodLoc, lookupLocalVariable(arg0));
	}

	private Variable createLocalVariable(Local arg0) {
		return new Variable(arg0.getName(), SootTranslationHelpers.v().getMemoryModel().lookupType(arg0.getType()),
				false, false);
	}

	public Variable createFreshLocal(String prefix, Type t, boolean constant, boolean unique) {

		// List<Type> elementTypes = new LinkedList<Type>();
		// elementTypes.add(newType);
		// TupleType ttype = new TupleType(elementTypes);
		// TupleVariable tupleVar = new TupleVariable("$new", ttype, true,
		// true);
		//

		Variable v = new Variable(prefix + this.freshLocals.size(), t, constant, unique);
		this.freshLocals.add(v);
		return v;
	}

	public CfgBlock lookupCfgBlock(Unit u) {
		if (!unitToBlockMap.containsKey(u)) {
			unitToBlockMap.put(u, new CfgBlock(getMethod()));
		}
		return unitToBlockMap.get(u);
	}

	public CfgBlock findBlock(Unit u) {
		return unitToBlockMap.get(u);
	}

	public CfgBlock getSource() {
		return this.source;
	}

	public void setSource(CfgBlock source) {
		this.source = source;
	}

	public CfgBlock getSink() {
		return sink;
	}

}
