package soot.jimple.infoflow.android.iccta;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import soot.Scene;
import soot.SootClass;
import soot.SootMethod;
import soot.Type;
import soot.VoidType;
import soot.jimple.Jimple;
import soot.jimple.JimpleBody;
import soot.jimple.infoflow.android.iccta.Component.ComponentType;
import soot.jimple.infoflow.entryPointCreators.AndroidEntryPointConstants;
import soot.jimple.infoflow.entryPointCreators.AndroidEntryPointCreator;
import soot.util.Chain;


public class ICCDummyMainCreator 
{
    private static ICCDummyMainCreator s = null;
    ICCDummyMainCreator () {}
    public static ICCDummyMainCreator v() 
    {
        if (s == null) 
        {
            s = new ICCDummyMainCreator();
        
        }
        return s;
    }
    
    public static final String DUMMY_MAIN_METHOD = "dummyMainMethod";
    
    /**
     * 
     * since flowdroid already resolve this problem, 
     * they will generate a dummyMain method for all comp class.
     * but here, we want to generate dummyMain method for each comp class.
     * this is kind of special use.
     * 
     * @param sootClassName
     * @return
     */
    public SootMethod generateDummyMainMethod(String sootClassName)
    {
    	return generateDummyMainMethod(new ArrayList<String>(), sootClassName);
    }
    
    public SootMethod generateDummyMainMethod(List<String> entryPoints, String sootClassName)
    {
    	List<String> androidClasses = new ArrayList<String>();
    	androidClasses.add(sootClassName);
    	
    	SootMethod mainMethod = new SootMethod(DUMMY_MAIN_METHOD, 
    			new ArrayList<Type>(), 
    			VoidType.v(), 
    			Modifier.PUBLIC);// | Modifier.STATIC);    //no need be static
    	JimpleBody body = Jimple.v().newBody(mainMethod);
    	mainMethod.setActiveBody(body);
    	
    	SootClass compSootClass = Scene.v().getSootClass(sootClassName);
    	compSootClass.addMethod(mainMethod);
    	
    	//this is mandatory, the default dummyMainMethod is static, so they 
    	//do not deal thisIdentity. since we don't need static dummyMainMethod, 
    	//we should define it explicit
    	body.insertIdentityStmts();
    	
    	Map<String, List<String>> callbackFunctions = new HashMap<String, List<String>>();
    	callbackFunctions.put(sootClassName, getCallbackFunctions(compSootClass));
    	
    	AndroidEntryPointCreator androidEPCreator = new AndroidEntryPointCreator(androidClasses);	
    	androidEPCreator.setCallbackFunctions(callbackFunctions);
    	
    	return androidEPCreator.createDummyMain(mainMethod);
    }
    
    private List<String> getCallbackFunctions(SootClass sc)
    {
    	List<String> callbacks = new ArrayList<String>();
    	
    	for (SootMethod sm : sc.getMethods())
        {
        	//<init> and <cinit>
        	if (sm.getName().contains("init>"))
        	{
        		continue;
        	}
        	
        	ComponentType compType = Component.getComponentType(sc);
        	switch (compType)
        	{
        	case Activity:
        		if (AndroidEntryPointConstants.getActivityLifecycleMethods().contains(sm.getName()))
            	{
            		continue;
            	}
        		
        		break;
        	case Service:
        		if (AndroidEntryPointConstants.getServiceLifecycleMethods().contains(sm.getName()))
            	{
            		continue;
            	}
        		break;
        	case BroadcastReceiver:
        		if (AndroidEntryPointConstants.getBroadcastLifecycleMethods().contains(sm.getName()))
            	{
            		continue;
            	}
        		break;
        	case ContentProvider:
        		if (AndroidEntryPointConstants.getContentproviderLifecycleMethods().contains(sm.getName()))
            	{
            		continue;
            	}
        		break;
        	default:
        		break;
        	}
        	
        	if (! isPotentialCallbackMethod(sc, sm.getName()))
        	{
        		continue;
        	}
        	
        	callbacks.add(sm.getSignature());
        }
    	
    	callbacks.addAll(getAnonymousCallbacks(sc));
    	
    	return callbacks;
    }
    
    
    private List<String> getAnonymousCallbacks(SootClass sootClass)
    {
    	List<String> rtVal = new ArrayList<String>();
    	
    	try
    	{
    		String clsName = sootClass.getName();
    		
    		if (clsName.contains("$"))
    		{
    			return rtVal;
    		}
    		
    		clsName = clsName + "$";
    		
    		Chain<SootClass> scs = Scene.v().getClasses();
    		
    		for (SootClass sc : scs)
    		{
    			if (sc.getName().startsWith(clsName))
    			{
    				List<SootMethod> sms = sc.getMethods();
    				
    				for (SootMethod sm : sms)
    				{
    					if (sm.getName().contains("<init>"))
    					{
    						continue;
    					}
    					
    					if (isPotentialCallbackMethod(sc, sm.getName()))
    					{
    						System.out.println("--------------------------->" + sc.getName() +":" + sm.getName());
    						rtVal.add(sm.getSignature());
    					}
    				}
    			}
    		}
    	}
    	catch (Exception ex)
    	{
    		ex.printStackTrace();
    	}
    	
    	return rtVal;
    }
    
    
    
    
    /**
     * a callback method at least is extended from its supper class
     * @param currentClass
     * @param methodName
     * @return
     */
    private boolean isPotentialCallbackMethod(SootClass currentClass, String methodName)
    {
    	boolean existCurrentMethod = false;
    	List<SootMethod> currentMethods = currentClass.getMethods();
		for (SootMethod method : currentMethods)
		{
			if (method.getName().equals(methodName))
			{
				existCurrentMethod = true;
				break;
			}
		}
		
		if (! existCurrentMethod)
		{
			throw new RuntimeException(methodName + " is not belong to class " + currentClass.getName());
		}
    	
    	List<SootClass> extendedClasses = Scene.v().getActiveHierarchy().getSuperclassesOf(currentClass);
		for(SootClass sc : extendedClasses)
		{
			List<SootMethod> methods = sc.getMethods();
			for (SootMethod method : methods)
			{
				if (method.getName().equals(methodName))
				{
					return true;
				}
			}
		}
		
		Chain<SootClass> interfaces = currentClass.getInterfaces();
    	for (SootClass i : interfaces)
    	{
    		List<SootMethod> methods = i.getMethods();
			for (SootMethod method : methods)
			{
				if (method.getName().equals(methodName))
				{
					return true;
				}
			}
    	}
		
    	
    	return false;
    }
}