/**
 * 
 */
package soottocfg.soot.transformers;

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 soot.Local;
import soot.RefType;
import soot.Scene;
import soot.SootClass;
import soot.SootField;
import soot.SootMethod;
import soot.Type;
import soot.Unit;
import soot.jimple.FieldRef;
import soot.jimple.InvokeExpr;
import soot.jimple.JimpleBody;
import soot.jimple.Stmt;
import soottocfg.spec.ListSimpleSpec;

/**
 * @author schaef
 *         This transformation takes a map from SootClass to SootClass
 *         that specifies which types to replace by stubs. For example,
 *         this could contain an entry java.util.List -> jayhorn.ListStub
 *         which indicates that all uses of List should be replaced by
 *         ListStub.
 *         This requires that ListStub extends List.
 * 
 *         !!IMPORTANT!!: This assumes that Arrays have been removed already!
 */
public class SpecClassTransformer extends AbstractSceneTransformer {

	private final Map<SootClass, SootClass> replacementMap = new HashMap<SootClass, SootClass>();

	private final Map<String, SootField> fieldSubstitutionMap = new HashMap<String, SootField>();
	private final Map<String, SootMethod> methodSubstitutionMap = new HashMap<String, SootMethod>();

	public SpecClassTransformer() {

		SootClass listSpec = Scene.v().loadClass(ListSimpleSpec.class.getName(), SootClass.SIGNATURES);
		replacementMap.put(Scene.v().getSootClass(java.util.LinkedList.class.getName()), listSpec);
		replacementMap.put(Scene.v().getSootClass(java.util.List.class.getName()), listSpec);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * soottocfg.soot.transformers.AbstractSceneTransformer#applyTransformation(
	 * )
	 */
	@Override
	public void applyTransformation() {
		List<SootClass> classes = new LinkedList<SootClass>(Scene.v().getClasses());
		List<JimpleBody> bodies = new LinkedList<JimpleBody>();
//		List<SootMethod> entryPoints = new LinkedList<SootMethod>(Scene.v().getEntryPoints());
		// Don't transform the classes that we replace anyway.
		classes.removeAll(replacementMap.keySet());

		for (SootClass sc : classes) {
			if (sc.resolvingLevel() >= SootClass.SIGNATURES) {
				// === change the type of all fields if they are in
				// replacementMap ===.
				Set<String> changedFieldSignatures = new HashSet<String>();
				for (SootField f : sc.getFields()) {
					Type newType = updateType(f.getType());
					if (!newType.equals(f.getType())) {
						changedFieldSignatures.add(f.getSignature());
						final String oldSig = f.getSignature();
						f.setType(newType);
						fieldSubstitutionMap.put(oldSig, f);
					}
				}
				// === remove original version of changed fields ===.
				for (String s : changedFieldSignatures) {
					if (sc.declaresField(s)) {
						sc.removeField(sc.getFieldByName(s));
					}
				}

				// === now update methods ===.
				for (SootMethod sm : sc.getMethods()) {
					final String oldSignature = sm.getSignature();
					// we also have to update the refs in the EntryPoint list.
//					boolean wasMain = sm.isEntryMethod();
//					if (wasMain) {
//						entryPoints.remove(sm);
//					}

					if (sc.resolvingLevel() >= SootClass.BODIES && sm.isConcrete() && !sc.isLibraryClass()
							&& !sc.isJavaLibraryClass()) {
						// record all methods for which we found a body.
						bodies.add((JimpleBody) sm.retrieveActiveBody());
					}
					// update return type
					sm.setReturnType(updateType(sm.getReturnType()));
					// update parameter types
					List<Type> newParamTypes = new LinkedList<Type>();
					for (Type t : sm.getParameterTypes()) {
						newParamTypes.add(updateType(t));
					}
					sm.setParameterTypes(newParamTypes);

//					if (wasMain) {
//						entryPoints.add(sm);
//					}

					methodSubstitutionMap.put(oldSignature, sm);
				}

				for (JimpleBody body : bodies) {
					for (Local local : body.getLocals()) {
						local.setType(updateType(local.getType()));
					}
					for (Unit u : new LinkedList<Unit>(body.getUnits())) {
						Stmt st = (Stmt) u;

						if (st.containsFieldRef()) {
							FieldRef fr = st.getFieldRef();
							if (fieldSubstitutionMap.containsKey(fr.getField().getSignature())) {
								fr.setFieldRef(fieldSubstitutionMap.get(fr.getField().getSignature()).makeRef());
							}
						}
						if (st.containsInvokeExpr()) {
							InvokeExpr ivk = st.getInvokeExpr();
							if (replacementMap.containsKey(ivk.getMethod().getDeclaringClass())) {
								SootClass replClass = replacementMap.get(ivk.getMethod().getDeclaringClass());
								List<Type> replTypes = new LinkedList<Type>();
								for (Type t : ivk.getMethod().getParameterTypes()) {
									// TODO: it might be better to only replace
									// this one type.
									// instead of every type in the map.
									replTypes.add(updateType(t));
								}

								SootMethod replMethod = replClass.getMethod(ivk.getMethod().getName(), replTypes);
								ivk.setMethodRef(replMethod.makeRef());
								
							} else if (methodSubstitutionMap.containsKey(ivk.getMethod().getSignature())) {
								ivk.setMethodRef(methodSubstitutionMap.get(ivk.getMethod().getSignature()).makeRef());
							}
						}
					}
				}
			}
		}
	}

	private Type updateType(Type t) {
		if (t instanceof RefType) {
			SootClass sc = ((RefType) t).getSootClass();
			if (replacementMap.containsKey(sc)) {
				return RefType.v(replacementMap.get(sc));
			}
		}
		return t;
	}

}
