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

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import com.google.common.base.Preconditions;
import com.google.common.base.Verify;

import soot.ArrayType;
import soot.Modifier;
import soot.PrimType;
import soot.RefType;
import soot.Scene;
import soot.SootClass;
import soot.SootField;
import soot.SootMethod;
import soot.Type;
import soot.Unit;
import soot.Value;
import soot.VoidType;
import soot.jimple.ClassConstant;
import soot.jimple.DoubleConstant;
import soot.jimple.FloatConstant;
import soot.jimple.IntConstant;
import soot.jimple.InvokeStmt;
import soot.jimple.Jimple;
import soot.jimple.LongConstant;
import soot.jimple.NullConstant;
import soot.jimple.Stmt;
import soot.tagkit.AbstractHost;
import soot.tagkit.Host;
import soot.tagkit.SourceFileTag;
import soot.tagkit.Tag;
import soottocfg.Options;
import soottocfg.cfg.Program;
import soottocfg.cfg.SourceLocation;
import soottocfg.cfg.expression.BinaryExpression;
import soottocfg.cfg.expression.BinaryExpression.BinaryOperator;
import soottocfg.cfg.expression.Expression;
import soottocfg.cfg.expression.IdentifierExpression;
import soottocfg.cfg.expression.TupleAccessExpression;
import soottocfg.cfg.method.Method;
import soottocfg.cfg.statement.Statement;
import soottocfg.cfg.type.ReferenceType;
import soottocfg.cfg.variable.ClassVariable;
import soottocfg.cfg.variable.Variable;
import soottocfg.soot.SootRunner;
import soottocfg.soot.SootToCfg.MemModel;
import soottocfg.soot.memory_model.MemoryModel;
import soottocfg.soot.memory_model.NewMemoryModel;

/**
 * @author schaef
 *
 */
public enum SootTranslationHelpers {
	INSTANCE;
	
	public static SootTranslationHelpers v() {
		Preconditions.checkArgument(initialized, "Call SootTranslationHelpers.initialize first!");
		return INSTANCE;
	}

	public static final String HavocClassName = "Havoc_Class";
	public static final String HavocMethodName = "havoc_";

	private static boolean initialized = false;
	
	/**
	 * Get a method that returns an unknown value of type t.
	 * 
	 * @param t
	 * @return
	 */
	public SootMethod getHavocMethod(soot.Type t) {
		if (!Scene.v().containsClass(HavocClassName)) {
			SootClass sClass = new SootClass(HavocClassName, Modifier.PUBLIC | Modifier.PUBLIC);
			sClass.setSuperclass(Scene.v().getSootClass("java.lang.Object"));
			sClass.setResolvingLevel(SootClass.SIGNATURES);
			Scene.v().addClass(sClass);
		}
		SootClass cls = Scene.v().getSootClass(HavocClassName);
		final String havocMethodName = HavocMethodName + t.toString();
		if (!cls.declaresMethodByName(havocMethodName)) {
			cls.addMethod(new SootMethod(havocMethodName, Arrays.asList(new Type[] {}), t,
					Modifier.PUBLIC | Modifier.STATIC));
		}
		return cls.getMethodByName("havoc_" + t.toString());
	}

	public static void initialize(Program program) {
		initialized = true;
		INSTANCE.reset();
		
		INSTANCE.setProgram(program);
		INSTANCE.setMemoryModelKind(Options.v().memModel());
	}

	private static final String parameterPrefix = "$in_";
//	public static final String typeFieldName = "$dynamicType";

	public static final String arrayElementTypeFieldName = "$elType";
	public static final String lengthFieldName = "$length";
	// public static final String indexFieldNamePrefix = "$idx_";

	private transient SootMethod currentMethod;
	// private transient SootClass currentClass;
	private transient String currentSourceFileName;

	private transient MemoryModel memoryModel;
	private MemModel memoryModelKind = MemModel.PullPush;

	private transient Program program;

	public void reset() {
		currentMethod = null;
		currentSourceFileName = null;
		memoryModel = null;
		program = null;
		writtenOnceFields = null;
	}


	private Set<SootField> writtenOnceFields;
	
	public boolean isWrittenOnce(SootField f) {
		if (writtenOnceFields==null) {
			writtenOnceFields = WriteOnceFieldCollector.getWriteOnceInstanceFields();
		}
		return writtenOnceFields.contains(f);
	}

	public static List<SootField> findFieldsRecursivelyForRef(Value v) {
		return findFieldsRecursively(((RefType) v.getType()).getSootClass());
	}
	
	public static List<SootField> findFieldsRecursively(SootClass sc) {
		List<SootField> res = new LinkedList<SootField>();
		if (sc.hasSuperclass() && sc.getSuperclass().resolvingLevel() > SootClass.DANGLING) {
			res.addAll(findFieldsRecursively(sc.getSuperclass()));
		}
		res.addAll(sc.getFields());
		return res;
	}

	public static List<SootField> findNonStaticFieldsRecursivelyForRef(Value v) {
		return findNonStaticFieldsRecursively(((RefType) v.getType()).getSootClass());
	}
	
	public static List<SootField> findNonStaticFieldsRecursively(SootClass sc) {
		List<SootField> res = new LinkedList<SootField>();
		for (SootField sf : findFieldsRecursively(sc)) {
			if (!sf.isStatic()) {
				res.add(sf);
			}
		}
		return res;
 	}
	
	public static BinaryExpression createInstanceOfExpression(SourceLocation loc, Variable v, ClassVariable typ) { 
		Expression lhs = new TupleAccessExpression(loc, v, ReferenceType.TypeFieldName);
		return createInstanceOfExpression(lhs, typ);
	}
	
	public static BinaryExpression createInstanceOfExpression(Expression lhs, ClassVariable typ) {
		SourceLocation loc = lhs.getSourceLocation();
		return new BinaryExpression(loc, BinaryOperator.PoLeq, lhs, new IdentifierExpression(loc, typ));
	}
	
	public Value getDefaultValue(soot.Type t) {
		Value rhs = null;
		if (t instanceof PrimType) {
			if (t instanceof soot.BooleanType) {
				rhs = IntConstant.v(0);
			} else if (t instanceof soot.ByteType) {
				rhs = IntConstant.v(0);
			} else if (t instanceof soot.CharType) {
				rhs = IntConstant.v(0);
			} else if (t instanceof soot.DoubleType) {
				rhs = DoubleConstant.v(0);
			} else if (t instanceof soot.FloatType) {
				rhs = FloatConstant.v(0);
			} else if (t instanceof soot.IntType) {
				rhs = IntConstant.v(0);
			} else if (t instanceof soot.LongType) {
				rhs = LongConstant.v(0);
			} else if (t instanceof soot.ShortType) {
				rhs = IntConstant.v(0);
			} else {
				throw new RuntimeException("Unknown type " + t);
			}
		} else {
			rhs = NullConstant.v();
		}
		return rhs;
	}

	public ClassVariable getClassVariable(SootClass sc) {
		return memoryModel.lookupClassVariable(getClassConstant(sc.getType()));
	}

	public ClassVariable getClassVariable(Type t) {
		return getClassVariable(((RefType)t).getSootClass());
	}
	
	public ClassConstant getClassConstant(Type t) {
		if (t instanceof RefType) {
			final String className = ((RefType) t).getClassName().replace(".", "/");
			return ClassConstant.v(className);
		} else if (t instanceof ArrayType) {
			// final String className =
			// getFakeArrayClass((ArrayType)t).getName().replace(".", "/");
			// return ClassConstant.v(className);
			throw new RuntimeException("Remove Arrays first! " + t);
		} else if (t instanceof PrimType) {
			final String className = ((PrimType) t).toString();
			return ClassConstant.v(className);
		}
		throw new RuntimeException("Not implemented");
	}

	public Method lookupOrCreateMethod(SootMethod m) {
		if (this.program.lookupMethod(m.getSignature()) != null) {
			return this.program.lookupMethod(m.getSignature());
		}
		int parameterCount = 0;
		final List<Variable> parameterList = new LinkedList<Variable>();
		if (!m.isStatic()) {
			parameterList.add(new Variable(parameterPrefix + (parameterCount++),
					getMemoryModel().lookupType(m.getDeclaringClass().getType())));
		}

		// if (Options.v().passCallerIdIntoMethods()) {
		// parameterList.add(new Variable(parameterPrefix + (parameterCount++),
		// IntType.instance()));
		// }

		for (int i = 0; i < m.getParameterCount(); i++) {
			parameterList.add(new Variable(parameterPrefix + (parameterCount++),
					getMemoryModel().lookupType(m.getParameterType(i))));
		}

		List<soottocfg.cfg.type.Type> outVarTypes = new LinkedList<soottocfg.cfg.type.Type>();
		if (!m.getReturnType().equals(VoidType.v())) {
			Verify.verifyNotNull(memoryModel);
			outVarTypes.add(memoryModel.lookupType(m.getReturnType()));
		} else if (m.isConstructor()) {
			/*
			 * For constructors, we assume that they return all fields
			 * that are assigned in this constructor
			 */			
			for (SootField sf : SootTranslationHelpers.findNonStaticFieldsRecursively(m.getDeclaringClass())) {
				outVarTypes.add(memoryModel.lookupType(sf.getType()));
			}
			
//			ClassVariable cv = ((ReferenceType) memoryModel.lookupType(m.getDeclaringClass().getType()))
//					.getClassVariable();
//			for (Variable fieldVar : cv.getAssociatedFields()) {
//				outVarTypes.add(fieldVar.getType());
//			}
		}
		return Method.createMethodInProgram(program, m.getSignature(), parameterList, outVarTypes,
				SootTranslationHelpers.v().getSourceLocation(m));
	}

	public Stmt getDefaultReturnStatement(Type returnType, Host createdFrom) {
		Stmt stmt;
		if (returnType instanceof VoidType) {
			stmt = Jimple.v().newReturnVoidStmt();
		} else {
			stmt = Jimple.v().newReturnStmt(getDefaultValue(returnType));
		}
		stmt.addAllTagsOf(createdFrom);
		return stmt;
	}

	void setProgram(Program p) {
		this.program = p;
	}

	public Program getProgram() {
		return this.program;
	}

	public SootClass getAssertionClass() {
		return Scene.v().getSootClass(SootRunner.assertionClassName);
	}

	public SootMethod getAssertMethod() {
		SootClass assertionClass = Scene.v().getSootClass(SootRunner.assertionClassName);
		return assertionClass.getMethodByName(SootRunner.assertionProcedureName);
	}

	public SootField getExceptionGlobal() {
		SootClass assertionClass = Scene.v().getSootClass(SootRunner.assertionClassName);
		return assertionClass.getFieldByName(SootRunner.exceptionGlobalName);
	}

	public Value getExceptionGlobalRef() {
		return Jimple.v().newStaticFieldRef(getExceptionGlobal().makeRef());
	}

	public Unit makeAssertion(Value cond, Host createdFrom) {
		List<Value> args = new LinkedList<Value>();
		args.add(cond);
		InvokeStmt stmt = Jimple.v().newInvokeStmt(Jimple.v().newStaticInvokeExpr(getAssertMethod().makeRef(), args));
		stmt.addAllTagsOf(createdFrom);
		return stmt;
	}

	public SourceLocation getSourceLocation(Unit u) {
		int lineNumber = u.getJavaSourceStartLineNumber();

		if (lineNumber < 0) {
			lineNumber = SootTranslationHelpers.v().getJavaSourceLine(SootTranslationHelpers.v().getCurrentMethod());
		}
		return new SourceLocation(this.currentSourceFileName, lineNumber);
	}

	public SourceLocation getSourceLocation(SootMethod sm) {
		int lineNumber = sm.getJavaSourceStartLineNumber();

		if (lineNumber < 0) {
			lineNumber = SootTranslationHelpers.v().getJavaSourceLine(SootTranslationHelpers.v().getCurrentMethod());
		}

		return new SourceLocation(this.currentSourceFileName, lineNumber);
	}

	void setMemoryModelKind(MemModel kind) {
		memoryModelKind = kind;
		getMemoryModel();
	}

	public MemoryModel getMemoryModel() {
		if (this.memoryModel == null) {
			// TODO:
			if (memoryModelKind == MemModel.PullPush) {
				this.memoryModel = new NewMemoryModel();
			} else {
				throw new RuntimeException("Unknown memory model");
			}
		}
		return this.memoryModel;
	}

	public void setCurrentClass(SootClass currentClass) {
		String fn = findFileName(currentClass.getTags());
		if (fn != null) {
			this.currentSourceFileName = fn;
		}

		// this.currentClass = currentClass;
	}

	private String findFileName(List<Tag> tags) {
		String fileName = null;
		for (Tag tag : tags) {
			if (tag instanceof SourceFileTag) {
				SourceFileTag t = (SourceFileTag) tag;
				if (t.getAbsolutePath() != null) {
					fileName = t.getAbsolutePath();
				} else {
					if (t.getSourceFile() != null) {
						fileName = t.getSourceFile();
					}
				}
			} else {
				// System.err.println("Unprocessed tag " + tag.getClass() + " -
				// " + tag);
			}
		}
		return fileName;
	}

	public SootMethod getCurrentMethod() {
		return currentMethod;
	}

	public void setCurrentMethod(SootMethod currentMethod) {
		String fn = findFileName(currentMethod.getTags());
		if (fn != null) {
			this.currentSourceFileName = fn;
		}
		this.currentMethod = currentMethod;
	}

	public String getCurrentSourceFileName() {
		return this.currentSourceFileName;
	}

	public int getJavaSourceLine(AbstractHost ah) {
		return ah.getJavaSourceStartLineNumber();
	}

//	public int getUniqueNumberForUnit(Unit u) {
//		return u.hashCode();
//	}
//
//	public int getUniqueNumberForUnit(Statement s) {
//		return s.hashCode();
//	}

}
