/*
 * 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 com.ibm.wala.shrikeCT.BootstrapMethodsReader.BootstrapMethod;
import com.ibm.wala.shrikeCT.ConstantPoolParser;

/**
 * A ConstantInstruction pushes some constant value onto the stack.
 */
public abstract class ConstantInstruction extends Instruction {

  public static class InvokeDynamicToken {
    private final BootstrapMethod bootstrapMethod;
    private final String name;
    private final String type;
    
    public InvokeDynamicToken(BootstrapMethod bootstrapMethod, String name, String type) {
      this.bootstrapMethod = bootstrapMethod;
      this.name = name;
      this.type = type;
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((bootstrapMethod == null) ? 0 : bootstrapMethod.hashCode());
      result = prime * result + ((name == null) ? 0 : name.hashCode());
      result = prime * result + ((type == null) ? 0 : type.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      InvokeDynamicToken other = (InvokeDynamicToken) obj;
      if (bootstrapMethod == null) {
        if (other.bootstrapMethod != null)
          return false;
      } else if (!bootstrapMethod.equals(other.bootstrapMethod))
        return false;
      if (name == null) {
        if (other.name != null)
          return false;
      } else if (!name.equals(other.name))
        return false;
      if (type == null) {
        if (other.type != null)
          return false;
      } else if (!type.equals(other.type))
        return false;
      return true;
    }
    
    
  }
  
  public static class ClassToken {
    private final String typeName;

    ClassToken(String typeName) {
      this.typeName = typeName;
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((typeName == null) ? 0 : typeName.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      ClassToken other = (ClassToken) obj;
      if (typeName == null) {
        if (other.typeName != null)
          return false;
      } else if (!typeName.equals(other.typeName))
        return false;
      return true;
    }

    public String getTypeName() {
      return typeName;
    }

  }

  public ConstantInstruction(short opcode) {
    super(opcode);
  }

  ConstantPoolReader getLazyConstantPool() {
    return null;
  }

  int getCPIndex() {
    return 0;
  }

  final static class ConstNull extends ConstantInstruction {
    protected ConstNull() {
      super(OP_aconst_null);
    }

    private final static ConstNull preallocated = new ConstNull();

    static ConstNull makeInternal() {
      return preallocated;
    }

    @Override
    public Object getValue() {
      return null;
    }

    @Override
    public String getType() {
      return TYPE_null;
    }
  }

  static class ConstInt extends ConstantInstruction {
    protected int value;

    private final static ConstInt[] preallocated = preallocate();

    protected ConstInt(short opcode, int value) {
      super(opcode);
      this.value = value;
    }

    private static ConstInt[] preallocate() {
      ConstInt[] r = new ConstInt[256];
      for (int i = 0; i < r.length; i++) {
        r[i] = new ConstInt(OP_bipush, i - 128);
      }
      for (int i = -1; i <= 5; i++) {
        r[i + 128] = new ConstInt((short) (i - (-1) + OP_iconst_m1), i);
      }
      return r;
    }

    static ConstInt makeInternal(int i) {
      if (((byte) i) == i) {
        return preallocated[i + 128];
      } else if (((short) i) == i) {
        return new ConstInt(OP_sipush, i);
      } else {
        return new ConstInt(OP_ldc_w, i);
      }
    }

    @Override
    final public Object getValue() {
      return Integer.valueOf(getIntValue());
    }

    @Override
    final public String getType() {
      return TYPE_int;
    }

    public int getIntValue() {
      return value;
    }
  }

  final static class LazyInt extends ConstInt {
    final private ConstantPoolReader cp;

    final private int index;

    private boolean isSet;

    protected LazyInt(short opcode, ConstantPoolReader cp, int index) {
      super(opcode, 0);
      this.cp = cp;
      this.index = index;
      this.isSet = false;
    }

    @Override
    public int getIntValue() {
      if (!isSet) {
        value = cp.getConstantPoolInteger(index);
        isSet = true;
      }
      return value;
    }

    @Override
    public ConstantPoolReader getLazyConstantPool() {
      return cp;
    }

    @Override
    public int getCPIndex() {
      return index;
    }
  }

  static class ConstLong extends ConstantInstruction {
    protected long value;

    private final static ConstLong[] preallocated = preallocate();

    protected ConstLong(short opcode, long value) {
      super(opcode);
      this.value = value;
    }

    private static ConstLong[] preallocate() {
      ConstLong[] r = { new ConstLong(OP_lconst_0, 0), new ConstLong(OP_lconst_1, 1) };
      return r;
    }

    static ConstLong makeInternal(long v) {
      if (v == 0 || v == 1) {
        return preallocated[(int) v];
      } else {
        return new ConstLong(OP_ldc2_w, v);
      }
    }

    @Override
    final public Object getValue() {
      return Long.valueOf(getLongValue());
    }

    @Override
    final public String getType() {
      return TYPE_long;
    }

    public long getLongValue() {
      return value;
    }
  }

  final static class LazyLong extends ConstLong {
    final private ConstantPoolReader cp;

    final private int index;

    private boolean isSet;

    protected LazyLong(short opcode, ConstantPoolReader cp, int index) {
      super(opcode, 0);
      this.cp = cp;
      this.index = index;
      this.isSet = false;
    }

    @Override
    public long getLongValue() {
      if (!isSet) {
        value = cp.getConstantPoolLong(index);
        isSet = true;
      }
      return value;
    }

    @Override
    public ConstantPoolReader getLazyConstantPool() {
      return cp;
    }

    @Override
    public int getCPIndex() {
      return index;
    }
  }

  static class ConstFloat extends ConstantInstruction {
    protected float value;

    private final static ConstFloat[] preallocated = preallocate();

    protected ConstFloat(short opcode, float value) {
      super(opcode);
      this.value = value;
    }

    private static ConstFloat[] preallocate() {
      ConstFloat[] r = { new ConstFloat(OP_fconst_0, 0), new ConstFloat(OP_fconst_1, 1), new ConstFloat(OP_fconst_2, 2) };
      return r;
    }

    static ConstFloat makeInternal(float v) {
      if (v == 0.0 || v == 1.0 || v == 2.0) {
        return preallocated[(int) v];
      } else {
        return new ConstFloat(OP_ldc_w, v);
      }
    }

    @Override
    final public Object getValue() {
      return Float.valueOf(getFloatValue());
    }

    @Override
    final public String getType() {
      return TYPE_float;
    }

    public float getFloatValue() {
      return value;
    }
  }

  final static class LazyFloat extends ConstFloat {
    final private ConstantPoolReader cp;

    final private int index;

    private boolean isSet;

    protected LazyFloat(short opcode, ConstantPoolReader cp, int index) {
      super(opcode, 0.0f);
      this.cp = cp;
      this.index = index;
      this.isSet = false;
    }

    @Override
    public float getFloatValue() {
      if (!isSet) {
        value = cp.getConstantPoolFloat(index);
        isSet = true;
      }
      return value;
    }

    @Override
    public ConstantPoolReader getLazyConstantPool() {
      return cp;
    }

    @Override
    public int getCPIndex() {
      return index;
    }
  }

  static class ConstDouble extends ConstantInstruction {
    protected double value;

    private final static ConstDouble[] preallocated = preallocate();

    protected ConstDouble(short opcode, double value) {
      super(opcode);
      this.value = value;
    }

    private static ConstDouble[] preallocate() {
      ConstDouble[] r = { new ConstDouble(OP_dconst_0, 0), new ConstDouble(OP_dconst_1, 1) };
      return r;
    }

    static ConstDouble makeInternal(double v) {
      if (v == 0.0 || v == 1.0) {
        return preallocated[(int) v];
      } else {
        return new ConstDouble(OP_ldc2_w, v);
      }
    }

    @Override
    final public Object getValue() {
      return Double.valueOf(getDoubleValue());
    }

    @Override
    final public String getType() {
      return TYPE_double;
    }

    public double getDoubleValue() {
      return value;
    }
  }

  final static class LazyDouble extends ConstDouble {
    final private ConstantPoolReader cp;

    final private int index;

    private boolean isSet;

    protected LazyDouble(short opcode, ConstantPoolReader cp, int index) {
      super(opcode, 0.0);
      this.cp = cp;
      this.index = index;
      this.isSet = false;
    }

    @Override
    public double getDoubleValue() {
      if (!isSet) {
        value = cp.getConstantPoolDouble(index);
        isSet = true;
      }
      return value;
    }

    @Override
    public ConstantPoolReader getLazyConstantPool() {
      return cp;
    }

    @Override
    public int getCPIndex() {
      return index;
    }
  }

  static class ConstString extends ConstantInstruction {
    protected String value;

    protected ConstString(short opcode, String value) {
      super(opcode);
      this.value = value;
    }

    static ConstString makeInternal(String v) {
      return new ConstString(OP_ldc_w, v);
    }

    @Override
    public Object getValue() {
      return value;
    }

    @Override
    final public String getType() {
      return TYPE_String;
    }
  }

  final static class LazyString extends ConstString {
    final private ConstantPoolReader cp;

    final private int index;

    protected LazyString(short opcode, ConstantPoolReader cp, int index) {
      super(opcode, null);
      this.cp = cp;
      this.index = index;
    }

    @Override
    public Object getValue() {
      if (value == null) {
        value = cp.getConstantPoolString(index);
      }
      return value;
    }

    @Override
    public ConstantPoolReader getLazyConstantPool() {
      return cp;
    }

    @Override
    public int getCPIndex() {
      return index;
    }
  }

  static class ConstClass extends ConstantInstruction {
    protected String typeName;

    protected ConstClass(short opcode, String typeName) {
      super(opcode);
      this.typeName = typeName;
    }

    static ConstClass makeInternal(String v) {
      return new ConstClass(OP_ldc_w, v);
    }

    @Override
    public Object getValue() {
      return new ClassToken(typeName);
    }

    @Override
    final public String getType() {
      return TYPE_Class;
    }

    @Override
    public boolean isPEI() {
      // load of a class constant may trigger a ClassNotFoundException
      return true;
    }
   
  }

  final static class LazyClass extends ConstClass {
    final private ConstantPoolReader cp;

    final private int index;

    protected LazyClass(short opcode, ConstantPoolReader cp, int index) {
      super(opcode, null);
      this.cp = cp;
      this.index = index;
    }

    @Override
    public Object getValue() {
      if (typeName == null) {
        typeName = cp.getConstantPoolClassType(index);
      }
      return new ClassToken(typeName);
    }

    @Override
    public ConstantPoolReader getLazyConstantPool() {
      return cp;
    }

    @Override
    public int getCPIndex() {
      return index;
    }
  }

  static class ConstMethodType extends ConstantInstruction {
    protected String descriptor;
    
    ConstMethodType(short opcode, String descriptor) {
      super(opcode);
      this.descriptor = descriptor;
    }

    @Override
    public Object getValue() {
      return descriptor;
    }

    @Override
    public String getType() {
      return TYPE_MethodType;
    }
  }
  
  static class LazyMethodType extends ConstMethodType {
    final private ConstantPoolReader cp;

    final private int index;

    LazyMethodType(short opcode, ConstantPoolReader cp, int index) {
      super(opcode, null);
      this.cp = cp;
      this.index = index;
    }
    
    @Override
    public Object getValue() {
      if (descriptor == null) {
        descriptor = cp.getConstantPoolMethodType(index);
      }
      return descriptor;
    }

    @Override
    public ConstantPoolReader getLazyConstantPool() {
      return cp;
    }

    @Override
    public int getCPIndex() {
      return index;
    }
  }
  
  static class ConstMethodHandle extends ConstantInstruction {
    protected Object value;
    
    public ConstMethodHandle(short opcode, Object value) {
      super(opcode);
      this.value = value;
    }

    @Override
    public Object getValue() {
      return value;
    }

    @Override
    public String getType() {
      return TYPE_MethodHandle;
    }
    
  }
  
  static class LazyMethodHandle extends ConstMethodHandle {
    final private ConstantPoolReader cp;

    final private int index;

    LazyMethodHandle(short opcode, ConstantPoolReader cp, int index) {
      super(opcode, null);
      this.cp = cp;
      this.index = index;
    }
    
    @Override
    public Object getValue() {
      if (value == null) {
        String className = cp.getConstantPoolHandleClassType(getCPIndex());
        String eltName = cp.getConstantPoolHandleName(getCPIndex());
        String eltDesc = cp.getConstantPoolHandleType(getCPIndex());
        byte kind = cp.getConstantPoolHandleKind(getCPIndex());
        value = new ConstantPoolParser.ReferenceToken(kind, className, eltName, eltDesc);
      }
      return value;
    }

    @Override
    public ConstantPoolReader getLazyConstantPool() {
      return cp;
    }

    @Override
    public int getCPIndex() {
      return index;
    }
  }

  static class ConstInvokeDynamic extends ConstantInstruction {
    protected Object value;
    
    public ConstInvokeDynamic(short opcode, Object value) {
      super(opcode);
      this.value = value;
    }

    @Override
    public Object getValue() {
      return value;
    }

    @Override
    public String getType() {
      return null;
    }
    
  }

  static class LazyInvokeDynamic extends ConstMethodHandle {
    final private ConstantPoolReader cp;

    final private int index;

    LazyInvokeDynamic(short opcode, ConstantPoolReader cp, int index) {
      super(opcode, null);
      this.cp = cp;
      this.index = index;
    }
    
    @Override
    public Object getValue() {
      if (value == null) {
        BootstrapMethod bootstrap = cp.getConstantPoolDynamicBootstrap(index); 
        String name = cp.getConstantPoolDynamicName(index);
        String type = cp.getConstantPoolDynamicType(index);
        value = new InvokeDynamicToken(bootstrap, name, type);
      }
      
      return value;
    }

    @Override
    public ConstantPoolReader getLazyConstantPool() {
      return cp;
    }

    @Override
    public int getCPIndex() {
      return index;
    }
  }

  /**
   * @return the constant value pushed: an Integer, a Long, a Float, a Double, a String, or null
   */
  public abstract Object getValue();

  /**
   * @return the type of the value pushed
   */
  public abstract String getType();

  public static ConstantInstruction make(String type, Object constant) throws IllegalArgumentException {
    if (type == null && constant != null) {
      throw new IllegalArgumentException("(type == null) and (constant != null)");
    }
    if (constant == null) {
      return ConstNull.makeInternal();
    } else if (type.equals(TYPE_String)) {
      return makeString((String) constant);
    } else if (type.equals(TYPE_Class)) {
      return makeClass((String) constant);
    } else {
      try {
        switch (Util.getTypeIndex(type)) {
        case TYPE_int_index:
          return make(((Number) constant).intValue());
        case TYPE_long_index:
          return make(((Number) constant).longValue());
        case TYPE_float_index:
          return make(((Number) constant).floatValue());
        case TYPE_double_index:
          return make(((Number) constant).doubleValue());
        default:
          throw new IllegalArgumentException("Invalid type for constant: " + type);
        }
      } catch (ClassCastException e) {
        throw new IllegalArgumentException(e);
      }
    }
  }

  public static ConstantInstruction make(int i) {
    return ConstInt.makeInternal(i);
  }

  public static ConstantInstruction make(long l) {
    return ConstLong.makeInternal(l);
  }

  public static ConstantInstruction make(float f) {
    return ConstFloat.makeInternal(f);
  }

  public static ConstantInstruction make(double d) {
    return ConstDouble.makeInternal(d);
  }

  public static ConstantInstruction makeString(String s) {
    return s == null ? (ConstantInstruction) ConstNull.makeInternal() : (ConstantInstruction) ConstString.makeInternal(s);
  }

  public static ConstantInstruction makeClass(String s) {
    return ConstClass.makeInternal(s);
  }

  public static ConstantInstruction make(ConstantPoolReader cp, int index) {
    switch (cp.getConstantPoolItemType(index)) {
    case CONSTANT_Integer:
      return new LazyInt(OP_ldc_w, cp, index);
    case CONSTANT_Long:
      return new LazyLong(OP_ldc2_w, cp, index);
    case CONSTANT_Float:
      return new LazyFloat(OP_ldc_w, cp, index);
    case CONSTANT_Double:
      return new LazyDouble(OP_ldc2_w, cp, index);
    case CONSTANT_String:
      return new LazyString(OP_ldc_w, cp, index);
    case CONSTANT_Class:
      return new LazyClass(OP_ldc_w, cp, index);
    case CONSTANT_MethodHandle:
      return new LazyMethodHandle(OP_ldc_w, cp, index);
    case CONSTANT_MethodType:
      return new LazyMethodType(OP_ldc_w, cp, index);
    case CONSTANT_InvokeDynamic:
      return new LazyInvokeDynamic(OP_ldc_w, cp, index);
    default:
      return null;
    }
  }

  @Override
  final public boolean equals(Object o) {
    if (o instanceof ConstantInstruction) {
      ConstantInstruction i = (ConstantInstruction) o;
      if (!i.getType().equals(getType())) {
        return false;
      }
      if (i.getValue() == null) {
        if (getValue() == null) {
          return true;
        } else {
          return false;
        }
      } else {
        if (getValue() == null) {
          return false;
        } else {
          return i.getValue().equals(getValue());
        }
      }
    } else {
      return false;
    }
  }

  @Override
  final public String getPushedType(String[] types) {
    return getType();
  }

  @Override
  final public byte getPushedWordSize() {
    return Util.getWordSize(getType());
  }

  @Override
  final public int hashCode() {
    int v = getValue() == null ? 0 : getValue().hashCode();
    return getType().hashCode() + 14411 * v;
  }

  @Override
  final public void visit(IInstruction.Visitor v) throws NullPointerException {
    v.visitConstant(this);
  }

  private static String quote(Object o) {
    if (o instanceof String) {
      String s = (String) o;
      StringBuilder buf = new StringBuilder("\"");
      int len = s.length();
      for (int i = 0; i < len; i++) {
        char ch = s.charAt(i);
        switch (ch) {
        case '"':
          buf.append('\\');
          buf.append(ch);
          break;
        case '\n':
          buf.append("\\\n");
          break;
        case '\t':
          buf.append("\\\t");
          break;
        default:
          buf.append(ch);
        }
      }
      buf.append('\"');
      return buf.toString();
    } else if (o == null) {
      return "null";
    } else {
      return o.toString();
    }
  }

  @Override
  final public String toString() {
    return "Constant(" + getType() + ',' + quote(getValue()) + ')';
  }

  @Override
  public boolean isPEI() {
    return false;
  }
}
