/*
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html.
 * 
 * This file is a derivative of code released under the terms listed below.  
 *
 */
/**
 *
 * Copyright (c) 2009-2012,
 *
 *  Galois, Inc. (Aaron Tomb <atomb@galois.com>, Rogan Creswick <creswick@galois.com>)
 *  Steve Suh    <suhsteve@gmail.com>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. The names of the contributors may not be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 *
 */
package org.scandroid.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.scandroid.spec.AndroidSpecs;
import org.scandroid.spec.MethodNamePattern;
import org.scandroid.util.LoaderUtils;

import com.ibm.wala.classLoader.ArrayClass;
import com.ibm.wala.classLoader.CallSiteReference;
import com.ibm.wala.classLoader.IClass;
import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.classLoader.Language;
import com.ibm.wala.classLoader.NewSiteReference;
import com.ibm.wala.ipa.callgraph.AnalysisScope;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.ipa.summaries.MethodSummary;
import com.ibm.wala.shrikeBT.IInvokeInstruction;
import com.ibm.wala.shrikeBT.IInvokeInstruction.IDispatch;
import com.ibm.wala.ssa.ConstantValue;
import com.ibm.wala.ssa.SSAAbstractInvokeInstruction;
import com.ibm.wala.ssa.SSAArrayStoreInstruction;
import com.ibm.wala.ssa.SSAInstructionFactory;
import com.ibm.wala.ssa.SSANewInstruction;
import com.ibm.wala.types.ClassLoaderReference;
import com.ibm.wala.types.Descriptor;
import com.ibm.wala.types.MethodReference;
import com.ibm.wala.types.TypeName;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.collections.HashMapFactory;
import com.ibm.wala.util.debug.UnimplementedError;
import com.ibm.wala.util.strings.Atom;

public class AppModelMethod {
	
	int nextLocal;
    /**
     * A mapping from String (variable name) -&gt; Integer (local number)
     */
    private Map<String, Integer> symbolTable = null;
    
    private MethodSummary methodSummary;
    
    private final IClassHierarchy cha;
    
    private final AnalysisScope scope;
    
    private Map<ConstantValue, Integer> constant2ValueNumber = HashMapFactory.make();    
    
    SSAInstructionFactory insts;

    
	//Maps a Type to variable name
	private Map<TypeReference, Integer> typeToID = new HashMap<> ();    
	//innerclass dependencies
	private Map<TypeReference, LinkedList<TypeReference>> icDependencies = new HashMap<> ();
    //all callbacks to consider
	private List<MethodParams> callBacks = new ArrayList<>();
	
	private Map<TypeReference, TypeReference> aClassToTR = new HashMap<> ();
	
	private class MethodParams{
		public IMethod im;
		public int params[];
		private MethodParams(IMethod method) {
			im = method;
		}
		private void setParams(int p[]) {
			params = p;
		}
		private IMethod getIMethod() {
			return im;
		}
		private int[] getParams() {
			return params;
		}
	}
	
	public AppModelMethod(IClassHierarchy cha, AnalysisScope scope, AndroidSpecs specs) {
    	this.cha = cha;
    	this.scope = scope;    	
	    Language lang = scope.getLanguage(ClassLoaderReference.Application.getLanguage());
	    insts = lang.instructionFactory();
		
		startMethod();
    	buildTypeMap(specs);
		processTypeMap();
		processCallBackParams();
		createLoopAndSwitch();
    }

	private void createLoopAndSwitch() {
		int callbackSize = callBacks.size();
		//start of while loop
		int loopLabel = methodSummary.getNumberOfStatements();
		int switchValue = nextLocal++;
		//default label, for now same as case1
		int defLabel = loopLabel+1;
		int[] casesAndLabels = new int[2*callbackSize];
        
        for (int i = 0; i < callbackSize; i++) {
        	casesAndLabels[i*2] = i+1;
        	casesAndLabels[i*2+1] = defLabel+i*2;
        }
        
        methodSummary.addStatement(
        		insts.SwitchInstruction(methodSummary.getNumberOfStatements(), switchValue, defLabel, casesAndLabels));
        
        for (int i = 0; i < callbackSize; i++) {
        	MethodParams mp = callBacks.get(i);
        	IMethod im = mp.getIMethod();
        	IDispatch dispatch;
        	if (im.isInit()) {
        		dispatch = IInvokeInstruction.Dispatch.SPECIAL;
        	}
        	else if (im.isAbstract()) {
        		dispatch = IInvokeInstruction.Dispatch.INTERFACE;
        	}
        	else if (im.isStatic()) {
        		dispatch = IInvokeInstruction.Dispatch.STATIC;
        	}
        	else
        		dispatch = IInvokeInstruction.Dispatch.VIRTUAL;
        	addInvocation(mp.getParams(), 
        			CallSiteReference.make(methodSummary.getNumberOfStatements(), 
        					mp.getIMethod().getReference(), 
        					dispatch));        	
        	methodSummary.addStatement(insts.GotoInstruction(methodSummary.getNumberOfStatements(), loopLabel));
        }		
	}
	
	private void startMethod() {
    	String className = "Lcom/SCanDroid/AppModel";
    	String methodName = "entry";
    	TypeReference governingClass = 
				TypeReference.findOrCreate(ClassLoaderReference.Application, TypeName.string2TypeName(className));
		Atom mName = Atom.findOrCreateUnicodeAtom(methodName);
		Language lang = scope.getLanguage(ClassLoaderReference.Application.getLanguage());
		Descriptor D = Descriptor.findOrCreateUTF8(lang, "()V");
		MethodReference mref = MethodReference.findOrCreate(governingClass, mName, D);
		
	
		methodSummary = new MethodSummary(mref);
				
		methodSummary.setStatic(true);
		methodSummary.setFactory(false);
		
		int nParams = mref.getNumberOfParameters();
		nextLocal = nParams + 1;
		symbolTable = HashMapFactory.make(5);
		for (int i = 0; i < nParams; i++) {
			symbolTable.put("arg" + i, Integer.valueOf(i + 1));
		}
    }	
    
    private void buildTypeMap(AndroidSpecs specs) {
		//Go through all possible callbacks found in Application code
		//Associate their TypeReference with a unique number in typeToID.
		//Also keep track of all anonymous classes found.
    	for (MethodNamePattern mnp:specs.getCallBacks()) {
    		for (IMethod im: mnp.getPossibleTargets(cha)) {
    			// limit to functions defined within the application
    			if(LoaderUtils.fromLoader(im, ClassLoaderReference.Application))
    			{   
    				callBacks.add(new MethodParams(im));
    				TypeReference tr = im.getDeclaringClass().getReference(); 
    				if (!typeToID.containsKey(tr)) {
						typeToID.put(tr, nextLocal++);
    					//class is an innerclass
    					if (tr.getName().getClassName().toString().contains("$")) {
    						addDependencies(tr);
    					}
    				}
    			}
    		}
    	}    	    
    }
    
  	private void addDependencies(TypeReference tr) {
		String packageName = 'L' +tr.getName().getPackage().toString()+ '/';
		String outerClassName;		
		String innerClassName = tr.getName().getClassName().toString();
		LinkedList<TypeReference> trLL = new LinkedList<> ();
		trLL.push(tr);
		int index = innerClassName.lastIndexOf('$');
		while (index != -1) {
    		outerClassName = innerClassName.substring(0, index);
    		TypeReference innerTR = TypeReference.findOrCreate(ClassLoaderReference.Application, packageName+outerClassName);
    		trLL.push(innerTR);    		
    		if (!typeToID.containsKey(innerTR)) {
    			typeToID.put(innerTR, nextLocal++);
    			aClassToTR.put(innerTR, tr);
    		}
    		
    		innerClassName = outerClassName;
    		index = outerClassName.lastIndexOf('$');
		}
		icDependencies.put(tr, trLL);
	}

        
    private void processTypeMap() {
    	Set<Integer> createdIDs = new HashSet<> ();    	
    	for (Entry<TypeReference, Integer> eSet:typeToID.entrySet()) {    		
    		Integer i = eSet.getValue();
    		if (createdIDs.contains(i))
    			continue;
    		
    		TypeReference tr = eSet.getKey();
    		String className = tr.getName().getClassName().toString();
    		
    		
    		//Not an anonymous innerclass
    		if (!className.contains("$")) {
    			processAllocation(tr, i, false);
    			createdIDs.add(i);
    		}
    		//Is an anonymous innerclass
    		else {
    			LinkedList<TypeReference> deps = icDependencies.get(tr);
    			if (deps == null) {
    				tr = aClassToTR.get(tr);
    			}

    			for (TypeReference trD:icDependencies.get(tr)) {
    				Integer j = typeToID.get(trD);
    				if (!createdIDs.contains(j)) {
    					String depClassName = trD.getName().getClassName().toString(); 
    					processAllocation(trD, j, depClassName.contains("$"));    					
    					createdIDs.add(j);
    				}
    			}
    		}
    	}
    	
    	assert(createdIDs.size() == typeToID.size()):"typeToID and createdID size do not match";    	
    }
    
    private void processCallBackParams() {
    	for (MethodParams mp:callBacks) {
    		int params[] = new int[mp.getIMethod().getNumberOfParameters()];
    		int startPos;
    		if (mp.getIMethod().isStatic()) {
    			startPos = 0;
    		}
    		else {
    			params[0] = typeToID.get(mp.getIMethod().getDeclaringClass().getReference());
    			startPos = 1;    			
    		}
    		for (int i = startPos; i < params.length; i++) {
    			params[i] = makeArgument(mp.getIMethod().getParameterType(i));
    		}
    		mp.setParams(params);    		
    	}    	    
    }
    
    private int makeArgument(TypeReference tr) {
    	if (tr.isPrimitiveType())
    		return addLocal();
    	else {
    		SSANewInstruction n = processAllocation(tr, nextLocal++, false);
    		return (n == null) ? -1 : n.getDef();
    	}
    }   
    
    private int addLocal() {
    	return nextLocal++;
    }
    
    private SSANewInstruction processAllocation (TypeReference tr, Integer i, boolean isInner) {
        // create the allocation statement and add it to the method summary
        NewSiteReference ref = NewSiteReference.make(methodSummary.getNumberOfStatements(), tr);        
        SSANewInstruction a = null;
        
        if (tr.isArrayType()) {
        	int[] sizes = new int[((ArrayClass)cha.lookupClass(tr)).getDimensionality()];
        	Arrays.fill(sizes, getValueNumberForIntConstant(1));
        	a = insts.NewInstruction(methodSummary.getNumberOfStatements(), i, ref, sizes);
        } else {
        	a = insts.NewInstruction(methodSummary.getNumberOfStatements(), i, ref);
        }
        
        methodSummary.addStatement(a);
        
        IClass klass = cha.lookupClass(tr);
        if (klass == null) {
          return null;
        }
        
        if (klass.isArrayClass()) {
        	int arrayRef = a.getDef();
        	TypeReference e = klass.getReference().getArrayElementType();
        	while (e != null && !e.isPrimitiveType()) {
        		// allocate an instance for the array contents
        		NewSiteReference n = NewSiteReference.make(methodSummary.getNumberOfStatements(), e);
        		int alloc = nextLocal++;
        		SSANewInstruction ni = null;
        		if (e.isArrayType()) {
        			int[] sizes = new int[((ArrayClass)cha.lookupClass(tr)).getDimensionality()];
        			Arrays.fill(sizes, getValueNumberForIntConstant(1));
        			ni = insts.NewInstruction(methodSummary.getNumberOfStatements(), alloc, n, sizes);
        		} else {
        			ni = insts.NewInstruction(methodSummary.getNumberOfStatements(), alloc, n);
        		}
        		methodSummary.addStatement(ni);

        		// emit an astore
        		SSAArrayStoreInstruction store = insts.ArrayStoreInstruction(methodSummary.getNumberOfStatements(), arrayRef, getValueNumberForIntConstant(0), alloc, e);
        		methodSummary.addStatement(store);

        		e = e.isArrayType() ? e.getArrayElementType() : null;
        		arrayRef = alloc;
        	}
        }
        
        //invoke constructor 
        IMethod ctor = cha.resolveMethod(klass, MethodReference.initSelector);
        
        if (ctor != null) {
        	//only check for more constructors when we're looking through the inner classes?
			if (isInner && !ctor.getDeclaringClass().getName().toString().equals(klass.getName().toString())) {
				boolean foundValidCtor = false;
				for (IMethod im: klass.getAllMethods()) {
					if (im.getDeclaringClass().getName().toString().equals(klass.getName().toString()) && 
							im.getSelector().getName().toString().equals(MethodReference.initAtom.toString())) {
						ctor = im;
						foundValidCtor = true;						
						//found a default constructor that takes only the outer class as a parameter					
						if (im.getDescriptor().getNumberOfParameters() == 1) {
							break;
						}						
					}					
				}
				if (!foundValidCtor) {
					throw new UnimplementedError("Check for other constructors, or just use default Object constructor");
				}
			}
			int[] params;
			if (ctor.getDescriptor().getNumberOfParameters() == 0)
				params = new int[] {i};
			else {
				params = new int[ctor.getNumberOfParameters()];
				params[0] = i;
				
				LinkedList<TypeReference> deps = icDependencies.get(tr);
				if (deps == null) {
					deps = icDependencies.get(aClassToTR.get(tr));
					int index = deps.lastIndexOf(tr);					
					TypeReference otr = deps.get(index-1);
					assert(ctor.getParameterType(1).equals(otr)) : "Type Mismatch";
					params[1] = typeToID.get(otr);
				}
				else {
					TypeReference otr = deps.get(deps.size()-2);
					assert(ctor.getParameterType(1).equals(otr)) : "Type Mismatch";
					params[1] = typeToID.get(otr);
				}
				
				//Allocate new instances for each of the other parameters
				//in the current constructor.
				for (int pI = 2; pI < params.length; pI++) {
					params[pI] = makeArgument(ctor.getParameterType(pI));
				}
				
			}
        	addInvocation(params, CallSiteReference.make(methodSummary.getNumberOfStatements(), ctor.getReference(),
        			IInvokeInstruction.Dispatch.SPECIAL));
        }

        return a;
    }
        
    public SSAAbstractInvokeInstruction addInvocation(int[] params, CallSiteReference site) {
    	if (site == null) {
    		throw new IllegalArgumentException("site is null");
    	}
    	CallSiteReference newSite = CallSiteReference.make(methodSummary.getNumberOfStatements(), site.getDeclaredTarget(), site.getInvocationCode());
    	SSAAbstractInvokeInstruction s = null;
    	if (newSite.getDeclaredTarget().getReturnType().equals(TypeReference.Void)) {
    		s = insts.InvokeInstruction(methodSummary.getNumberOfStatements(), params, nextLocal++, newSite, null);
    	} else {
    		s = insts.InvokeInstruction(methodSummary.getNumberOfStatements(), nextLocal++, params, nextLocal++, newSite, null);
    	}
    	methodSummary.addStatement(s);
    	//        	cache.invalidate(this, Everywhere.EVERYWHERE);
    	return s;
    }        
  	
    protected int getValueNumberForIntConstant(int c) {
        ConstantValue v = new ConstantValue(c);
        Integer result = constant2ValueNumber.get(v);
        if (result == null) {
          result = nextLocal++;
          constant2ValueNumber.put(v, result);
        }
        return result;
      }
    
    public MethodSummary getSummary() {
    	return methodSummary;
    }
}
