/*
 * 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 com.ibm.wala.dalvik.util.AndroidComponent;
import com.ibm.wala.dalvik.util.AndroidEntryPointManager;
import com.ibm.wala.ipa.callgraph.ContextItem;
import com.ibm.wala.ipa.callgraph.ContextKey;
import com.ibm.wala.types.TypeName;
import com.ibm.wala.util.strings.Atom;

/**
 * Determines the target of an Android-Intent.
 *
 * <p>An Intent is generated at each Constructor-Call of an Android-Intent. Information to that
 * Constructor is added as target.
 *
 * <p>If you want to change the target don't change this Object! Instead place an override using the
 * AndroidEntryPointManager so no information is lost.
 *
 * <p>Upon the creation of an Intent it's target-type almost always points to UNKNOWN. Again change
 * this using an override.
 *
 * <p>This class contains functions that determine the target-type on the Intent itself. They are
 * intended as fallback only.
 *
 * <p>CAUTION: If you inherit from this class - keep hashCodes and equals clashing!
 *
 * @see com.ibm.wala.dalvik.util.AndroidEntryPointManager
 * @see com.ibm.wala.dalvik.ipa.callgraph.propagation.cfa.IntentContextSelector
 * @see com.ibm.wala.dalvik.ipa.callgraph.propagation.cfa.IntentContextInterpreter
 * @author Tobias Blaschke &lt;code@tobiasblaschke.de&gt;
 * @since 2013-10-12
 */
public class Intent implements ContextItem, Comparable<Intent> {
  /** Key into the Context that represents the Intent. */
  public static final ContextKey INTENT_KEY = new ContextKey() {};

  public enum IntentType {
    /** The target could not be Identified for sure */
    UNKNOWN_TARGET,
    /** The Class the Intent Action refers to belongs to this App */
    INTERNAL_TARGET,
    /** The Class the Intent Action refers to belongs to an external App */
    EXTERNAL_TARGET,
    /** The Action is a Constant defined on the Android-Reference Manual */
    STANDARD_ACTION,
    SYSTEM_SERVICE,
    /** So External and maybe internal */
    BROADCAST,
    /** Do not handle intent */
    IGNORE
  }

  private enum Explicit {
    UNSET,
    IMPLICIT,
    EXPLICIT,
    /** An other target was set for an explicit Intent */
    MULTI
  }

  public static final Atom UNBOUND = Atom.findOrCreateAsciiAtom("Unbound");

  private Atom action;
  public Atom uri;
  private IntentType type;
  private Explicit explicit = Explicit.UNSET;
  private AndroidComponent targetCompontent; // Activity, Service, ...
  private boolean immutable = false;

  public Intent() {
    this.action = null;
  }

  public Intent(String action) {
    this(Atom.findOrCreateAsciiAtom(action));
  }

  public Intent(Atom action) {
    this(action, null);
  }

  public Intent(Atom action, Atom uri) {
    this.action = action;
    this.uri = uri;
    this.type = null; // Delay computation upon it's need
    this.targetCompontent = null;
  }

  public Intent(TypeName action, Atom uri) {
    this(Atom.findOrCreateAsciiAtom(action.toString()), uri);
  }

  public Intent(TypeName action) {
    this(action, null);
  }

  public void setExplicit() {
    switch (explicit) {
      case UNSET:
        // TODO: This is dangerous?
        explicit = Explicit.EXPLICIT;
        break;
      case EXPLICIT:
        unbind();
        break;
      default:
        throw new UnsupportedOperationException(
            String.format("unexpected explicitness setting %s", explicit));
    }
  }

  public boolean isExplicit() {
    return explicit == Explicit.EXPLICIT;
  }

  public void setImmutable() {
    this.immutable = true;
  }

  @Override
  public Intent clone() {
    final Intent clone = new Intent();
    clone.action = this.action; // OK here?
    clone.uri = this.uri;
    clone.type = this.type;
    clone.explicit = this.explicit;
    clone.targetCompontent = this.targetCompontent;
    return clone;
  }

  /**
   * Set the explicit target of the intent.
   *
   * <p>If setAction is called multible times on an Intent it becomes UNBOUND.
   */
  public void setActionExplicit(Atom action) {
    if (action == null) {
      throw new IllegalArgumentException("Action may not be null!");
    }

    if (this.action == null) {
      assert (this.explicit == Explicit.UNSET)
          : "No Action but Intent is not UNSET - is " + this.explicit;
      assert (!immutable) : "Intent was marked immutable - can't change it.";
      this.action = action;
      this.explicit = Explicit.EXPLICIT;
    } else if (isExplicit() && (!this.action.equals(action))) {
      // We already have the explicit target. Ignore the change.

      unbind();
    } else if (!isExplicit()) {

      assert (!immutable) : "Intent was marked immutable - can't change it.";
      this.action = action;
      this.explicit = Explicit.EXPLICIT;
      // TODO: Set type?
    } else {
      // Set to same values - OK
    }
  }

  public void unbind() {
    assert (!immutable) : "Intent was marked immutable - can't change it.";
    this.action = UNBOUND;
    this.type = IntentType.UNKNOWN_TARGET;
    this.explicit = Explicit.MULTI; // XXX shoulb we do this?
  }

  /**
   * Set the target of the intent.
   *
   * <p>If setAction is called multible times on an Intent it becomes UNBOUND.
   */
  public void setAction(Atom action) {
    if (this.action == null) {
      assert (!immutable) : "Intent was marked immutable - can't change it.";
      this.action = action;
    } else if (isExplicit()) {
      // We already have the explicit target. Ignore the change.
    } else if (!action.equals(this.action)) {
      unbind();
    }
  }

  public Atom getAction() {
    if (this.action == null) {
      assert (!isExplicit()) : "Beeing explicit implies having an action!";
      return UNBOUND;
    }
    return this.action;
  }

  public IntentType getType() {
    if (this.type != null) {
      return this.type;
    } else {
      if (isSystemService(this)) {
        this.type = IntentType.SYSTEM_SERVICE;
      } else if (isStandardAction(this)) {
        this.type = IntentType.STANDARD_ACTION;
      } else if (isInternal(this)) {
        this.type = IntentType.INTERNAL_TARGET;
      } else if (isExternal(this)) {
        this.type = IntentType.EXTERNAL_TARGET;
      } else {
        this.type = IntentType.UNKNOWN_TARGET;
      }
    }
    return this.type;
  }

  /**
   * Return the type of Component associated with this Intent.
   *
   * <p>May return null (especially on an UNKNOWN target). The IntentContextInterpreter uses the
   * IntentStarters.StartInfo to determine the Target. However it is nicer to set the Component
   * here.
   *
   * <p>TODO: Set the Component somewhere
   */
  public AndroidComponent getComponent() {
    return this.targetCompontent;
  }

  private static boolean isSystemService(Intent intent) {
    assert (intent.action != null);
    return (intent.action.getVal(0) != 'L')
        && (intent.action.rIndex((byte) '/') < 0)
        && (intent.action.rIndex((byte) '.') < 0);
  }

  /**
   * Fallback: tries to determine on the Intent itself if it's internal.
   *
   * <p>Use {@link #isInternal(boolean)} instead.
   *
   * <p>Recomputes if the Intent is internal. TODO:
   *
   * @param intent TODO: Implement it ;) TODO: What to return if it does not, but
   *     Summary-Information is available? TODO: We should read in the Manifest.xml rather than
   *     relying on the packet name!
   */
  private static boolean isInternal(Intent intent) { // XXX: This may loop forever!
    /*final Intent override = AndroidEntryPointManager.MANAGER.getIntent(intent);

    logger.warn("Intent.isInternal(Intent) is an unsafe fallback!");

    if (override.getType() != IntentType.UNKNOWN_TARGET) {
        return override.isInternal(true);   // The isInternal defined later not this one!
    }*/

    return false;
  }

  /**
   * Fallback: tries to determine on the Intent itself if it's external.
   *
   * <p>Use {@link #isExternal(boolean)} instead.
   */
  private static boolean isExternal(Intent intent) { // XXX: This may loop forever!
    /*final Intent override = AndroidEntryPointManager.MANAGER.getIntent(intent);

    logger.warn("Intent.isExternal(Intent) is an unsafe fallback!");

    if (override.getType() != IntentType.UNKNOWN_TARGET) {
        return override.isExternal(true);   // The isExternal defined later not this one!
    }*/

    if ((intent.action == null) || (intent.action.equals(UNBOUND))) {
      return false; // Is Unknown
    }

    String pack = AndroidEntryPointManager.MANAGER.guessPackage();

    if (pack == null) {
      // Unknown so not selected as external
      return false;
    }
    return (!(intent.action.toString().startsWith('L' + pack)
        || intent.action.toString().startsWith(pack)));
  }

  /** Fallback: tries to determine on the Intent itself if it's a standard action. */
  private static boolean isStandardAction(Intent intent) { // TODO: This may loop forever!
    /*final Intent override = AndroidEntryPointManager.MANAGER.getIntent(intent);

    logger.warn("Intent.isStandardAction(Intent) is an unsafe fallback!");

    if (override.getType() != IntentType.UNKNOWN_TARGET) {
        return override.isStandard(true);
    }*/
    if ((intent.action == null) || (intent.action.equals(UNBOUND))) {
      return false; // Is Unknown
    }

    // TODO: Make this static or so
    final Atom andoidIntentAction = Atom.findOrCreateAsciiAtom("Landroid/intent/action");
    return intent.action.startsWith(andoidIntentAction);
  }

  /**
   * Is the Intents target internally resolvable.
   *
   * @return if the Intent is associated to a class in the analyzed application.
   * @param strict if false return unknown target as internal
   */
  public boolean isInternal(boolean strict) {
    IntentType type = getType(); // Asserts type is computed

    return ((type == IntentType.INTERNAL_TARGET)
        || ((!strict) && (type == IntentType.UNKNOWN_TARGET)));
  }

  /**
   * Has the target to be resolved by an external App.
   *
   * <p>The Intent is not associated to a class in this application or it's a Standard action
   * defined in the Android Reference Manual.
   *
   * @param strict if false return unknown target as external
   */
  public boolean isExternal(boolean strict) {
    IntentType type = getType(); // Asserts type is computed

    return ((type == IntentType.EXTERNAL_TARGET)
        || (type == IntentType.STANDARD_ACTION)
        || ((!strict) && (type == IntentType.UNKNOWN_TARGET)));
  }

  /**
   * Is the Intent one of the System-Defined ones.
   *
   * <p>It's a Standard action defined in the Android Reference Manual. Implies isExternal.
   *
   * @param strict if false return unknown target as standard
   */
  public boolean isStandard(boolean strict) {
    IntentType type = getType(); // Asserts type is computed

    return ((type == IntentType.STANDARD_ACTION)
        || ((!strict) && (type == IntentType.UNKNOWN_TARGET)));
  }

  @Override
  public String toString() {
    StringBuilder ret;
    if ((this.action == null) || (this.action.equals(UNBOUND))) {
      return "Unbound Intent";
    } else if (getType() == IntentType.SYSTEM_SERVICE) {
      ret = new StringBuilder("SystemService(");
    } else {
      ret = new StringBuilder("Intent(");
    }
    ret.append(this.action.toString());
    if (uri != null) {
      ret.append(", ");
      ret.append(this.uri.toString());
    }
    ret.append(") of type ");
    ret.append(getType());
    return ret.toString();
  }

  /**
   * CLASHES: Does not consider intent-type.
   *
   * <p>This clash is however intended: This aids in resolving the override of an Intent. The
   * AndroidEntryPointManager generates new Intent Objects. Instead of searching all overrides we
   * get it for free.
   */
  @Override
  public int hashCode() {
    // DO NOT USE TYPE!
    if (this.uri != null) {
      return getAction().hashCode() * this.uri.hashCode();
    } else {
      return getAction().hashCode();
    }
  }

  /** Does not consider the associated URI. */
  public boolean equalAction(Intent other) {
    return getAction().equals(other.getAction());
  }

  /**
   * Intents are equal to Intents with other type.
   *
   * <p>This clash is however intended: This aids in resolving the override of an Intent. The
   * AndroidEntryPointManager generates new Intent Objects. Instead of searching all overrides we
   * get it for free.
   */
  @Override
  public boolean equals(Object o) {
    if (o instanceof Intent) {
      Intent other = (Intent) o;

      // DO NOT USE TYPE!
      if (this.uri != null) {
        return ((this.uri.equals(other.uri))
            && equalAction(other)); // && (this.explicit == other.explicit));
      } else {
        return ((other.uri == null)
            && equalAction(other)); // && (this.explicit == other.explicit)) ;
      }
    } else {
      System.err.println("WARNING: Can't compare Intent to " + o.getClass());
      return false;
    }
  }

  public Intent resolve() {
    return AndroidEntryPointManager.MANAGER.getIntent(this);
  }

  @Override
  public int compareTo(Intent other) {
    return getAction().toString().compareTo(other.getAction().toString());
  }
}
