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

import com.ibm.wala.shrike.shrikeCT.ClassReader.AttrIterator;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;

public class BootstrapMethodsReader extends AttributeReader {

  public interface BootstrapMethod {

    String LAMBDA_METAFACTORY_CLASS = "java/lang/invoke/LambdaMetafactory";
    String BOOTSTRAP_METHOD_NAME = "metafactory";
    String BOOTSTRAP_METHOD_TYPE =
        "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;";

    byte invokeType();

    String methodClass();

    String methodName();

    String methodType();

    int callArgumentCount();

    Object callArgument(ClassLoader cl, int i);

    int callArgumentIndex(int i);

    int callArgumentKind(int i);

    ConstantPoolParser getCP();

    int getIndexInClassFile();

    /**
     * Is this the bootstrap method used for compiling Java lambdas?
     *
     * @return {@code true} if the method is {@link
     *     java.lang.invoke.LambdaMetafactory#metafactory(java.lang.invoke.MethodHandles.Lookup,
     *     String, java.lang.invoke.MethodType, java.lang.invoke.MethodType,
     *     java.lang.invoke.MethodHandle, java.lang.invoke.MethodType)}
     */
    default boolean isBootstrapForJavaLambdas() {
      return methodClass().equals(LAMBDA_METAFACTORY_CLASS)
          && methodName().equals(BOOTSTRAP_METHOD_NAME)
          && methodType().equals(BOOTSTRAP_METHOD_TYPE);
    }
  }

  private BootstrapMethod entries[];

  protected BootstrapMethodsReader(AttrIterator attr) throws InvalidClassFileException {
    super(attr, "BootstrapMethods");
    readBootstrapEntries();
  }

  private void readBootstrapEntries() throws InvalidClassFileException {
    final ConstantPoolParser cp = cr.getCP();

    entries = new BootstrapMethod[cr.getUShort(attr + 6)];
    int base = 8;
    for (int i = 0; i < entries.length; i++) {
      final int methodHandleOffset = cr.getUShort(attr + base);
      final int argsBase = attr + base + 4;
      final int index = i;

      final int argumentCount = cr.getUShort(attr + base + 2);
      entries[i] =
          new BootstrapMethod() {
            private final byte invokeType = cp.getCPHandleKind(methodHandleOffset);
            private final String methodClass = cp.getCPHandleClass(methodHandleOffset);
            private final String methodName = cp.getCPHandleName(methodHandleOffset);
            private final String methodType = cp.getCPHandleType(methodHandleOffset);

            @Override
            public String toString() {
              return methodClass + ':' + methodName + methodType;
            }

            @Override
            public byte invokeType() {
              return invokeType;
            }

            @Override
            public String methodClass() {
              return methodClass;
            }

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

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

            @Override
            public int callArgumentCount() {
              return argumentCount;
            }

            @Override
            public int callArgumentKind(int i) {
              return cp.getItemType(callArgumentIndex(i));
            }

            @Override
            public int callArgumentIndex(int i) {
              assert 0 <= i && i < argumentCount;
              int index = argsBase + (2 * i);
              return cr.getUShort(index);
            }

            @Override
            public Object callArgument(ClassLoader cl, int i) {
              try {
                int index = callArgumentIndex(i);
                int t = callArgumentKind(i);
                switch (t) {
                  case ClassConstants.CONSTANT_Utf8:
                    return cp.getCPUtf8(index);
                  case ClassConstants.CONSTANT_Class:
                    return cp.getCPClass(index);
                  case ClassConstants.CONSTANT_String:
                    return cp.getCPString(index);
                  case ClassConstants.CONSTANT_Integer:
                    return cp.getCPInt(index);
                  case ClassConstants.CONSTANT_Float:
                    return cp.getCPFloat(index);
                  case ClassConstants.CONSTANT_Double:
                    return cp.getCPDouble(index);
                  case ClassConstants.CONSTANT_Long:
                    return cp.getCPLong(index);
                  case ClassConstants.CONSTANT_MethodHandle:
                    String className = cp.getCPHandleClass(index);
                    String eltName = cp.getCPHandleName(index);
                    String eltDesc = cp.getCPHandleType(index);
                    MethodType type = MethodType.fromMethodDescriptorString(eltDesc, cl);
                    Class<?> cls = Class.forName(className.replace('/', '.'), false, cl);
                    Method m =
                        cls.getDeclaredMethod(
                            eltName,
                            type.parameterList().toArray(new Class[type.parameterCount()]));
                    Lookup lk = MethodHandles.lookup().in(cls);
                    m.setAccessible(true);
                    return lk.unreflect(m);
                  case ClassConstants.CONSTANT_MethodType:
                    return MethodType.fromMethodDescriptorString(cp.getCPMethodType(index), cl);
                  default:
                    assert false : "invalid type " + t;
                }
              } catch (IllegalArgumentException
                  | IllegalAccessException
                  | SecurityException
                  | NoSuchMethodException
                  | ClassNotFoundException
                  | InvalidClassFileException e) {
                throw new RuntimeException(e);
              }
              return null;
            }

            @Override
            public ConstantPoolParser getCP() {
              return cp;
            }

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

      base += (argumentCount * 2) + 4;
    }
  }

  public int count() {
    return entries.length;
  }

  public BootstrapMethod getEntry(int i) {
    return entries[i];
  }
}
