/*
 * 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) 2013,
 *      Tobias Blaschke <code@tobiasblaschke.de>
 *  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 com.ibm.wala.dalvik.ipa.callgraph.propagation.cfa;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ibm.wala.classLoader.CallSiteReference;
import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.dalvik.ipa.callgraph.propagation.cfa.IntentStarters.StartInfo;
import com.ibm.wala.dalvik.util.AndroidEntryPointManager;
import com.ibm.wala.dalvik.util.AndroidTypes;
import com.ibm.wala.ipa.callgraph.CGNode;
import com.ibm.wala.ipa.callgraph.Context;
import com.ibm.wala.ipa.callgraph.ContextSelector;
import com.ibm.wala.ipa.callgraph.propagation.ConstantKey;
import com.ibm.wala.ipa.callgraph.propagation.InstanceKey;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.types.MethodReference;
import com.ibm.wala.types.Selector;
import com.ibm.wala.util.intset.EmptyIntSet;
import com.ibm.wala.util.intset.IntSet;
import com.ibm.wala.util.intset.IntSetUtil;

/**
 *  Adds Intents to the Context of functions that start Android-Components.
 *
 *  This is done by remembering all new-sites where Intent-Objects are built and the parameters to its
 *  Constructor. When a function managed by this Selector (see IntentStarters) is encountered the stored
 *  information is added to its Context.
 *
 *  @see    com.ibm.wala.dalvik.ipa.callgraph.propagation.cfa.IntentContextInterpreter
 *  @see    com.ibm.wala.dalvik.ipa.callgraph.propagation.cfa.IntentStarters
 *
 *  @author Tobias Blaschke &lt;code@tobiasblaschke.de&gt;
 *  @since  2013-10-14
 */
public class IntentContextSelector implements ContextSelector {
    private static final Logger logger = LoggerFactory.getLogger(IntentContextSelector.class);

    private final IntentMap intents = new IntentMap();
    private final ContextSelector parent;
    private final IntentStarters intentStarters;

    public IntentContextSelector(final IClassHierarchy cha) {
        this(null, cha);
    }

    /**
     *  @param  parent  is always asked to build a Context first. Context generated by this class is added then.
     */
    public IntentContextSelector(final ContextSelector parent, final IClassHierarchy cha) {
        this.parent = parent;
        this.intentStarters = new IntentStarters(cha);
    }

    /**
     *  Given a calling node and a call site, returns the Context in which the callee should be evaluated.
     *
     *  {@inheritDoc}
     *
     *  @throws IllegalArgumentException    if the type of a parameter given as actualParameters does not match an expected one
     */
    @Override
    public Context getCalleeTarget(CGNode caller, CallSiteReference site, IMethod callee, InstanceKey[] actualParameters) {
        Context ctx = null;

        if (this.parent != null) {
            ctx = parent.getCalleeTarget(caller, site, callee, actualParameters);
            assert (ctx.get(Intent.INTENT_KEY) == null) : "Already have Intent: " + ctx + " caller " + caller + " callee " + callee;
        }

        if (intentStarters.isStarter(callee.getReference())) {
            // Handle startActivity(), startActivityForResult(), startService() and such

            // Search Android-Context and attach corresponding WALA-Context
            /* {
                final InstanceKey self = actualParameters[0];
                assert (self != null) : "This-Pointer was not marked as relevant!";

                if (seenContext.containsKey(self)) {
                    ctx = new AndroidContext(ctx, seenContext.get(self).getContextType());
                } else {
                    logger.warn("No Android-Context seen for {}", caller);
                }
            } // */


            Intent intent = null;
            { // Seach intent
                for (InstanceKey actualParameter : actualParameters) {
                    final InstanceKey param = actualParameter;
                    if (param == null) {
                        continue;
                    } else if (param.getConcreteType().getName().equals(AndroidTypes.IntentName)) {
                        if (! intents.contains(param) ) {
                            logger.error("Unable to resolve Intent called from {}", caller.getMethod());
                            logger.error("Search Key: {} hash: {}", param, param.hashCode());
                            break;
                        } else {
                            intent = intents.find(param);
                            break;
                        }
                    }
                }
            }

            // Add the context
            if (intent != null) {
                AndroidEntryPointManager.MANAGER.addCallSeen(site, intent);
                final Intent iintent = intents.findOrCreateImmutable(intent);
                return new IntentContext(ctx, iintent);
                //return new IntentContext(iintent);
            } else {
                logger.warn("Encountered unresolvable Intent");
                intent = new Intent("Unresolvable");
                intent.setImmutable();
                AndroidEntryPointManager.MANAGER.addCallSeen(site, intent);
                return new IntentContext(ctx, intent);
                //return new IntentContext(intent);
            }
        } else if (callee.getReference().toString().contains("getSystemService")) {
            assert(actualParameters.length == 2) : "PARAMS LENGTH IS" + actualParameters.length;
            final InstanceKey param = actualParameters[1];

            final Intent intent;
            { // Extract target-Service as intent
                if (param instanceof ConstantKey) {
					final String target = (String) ((ConstantKey<?>)param).getValue();
                    intent = new Intent(target) {
                        @Override
                        public Intent.IntentType getType() {
                            return Intent.IntentType.SYSTEM_SERVICE;
                        }
                        // TODO override equals and hashCode?
                    };
                } else {
                    intent = null;
                    if (param == null) {
                        logger.warn("Got param as 'null'. Obviously can't handle this. Caller was: {}", caller.getMethod());
                    } else {
                        logger.warn("Got param as {}. Can't handle this :(", param.getClass());
                    }
                }
            }

            // Add the context
            if (intent != null) {
                AndroidEntryPointManager.MANAGER.addCallSeen(site, intent);
                logger.info("SystemService {} in {} by {}", intent, site, caller);
                final Intent iintent = intents.findOrCreateImmutable(intent);
                return new IntentContext(ctx, iintent);
                //return new IntentContext(iintent);
            }
        } else if (callee.isInit() && callee.getDeclaringClass().getName().equals(AndroidTypes.IntentName)) {
            //
            //  Handle the different Constructors of Intent
            //
            final InstanceKey self = actualParameters[0];
            final Selector calleeSel = callee.getSelector();

            boolean isExplicit = false;
            final InstanceKey actionKey;
            { // fetch actionKey, uriKey
                switch (callee.getNumberOfParameters()) {
                    case 1:
                        logger.debug("Handling Intent()");
                        actionKey = null;
                        break;
                    case 2:
                        if (calleeSel.equals(Selector.make("<init>(Ljava/lang/String;)V"))) {
                            logger.debug("Handling Intent(String action)");
                            actionKey = actualParameters[1];
                        } else if (calleeSel.equals(Selector.make("<init>(Landroid/content/Intent;)V"))) {
                            logger.debug("Handling Intent(Intent other)");

                            final InstanceKey inIntent = actualParameters[1];

                            if (intents.contains(inIntent)) {
                                intents.put(self, intents.find(inIntent));
                            } else {
                                logger.warn("In Intent-Copy constructor: Unable to find the original");
                            }
                            actionKey = null;
                        } else {
                            logger.error("No handling implemented for: {}", callee);
                            actionKey = null;
                        }
                        break;
                    case 3:
                        if (calleeSel.equals(Selector.make("<init>(Ljava/lang/String;Landroid/net/Uri;)V"))) {
                            logger.debug("Handling Intent(String action, Uri uri)");
                            // TODO: Use Information of the URI...
                            actionKey = actualParameters[1];
                        } else if (calleeSel.equals(Selector.make("<init>(Landroid/content/Context;Ljava/lang/Class;)V"))) {
                            logger.debug("Handling Intent(Context, Class)");
                            actionKey = actualParameters[2];
                            isExplicit = true;
                        } else {
                            logger.error("No handling implemented for: {}",  callee);
                            actionKey = null;
                        }
                        break;
                    case 5:
                        if (calleeSel.equals(Selector.make("<init>(Ljava/lang/String;Landroid/net/Uri;Landroid/content/Context;Ljava/lang/Class;)V"))) {
                            logger.debug("Handling Intent(String action, Uri uri, Context, Class)");
                            actionKey = actualParameters[4];
                            isExplicit = true;
                        } else {
                            logger.error("No handling implemented for: {}", callee);
                            actionKey = null;
                        }
                        break;
                    default:
                        logger.error("Can't extract Info from Intent-Constructor: {} (not implemented)", site);
                        actionKey = null;
                }
            } // of fetch actionKey

            final Intent intent = intents.findOrCreate(self);   // Creates Wala-internal Intent

            if (actionKey == null) {
                logger.trace("Got action as 'null'. Obviously can't handle this. Caller was {}", caller.getMethod());
                if (isExplicit) {
                    logger.warn("An Intent with undeteminable target would be explicit - unbinding. Caller was {}", caller.getMethod());
                    intent.unbind();
                }
            } else {
                intents.setAction(self, actionKey, isExplicit);
            }
            //final Intent intent = intents.find(self);
            //if (isExplicit && (! intent.isExplicit())) {    // Has to check if already explicit as we get here multiple times
            //    intents.setExplicit(self);
            //}

            logger.debug("Setting the target of Intent {} in {} by {}", intent, site, caller);
            // TODO: Evaluate uriKey
        } else if (callee.getSelector().equals(Selector.make("setAction(Ljava/lang/String;)Landroid/content/Intent;")) &&
                callee.getDeclaringClass().getName().equals(AndroidTypes.IntentName)) {
            final InstanceKey self = actualParameters[0];
            final InstanceKey actionKey = actualParameters[1];
            final Intent intent = intents.find(self);

            if (AndroidEntryPointManager.MANAGER.isAllowIntentRerouting()) {
                logger.warn("Re-Setting the target of Intent {} in {} by {}", intent, site, caller);
                intents.setAction(self, actionKey, false); // May unbind internally
            } else {
                intents.unbind(self);
            }
            logger.info("Encountered Intent.setAction - Intent is now: {}", intent);
        } else if (callee.getSelector().equals(Selector.make("setComponent(Landroid/content/ComponentName;)Landroid/content/Intent;"))) {
            // TODO: We can't extract from ComponentName yet.
            final InstanceKey self = actualParameters[0];
            final Intent intent = intents.find(self);

            logger.warn("Re-Setting the target of Intent {} in {} by {}", intent, site, caller);

            intent.setExplicit();
            intents.unbind(self);
        } else if (callee.getSelector().equals(Selector.make("setClass(Landroid/content/Context;Ljava/lang/Class;)Landroid/content/Intent;")) ||
                callee.getSelector().equals(Selector.make("setClassName(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;")) ||
                callee.getSelector().equals(Selector.make("setClassName(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;"))) {
            final InstanceKey self = actualParameters[0];
            final InstanceKey actionKey = actualParameters[2];
            final Intent intent = intents.find(self);

            if (AndroidEntryPointManager.MANAGER.isAllowIntentRerouting()) {
                logger.warn("Re-Setting the target of Intent {} in {} by {}", intent, site, caller);
                intents.setAction(self, actionKey, true);
            } else {
                intents.unbind(self);
            }
            logger.info("Encountered Intent.setClass - Intent is now: {}", intent);
        } else if (callee.getSelector().equals(Selector.make("fillIn(Landroid/content/Intent;I)I"))) {
            // See 'setAction' before...                                                        TODO
            logger.warn("Intent.fillIn not implemented - Caller: {}", caller);
            final InstanceKey self = actualParameters[0];
            intents.unbind(self);
        } else if (callee.isInit() && callee.getDeclaringClass().getName().equals(AndroidTypes.IntentSenderName)) {
            logger.error("Unable to evaluate IntentSender: Not implemented!");   // TODO
        } /*else if (site.isSpecial() && callee.getDeclaringClass().getName().equals(
                    AndroidTypes.ContextWrapperName)) {
            final InstanceKey baseKey = actualParameters[1];
            final InstanceKey wrapperKey = actualParameters[0];

            logger.debug("Handling ContextWrapper(Context base)");
            if (seenContext.containsKey(baseKey)) {
                seenContext.put(wrapperKey, seenContext.get(baseKey));
            } else {
                if (baseKey == null) {
                    logger.trace("Got baseKey as 'null'. Obviously can't handle this. Caller was: {}", caller.getMethod());
                } else {
                    logger.warn("ContextWrapper: No AndroidContext was seen for baseKey");
                }
            }
        } else if ((site.isSpecial() && callee.getDeclaringClass().getName().equals(
                        AndroidTypes.ContextImplName))) {
            final InstanceKey self = actualParameters[0];
            seenContext.put(self, new AndroidContext(ctx, AndroidTypes.AndroidContextType.CONTEXT_IMPL));
        } else if (callee.getDeclaringClass().getName().equals(AndroidTypes.ContextWrapperName) &&
                callee.getSelector().equals(Selector.make("attachBaseContext(Landroid/content/Context;)V"))) {
            final InstanceKey baseKey = actualParameters[1];
            final InstanceKey wrapperKey = actualParameters[0];

            logger.debug("Handling ContextWrapper.attachBaseContext(base)");
             if (seenContext.containsKey(baseKey)) {
                seenContext.put(wrapperKey, seenContext.get(baseKey));
            } else {
                if (baseKey == null) {
                    logger.trace("Got baseKey as 'null'. Obviously can't handle this. Caller was: {}", caller.getMethod());
                } else {
                    logger.warn("ContextWrapper: No AndroidContext was seen for baseKey");
                }
            }
        } */

        return ctx;
    }

    /**
     *  Given a calling node and a call site, return the set of parameters based on which this selector may choose
     *  to specialize contexts.
     *
     *  {@inheritDoc}
     */
    @Override
    public IntSet getRelevantParameters(CGNode caller, CallSiteReference site) {
        IntSet ret;
        if (this.parent != null) {
            ret = this.parent.getRelevantParameters(caller, site);
        } else {
            ret = EmptyIntSet.instance;
        }

        final MethodReference target = site.getDeclaredTarget();
        if (intentStarters.isStarter(target)) {
            final StartInfo info = intentStarters.getInfo(target);
            final int[] relevant = info.getRelevant();

            if (relevant != null) {
                for (int element : relevant) {
                    ret = IntSetUtil.add(ret, element);
                }
            }

            logger.debug("Get relevant for {} is {}", site, ret);
        } else if (site.isSpecial() && target.getDeclaringClass().getName().equals(
                    AndroidTypes.IntentName)) {

            final MethodReference mRef = site.getDeclaredTarget();
            final int numArgs = mRef.getNumberOfParameters();

            // Intent()
            // Intent(Intent o)
            // Intent(String action)
            // Intent(String action, Uri uri)
            // Intent(Context packageContext, Class<?> cls)
            // Intent(String action, Uri uri, Context packageContext, Class<?> cls)

            // Select all params;
            switch (numArgs) {
                case 0:
                    return EmptyIntSet.instance;
                case 1:
                    return IntSetUtil.make(new int[] { 0, 1 });
                case 2:
                    logger.debug("Got Intent Constructor of: {}", site.getDeclaredTarget().getSelector());
                    return IntSetUtil.make(new int[] { 0, 1, 2 });
                case 3:
                    logger.debug("Got Intent Constructor of: {}", site.getDeclaredTarget().getSelector());
                    return IntSetUtil.make(new int[] { 0, 1, 2, 3 });
                case 4:
                    logger.debug("Got Intent Constructor of: {}", site.getDeclaredTarget().getSelector());
                    return IntSetUtil.make(new int[] { 0, 1, 2, 3, 4 });
                default:
                    logger.debug("Got Intent Constructor of: {}", site.getDeclaredTarget().getSelector());
                    return IntSetUtil.make(new int[] { 0, 1, 2, 3, 4, 5 });
            }
        } else if (site.isSpecial() && target.getDeclaringClass().getName().equals(
                    AndroidTypes.IntentSenderName)) {

            logger.warn("Encountered an IntentSender-Object: {}", target);
            if (target.getNumberOfParameters() == 0) {
            	// public IntentSender()
            	return IntSetUtil.make(new int[] { 0 });
            } else {
            	// public IntentSender(IIntentSender target)
                // public IntentSender(IBinder target)
            	return IntSetUtil.make(new int[] { 0, 1 });
            }
        } /*else if (site.isSpecial() && target.getDeclaringClass().getName().equals(
                    AndroidTypes.ContextWrapperName)) {
            logger.debug("Fetched ContextWrapper ctor");
            return IntSetUtil.make(new int[] { 0, 1 });
        } else if ((site.isSpecial() && target.getDeclaringClass().getName().equals(
                        AndroidTypes.ContextImplName))) {
            logger.debug("Fetched Context ctor");
            return IntSetUtil.make(new int[] { 0 });
        } else if (target.getDeclaringClass().getName().equals(AndroidTypes.ContextWrapperName) &&
                target.getSelector().equals(Selector.make("attachBaseContext(Landroid/content/Context;)V"))) {
            logger.debug("Encountered ContextWrapper.attachBaseContext()");
            return IntSetUtil.make(new int[] { 0, 1 });
        }*/ else if (target.getSelector().equals(Selector.make("getSystemService(Ljava/lang/String;)Ljava/lang/Object;"))) {
            logger.debug("Encountered Context.getSystemService()");
            return IntSetUtil.make(new int[] { 0, 1 });
        } else if (target.getSelector().equals(Selector.make("setAction(Ljava/lang/String;)Landroid/content/Intent;"))) {
            return IntSetUtil.make(new int[] { 0, 1 });
        } else if (target.getSelector().equals(Selector.make("setComponent(Landroid/content/ComponentName;)Landroid/content/Intent;"))) {
            return IntSetUtil.make(new int[] { 0 });
        } else if (target.getSelector().equals(Selector.make("setClass(Landroid/content/Context;Ljava/lang/Class;)Landroid/content/Intent;"))) {
            return IntSetUtil.make(new int[] { 0, 2 });
        } else if (target.getSelector().equals(Selector.make("setClassName(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;"))) {
            return IntSetUtil.make(new int[] { 0, 2 });
        } else if (target.getSelector().equals(Selector.make("setClassName(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;"))) {
            return IntSetUtil.make(new int[] { 0, 2 });
        }



        return ret;
    } //else if (site.isSpecial() && target.getDeclaringClass().getName().equals(

}
