/*
 * Copyright (c) 2002 - 2014 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 java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class InvokeDynamicInstruction extends Instruction implements IInvokeInstruction {
  protected BootstrapMethod bootstrap;
  protected String methodName;
  protected String methodType;

  public InvokeDynamicInstruction(
      short opcode, BootstrapMethod bootstrap, String methodName, String methodType) {
    super(opcode);
    this.bootstrap = bootstrap;
    this.methodName = methodName;
    this.methodType = methodType;
  }

  ConstantPoolReader getLazyConstantPool() {
    return null;
  }

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

  @Override
  public Dispatch getInvocationCode() {
    int invokeType = getBootstrap().invokeType();
    switch (invokeType) {
      case 5:
        return Dispatch.VIRTUAL;
      case 6:
        return Dispatch.STATIC;
      case 7:
        return Dispatch.SPECIAL;
      case 9:
        return Dispatch.INTERFACE;
      default:
        throw new Error("unexpected dynamic invoke type " + invokeType);
    }
  }

  @Override
  public final int getPoppedCount() {
    return (getInvocationCode().equals(Dispatch.STATIC) ? 0 : 1)
        + Util.getParamsCount(getMethodSignature());
  }

  @Override
  public final String getPushedType(String[] types) {
    String t = Util.getReturnType(getMethodSignature());
    if (t.equals(Constants.TYPE_void)) {
      return null;
    } else {
      return t;
    }
  }

  @Override
  public final byte getPushedWordSize() {
    String t = getMethodSignature();
    int index = t.lastIndexOf(')');
    return Util.getWordSize(t, index + 1);
  }

  public BootstrapMethod getBootstrap() {
    return bootstrap;
  }

  @Override
  public String getMethodSignature() {
    return methodType;
  }

  @Override
  public String getMethodName() {
    return methodName;
  }

  @Override
  public String getClassType() {
    return 'L' + getBootstrap().methodClass();
  }

  @Override
  public void visit(Visitor v) {
    v.visitInvoke(this);
  }

  @Override
  public String toString() {
    return "InvokeDynamic [" + getBootstrap() + "] " + getMethodName() + getMethodSignature();
  }

  static final class Lazy extends InvokeDynamicInstruction {
    private final ConstantPoolReader cp;

    private final int index;

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

    int getCPIndex() {
      return index;
    }

    @Override
    public BootstrapMethod getBootstrap() {
      if (bootstrap == null) {
        bootstrap = cp.getConstantPoolDynamicBootstrap(index);
      }
      return bootstrap;
    }

    @Override
    public String getMethodName() {
      if (methodName == null) {
        methodName = cp.getConstantPoolDynamicName(index);
      }
      return methodName;
    }

    @Override
    public String getMethodSignature() {
      if (methodType == null) {
        methodType = cp.getConstantPoolDynamicType(index);
      }
      return methodType;
    }

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

  public CallSite bootstrap(Class<?> cl)
      throws ClassNotFoundException, NoSuchMethodException, SecurityException,
          IllegalAccessException, IllegalArgumentException, InvocationTargetException,
          NoSuchFieldException {
    ClassLoader classLoader = cl.getClassLoader();
    ClassLoader bootstrapCL = classLoader;

    Class<?> bootstrapClass =
        Class.forName(getBootstrap().methodClass().replace('/', '.'), false, bootstrapCL);
    MethodType bt = makeMethodType(bootstrapCL, bootstrap.methodType());
    Method bootstrap =
        bootstrapClass.getMethod(
            this.bootstrap.methodName(),
            bt.parameterList().toArray(new Class[bt.parameterCount()]));
    Object[] args = new Object[bt.parameterCount()];

    Lookup myLookup = MethodHandles.lookup().in(cl);
    Field impl_lookup =
        Lookup.class.getDeclaredField("IMPL_LOOKUP"); // get the required field via reflections
    impl_lookup.setAccessible(true); // set it accessible
    Lookup lutrusted =
        (Lookup)
            impl_lookup.get(
                myLookup); // get the value of IMPL_LOOKUP from the Lookup instance and save it in a
    // new Lookup object
    args[0] = lutrusted;
    args[1] = getMethodName();
    args[2] = makeMethodType(classLoader, getMethodSignature());
    for (int i = 3; i < bt.parameterCount(); i++) {
      args[i] = getBootstrap().callArgument(bootstrapCL, i - 3);
    }

    bootstrap.setAccessible(true);

    System.err.println(cl + " : " + bootstrap);

    return (CallSite) bootstrap.invoke(null, args);
  }

  public static MethodType makeMethodType(ClassLoader classLoader, String descriptor)
      throws ClassNotFoundException {
    String returnType = Util.makeClass(Util.getReturnType(descriptor));
    Class<?> returnClass = Class.forName(returnType, false, classLoader);
    String[] paramTypes = Util.getParamsTypes(null, descriptor);
    Class<?>[] paramClasses = new Class[paramTypes.length];
    for (int i = 0; i < paramTypes.length; i++) {
      paramClasses[i] = Class.forName(Util.makeClass(paramTypes[i]), false, classLoader);
    }
    MethodType mt = MethodType.methodType(returnClass, paramClasses);
    return mt;
  }

  static InvokeDynamicInstruction make(ConstantPoolReader cp, int index, int mode) {
    if (mode != OP_invokedynamic) {
      throw new IllegalArgumentException("Unknown mode: " + mode);
    }
    return new Lazy((short) mode, cp, index);
  }
}
