package soottocfg.cfg.optimization;

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

import com.google.common.base.Verify;

import soot.SootMethod;
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.expression.literal.IntegerLiteral;
import soottocfg.cfg.expression.literal.NullLiteral;
import soottocfg.cfg.method.CfgBlock;
import soottocfg.cfg.method.Method;
import soottocfg.cfg.statement.AssignStatement;
import soottocfg.cfg.statement.AssumeStatement;
import soottocfg.cfg.statement.NewStatement;
import soottocfg.cfg.statement.PushStatement;
import soottocfg.cfg.type.IntType;
import soottocfg.cfg.type.ReferenceType;
import soottocfg.cfg.type.Type;
import soottocfg.cfg.variable.ClassVariable;
import soottocfg.cfg.variable.Variable;
import soottocfg.soot.transformers.ArrayTransformer;
import soottocfg.soot.util.MethodInfo;
import soottocfg.soot.util.SootTranslationHelpers;

/**
 * @author schaef
 * @author rodykers
 *
 */
public class CfgStubber {

	public void stubUnboundFieldsAndMethods(Program program) {
		Queue<Method> todo = new LinkedList<Method>();
		todo.addAll(Arrays.asList(program.getMethods()));
		while (!todo.isEmpty()) {
			Method method = todo.remove();

			if (method.getSource() == null) {
				/*
				 * If the method does not have a body, we just add a non-det
				 * assignment to the
				 * exception global to indicate that this method might have
				 * thrown an exception.
				 * 
				 * TODO: We should add some support to look for user provided
				 * specs here!
				 */
				CfgBlock block = new CfgBlock(method);
				SourceLocation loc = method.getLocation();

				if (method.getMethodName().contains(SootMethod.constructorName)) {
					// ensure that no exception is thrown
					LinkedList<Variable> outParams = new LinkedList<Variable>();
					// when stubbing, set the exceptional return to null.
					Variable exceptionalRetVar = new Variable("exc", method.getReturnType().get(0));
					outParams.add(exceptionalRetVar);

					AssignStatement noException = new AssignStatement(loc,
							new IdentifierExpression(loc, exceptionalRetVar),
							SootTranslationHelpers.v().getMemoryModel().mkNullConstant());
					block.addStatement(noException);

					Variable thisPointer = method.getInParams().get(0);
					ReferenceType rt = getRefTypeFrom(thisPointer);

					List<Expression> rhs = new LinkedList<Expression>();
					int i = 0;
					for (Variable v : rt.getClassVariable().getAssociatedFields()) {
						++i;
						Variable outVar = new Variable("$out" + i, v.getType());
						outParams.add(outVar);
						Variable other;
						other = new Variable("undef_field" + i, v.getType());

						// assign the outParams to fresh values.
						block.addStatement(new AssignStatement(loc, new IdentifierExpression(loc, outVar),
								new IdentifierExpression(loc, other)));
						// add an element to the push.
						rhs.add(new IdentifierExpression(loc, other));
					}

					PushStatement push = new PushStatement(loc, rt.getClassVariable(),
							new IdentifierExpression(loc, thisPointer), rhs);
					block.addStatement(push);

					// verify that the size is correct.
					Verify.verify(outParams.size() == method.getReturnType().size(),
							outParams.size() + "!=" + method.getReturnType().size());

					method.setOutParam(outParams);
				} else if (method.getReturnType().size() > 0) {
					LinkedList<Variable> rets = new LinkedList<Variable>();
					// when stubbing, set the exceptional return to null.
					// System.out.println("Return types of " +
					// method.getMethodName() + ": " + method.getReturnType());
					Variable exceptionalRetVar = new Variable("exc", method.getReturnType().get(0));
					rets.add(exceptionalRetVar);
					AssignStatement noException = new AssignStatement(loc,
							new IdentifierExpression(loc, exceptionalRetVar),
							SootTranslationHelpers.v().getMemoryModel().mkNullConstant());
					block.addStatement(noException);

					// start from 1 because 0 is already the exceptional return.
					int f = 0;
					for (int i = 1; i < method.getReturnType().size(); i++) {
						Type t = method.getReturnType().get(i);
						// add push with undef values to havoc methods
						if (t instanceof ReferenceType) {

							// Rody: because of issue 116, create
							// non-deterministic choice between
							// null and instance with undef fields
							CfgBlock caseNull = new CfgBlock(method);
							method.addEdge(block, caseNull);
							CfgBlock caseInstance = new CfgBlock(method);
							method.addEdge(block, caseInstance);
							
							ReferenceType rt = (ReferenceType) t;
							List<Expression> rhs = new LinkedList<Expression>();

							if (t.toString().contains(ArrayTransformer.arrayTypeName)) {
								// for an array, make sure we add the constraint
								// that length >= 0
								Variable sizeLocal = new Variable("undef_size", IntType.instance());
								AssumeStatement asm = new AssumeStatement(loc,
										new BinaryExpression(loc, BinaryOperator.Ge,
												new IdentifierExpression(loc, sizeLocal), IntegerLiteral.zero()));
								caseInstance.addStatement(0, asm);
								rhs.add(new IdentifierExpression(loc, sizeLocal));
								ClassVariable c = rt.getClassVariable();
								// TODO: @Rody: the dynamic type is not pushed
								// anymore.
								// rhs.add(new IdentifierExpression(loc, c));

								// this is an array, so initialize the remaining
								// fields with sth as well
								int n = rhs.size();
								while (n < c.getAssociatedFields().length) {
									Variable undefLocal = new Variable("undef_field" + (f++),
											c.getAssociatedFields()[n++].getType());
									rhs.add(new IdentifierExpression(loc, undefLocal));
								}
							} else {
								for (Variable v : rt.getClassVariable().getAssociatedFields()) {
									Variable undefLocal = new Variable("undef_field" + (f++), v.getType());
									rhs.add(new IdentifierExpression(loc, undefLocal));
								}
							}
							Variable outVar = new Variable(MethodInfo.returnVariableName, rt);

							rets.add(outVar);
							IdentifierExpression ret = new IdentifierExpression(loc, outVar);
							caseInstance.addStatement(new NewStatement(loc, ret, rt.getClassVariable()));

							PushStatement push = new PushStatement(loc, rt.getClassVariable(), ret, rhs);
							caseInstance.addStatement(push);

							caseNull.addStatement(new AssignStatement(loc, ret, new NullLiteral(loc)));

						} else {
							Variable outVar = new Variable(MethodInfo.returnVariableName, t);
							rets.add(outVar);
						}
					}
					method.setOutParam(rets);
					method.findOrCreateUniqueSink();
				}

			} else if (method.isProgramEntryPoint()) {
				if (method.getInParams().size() == 1 && method.getMethodName().contains("main")) {
					CfgBlock entry = method.getSource();
					SourceLocation loc = method.getLocation();

					Variable argsParam = method.getInParams().get(0);
					ReferenceType argsType = getRefTypeFrom(argsParam);
					ClassVariable c = argsType.getClassVariable();
					Verify.verify("JayArray_java_lang_String".equals(c.getName()),
							"Main needs a string array as argument");
					/*
					 * The length field is now part of the tuple.
					 */
					TupleAccessExpression tae = new TupleAccessExpression(loc, argsParam,
							SootTranslationHelpers.lengthFieldName);
					AssumeStatement asm = new AssumeStatement(loc,
							new BinaryExpression(loc, BinaryOperator.Ge, tae, IntegerLiteral.zero()));
					entry.addStatement(0, asm);

					// push(JayHornArr12, r0, [JayHornArr12.$length,
					// JayHornArr12.$elType, JayHornArr12.$dynamicType])

					List<Expression> rhs = new LinkedList<Expression>();
					/*
					 * TODO: @Rody, the push doesn't take the the dynamic
					 * type and size as first two parameters because stuff
					 * like this should be part of the tuples.
					 */
					// rhs.add(new IdentifierExpression(loc, sizeLocal));

					// rhs.add(new IdentifierExpression(loc, c));
					// this is an array, so initialize the remaining fields with
					// sth as well
					int i = 0;
					for (Variable fieldVar : c.getAssociatedFields()) {
						if (fieldVar.getName().equals(SootTranslationHelpers.lengthFieldName)) {
							// TODO: this should be unreachable later since the
							// associated
							// fields should not contain any final fields.
							rhs.add(new TupleAccessExpression(loc, argsParam,
									SootTranslationHelpers.lengthFieldName));
						} else {
							Variable undefLocal = new Variable("undef_field" + i++, fieldVar.getType());
							rhs.add(new IdentifierExpression(loc, undefLocal));
						}
					}

					IdentifierExpression argsLocal = new IdentifierExpression(loc, argsParam);

					// AssumeStatement asmNotNull = new AssumeStatement(loc,
					// new BinaryExpression(loc, BinaryOperator.Ne,
					// argsLocal, new NullLiteral(loc)));
					// entry.addStatement(1, asmNotNull);

					PushStatement push = new PushStatement(loc, c, argsLocal, rhs);
					entry.addStatement(1, push);
				}
			}
		}
	}

	private ReferenceType getRefTypeFrom(Variable var) {
		return (ReferenceType) var.getType();
	}
}
