/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.turbine.bytecode;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.FormatMethod;
import com.google.turbine.bytecode.ClassFile.AnnotationInfo;
import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue;
import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.ConstTurbineAnnotationValue;
import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.ConstTurbineClassValue;
import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.ConstValue;
import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.EnumConstValue;
import com.google.turbine.bytecode.ClassFile.MethodInfo.ParameterInfo;
import com.google.turbine.bytecode.ClassFile.ModuleInfo;
import com.google.turbine.bytecode.ClassFile.ModuleInfo.ExportInfo;
import com.google.turbine.bytecode.ClassFile.ModuleInfo.OpenInfo;
import com.google.turbine.bytecode.ClassFile.ModuleInfo.ProvideInfo;
import com.google.turbine.bytecode.ClassFile.ModuleInfo.RequireInfo;
import com.google.turbine.bytecode.ClassFile.ModuleInfo.UseInfo;
import com.google.turbine.model.Const;
import com.google.turbine.model.TurbineFlag;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;

/** A JVMS ยง4 class file reader. */
public class ClassReader {

  /** Reads the given bytes into an {@link ClassFile}. */
  @Deprecated
  public static ClassFile read(byte[] bytes) {
    return read(null, bytes);
  }

  /** Reads the given bytes into an {@link ClassFile}. */
  public static ClassFile read(@Nullable String path, byte[] bytes) {
    return new ClassReader(path, bytes).read();
  }

  @Nullable private final String path;
  private final ByteReader reader;

  private ClassReader(@Nullable String path, byte[] bytes) {
    this.path = path;
    this.reader = new ByteReader(bytes, 0);
  }

  @FormatMethod
  @CheckReturnValue
  Error error(String format, Object... args) {
    StringBuilder sb = new StringBuilder();
    if (path != null) {
      sb.append(path).append(": ");
    }
    sb.append(String.format(format, args));
    return new AssertionError(sb.toString());
  }

  private ClassFile read() {
    int magic = reader.u4();
    if (magic != 0xcafebabe) {
      throw error("bad magic: 0x%x", magic);
    }
    int minorVersion = reader.u2();
    int majorVersion = reader.u2();
    if (majorVersion < 45) {
      throw error("bad version: %d.%d", majorVersion, minorVersion);
    }
    ConstantPoolReader constantPool = ConstantPoolReader.readConstantPool(reader);
    int accessFlags = reader.u2();
    String thisClass = constantPool.classInfo(reader.u2());
    int superClassIndex = reader.u2();
    String superClass;
    if (superClassIndex != 0) {
      superClass = constantPool.classInfo(superClassIndex);
    } else {
      superClass = null;
    }
    int interfacesCount = reader.u2();
    List<String> interfaces = new ArrayList<>();
    for (int i = 0; i < interfacesCount; i++) {
      interfaces.add(constantPool.classInfo(reader.u2()));
    }

    List<ClassFile.FieldInfo> fieldinfos = readFields(constantPool);

    List<ClassFile.MethodInfo> methodinfos = readMethods(constantPool);

    String signature = null;
    List<ClassFile.InnerClass> innerclasses = ImmutableList.of();
    ImmutableList.Builder<ClassFile.AnnotationInfo> annotations = ImmutableList.builder();
    ClassFile.ModuleInfo module = null;
    int attributesCount = reader.u2();
    for (int j = 0; j < attributesCount; j++) {
      int attributeNameIndex = reader.u2();
      String name = constantPool.utf8(attributeNameIndex);
      switch (name) {
        case "RuntimeInvisibleAnnotations":
        case "RuntimeVisibleAnnotations":
          readAnnotations(annotations, constantPool);
          break;
        case "Signature":
          signature = readSignature(constantPool);
          break;
        case "InnerClasses":
          innerclasses = readInnerClasses(constantPool, thisClass);
          break;
        case "Module":
          module = readModule(constantPool);
          break;
        default:
          reader.skip(reader.u4());
          break;
      }
    }

    return new ClassFile(
        accessFlags,
        thisClass,
        signature,
        superClass,
        interfaces,
        methodinfos,
        fieldinfos,
        annotations.build(),
        innerclasses,
        ImmutableList.of(),
        module);
  }

  /** Reads a JVMS 4.7.9 Signature attribute. */
  private String readSignature(ConstantPoolReader constantPool) {
    String signature;
    reader.u4(); // length
    signature = constantPool.utf8(reader.u2());
    return signature;
  }

  /** Reads JVMS 4.7.6 InnerClasses attributes. */
  private List<ClassFile.InnerClass> readInnerClasses(
      ConstantPoolReader constantPool, String thisClass) {
    reader.u4(); // length
    int numberOfClasses = reader.u2();
    List<ClassFile.InnerClass> innerclasses = new ArrayList<>();
    for (int i = 0; i < numberOfClasses; i++) {
      int innerClassInfoIndex = reader.u2();
      String innerClass = constantPool.classInfo(innerClassInfoIndex);
      int outerClassInfoIndex = reader.u2();
      String outerClass =
          outerClassInfoIndex != 0 ? constantPool.classInfo(outerClassInfoIndex) : null;
      int innerNameIndex = reader.u2();
      String innerName = innerNameIndex != 0 ? constantPool.utf8(innerNameIndex) : null;
      int innerClassAccessFlags = reader.u2();
      if (innerName != null && (thisClass.equals(innerClass) || thisClass.equals(outerClass))) {
        innerclasses.add(
            new ClassFile.InnerClass(innerClass, outerClass, innerName, innerClassAccessFlags));
      }
    }
    return innerclasses;
  }

  /**
   * Processes a JVMS 4.7.16 RuntimeVisibleAnnotations attribute.
   *
   * <p>The only annotations that affect header compilation are {@link @Retention} and
   * {@link @Target} on annotation declarations.
   */
  private void readAnnotations(
      ImmutableList.Builder<ClassFile.AnnotationInfo> annotations,
      ConstantPoolReader constantPool) {
    reader.u4(); // length
    int numAnnotations = reader.u2();
    for (int n = 0; n < numAnnotations; n++) {
      annotations.add(readAnnotation(constantPool));
    }
  }

  /** Processes a JVMS 4.7.18 RuntimeVisibleParameterAnnotations attribute */
  public void readParameterAnnotations(
      List<ImmutableList.Builder<AnnotationInfo>> annotations, ConstantPoolReader constantPool) {
    reader.u4(); // length
    int numParameters = reader.u1();
    while (annotations.size() < numParameters) {
      annotations.add(ImmutableList.builder());
    }
    for (int i = 0; i < numParameters; i++) {
      int numAnnotations = reader.u2();
      for (int n = 0; n < numAnnotations; n++) {
        annotations.get(i).add(readAnnotation(constantPool));
      }
    }
  }

  /** Processes a JVMS 4.7.24 MethodParameters attribute. */
  private void readMethodParameters(
      ImmutableList.Builder<ParameterInfo> parameters, ConstantPoolReader constantPool) {
    reader.u4(); // length
    int numParameters = reader.u1();
    for (int i = 0; i < numParameters; i++) {
      String name = constantPool.utf8(reader.u2());
      int access = reader.u2();
      if ((access & (TurbineFlag.ACC_SYNTHETIC | TurbineFlag.ACC_MANDATED)) != 0) {
        // ExecutableElement#getParameters doesn't expect synthetic or mandated
        // parameters
        continue;
      }
      parameters.add(new ParameterInfo(name, access));
    }
  }

  /** Processes a JVMS 4.7.25 Module attribute. */
  private ModuleInfo readModule(ConstantPoolReader constantPool) {
    reader.u4(); // length
    String name = constantPool.moduleInfo(reader.u2());
    int flags = reader.u2();
    int versionIndex = reader.u2();
    String version = (versionIndex != 0) ? constantPool.utf8(versionIndex) : null;

    ImmutableList.Builder<ClassFile.ModuleInfo.RequireInfo> requires = ImmutableList.builder();
    int numRequires = reader.u2();
    for (int i = 0; i < numRequires; i++) {
      String requiresModule = constantPool.moduleInfo(reader.u2());
      int requiresFlags = reader.u2();
      int requiresVersionIndex = reader.u2();
      String requiresVersion =
          (requiresVersionIndex != 0) ? constantPool.utf8(requiresVersionIndex) : null;
      requires.add(new RequireInfo(requiresModule, requiresFlags, requiresVersion));
    }

    ImmutableList.Builder<ClassFile.ModuleInfo.ExportInfo> exports = ImmutableList.builder();
    int numExports = reader.u2();
    for (int i = 0; i < numExports; i++) {
      String exportsModule = constantPool.packageInfo(reader.u2());
      int exportsFlags = reader.u2();
      int numExportsTo = reader.u2();
      ImmutableList.Builder<String> exportsToModules = ImmutableList.builder();
      for (int n = 0; n < numExportsTo; n++) {
        String exportsToModule = constantPool.moduleInfo(reader.u2());
        exportsToModules.add(exportsToModule);
      }
      exports.add(new ExportInfo(exportsModule, exportsFlags, exportsToModules.build()));
    }

    ImmutableList.Builder<ClassFile.ModuleInfo.OpenInfo> opens = ImmutableList.builder();
    int numOpens = reader.u2();
    for (int i = 0; i < numOpens; i++) {
      String opensModule = constantPool.packageInfo(reader.u2());
      int opensFlags = reader.u2();
      int numOpensTo = reader.u2();
      ImmutableList.Builder<String> opensToModules = ImmutableList.builder();
      for (int n = 0; n < numOpensTo; n++) {
        String opensToModule = constantPool.moduleInfo(reader.u2());
        opensToModules.add(opensToModule);
      }
      opens.add(new OpenInfo(opensModule, opensFlags, opensToModules.build()));
    }

    ImmutableList.Builder<ClassFile.ModuleInfo.UseInfo> uses = ImmutableList.builder();
    int numUses = reader.u2();
    for (int i = 0; i < numUses; i++) {
      String use = constantPool.classInfo(reader.u2());
      uses.add(new UseInfo(use));
    }

    ImmutableList.Builder<ClassFile.ModuleInfo.ProvideInfo> provides = ImmutableList.builder();
    int numProvides = reader.u2();
    for (int i = 0; i < numProvides; i++) {
      String typeName = constantPool.classInfo(reader.u2());
      int numProvidesWith = reader.u2();
      ImmutableList.Builder<String> impls = ImmutableList.builder();
      for (int n = 0; n < numProvidesWith; n++) {
        String impl = constantPool.classInfo(reader.u2());
        impls.add(impl);
      }
      provides.add(new ProvideInfo(typeName, impls.build()));
    }

    return new ClassFile.ModuleInfo(
        name,
        flags,
        version,
        requires.build(),
        exports.build(),
        opens.build(),
        uses.build(),
        provides.build());
  }

  /**
   * Extracts an {@link @Retention} or {@link ElementType} {@link ClassFile.AnnotationInfo}, or else
   * skips over the annotation.
   */
  private ClassFile.AnnotationInfo readAnnotation(ConstantPoolReader constantPool) {
    int typeIndex = reader.u2();
    String annotationType = constantPool.utf8(typeIndex);
    int numElementValuePairs = reader.u2();
    ImmutableMap.Builder<String, ElementValue> values = ImmutableMap.builder();
    for (int e = 0; e < numElementValuePairs; e++) {
      int elementNameIndex = reader.u2();
      String key = constantPool.utf8(elementNameIndex);
      ElementValue value = readElementValue(constantPool);
      values.put(key, value);
    }
    return new ClassFile.AnnotationInfo(
        annotationType,
        // The runtimeVisible bit in AnnotationInfo is only used during lowering; earlier passes
        // read the meta-annotations.
        /* runtimeVisible= */ false,
        values.build());
  }

  private ElementValue readElementValue(ConstantPoolReader constantPool) {
    int tag = reader.u1();
    switch (tag) {
      case 'B':
        return new ConstValue(readConst(constantPool).asByte());
      case 'C':
        return new ConstValue(readConst(constantPool).asChar());
      case 'S':
        return new ConstValue(readConst(constantPool).asShort());
      case 'D':
      case 'F':
      case 'I':
      case 'J':
      case 's':
        return new ConstValue(readConst(constantPool));
      case 'Z':
        {
          Const.Value value = readConst(constantPool);
          // boolean constants are encoded as integers
          return new ConstValue(new Const.BooleanValue(value.asInteger().value() != 0));
        }
      case 'e':
        {
          int typeNameIndex = reader.u2();
          int constNameIndex = reader.u2();
          String typeName = constantPool.utf8(typeNameIndex);
          String constName = constantPool.utf8(constNameIndex);
          return new EnumConstValue(typeName, constName);
        }
      case 'c':
        {
          int classInfoIndex = reader.u2();
          String className = constantPool.utf8(classInfoIndex);
          return new ConstTurbineClassValue(className);
        }
      case '@':
        return new ConstTurbineAnnotationValue(readAnnotation(constantPool));
      case '[':
        {
          int numValues = reader.u2();
          ImmutableList.Builder<ElementValue> elements = ImmutableList.builder();
          for (int i = 0; i < numValues; i++) {
            elements.add(readElementValue(constantPool));
          }
          return new ElementValue.ArrayValue(elements.build());
        }
      default: // fall out
    }
    throw new AssertionError(String.format("bad tag value %c", tag));
  }

  private Const.Value readConst(ConstantPoolReader constantPool) {
    int constValueIndex = reader.u2();
    return constantPool.constant(constValueIndex);
  }

  /** Reads JVMS 4.6 method_infos. */
  private List<ClassFile.MethodInfo> readMethods(ConstantPoolReader constantPool) {
    int methodsCount = reader.u2();
    List<ClassFile.MethodInfo> methods = new ArrayList<>();
    for (int i = 0; i < methodsCount; i++) {
      int accessFlags = reader.u2();
      int nameIndex = reader.u2();
      String name = constantPool.utf8(nameIndex);
      int descriptorIndex = reader.u2();
      String desc = constantPool.utf8(descriptorIndex);
      int attributesCount = reader.u2();
      String signature = null;
      ImmutableList<String> exceptions = ImmutableList.of();
      ImmutableList.Builder<ClassFile.AnnotationInfo> annotations = ImmutableList.builder();
      List<ImmutableList.Builder<ClassFile.AnnotationInfo>> parameterAnnotationsBuilder =
          new ArrayList<>();
      ImmutableList.Builder<ParameterInfo> parameters = ImmutableList.builder();
      ElementValue defaultValue = null;
      for (int j = 0; j < attributesCount; j++) {
        String attributeName = constantPool.utf8(reader.u2());
        switch (attributeName) {
          case "Exceptions":
            exceptions = readExceptions(constantPool);
            break;
          case "Signature":
            signature = readSignature(constantPool);
            break;
          case "AnnotationDefault":
            reader.u4(); // length
            defaultValue = readElementValue(constantPool);
            break;
          case "RuntimeInvisibleAnnotations":
          case "RuntimeVisibleAnnotations":
            readAnnotations(annotations, constantPool);
            break;
          case "RuntimeInvisibleParameterAnnotations":
          case "RuntimeVisibleParameterAnnotations":
            readParameterAnnotations(parameterAnnotationsBuilder, constantPool);
            break;
          case "MethodParameters":
            readMethodParameters(parameters, constantPool);
            break;
          default:
            reader.skip(reader.u4());
            break;
        }
      }
      ImmutableList.Builder<ImmutableList<AnnotationInfo>> parameterAnnotations =
          ImmutableList.builder();
      for (ImmutableList.Builder<AnnotationInfo> x : parameterAnnotationsBuilder) {
        parameterAnnotations.add(x.build());
      }
      if ((accessFlags & (TurbineFlag.ACC_BRIDGE | TurbineFlag.ACC_SYNTHETIC)) != 0) {
        // javac doesn't enter synthetic members for reasons 'lost to history', so we don't either
        continue;
      }
      methods.add(
          new ClassFile.MethodInfo(
              accessFlags,
              name,
              desc,
              signature,
              exceptions,
              defaultValue,
              annotations.build(),
              parameterAnnotations.build(),
              /* typeAnnotations= */ ImmutableList.of(),
              parameters.build()));
    }
    return methods;
  }

  /** Reads an Exceptions attribute. */
  private ImmutableList<String> readExceptions(ConstantPoolReader constantPool) {
    ImmutableList.Builder<String> exceptions = ImmutableList.builder();
    reader.u4(); // length
    int numberOfExceptions = reader.u2();
    for (int exceptionIndex = 0; exceptionIndex < numberOfExceptions; exceptionIndex++) {
      exceptions.add(constantPool.classInfo(reader.u2()));
    }
    return exceptions.build();
  }

  /** Reads JVMS 4.5 field_infos. */
  private List<ClassFile.FieldInfo> readFields(ConstantPoolReader constantPool) {
    int fieldsCount = reader.u2();
    List<ClassFile.FieldInfo> fields = new ArrayList<>();
    for (int i = 0; i < fieldsCount; i++) {
      int accessFlags = reader.u2();
      int nameIndex = reader.u2();
      String name = constantPool.utf8(nameIndex);
      int descriptorIndex = reader.u2();
      String desc = constantPool.utf8(descriptorIndex);
      int attributesCount = reader.u2();
      Const.Value value = null;
      ImmutableList.Builder<ClassFile.AnnotationInfo> annotations = ImmutableList.builder();
      String signature = null;
      for (int j = 0; j < attributesCount; j++) {
        String attributeName = constantPool.utf8(reader.u2());
        switch (attributeName) {
          case "ConstantValue":
            reader.u4(); // length
            value = constantPool.constant(reader.u2());
            break;
          case "RuntimeInvisibleAnnotations":
          case "RuntimeVisibleAnnotations":
            readAnnotations(annotations, constantPool);
            break;
          case "Signature":
            signature = readSignature(constantPool);
            break;
          default:
            reader.skip(reader.u4());
            break;
        }
      }
      fields.add(
          new ClassFile.FieldInfo(
              accessFlags,
              name,
              desc,
              signature,
              value,
              annotations.build(),
              /* typeAnnotations= */ ImmutableList.of()));
    }
    return fields;
  }
}