/*
 * 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.impl;

import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.dalvik.util.AndroidComponent;
import com.ibm.wala.dalvik.util.AndroidEntryPointLocator.AndroidPossibleEntryPoint;
import com.ibm.wala.dalvik.util.AndroidTypes;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.strings.Atom;
import java.util.Comparator;

/**
 * An AdnroidEntryPoint is basically the same as a DexEntryPoint. The Difference is, that further
 * actions are taken to give a list of EntryPoints a specific order an to add information on how the
 * entrypoints are to be called in a model which represents the usage of an android application.
 *
 * <p>You might want to use AndroidEntryPointLocator to generate a list of all EntryPoints in an
 * android application
 *
 * @see com.ibm.wala.dalvik.util.AndroidEntryPointLocator
 * @author Tobias Blaschke &lt;code@toiasblaschke.de&gt;
 * @since 2013-09-01
 */
public class AndroidEntryPoint extends DexEntryPoint {
  // implements AndroidEntryPoint.IExecutionOrder {
  public ExecutionOrder order; // XXX protect?

  protected AndroidComponent superType;

  public AndroidEntryPoint(
      AndroidPossibleEntryPoint p,
      IMethod method,
      IClassHierarchy cha,
      AndroidComponent inComponent) {
    super(method, cha);
    this.order = p.order;
    this.superType = inComponent;
  }

  public AndroidEntryPoint(AndroidPossibleEntryPoint p, IMethod method, IClassHierarchy cha) {
    super(method, cha);
    this.order = p.order;
    this.superType = AndroidComponent.from(method, cha);
  }

  public AndroidEntryPoint(
      ExecutionOrder o, IMethod method, IClassHierarchy cha, AndroidComponent inComponent) {
    this(o, method, cha);
    this.superType = inComponent;
  }

  public AndroidEntryPoint(ExecutionOrder o, IMethod method, IClassHierarchy cha) {
    super(method, cha);
    if (o == null) {
      throw new IllegalArgumentException("The execution order may not be null.");
    }
    this.order = o;
    this.superType = AndroidComponent.from(method, cha);
  }

  public AndroidComponent getComponent() {
    return this.superType;
  }

  /** If the function is defined in a class that extends an Activity. */
  public boolean isActivity() {
    IClassHierarchy cha = getClassHierarchy();
    final TypeReference activity = AndroidTypes.Activity;
    return cha.isSubclassOf(method.getDeclaringClass(), cha.lookupClass(activity));
  }

  public boolean belongsTo(AndroidComponent compo) {
    if ((compo == AndroidComponent.SERVICE)
        && (this.superType.equals(AndroidComponent.INTENT_SERVICE))) {
      return true;
    }
    return (this.superType.equals(compo));
  }

  public boolean isMemberOf(Atom klass) {
    return method.getDeclaringClass().getName().toString().startsWith(klass.toString());

    // IClassHierarchy cha = getClassHierarchy();
    // final TypeReference type = TypeReference.find(ClassLoaderReference.Primordial,
    // klass.toString());
    // if (type == null) {
    //    throw new IllegalArgumentException("Unable to look up " + klass.toString());
    // }
    // return cha.isSubclassOf(method.getDeclaringClass(), cha.lookupClass(type));
  }
  /**
   * Implement this interface to put entitys into the AndroidModel.
   *
   * <p>Currently only AndroidEntryPoints are supportet directly. If you want to add other stuff you
   * might want to subclass AbstractAndroidModel.
   */
  public interface IExecutionOrder extends Comparable<IExecutionOrder> {
    /** Returns an integer-representation of the ExecutionOrder. */
    public int getOrderValue();
    /**
     * AbstractAndroidModel inserts code at section switches.
     *
     * <p>There are eight hardcoded sections. Sections are derived by rounding the
     * integer-representation.
     *
     * @return the section of this entity
     */
    public ExecutionOrder getSection();
  }

  /** AndroidEntryPoints have to be sorted before building the model. */
  public static class ExecutionOrderComperator implements Comparator<AndroidEntryPoint> {
    @Override
    public int compare(AndroidEntryPoint a, AndroidEntryPoint b) {
      return a.order.compareTo(b.order);
    }
  }

  /**
   * The section is used to build classes of EntryPoints on how they are to be called.
   *
   * <p>The section is represented by the last label passed before the entity is reached.
   *
   * @return the section of this entrypoint
   */
  public ExecutionOrder getSection() {
    if (this.order.compareTo(ExecutionOrder.BEFORE_LOOP) < 0) return ExecutionOrder.AT_FIRST;
    if (this.order.compareTo(ExecutionOrder.START_OF_LOOP) < 0) return ExecutionOrder.BEFORE_LOOP;
    if (this.order.compareTo(ExecutionOrder.MIDDLE_OF_LOOP) < 0)
      return ExecutionOrder.START_OF_LOOP;
    if (this.order.compareTo(ExecutionOrder.MULTIPLE_TIMES_IN_LOOP) < 0)
      return ExecutionOrder.MIDDLE_OF_LOOP;
    if (this.order.compareTo(ExecutionOrder.END_OF_LOOP) < 0)
      return ExecutionOrder.MULTIPLE_TIMES_IN_LOOP;
    if (this.order.compareTo(ExecutionOrder.AFTER_LOOP) < 0) return ExecutionOrder.END_OF_LOOP;
    if (this.order.compareTo(ExecutionOrder.AT_LAST) < 0) return ExecutionOrder.AFTER_LOOP;
    return ExecutionOrder.AT_LAST;
  }

  public int getOrderValue() {
    return order.getOrderValue();
  }

  public int compareTo(AndroidEntryPoint.IExecutionOrder o) {
    return this.order.compareTo(o);
  }

  /**
   * The ExecutionOrder is used to partially order EntryPoints.
   *
   * <p>The order has to be understood inclusive! E.g. "after(END_OF_LOOP)" means that the position
   * is __BEFORE__ the loop is actually closed!
   *
   * <p>Before building the model a list of AdroidEntryPoints is to be sorted by that criterion. You
   * can use AndroidEntryPoint.ExecutionOrderComperator for that task.
   */
  public static class ExecutionOrder implements IExecutionOrder {
    // This is an Enum-Style class
    /** Visit the EntryPoint once at the beginning of the model use that for initialization stuff */
    public static final ExecutionOrder AT_FIRST = new ExecutionOrder(0);
    /** Basicly the same as AT_FIRST but visited after AT_FIRST */
    public static final ExecutionOrder BEFORE_LOOP = new ExecutionOrder(Integer.MAX_VALUE / 8);
    /** Visit multiple times (endless) in the loop */
    public static final ExecutionOrder START_OF_LOOP =
        new ExecutionOrder(Integer.MAX_VALUE / 8 * 2);
    /** Basicly the same as START_OF_LOOP */
    public static final ExecutionOrder MIDDLE_OF_LOOP =
        new ExecutionOrder(Integer.MAX_VALUE / 8 * 3);
    /** Do multiple calls in the loop. Visited after MIDDLE_OF_LOOP, before EEN_OF_LOOP */
    public static final ExecutionOrder MULTIPLE_TIMES_IN_LOOP =
        new ExecutionOrder(Integer.MAX_VALUE / 8 * 4);
    /**
     * Things in END_OF_LOOP are acutually part of the loop. Use AFTER_LOOP if you want them
     * executed only once
     */
    public static final ExecutionOrder END_OF_LOOP = new ExecutionOrder(Integer.MAX_VALUE / 8 * 5);
    /** Basicly the same as AT_LAST but visited before */
    public static final ExecutionOrder AFTER_LOOP = new ExecutionOrder(Integer.MAX_VALUE / 8 * 6);
    /** Last calls in the model */
    public static final ExecutionOrder AT_LAST = new ExecutionOrder(Integer.MAX_VALUE / 8 * 7);
    /** This value getts used by the detection heuristic - It is not recommended for manual use. */
    public static final ExecutionOrder DEFAULT = MIDDLE_OF_LOOP;

    private int value;
    /**
     * Unrecommended way to generate the Order based on an Integer.
     *
     * <p>This method is handy when reading back files. In your code you should prefer the methods
     * {@link #after(com.ibm.wala.dalvik.ipa.callgraph.impl.AndroidEntryPoint.IExecutionOrder)} and
     * {@link #between(com.ibm.wala.dalvik.ipa.callgraph.impl.AndroidEntryPoint.IExecutionOrder,
     * com.ibm.wala.dalvik.ipa.callgraph.impl.AndroidEntryPoint.IExecutionOrder)}.
     */
    public ExecutionOrder(int val) {
      this.value = val;
    }
    /**
     * Unrecommended way to generate the Order based on a Label-String.
     *
     * <p>This method is handy when reading back files. If you want to refer to a label you should
     * prefer the static members.
     */
    public ExecutionOrder(String label) {
      if (label.equals("AT_FIRST")) {
        this.value = ExecutionOrder.AT_FIRST.getOrderValue();
        return;
      }
      if (label.equals("BEFORE_LOOP")) {
        this.value = ExecutionOrder.BEFORE_LOOP.getOrderValue();
        return;
      }
      if (label.equals("START_OF_LOOP")) {
        this.value = ExecutionOrder.START_OF_LOOP.getOrderValue();
        return;
      }
      if (label.equals("MIDDLE_OF_LOOP")) {
        this.value = ExecutionOrder.MIDDLE_OF_LOOP.getOrderValue();
        return;
      }
      if (label.equals("MULTIPLE_TIMES_IN_LOOP")) {
        this.value = ExecutionOrder.MULTIPLE_TIMES_IN_LOOP.getOrderValue();
        return;
      }
      if (label.equals("END_OF_LOOP")) {
        this.value = ExecutionOrder.END_OF_LOOP.getOrderValue();
        return;
      }
      if (label.equals("AFTER_LOOP")) {
        this.value = ExecutionOrder.AFTER_LOOP.getOrderValue();
        return;
      }
      if (label.equals("AT_LAST")) {
        this.value = ExecutionOrder.AT_LAST.getOrderValue();
        return;
      }
      throw new IllegalArgumentException(
          "ExecutionOrder was constructed from an illegal label: " + label);
    }

    @Override
    public int getOrderValue() {
      return this.value;
    }

    /**
     * Use {@link #between(IExecutionOrder, IExecutionOrder)} instead.
     *
     * <p>Does the internal calculations for the placement.
     */
    private static ExecutionOrder between(int after, int before) {
      if ((before - after) == 1) {
        // It could be ok to warn here instead of throwing: Functions would be located at the same
        // 'slot' in this case.
        // An alternative solution could be to choose other values for before or after.
        throw new ArithmeticException(
            "Precision to low when cascading the ExecutionOrder. You can prevent this error by "
                + "assigning the ExecutionOrders (e.g. using before() and after()) in a different order.");
      }
      if (after > before) {
        throw new IllegalArgumentException(
            "The requested ordering could not be established due to the after-parameter "
                + "being greater than the before parameter! after="
                + after
                + " before="
                + before);
      }
      return new ExecutionOrder(after + (before - after) / 2);
    }

    /**
     * Use this to place a call to an EntryPoint between two other EntryPoint calls or
     * ExecutionOrder "labels". between() does not care about section-boundaries by itself!
     *
     * <p>Use {@link
     * #between(com.ibm.wala.dalvik.ipa.callgraph.impl.AndroidEntryPoint.IExecutionOrder[],
     * com.ibm.wala.dalvik.ipa.callgraph.impl.AndroidEntryPoint.IExecutionOrder[])} and use labels
     * as additional placement-information to prevent unexpected misplacement.
     *
     * @param after the call or "label" to be executed before this one
     * @param before the call or "label" to be executed after this one (inclusive)
     * @return A sortable object to represent the execution order
     * @throws ArithmeticException when the precision is no more suitable for further cascading
     * @throws IllegalArgumentException if parameter after is larger than before.
     * @throws NullPointerException if either parameter is null
     */
    public static ExecutionOrder between(IExecutionOrder after, IExecutionOrder before) {
      if (after == null) {
        throw new NullPointerException("after may not be null");
      }
      if (before == null) {
        throw new NullPointerException("after may not be null");
      }
      return between(after.getOrderValue(), before.getOrderValue());
    }

    /** Helper for internal use. */
    private static ExecutionOrder between(IExecutionOrder after, int before) {
      return between(after.getOrderValue(), before);
    }

    /**
     * Use this variant to refer to multiple locations.
     *
     * <p>The minimum / maximum is computed before the placement of the ExecutionOrder.
     *
     * <p>This method is intended to be more robust when changing the position-information of
     * referred-to ExecutionOrders.
     *
     * <p>In any other means it behaves exactly like {@link
     * #between(com.ibm.wala.dalvik.ipa.callgraph.impl.AndroidEntryPoint.IExecutionOrder,
     * com.ibm.wala.dalvik.ipa.callgraph.impl.AndroidEntryPoint.IExecutionOrder)}.
     *
     * @param after the calls or "labels" to be executed before this one
     * @param before the calls or "labels" to be executed after this one (inclusive)
     * @return A sortable object to represent the execution order
     * @throws ArithmeticException when the precision is no more suitable for further cascading
     * @throws IllegalArgumentException if parameter after is larger than before.
     * @throws NullPointerException if either parameter is null
     */
    public static ExecutionOrder between(IExecutionOrder[] after, IExecutionOrder[] before) {
      if ((after == null) || (after.length == 0)) {
        throw new NullPointerException("after may not be null or empty array");
      }
      if ((before == null) || (before.length == 0)) {
        throw new NullPointerException("before may not be null or empty array");
      }
      int max = Integer.MIN_VALUE;
      int min = Integer.MAX_VALUE;
      for (IExecutionOrder i : after) if (max < i.getOrderValue()) max = i.getOrderValue();
      for (IExecutionOrder i : before) if (min > i.getOrderValue()) min = i.getOrderValue();
      return between(max, min);
    }

    public static ExecutionOrder between(IExecutionOrder after, IExecutionOrder[] before) {
      if ((before == null) || (before.length == 0)) {
        throw new NullPointerException("after may not be null or empty array");
      }
      int min = Integer.MAX_VALUE;
      for (IExecutionOrder i : before) if (min > i.getOrderValue()) min = i.getOrderValue();
      return between(after, min);
    }

    public static ExecutionOrder between(IExecutionOrder[] after, IExecutionOrder before) {
      if ((after == null) || (after.length == 0)) {
        throw new NullPointerException("after may not be null or empty array");
      }
      if (before == null) {
        throw new NullPointerException("before may not be null");
      }
      int max = Integer.MIN_VALUE;
      for (IExecutionOrder i : after) if (max < i.getOrderValue()) max = i.getOrderValue();
      return between(max, before.getOrderValue());
    }

    /**
     * Place the call in the same section after the given call or "label".
     *
     * @param after the call to be executed before this one or label the call belongs to
     * @return A sortable object to represent the execution order
     * @throws ArithmeticException when the precision is no more suitable for further cascading
     * @throws NullPointerException if the parameter is null
     */
    public static ExecutionOrder after(IExecutionOrder after) {
      if (after == null) {
        throw new NullPointerException("after may not be null");
      }
      return after(after.getOrderValue());
    }

    /**
     * Prefer {@link
     * #after(com.ibm.wala.dalvik.ipa.callgraph.impl.AndroidEntryPoint.IExecutionOrder)} whenever
     * possible.
     */
    public static ExecutionOrder after(int after) {
      return between(after, ((after / (Integer.MAX_VALUE / 8)) + 1) * (Integer.MAX_VALUE / 8));
    }

    /**
     * Use this variant to refer to multiple locations.
     *
     * <p>The maximum is computed before the placement of the ExecutionOrder.
     *
     * @param after the call to be executed before this one or label the call belongs to
     * @return A sortable object to represent the execution order
     * @throws ArithmeticException when the precision is no more suitable for further cascading
     * @throws NullPointerException if the parameter is null
     */
    public static ExecutionOrder after(IExecutionOrder[] after) {
      if (after == null) {
        throw new NullPointerException("after may not be null");
      }

      int max = Integer.MIN_VALUE;
      for (IExecutionOrder i : after) if (max < i.getOrderValue()) max = i.getOrderValue();
      return after(max);
    }

    public static ExecutionOrder directlyBefore(IExecutionOrder before) {
      return new ExecutionOrder(before.getOrderValue() - 1);
    }

    public static ExecutionOrder directlyAfter(IExecutionOrder before) {
      return new ExecutionOrder(before.getOrderValue() + 1);
    }

    @Override
    public ExecutionOrder getSection() {
      if (this.compareTo(ExecutionOrder.BEFORE_LOOP) < 0) return ExecutionOrder.AT_FIRST;
      if (this.compareTo(ExecutionOrder.START_OF_LOOP) < 0) return ExecutionOrder.BEFORE_LOOP;
      if (this.compareTo(ExecutionOrder.MIDDLE_OF_LOOP) < 0) return ExecutionOrder.START_OF_LOOP;
      if (this.compareTo(ExecutionOrder.MULTIPLE_TIMES_IN_LOOP) < 0)
        return ExecutionOrder.MIDDLE_OF_LOOP;
      if (this.compareTo(ExecutionOrder.END_OF_LOOP) < 0)
        return ExecutionOrder.MULTIPLE_TIMES_IN_LOOP;
      if (this.compareTo(ExecutionOrder.AFTER_LOOP) < 0) return ExecutionOrder.END_OF_LOOP;
      if (this.compareTo(ExecutionOrder.AT_LAST) < 0) return ExecutionOrder.AFTER_LOOP;
      return ExecutionOrder.AT_LAST;
    }

    @Override
    public int compareTo(IExecutionOrder o) {
      return this.value - o.getOrderValue();
    }

    @Override
    public String toString() {
      if (this.compareTo(ExecutionOrder.AT_FIRST) == 0) return "ExecutionOrder.AT_FIRST";
      if (this.compareTo(ExecutionOrder.BEFORE_LOOP) == 0) return "ExecutionOrder.BEFORE_LOOP";
      if (this.compareTo(ExecutionOrder.START_OF_LOOP) == 0) return "ExecutionOrder.START_OF_LOOP";
      if (this.compareTo(ExecutionOrder.MIDDLE_OF_LOOP) == 0)
        return "ExecutionOrder.MIDDLE_OF_LOOP";
      if (this.compareTo(ExecutionOrder.MULTIPLE_TIMES_IN_LOOP) == 0)
        return "ExecutionOrder.MULTIPLE_TIMES_IN_LOOP";
      if (this.compareTo(ExecutionOrder.END_OF_LOOP) == 0) return "ExecutionOrder.END_OF_LOOP";
      if (this.compareTo(ExecutionOrder.AFTER_LOOP) == 0) return "ExecutionOrder.AFTER_LOOP";
      if (this.compareTo(ExecutionOrder.AT_LAST) == 0) return "ExecutionOrder.AT_LAST";

      if (this.compareTo(ExecutionOrder.BEFORE_LOOP) < 0)
        return "in section ExecutionOrder.AT_FIRST";
      if (this.compareTo(ExecutionOrder.START_OF_LOOP) < 0)
        return "in section ExecutionOrder.BEFORE_LOOP";
      if (this.compareTo(ExecutionOrder.MIDDLE_OF_LOOP) < 0)
        return "in section ExecutionOrder.START_OF_LOOP";
      if (this.compareTo(ExecutionOrder.MULTIPLE_TIMES_IN_LOOP) < 0)
        return "in section ExecutionOrder.MIDDLE_OF_LOOP";
      if (this.compareTo(ExecutionOrder.END_OF_LOOP) < 0)
        return "in section ExecutionOrder.MULTIPLE_TIMES_IN_LOOP";
      if (this.compareTo(ExecutionOrder.AFTER_LOOP) < 0)
        return "in section ExecutionOrder.END_OF_LOOP";
      if (this.compareTo(ExecutionOrder.AT_LAST) < 0) return "in section ExecutionOrder.AFTER_LOOP";
      return "in section ExecutionOrder.AT_LAST";
    }
  }

  @Override
  public boolean equals(Object o) {
    if (o instanceof AndroidEntryPoint) {
      AndroidEntryPoint other = (AndroidEntryPoint) o;
      return this.getMethod().equals(other.getMethod());
    } else {
      return false;
    }
  }

  @Override
  public int hashCode() {
    return 3 * this.getMethod().hashCode();
  }
}
