/*
 * Copyright (c) 2002,2006 IBM Corporation.
 * All rights reserved. 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
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.shrike.shrikeBT;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;

/**
 * This class is a container for a bunch of information that we might know about a method. It's here
 * for convenience so users can just pass one MethodData around instead of passing around an array
 * of instructions, an array of exception handler lists, etc.
 *
 * <p>It also provides a place to hang annotations. We provide a table mapping abstract "keys" to
 * annotation objects. We also provide an invalidation protocol so that the annotation objects can
 * be notified of code changes and react appropriately. This is useful for caching analysis results
 * for a method.
 */
public final class MethodData {
  private final HashMap<Object, Results> map = new HashMap<>();

  private final int access;

  private final String classType;

  private final String name;

  private final String signature;

  private IInstruction[] instructions;

  private ExceptionHandler[][] handlers;

  private int[] instructionsToBytecodes;

  private boolean hasChanged = false;

  /**
   * Create information for a method, with no exception handlers and a dummy mapping of instructions
   * to original bytecodes.
   *
   * @param access the access flags
   * @param classType the class in which the method is defined, in JVM type format (e.g.,
   *     Ljava/lang/Object;)
   * @param name the method name
   * @param signature the method signature, in JVM type format (e.g., (ILjava/lang/Object;)V)
   * @param instructions the instructions making up the method
   */
  public static MethodData makeWithDefaultHandlersAndInstToBytecodes(
      int access, String classType, String name, String signature, IInstruction[] instructions) {
    ExceptionHandler[][] handlers = new ExceptionHandler[instructions.length][];
    Arrays.fill(handlers, new ExceptionHandler[0]);
    int[] i2b = new int[instructions.length];
    Arrays.setAll(i2b, i -> i);
    return new MethodData(access, classType, name, signature, instructions, handlers, i2b);
  }

  /**
   * Gather the information for a method "from scratch".
   *
   * @param access the access flags
   * @param classType the class in which the method is defined, in JVM type format (e.g.,
   *     Ljava/lang/Object;)
   * @param name the method name
   * @param signature the method signature, in JVM type format (e.g., (ILjava/lang/Object;)V)
   * @param instructions the instructions making up the method
   * @param handlers a list of exception handlers for each instruction
   * @param instructionsToBytecodes a map stating, for each instruction, the offset of the original
   *     bytecode instruction(s) giving rise to this instruction
   */
  public MethodData(
      int access,
      String classType,
      String name,
      String signature,
      IInstruction[] instructions,
      ExceptionHandler[][] handlers,
      int[] instructionsToBytecodes) {
    this.classType = classType;
    this.access = access;
    this.name = name;
    this.signature = signature;
    this.instructions = instructions;
    this.handlers = handlers;
    this.instructionsToBytecodes = instructionsToBytecodes;

    if (instructions == null) {
      throw new IllegalArgumentException("Instruction array cannot be null");
    }
    if (handlers == null) {
      throw new IllegalArgumentException("Handler array cannot be null");
    }
    if (instructionsToBytecodes == null) {
      throw new IllegalArgumentException("InstructionToBytecodes array cannot be null");
    }
    if (instructions.length != handlers.length) {
      throw new IllegalArgumentException(
          "Handlers array must be the same length as the instructions");
    }
    if (instructions.length != instructionsToBytecodes.length) {
      throw new IllegalArgumentException(
          "Bytecode map array must be the same length as the instructions");
    }
  }

  /**
   * Gather the information for a method after it has been decoded.
   *
   * @param d the decoder which has decoded the method
   * @param access the access flags
   * @param classType the class in which the method is defined, in JVM type format (e.g.,
   *     Ljava/lang/Object;)
   * @param name the method name
   * @param signature the method signature, in JVM type format (e.g., (ILjava/lang/Object;)V)
   * @throws NullPointerException if d is null
   */
  public MethodData(Decoder d, int access, String classType, String name, String signature)
      throws NullPointerException {
    this(
        access,
        classType,
        name,
        signature,
        d.getInstructions(),
        d.getHandlers(),
        d.getInstructionsToBytecodes());
  }

  public void setHasChanged() {
    hasChanged = true;
  }

  /** @return the method signature, in JVM format */
  public String getSignature() {
    return signature;
  }

  /** @return the method name */
  public String getName() {
    return name;
  }

  /** @return the method access flags */
  public int getAccess() {
    return access;
  }

  /** @return the JVM type for the class defining the method (e.g., Ljava/lang/Object;) */
  public String getClassType() {
    return classType;
  }

  /** @return whether or not the method is static */
  public boolean getIsStatic() {
    return (access & Constants.ACC_STATIC) != 0;
  }

  /** @return whether or not the method is synchronized */
  public boolean getIsSynchronized() {
    return (access & Constants.ACC_SYNCHRONIZED) != 0;
  }

  /** @return the exception handler lists */
  public ExceptionHandler[][] getHandlers() {
    return handlers;
  }

  /** @return the instruction array */
  public IInstruction[] getInstructions() {
    return instructions;
  }

  /** @return the map from instructions to bytecode offsets */
  public int[] getInstructionsToBytecodes() {
    return instructionsToBytecodes;
  }

  /**
   * Annotation objects implement this Results interface. The Results interface is used to notify an
   * annotation that the method code has been updated.
   */
  public interface Results {
    /**
     * This method is called just before the code for a method changes. The existing instructions,
     * handlers, etc can be read from the current info.
     *
     * @param info the method data this annotation is attached to
     * @param newInstructions the instructions the method will change to
     * @param newHandlers the handler lists the method will change to
     * @param newInstructionMap the instructions-to-bytecodes map the method will change to
     * @return true to remove the object from the info set, for example because the annotation is
     *     now invalid
     */
    boolean notifyUpdate(
        MethodData info,
        IInstruction[] newInstructions,
        ExceptionHandler[][] newHandlers,
        int[] newInstructionMap);
  }

  /**
   * Get the annotation for the given key.
   *
   * @return the annotation or null if there isn't one
   */
  public Results getInfo(Object key) {
    return map.get(key);
  }

  /** Set the annotation for the given key. */
  public void putInfo(Object key, Results value) {
    map.put(key, value);
  }

  void update(
      IInstruction[] instructions,
      ExceptionHandler[][] handlers,
      int[] newInstructionMap,
      int[] instructionsToBytecodes) {
    for (Iterator<Object> i = map.keySet().iterator(); i.hasNext(); ) {
      Object key = i.next();
      Results r = map.get(key);
      if (r.notifyUpdate(this, instructions, handlers, newInstructionMap)) {
        i.remove();
      }
    }

    this.instructions = instructions;
    this.handlers = handlers;
    this.instructionsToBytecodes = instructionsToBytecodes;
    hasChanged = true;
  }

  /** @return true iff the code has been updated at least once */
  public boolean getHasChanged() {
    return hasChanged;
  }

  @Override
  public String toString() {
    return getClassType() + '.' + getName() + getSignature();
  }
}
