// ASM: a very small and fast Java bytecode manipulation framework
// Copyright (c) 2000-2011 INRIA, France Telecom
// 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. Neither the name of the copyright holders nor the names of its
//    contributors may 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 org.apache.tapestry5.internal.plastic.asm.tree.analysis;

import java.util.List;

import org.apache.tapestry5.internal.plastic.asm.Opcodes;
import org.apache.tapestry5.internal.plastic.asm.Type;
import org.apache.tapestry5.internal.plastic.asm.tree.AbstractInsnNode;
import org.apache.tapestry5.internal.plastic.asm.tree.FieldInsnNode;
import org.apache.tapestry5.internal.plastic.asm.tree.InvokeDynamicInsnNode;
import org.apache.tapestry5.internal.plastic.asm.tree.MethodInsnNode;

/**
 * An extended {@link BasicInterpreter} that checks that bytecode instructions are correctly used.
 *
 * @author Eric Bruneton
 * @author Bing Ran
 */
public class BasicVerifier extends BasicInterpreter {

  /**
   * Constructs a new {@link BasicVerifier} for the latest ASM API version. <i>Subclasses must not
   * use this constructor</i>. Instead, they must use the {@link #BasicVerifier(int)} version.
   */
  public BasicVerifier() {
    super(/* latest api = */ ASM9);
    if (getClass() != BasicVerifier.class) {
      throw new IllegalStateException();
    }
  }

  /**
   * Constructs a new {@link BasicVerifier}.
   *
   * @param api the ASM API version supported by this interpreter. Must be one of the {@code
   *     ASM}<i>x</i> values in {@link Opcodes}.
   */
  protected BasicVerifier(final int api) {
    super(api);
  }

  @Override
  public BasicValue copyOperation(final AbstractInsnNode insn, final BasicValue value)
      throws AnalyzerException {
    Value expected;
    switch (insn.getOpcode()) {
      case ILOAD:
      case ISTORE:
        expected = BasicValue.INT_VALUE;
        break;
      case FLOAD:
      case FSTORE:
        expected = BasicValue.FLOAT_VALUE;
        break;
      case LLOAD:
      case LSTORE:
        expected = BasicValue.LONG_VALUE;
        break;
      case DLOAD:
      case DSTORE:
        expected = BasicValue.DOUBLE_VALUE;
        break;
      case ALOAD:
        if (!value.isReference()) {
          throw new AnalyzerException(insn, null, "an object reference", value);
        }
        return value;
      case ASTORE:
        if (!value.isReference() && !BasicValue.RETURNADDRESS_VALUE.equals(value)) {
          throw new AnalyzerException(insn, null, "an object reference or a return address", value);
        }
        return value;
      default:
        return value;
    }
    if (!expected.equals(value)) {
      throw new AnalyzerException(insn, null, expected, value);
    }
    return value;
  }

  @Override
  public BasicValue unaryOperation(final AbstractInsnNode insn, final BasicValue value)
      throws AnalyzerException {
    BasicValue expected;
    switch (insn.getOpcode()) {
      case INEG:
      case IINC:
      case I2F:
      case I2L:
      case I2D:
      case I2B:
      case I2C:
      case I2S:
      case IFEQ:
      case IFNE:
      case IFLT:
      case IFGE:
      case IFGT:
      case IFLE:
      case TABLESWITCH:
      case LOOKUPSWITCH:
      case IRETURN:
      case NEWARRAY:
      case ANEWARRAY:
        expected = BasicValue.INT_VALUE;
        break;
      case FNEG:
      case F2I:
      case F2L:
      case F2D:
      case FRETURN:
        expected = BasicValue.FLOAT_VALUE;
        break;
      case LNEG:
      case L2I:
      case L2F:
      case L2D:
      case LRETURN:
        expected = BasicValue.LONG_VALUE;
        break;
      case DNEG:
      case D2I:
      case D2F:
      case D2L:
      case DRETURN:
        expected = BasicValue.DOUBLE_VALUE;
        break;
      case GETFIELD:
        expected = newValue(Type.getObjectType(((FieldInsnNode) insn).owner));
        break;
      case ARRAYLENGTH:
        if (!isArrayValue(value)) {
          throw new AnalyzerException(insn, null, "an array reference", value);
        }
        return super.unaryOperation(insn, value);
      case CHECKCAST:
      case ARETURN:
      case ATHROW:
      case INSTANCEOF:
      case MONITORENTER:
      case MONITOREXIT:
      case IFNULL:
      case IFNONNULL:
        if (!value.isReference()) {
          throw new AnalyzerException(insn, null, "an object reference", value);
        }
        return super.unaryOperation(insn, value);
      case PUTSTATIC:
        expected = newValue(Type.getType(((FieldInsnNode) insn).desc));
        break;
      default:
        throw new AssertionError();
    }
    if (!isSubTypeOf(value, expected)) {
      throw new AnalyzerException(insn, null, expected, value);
    }
    return super.unaryOperation(insn, value);
  }

  @Override
  public BasicValue binaryOperation(
      final AbstractInsnNode insn, final BasicValue value1, final BasicValue value2)
      throws AnalyzerException {
    BasicValue expected1;
    BasicValue expected2;
    switch (insn.getOpcode()) {
      case IALOAD:
        expected1 = newValue(Type.getType("[I"));
        expected2 = BasicValue.INT_VALUE;
        break;
      case BALOAD:
        if (isSubTypeOf(value1, newValue(Type.getType("[Z")))) {
          expected1 = newValue(Type.getType("[Z"));
        } else {
          expected1 = newValue(Type.getType("[B"));
        }
        expected2 = BasicValue.INT_VALUE;
        break;
      case CALOAD:
        expected1 = newValue(Type.getType("[C"));
        expected2 = BasicValue.INT_VALUE;
        break;
      case SALOAD:
        expected1 = newValue(Type.getType("[S"));
        expected2 = BasicValue.INT_VALUE;
        break;
      case LALOAD:
        expected1 = newValue(Type.getType("[J"));
        expected2 = BasicValue.INT_VALUE;
        break;
      case FALOAD:
        expected1 = newValue(Type.getType("[F"));
        expected2 = BasicValue.INT_VALUE;
        break;
      case DALOAD:
        expected1 = newValue(Type.getType("[D"));
        expected2 = BasicValue.INT_VALUE;
        break;
      case AALOAD:
        expected1 = newValue(Type.getType("[Ljava/lang/Object;"));
        expected2 = BasicValue.INT_VALUE;
        break;
      case IADD:
      case ISUB:
      case IMUL:
      case IDIV:
      case IREM:
      case ISHL:
      case ISHR:
      case IUSHR:
      case IAND:
      case IOR:
      case IXOR:
      case IF_ICMPEQ:
      case IF_ICMPNE:
      case IF_ICMPLT:
      case IF_ICMPGE:
      case IF_ICMPGT:
      case IF_ICMPLE:
        expected1 = BasicValue.INT_VALUE;
        expected2 = BasicValue.INT_VALUE;
        break;
      case FADD:
      case FSUB:
      case FMUL:
      case FDIV:
      case FREM:
      case FCMPL:
      case FCMPG:
        expected1 = BasicValue.FLOAT_VALUE;
        expected2 = BasicValue.FLOAT_VALUE;
        break;
      case LADD:
      case LSUB:
      case LMUL:
      case LDIV:
      case LREM:
      case LAND:
      case LOR:
      case LXOR:
      case LCMP:
        expected1 = BasicValue.LONG_VALUE;
        expected2 = BasicValue.LONG_VALUE;
        break;
      case LSHL:
      case LSHR:
      case LUSHR:
        expected1 = BasicValue.LONG_VALUE;
        expected2 = BasicValue.INT_VALUE;
        break;
      case DADD:
      case DSUB:
      case DMUL:
      case DDIV:
      case DREM:
      case DCMPL:
      case DCMPG:
        expected1 = BasicValue.DOUBLE_VALUE;
        expected2 = BasicValue.DOUBLE_VALUE;
        break;
      case IF_ACMPEQ:
      case IF_ACMPNE:
        expected1 = BasicValue.REFERENCE_VALUE;
        expected2 = BasicValue.REFERENCE_VALUE;
        break;
      case PUTFIELD:
        FieldInsnNode fieldInsn = (FieldInsnNode) insn;
        expected1 = newValue(Type.getObjectType(fieldInsn.owner));
        expected2 = newValue(Type.getType(fieldInsn.desc));
        break;
      default:
        throw new AssertionError();
    }
    if (!isSubTypeOf(value1, expected1)) {
      throw new AnalyzerException(insn, "First argument", expected1, value1);
    } else if (!isSubTypeOf(value2, expected2)) {
      throw new AnalyzerException(insn, "Second argument", expected2, value2);
    }
    if (insn.getOpcode() == AALOAD) {
      return getElementValue(value1);
    } else {
      return super.binaryOperation(insn, value1, value2);
    }
  }

  @Override
  public BasicValue ternaryOperation(
      final AbstractInsnNode insn,
      final BasicValue value1,
      final BasicValue value2,
      final BasicValue value3)
      throws AnalyzerException {
    BasicValue expected1;
    BasicValue expected3;
    switch (insn.getOpcode()) {
      case IASTORE:
        expected1 = newValue(Type.getType("[I"));
        expected3 = BasicValue.INT_VALUE;
        break;
      case BASTORE:
        if (isSubTypeOf(value1, newValue(Type.getType("[Z")))) {
          expected1 = newValue(Type.getType("[Z"));
        } else {
          expected1 = newValue(Type.getType("[B"));
        }
        expected3 = BasicValue.INT_VALUE;
        break;
      case CASTORE:
        expected1 = newValue(Type.getType("[C"));
        expected3 = BasicValue.INT_VALUE;
        break;
      case SASTORE:
        expected1 = newValue(Type.getType("[S"));
        expected3 = BasicValue.INT_VALUE;
        break;
      case LASTORE:
        expected1 = newValue(Type.getType("[J"));
        expected3 = BasicValue.LONG_VALUE;
        break;
      case FASTORE:
        expected1 = newValue(Type.getType("[F"));
        expected3 = BasicValue.FLOAT_VALUE;
        break;
      case DASTORE:
        expected1 = newValue(Type.getType("[D"));
        expected3 = BasicValue.DOUBLE_VALUE;
        break;
      case AASTORE:
        expected1 = value1;
        expected3 = BasicValue.REFERENCE_VALUE;
        break;
      default:
        throw new AssertionError();
    }
    if (!isSubTypeOf(value1, expected1)) {
      throw new AnalyzerException(
          insn, "First argument", "a " + expected1 + " array reference", value1);
    } else if (!BasicValue.INT_VALUE.equals(value2)) {
      throw new AnalyzerException(insn, "Second argument", BasicValue.INT_VALUE, value2);
    } else if (!isSubTypeOf(value3, expected3)) {
      throw new AnalyzerException(insn, "Third argument", expected3, value3);
    }
    return null;
  }

  @Override
  public BasicValue naryOperation(
      final AbstractInsnNode insn, final List<? extends BasicValue> values)
      throws AnalyzerException {
    int opcode = insn.getOpcode();
    if (opcode == MULTIANEWARRAY) {
      for (BasicValue value : values) {
        if (!BasicValue.INT_VALUE.equals(value)) {
          throw new AnalyzerException(insn, null, BasicValue.INT_VALUE, value);
        }
      }
    } else {
      int i = 0;
      int j = 0;
      if (opcode != INVOKESTATIC && opcode != INVOKEDYNAMIC) {
        Type owner = Type.getObjectType(((MethodInsnNode) insn).owner);
        if (!isSubTypeOf(values.get(i++), newValue(owner))) {
          throw new AnalyzerException(insn, "Method owner", newValue(owner), values.get(0));
        }
      }
      String methodDescriptor =
          (opcode == INVOKEDYNAMIC)
              ? ((InvokeDynamicInsnNode) insn).desc
              : ((MethodInsnNode) insn).desc;
      Type[] args = Type.getArgumentTypes(methodDescriptor);
      while (i < values.size()) {
        BasicValue expected = newValue(args[j++]);
        BasicValue actual = values.get(i++);
        if (!isSubTypeOf(actual, expected)) {
          throw new AnalyzerException(insn, "Argument " + j, expected, actual);
        }
      }
    }
    return super.naryOperation(insn, values);
  }

  @Override
  public void returnOperation(
      final AbstractInsnNode insn, final BasicValue value, final BasicValue expected)
      throws AnalyzerException {
    if (!isSubTypeOf(value, expected)) {
      throw new AnalyzerException(insn, "Incompatible return type", expected, value);
    }
  }

  /**
   * Returns whether the given value corresponds to an array reference.
   *
   * @param value a value.
   * @return whether 'value' corresponds to an array reference.
   */
  protected boolean isArrayValue(final BasicValue value) {
    return value.isReference();
  }

  /**
   * Returns the value corresponding to the type of the elements of the given array reference value.
   *
   * @param objectArrayValue a value corresponding to array of object (or array) references.
   * @return the value corresponding to the type of the elements of 'objectArrayValue'.
   * @throws AnalyzerException if objectArrayValue does not correspond to an array type.
   */
  protected BasicValue getElementValue(final BasicValue objectArrayValue) throws AnalyzerException {
    return BasicValue.REFERENCE_VALUE;
  }

  /**
   * Returns whether the type corresponding to the first argument is a subtype of the type
   * corresponding to the second argument.
   *
   * @param value a value.
   * @param expected another value.
   * @return whether the type corresponding to 'value' is a subtype of the type corresponding to
   *     'expected'.
   */
  protected boolean isSubTypeOf(final BasicValue value, final BasicValue expected) {
    return value.equals(expected);
  }
}
