/**
 * 
 */
package soottocfg.soot.memory_model;

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

import javax.lang.model.type.NullType;

import com.google.common.base.Verify;

import soot.ArrayType;
import soot.RefLikeType;
import soot.RefType;
import soot.Scene;
import soot.SootClass;
import soot.SootField;
import soot.SootMethod;
import soot.Unit;
import soot.Value;
import soot.jimple.ArrayRef;
import soot.jimple.ClassConstant;
import soot.jimple.Constant;
import soot.jimple.DoubleConstant;
import soot.jimple.FloatConstant;
import soot.jimple.NewArrayExpr;
import soot.jimple.NewMultiArrayExpr;
import soot.jimple.StringConstant;
import soottocfg.cfg.Program;
import soottocfg.cfg.expression.Expression;
import soottocfg.cfg.expression.IdentifierExpression;
import soottocfg.cfg.expression.literal.NullLiteral;
import soottocfg.cfg.method.Method;
import soottocfg.cfg.statement.CallStatement;
import soottocfg.cfg.type.BoolType;
import soottocfg.cfg.type.IntType;
import soottocfg.cfg.type.ReferenceType;
import soottocfg.cfg.type.Type;
import soottocfg.cfg.type.TypeType;
import soottocfg.cfg.variable.ClassVariable;
import soottocfg.cfg.variable.Variable;
import soottocfg.soot.util.SootTranslationHelpers;

/**
 * @author schaef
 *
 */
public abstract class BasicMemoryModel extends MemoryModel {

	protected Program program;
	protected final Map<soot.Type, soottocfg.cfg.type.Type> types = new HashMap<soot.Type, soottocfg.cfg.type.Type>();
	protected final Map<SootField, Variable> fieldGlobals = new HashMap<SootField, Variable>();

	protected final Map<Constant, Variable> constantDictionary = new HashMap<Constant, Variable>();

	// protected final Type nullType;

	public BasicMemoryModel() {
		this.program = SootTranslationHelpers.v().getProgram();
	}

	@Override
	public boolean isNullReference(Expression e) {
		return e instanceof NullLiteral;
	}

	@Override
	public void mkArrayWriteStatement(Unit u, ArrayRef arrayRef, Value rhs) {
		throw new RuntimeException("This should have been removed by the array abstraction.");
	}

	@Override
	public void mkArrayReadStatement(Unit u, ArrayRef arrayRef, Value lhs) {
		throw new RuntimeException("This should have been removed by the array abstraction.");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jayhorn.soot.memory_model.MemoryModel#mkNewArrayExpr(soot.jimple.
	 * NewArrayExpr )
	 */
	@Override
	public Expression mkNewArrayExpr(NewArrayExpr arg0) {
		throw new RuntimeException("This should have been removed by the array abstraction.");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * jayhorn.soot.memory_model.MemoryModel#mkNewMultiArrayExpr(soot.jimple
	 * .NewMultiArrayExpr)
	 */
	@Override
	public Expression mkNewMultiArrayExpr(NewMultiArrayExpr arg0) {
		throw new RuntimeException("This should have been removed by the array abstraction.");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jayhorn.soot.memory_model.MemoryModel#mkStringLengthExpr(soot.Value)
	 */
	@Override
	public Expression mkStringLengthExpr(Value arg0) {
		//TODO
		Variable v = SootTranslationHelpers.v().getProgram().lookupGlobalVariable(
				"TODO" + constantDictionary.size(), IntType.instance());
		return new IdentifierExpression(this.statementSwitch.getCurrentLoc(),v);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jayhorn.soot.memory_model.MemoryModel#mkNullConstant()
	 */
	@Override
	public Expression mkNullConstant() {
		return new NullLiteral(null);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jayhorn.soot.memory_model.MemoryModel#mkStringConstant(soot.jimple.
	 * StringConstant)
	 */
	@Override
	public Expression mkStringConstant(StringConstant arg0) {
		if (!constantDictionary.containsKey(arg0)) {
			constantDictionary.put(arg0, SootTranslationHelpers.v().getProgram().lookupGlobalVariable(
					"$string" + constantDictionary.size(), lookupType(arg0.getType())));
		}
		return new IdentifierExpression(this.statementSwitch.getCurrentLoc(), constantDictionary.get(arg0));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jayhorn.soot.memory_model.MemoryModel#mkDoubleConstant(soot.jimple.
	 * DoubleConstant)
	 */
	@Override
	public Expression mkDoubleConstant(DoubleConstant arg0) {
		if (!constantDictionary.containsKey(arg0)) {
			constantDictionary.put(arg0, SootTranslationHelpers.v().getProgram().lookupGlobalVariable(
					"$double" + constantDictionary.size(), lookupType(arg0.getType())));
		}
		return new IdentifierExpression(this.statementSwitch.getCurrentLoc(), constantDictionary.get(arg0));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jayhorn.soot.memory_model.MemoryModel#mkFloatConstant(soot.jimple.
	 * FloatConstant)
	 */
	@Override
	public Expression mkFloatConstant(FloatConstant arg0) {
		if (!constantDictionary.containsKey(arg0)) {
			constantDictionary.put(arg0, SootTranslationHelpers.v().getProgram().lookupGlobalVariable(
					"$float" + constantDictionary.size(), lookupType(arg0.getType())));
		}
		return new IdentifierExpression(this.statementSwitch.getCurrentLoc(), constantDictionary.get(arg0));
	}

	// @Override
	// public Expression lookupClassConstant(ClassConstant arg0) {
	// if (!constantDictionary.containsKey(arg0)) {
	// constantDictionary.put(arg0,
	// SootTranslationHelpers.v().getProgram().lookupGlobalVariable(
	// "$cc" + arg0.getValue(), lookupType(arg0.getType()), true, true));
	// }
	// return new IdentifierExpression(this.statementSwitch.getCurrentLoc(),
	// constantDictionary.get(arg0));
	// }

	/*
	 * (non-Javadoc)
	 * 
	 * @see soottocfg.soot.memory_model.MemoryModel#lookupType(soot.Type)
	 * TODO: check which types to use for Short, Lond, Double, and Float.
	 */
	@Override
	public Type lookupType(soot.Type t) {
		if (!types.containsKey(t)) {
			Type type = null;
			if (t instanceof soot.BooleanType) {
				type = BoolType.instance();
			} else if (t instanceof soot.ByteType) {
				type = IntType.instance();
			} else if (t instanceof soot.CharType) {
				type = IntType.instance();
			} else if (t instanceof soot.DoubleType) {
				type = IntType.instance();
			} else if (t instanceof soot.FloatType) {
				type = IntType.instance();
			} else if (t instanceof soot.IntType) {
				type = IntType.instance();
			} else if (t instanceof soot.LongType) {
				type = IntType.instance();
			} else if (t instanceof soot.ShortType) {
				type = IntType.instance();
			} else if (t instanceof RefLikeType) {
				type = lookupRefLikeType((RefLikeType) t);
			} else {
				throw new RuntimeException("Don't know what to do with type " + t);
			}
			types.put(t, type);
		}
		return types.get(t);
	}

	@Override
	public void mkConstructorCall(Unit u, SootMethod constructor, List<Expression> args) {
		List<Expression> receiver = new LinkedList<Expression>();
		Method method = SootTranslationHelpers.v().lookupOrCreateMethod(constructor);
		CallStatement stmt = new CallStatement(SootTranslationHelpers.v().getSourceLocation(u), method, args, receiver);
		this.statementSwitch.push(stmt);
	}

	protected Type lookupRefLikeType(RefLikeType t) {
		if (t instanceof ArrayType) {
			throw new RuntimeException("Remove Arrays first. " + t);
		} else if (t instanceof RefType) {
			if ( ((RefType)t).getSootClass().equals(Scene.v().getSootClass("java.lang.Class"))) {
				return new TypeType();
			}
			return new ReferenceType(lookupClassVariable(SootTranslationHelpers.v().getClassConstant(t)));
		} else if (t instanceof NullType) {
			return (ReferenceType) (new NullLiteral(null)).getType();
		}
		throw new UnsupportedOperationException("Unsupported type " + t.getClass());
	}

	private String classNameToSootName(String className) {
		return className.replace('/', '.');
	}
	
	public ClassVariable lookupClassVariable(ClassConstant cc) {
		if (!this.constantDictionary.containsKey(cc)) {
			final String name = cc.getValue();
			final String sootClassName = classNameToSootName(name);

			if (Scene.v().containsClass(sootClassName)) {
				SootClass c = Scene.v().getSootClass(sootClassName);
				Collection<ClassVariable> parents = new HashSet<ClassVariable>();				
				if (c.resolvingLevel() >= SootClass.HIERARCHY) {
					if (c.hasSuperclass()) {
						/**
						 * TODO: WARNING: we had bugs before from mixing
						 * qualified and non-qualified class
						 * names. E.g., we had:
						 * java.lang.Object and Object in the class hierarchy.
						 */
						SootClass superClass = c.getSuperclass();
						final String bytecodeName = superClass.getType().getClassName().replace('.', '/');
						parents.add(lookupClassVariable(ClassConstant.v(bytecodeName)));
					} else {
						/*
						 * This is a hack because, for whatever reason,
						 * Throwable and some other classes
						 * do not have Object as their superclass.
						 */
						if (c != Scene.v().getSootClass(Object.class.getName())) {
							SootClass superClass = Scene.v().getSootClass(Object.class.getName());
							final String bytecodeName = superClass.getType().getClassName().replace('.', '/');
							parents.add(lookupClassVariable(ClassConstant.v(bytecodeName)));
						}
					}
				}

				ClassVariable cv = new ClassVariable(name, parents);
				this.constantDictionary.put(cc, cv);

				List<Variable> fields = new LinkedList<Variable>();
				if (c.resolvingLevel() > SootClass.DANGLING) {					
					for (SootField f : SootTranslationHelpers.findNonStaticFieldsRecursively(c)) {
						boolean isConst = SootTranslationHelpers.v().isWrittenOnce(f);						
						fields.add(new Variable(f.getName(), this.lookupType(f.getType()), isConst, false));						
					}
				}
				cv.addFields(fields);
			} else {
				// System.err.println("Class not in scene: "+sootClassName);
				this.constantDictionary.put(cc, new ClassVariable(name, new HashSet<ClassVariable>()));
			}			
			Variable v = this.constantDictionary.get(cc);
			Verify.verifyNotNull(v);
			this.program.addClassVariable((ClassVariable) v);
			/*
			 * After we added the type to the dictionary, update the ref
			 * type to avoid going into an endless loop.
			 */
//			SootClass javaLangClass = Scene.v().getSootClass("java.lang.Class");
//			ReferenceType rt = lookupRefLikeType(javaLangClass.getType());
//			((ClassVariable)v).setType(rt);
		}
		return (ClassVariable) this.constantDictionary.get(cc);
	}

}
