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

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;

import com.ibm.wala.shrikeBT.IInvokeInstruction.Dispatch;

/**
 * This class contains miscellaneous useful functions.
 * 
 * In the documentation below, we refer to a 'Java class name'. These are formatted according to the rules for Class.forName() and
 * Class.getName(). A Java class name must use '$' to separate inner class names from their containing class. There is no way to for
 * Shrike to disambiguate 'A.B' otherwise.
 */
public final class Util {
  private Util() {
    // prevent instantiation
  }

  /**
   * @return the JVM "stack word size" for the given JVM type
   * @throws IllegalArgumentException if s is null
   */
  public static byte getWordSize(String s) {
    if (s == null || s.length() == 0) {
      throw new IllegalArgumentException("invalid s: " + s);
    }
    return getWordSize(s, 0);
  }

  /**
   * @return the JVM "stack word size" for the given JVM type, looking at index 'index'
   */
  static byte getWordSize(String s, int index) {
    switch (s.charAt(index)) {
    case 'V':
      return 0;
    case 'J':
    case 'D':
      return 2;
    default:
      return 1;
    }
  }

  /**
   * Computes the character length of the internal JVM type given by s.substring(i).
   */
  private static int getTypeLength(String s, int i) {
    switch (s.charAt(i)) {
    case 'L':
      return s.indexOf(';', i) - i + 1;
    case '[':
      return getTypeLength(s, i + 1) + 1;
    default:
      return 1;
    }
  }

  /**
   * Compute the total number of JVM "stack words" occupied by the method parameters for method signature "type". Any "this"
   * parameter is not included.
   * 
   * @throws IllegalArgumentException if type is null
   */
  public static int getParamsWordSize(String type) throws IllegalArgumentException {
    if (type == null) {
      throw new IllegalArgumentException("type is null");
    }
    int index = 1;
    int count = 0;

    if (type.indexOf(')', 1) < 0) {
      throw new IllegalArgumentException("Invalid method descriptor (missing ')'): " + type);
    }
    while (type.charAt(index) != ')') {
      count += getWordSize(type, index);
      index += getTypeLength(type, index);
    }
    return count;
  }

  /**
   * Convert a fully-qualified Java class name ('.' separated) into an internal JVM type name ('/' separated, starting with 'L' and
   * ending with ';').
   * 
   * @throws IllegalArgumentException if c is null
   */
  public static String makeType(String c) {
    if (c == null) {
      throw new IllegalArgumentException("c is null");
    }
    if (c.startsWith("[")) {
      return c.replace('.', '/');
    } else if (!c.endsWith(";")) {
      return "L" + c.replace('.', '/') + ";";
    } else {
      return c;
    }
  }

  /**
   * Convert a fully-qualified Java type name (either primitive or class name, '.' separated) into an internal JVM type name (one
   * letter for primitive and '/' separated, starting with 'L' and ending with ';' for class name).
   */
  public static String makeTypeAll(String c) {
    String alias = typeAliases.get(c);
    if (alias != null) {
      return alias;
    } else {
      return makeType(c);
    }
  }

  /**
   * Convert a JVM type name back into a Java class name.
   * 
   * @throws IllegalArgumentException if t is null
   */
  public static String makeClass(String t) throws IllegalArgumentException {
    if (t == null) {
      throw new IllegalArgumentException("t is null");
    }
    if (t.startsWith("[")) {
      return t;
    } else if (!t.startsWith("L")) {
      throw new IllegalArgumentException(t + " is not a valid class type");
    } else {
      return t.substring(1, t.length() - 1).replace('/', '.');
    }
  }

  /**
   * Convert a JVM type name (either for a primitive or a class name) into a Java type name.
   */
  static String makeClassAll(String t) {
    String alias = classAliases.get(t);
    if (alias != null) {
      return alias;
    } else {
      return makeClass(t);
    }
  }

  final private static HashMap<String, String> classAliases;

  final private static HashMap<String, String> typeAliases;

  private static void addAlias(String c, String t) {
    typeAliases.put(c, t);
    classAliases.put(t, c);
  }

  static {
    typeAliases = new HashMap<String, String>();
    classAliases = new HashMap<String, String>();
    addAlias("void", "V");
    addAlias("int", "I");
    addAlias("long", "J");
    addAlias("float", "F");
    addAlias("double", "D");
    addAlias("byte", "B");
    addAlias("char", "C");
    addAlias("short", "S");
    addAlias("boolean", "Z");
  }

  /**
   * Compute the JVM type name for an actual Java class. Names such as "int", "void", etc are also converted to their JVM type
   * names.
   * 
   * @throws IllegalArgumentException if c is null
   */
  public static String makeType(Class<?> c) {
    if (c == null) {
      throw new IllegalArgumentException("c is null");
    }
    String name = c.getName();
    String alias = typeAliases.get(name);
    if (alias != null) {
      return alias;
    } else {
      return makeType(name);
    }
  }

  /**
   * Compute the number of parameters given by method signature "type". Any "this" parameter is not included.
   * 
   * @throws IllegalArgumentException if type == null
   */
  static int getParamsCount(String type) throws IllegalArgumentException {
    if (type == null || type.length() < 2) {
      throw new IllegalArgumentException("invalid type: " + type);
    }
    int index = 1;
    int count = 0;

    try {
      while (type.charAt(index) != ')') {
        count++;
        index += getTypeLength(type, index);
      }
      return count;
    } catch (StringIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid type: " + type);
    }
  }

  /**
   * Extract the types of the parameters given by method signature "type".
   * 
   * @param thisClassType null if the method is static, otherwise the type of "this"
   * @return an array of the parameter types in order, including "this" as the first parameter if thisClassType was non-null
   * @throws IllegalArgumentException if type == null
   */
  public static String[] getParamsTypes(String thisClassType, String type) throws IllegalArgumentException {
    if (type == null) {
      throw new IllegalArgumentException("type == null");
    }
    int count = thisClassType != null ? 1 : 0;
    String[] r = new String[getParamsCount(type) + count];
    int index = 1;

    if (thisClassType != null) {
      r[0] = thisClassType;
    }

    while (type.charAt(index) != ')') {
      int len = getTypeLength(type, index);

      r[count] = type.substring(index, index + len);
      count++;
      index += len;
    }
    return r;
  }

  /**
   * Compute the types of the local variables on entry to a method. Similar to "getParamsTypes" except null array entries are
   * inserted to account for unused local variables because of 2-word parameter values.
   * 
   * @throws IllegalArgumentException if type == null
   */
  public static String[] getParamsTypesInLocals(String thisClassType, String type) throws IllegalArgumentException {
    if (type == null) {
      throw new IllegalArgumentException("type == null");
    }
    int count = thisClassType != null ? 1 : 0;
    String[] r = new String[getParamsWordSize(type) + count];
    int index = 1;

    if (thisClassType != null) {
      r[0] = thisClassType;
    }

    while (type.charAt(index) != ')') {
      int len = getTypeLength(type, index);
      String t = getStackType(type.substring(index, index + len));

      r[count] = t;
      count += getWordSize(t);
      index += len;
    }
    return r;
  }

  /**
   * Compute the promoted type that the JVM uses to manipulate values of type "t" on its working stack.
   * 
   * @throws IllegalArgumentException if t is null
   */
  public static String getStackType(String t) {
    if (t == null || t.length() < 1) {
      throw new IllegalArgumentException("invalid t: " + t);
    }
    switch (t.charAt(0)) {
    case 'Z':
    case 'C':
    case 'B':
    case 'S':
      return "I";
    default:
      return t;
    }
  }

  /**
   * Compute the type "array of t".
   */
  public static String makeArray(String t) {
    return ("[" + t).intern();
  }

  /**
   * @return true iff t is an array type
   */
  public static boolean isArrayType(String t) {
    if (t == null || t.length() == 0) {
      return false;
    } else {
      switch (t.charAt(0)) {
      case '[':
        return true;
      default:
        return false;
      }
    }
  }

  /**
   * @return true iff t is a primitive type
   */
  public static boolean isPrimitiveType(String t) {
    if (t == null || t.length() == 0) {
      return false;
    } else {
      switch (t.charAt(0)) {
      case 'L':
      case '[':
        return false;
      default:
        return true;
      }
    }
  }

  /**
   * Get the return type from a method signature.
   * 
   * @throws IllegalArgumentException if s is null
   */
  public static String getReturnType(String s) {
    if (s == null) {
      throw new IllegalArgumentException("s is null");
    }
    return s.substring(s.lastIndexOf(')') + 1);
  }

  /**
   * General "print an error" routine.
   */
  public static void error(String s) {
    System.err.println(s);
    (new Error("Stack Trace")).printStackTrace();
  }

  /**
   * Given a Java Method, compute the VM-style type signature.
   * 
   * @throws IllegalArgumentException if params == null
   */
  public static String computeSignature(Class<?>[] params, Class<?> result) throws IllegalArgumentException {
    if (params == null) {
      throw new IllegalArgumentException("params == null");
    }
    StringBuffer buf = new StringBuffer();
    buf.append("(");
    for (int i = 0; i < params.length; i++) {
      buf.append(makeType(params[i]));
    }
    buf.append(")");
    buf.append(makeType(result));
    return buf.toString();
  }

  /**
   * Make an Instruction which loads the value of a field, given its name and Java Class. The field type is obtained using
   * reflection.
   * 
   * @throws IllegalArgumentException if c is null
   */
  public static GetInstruction makeGet(Class<?> c, String name) {
    if (c == null) {
      throw new IllegalArgumentException("c is null");
    }
    try {
      Field f = c.getField(name);
      return GetInstruction.make(makeType(f.getType()), makeType(c), name, (f.getModifiers() & Constants.ACC_STATIC) != 0);
    } catch (SecurityException e) {
      throw new IllegalArgumentException(e.getMessage());
    } catch (NoSuchFieldException e) {
      throw new IllegalArgumentException(e.getMessage());
    }
  }

  /**
   * Make an Instruction which stores the value of a field, given its name and Java Class. The field type is obtained using
   * reflection.
   * 
   * @throws IllegalArgumentException if c is null
   */
  public static PutInstruction makePut(Class<?> c, String name) {
    if (c == null) {
      throw new IllegalArgumentException("c is null");
    }
    try {
      Field f = c.getField(name);
      return PutInstruction.make(makeType(f.getType()), makeType(c), name, (f.getModifiers() & Constants.ACC_STATIC) != 0);
    } catch (SecurityException e) {
      throw new IllegalArgumentException(e.getMessage());
    } catch (NoSuchFieldException e) {
      throw new IllegalArgumentException(e.getMessage());
    }
  }

  private static String makeName(String name, Class<?>[] paramTypes) {
    if (paramTypes == null) {
      return name;
    } else {
      StringBuffer buf = new StringBuffer(name);
      buf.append("(");
      for (int i = 0; i < paramTypes.length; i++) {
        if (i > 0) {
          buf.append(",");
        }
        buf.append(paramTypes[i].getName());
      }
      buf.append(")");
      return buf.toString();
    }
  }

  public static Method findMethod(Class<?> c, String name) {
    return findMethod(c, name, null);
  }

  public static Method findMethod(Class<?> c, String name, Class<?>[] paramTypes) {
    if (c == null) {
      throw new IllegalArgumentException("c is null");
    }
    Method[] methods = c.getMethods();
    Method result = null;
    for (int i = 0; i < methods.length; i++) {
      Method m = methods[i];
      if (m.getName().equals(name) && (paramTypes == null || Arrays.equals(m.getParameterTypes(), paramTypes))) {
        if (result != null) {
          throw new IllegalArgumentException("Method " + makeName(name, paramTypes) + " is ambiguous in class " + c);
        }
        result = m;
      }
    }
    return result;
  }

  /**
   * Make an Instruction which calls a method, given its name, Java Class, and a list of parameter classes to use for overload
   * resolution. Method information is obtained using reflection.
   * 
   * @throws IllegalArgumentException if name is null
   */
  public static InvokeInstruction makeInvoke(Class<?> c, String name, Class<?>[] paramTypes) {
    if (name == null) {
      throw new IllegalArgumentException("name is null");
    }
    InvokeInstruction result = null;

    if (name.equals("<init>")) {
      Constructor<?>[] cs = c.getConstructors();
      for (int i = 0; i < cs.length; i++) {
        Constructor<?> con = cs[i];
        if (paramTypes == null || Arrays.equals(con.getParameterTypes(), paramTypes)) {
          if (result != null) {
            throw new IllegalArgumentException("Constructor " + makeName(name, paramTypes) + " is ambiguous in class " + c);
          }
          result = InvokeInstruction
              .make(computeSignature(con.getParameterTypes(), Void.TYPE), makeType(c), name, Dispatch.SPECIAL);
        }
      }
    } else {
      Method m = findMethod(c, name, paramTypes);
      if (m != null) {
        Dispatch opcode = Dispatch.VIRTUAL;
        if ((m.getModifiers() & Constants.ACC_STATIC) != 0) {
          opcode = Dispatch.STATIC;
        } else if (m.getDeclaringClass().isInterface()) {
          opcode = Dispatch.INTERFACE;
        }
        result = InvokeInstruction.make(computeSignature(m.getParameterTypes(), m.getReturnType()), makeType(c), name, opcode);
      }
    }

    if (result == null) {
      throw new IllegalArgumentException("Method " + makeName(name, paramTypes) + " is not present in class " + c);
    } else {
      return result;
    }
  }

  /**
   * Make an Instruction which calls a method, given its name and Java Class. Method information is obtained using reflection. If
   * there is more than one method with the given name, an error will be thrown.
   * 
   * @throws IllegalArgumentException if name is null
   */
  public static InvokeInstruction makeInvoke(Class<?> c, String name) {
    return makeInvoke(c, name, null);
  }

  /**
   * Compute the type index constant (Constants.TYPE_...) from the JVM type. Returns -1 if the type is not one of the predefined
   * constants.
   */
  static int getTypeIndex(String t) {
    if (t == null) {
      return -1;
    } else {
      int len = t.length();
      if (len < 1) {
        return -1;
      } else {
        char ch = t.charAt(0);
        if (ch < typeIndices.length) {
          int r = typeIndices[ch];
          if (r != 4) {
            if (len > 1) {
              return -1;
            } else {
              return r;
            }
          } else {
            return r;
          }
        } else {
          return -1;
        }
      }
    }
  }

  private static final byte[] typeIndices = makeTypeIndices();

  private static byte[] makeTypeIndices() {
    byte[] r = new byte[128];
    for (int i = 0; i < r.length; i++) {
      r[i] = -1;
    }
    r['I'] = 0;
    r['J'] = 1;
    r['F'] = 2;
    r['D'] = 3;
    r['L'] = 4;
    r['['] = 4;
    r['B'] = 5;
    r['C'] = 6;
    r['S'] = 7;
    r['Z'] = 8;

    return r;
  }

  public static void readFully(InputStream s, byte[] bytes) throws IllegalArgumentException, IllegalArgumentException, IOException {
    if (s == null) {
      throw new IllegalArgumentException("s == null");
    }
    if (bytes == null) {
      throw new IllegalArgumentException("bytes == null");
    }
    int offset = 0;
    while (offset < bytes.length) {
      int r = s.read(bytes, offset, bytes.length - offset);
      if (r < 0) {
        throw new IOException("Class truncated");
      }
      offset += r;
    }
  }

  public static byte[] readFully(InputStream s) throws IOException {
    if (s == null) {
      throw new IllegalArgumentException("s is null");
    }
    byte[] bytes = new byte[s.available()];
    readFully(s, bytes);
    int b = s.read();
    if (b < 0) {
      return bytes;
    } else {
      byte[] big = new byte[bytes.length * 2 + 1];
      System.arraycopy(bytes, 0, big, 0, bytes.length);
      big[bytes.length] = (byte) b;
      int offset = bytes.length + 1;
      do {
        if (big.length == offset) {
          // grow array by factor of 2
          bytes = new byte[offset * 2];
          System.arraycopy(big, 0, bytes, 0, offset);
          big = bytes;
        }
        int r = s.read(big, offset, big.length - offset);
        if (r < 0) {
          bytes = new byte[offset];
          System.arraycopy(big, 0, bytes, 0, offset);
          return bytes;
        }
        offset += r;
      } while (true);
    }
  }
}