// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.psi.impl.compiled;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.impl.cache.TypeInfo;
import com.intellij.psi.impl.cache.TypeInfo.RefTypeInfo;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.jetbrains.org.objectweb.asm.*;

import java.io.IOException;
import java.util.*;

import static com.intellij.util.BitUtil.isSet;

/**
 * Information retrieved during the first pass of a class file parsing
 */
class FirstPassData implements SignatureParsing.TypeInfoProvider {
  private static final Logger LOG = Logger.getInstance(FirstPassData.class);

  private abstract static class ClassEntry {}
  
  private static final class RegularClassEntry extends ClassEntry {
    final @NotNull String myName;

    private RegularClassEntry(@NotNull String name) { myName = name; }
  }
  
  private static final class StringInnerClassEntry extends ClassEntry {
    final @NotNull String myOuterName;
    final @NotNull String myInnerName;
    final boolean myStatic;

    private StringInnerClassEntry(@NotNull String outerName, @NotNull String innerName, boolean aStatic) {
      myOuterName = outerName;
      myInnerName = innerName;
      myStatic = aStatic;
    }
  }

  private static final class TypeInfoInnerClassEntry extends ClassEntry {
    private final RefTypeInfo myOuterType;
    private final String myInnerName;
    private final boolean myStatic;

    private TypeInfoInnerClassEntry(@NotNull RefTypeInfo outerType, @NotNull String innerName, boolean aStatic) {
      myOuterType = outerType;
      myInnerName = innerName;
      myStatic = aStatic;
    }
  }

  private static final FirstPassData NO_DATA = new FirstPassData(Collections.emptyMap(), "", null, Collections.emptySet(), true, false);
  private final @NotNull Map<String, ClassEntry> myMap;
  private final @NotNull Set<ObjectMethod> mySyntheticMethods;
  private final @NotNull String myTopLevelName;
  private final @Nullable String myVarArgRecordComponent;
  private final boolean myTrustInnerClasses;
  private final boolean mySealed;

  private FirstPassData(@NotNull Map<String, ClassEntry> map,
                        @NotNull String topLevelName,
                        @Nullable String component,
                        @NotNull Set<ObjectMethod> syntheticMethods,
                        boolean trustInnerClasses, boolean sealed) {
    myMap = map;
    myTopLevelName = topLevelName;
    myVarArgRecordComponent = component;
    mySyntheticMethods = syntheticMethods;
    myTrustInnerClasses = trustInnerClasses;
    mySealed = sealed;
  }

  /**
   * @param componentName record component name
   * @return true if given component is var-arg
   */
  boolean isVarArgComponent(@NotNull String componentName) {
    return componentName.equals(myVarArgRecordComponent);
  }

  /**
   * @return true if class is sealed (has at least one permitted subclass)
   */
  boolean isSealed() {
    return mySealed;
  }

  /**
   * @param methodName method name
   * @param methodDesc method descriptor
   * @return true if given method is a synthetic method of the record (autogenerated equals, hashCode or toString)
   */
  boolean isSyntheticRecordMethod(@NotNull String methodName, @NotNull String methodDesc) {
    return !mySyntheticMethods.isEmpty() && mySyntheticMethods.contains(ObjectMethod.from(methodName, methodDesc));
  }

  /**
   * @param jvmNames array of JVM type names (e.g., throws-list, implements-list)
   * @return list of TypeInfo objects that correspond to given types. GUESSING_MAPPER is not used.
   */
  @Contract("null -> null; !null -> !null")
  @Unmodifiable
  List<TypeInfo> createTypes(String @Nullable [] jvmNames) {
    return jvmNames == null ? null :
           ContainerUtil.map(jvmNames, jvmName -> toTypeInfo(jvmName, false));
  }

  @Override
  public boolean isKnownStatic(@NotNull String jvmClassName) {
    ClassEntry entry = myMap.get(jvmClassName);
    return entry instanceof StringInnerClassEntry && ((StringInnerClassEntry)entry).myStatic ||
           entry instanceof TypeInfoInnerClassEntry && ((TypeInfoInnerClassEntry)entry).myStatic;
  }

  /**
   * @param jvmName JVM class name like java/util/Map$Entry
   * @return Java class name like java.util.Map.Entry
   */
  @Override
  public @NotNull RefTypeInfo toTypeInfo(@NotNull String jvmName) {
    return toTypeInfo(jvmName, true);
  }

  /**
   * @param jvmName JVM class name like java/util/Map$Entry
   * @param useGuesser if true, {@link StubBuildingVisitor#GUESSING_PROVIDER} will be used in case if the entry was absent in
   *                   InnerClasses table.
   * @return Java class name like java.util.Map.Entry
   */
  @NotNull RefTypeInfo toTypeInfo(@NotNull String jvmName, boolean useGuesser) {
    ClassEntry p = myMap.get(jvmName);
    if (p != null) {
      if (p instanceof RegularClassEntry) {
        return new RefTypeInfo(((RegularClassEntry)p).myName);
      }
      if (p instanceof StringInnerClassEntry) {
        StringInnerClassEntry entry = (StringInnerClassEntry)p;
        RefTypeInfo outer = toTypeInfo(entry.myOuterName, false);
        p = new TypeInfoInnerClassEntry(outer, entry.myInnerName, entry.myStatic);
        myMap.put(jvmName, p);
      }
      assert p instanceof TypeInfoInnerClassEntry;
      return new RefTypeInfo(((TypeInfoInnerClassEntry)p).myInnerName, ((TypeInfoInnerClassEntry)p).myOuterType);
    }
    else if (jvmName.indexOf('$') >= 0 && !jvmName.equals(myTopLevelName) && (useGuesser || !myTrustInnerClasses)) {
      return StubBuildingVisitor.GUESSING_PROVIDER.toTypeInfo(jvmName);
    }
    String name = jvmName.replace('/', '.');
    myMap.put(jvmName, new RegularClassEntry(name));
    return new RefTypeInfo(name);
  }

  static @NotNull FirstPassData create(Object classSource) {
    ClassReader reader = null;
    if (classSource instanceof ClsFileImpl.FileContentPair) {
      reader = ((ClsFileImpl.FileContentPair)classSource).getContent();
    }
    else if (classSource instanceof VirtualFile) {
      try {
        reader = new ClassReader(((VirtualFile)classSource).contentsToByteArray(false));
      }
      catch (IOException ignored) {
      }
    }

    if (reader != null) {
      return fromReader(reader);
    }

    return NO_DATA;
  }

  private static @NotNull FirstPassData fromReader(@NotNull ClassReader reader) {
    
    class FirstPassVisitor extends ClassVisitor {
      final Map<String, ClassEntry> mapping = new HashMap<>();
      Set<String> varArgConstructors;
      Set<ObjectMethod> syntheticSignatures;
      StringBuilder canonicalSignature;
      String lastComponent;
      String name;
      boolean trustInnerClasses = true;
      boolean sealed = false;

      FirstPassVisitor() {
        super(Opcodes.API_VERSION);
      }

      @Override
      public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        if (isSet(access, Opcodes.ACC_RECORD)) {
          varArgConstructors = new HashSet<>();
          canonicalSignature = new StringBuilder("(");
          syntheticSignatures = EnumSet.noneOf(ObjectMethod.class);
        }
        this.name = name;
      }

      @Override
      public void visitSource(String source, String debug) {
        String suffix = ".groovy";
        if (source != null && source.length() > suffix.length() &&
            source.regionMatches(true, source.length() - suffix.length(), suffix, 0, suffix.length())) {
          trustInnerClasses = false;
        }
        super.visitSource(source, debug);
      }

      @Override
      public void visitPermittedSubclass(String permittedSubclass) {
        sealed = true;
      }

      @Override
      public RecordComponentVisitor visitRecordComponent(String name, String descriptor, String signature) {
        if (isRecord()) {
          canonicalSignature.append(descriptor);
          lastComponent = name;
        }
        return null;
      }

      private boolean isRecord() {
        return varArgConstructors != null;
      }

      @Override
      public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        if (isRecord()) {
          if (name.equals("<init>") && isSet(access, Opcodes.ACC_VARARGS)) {
            varArgConstructors.add(descriptor);
          }
          ObjectMethod method = ObjectMethod.from(name, descriptor);
          if (method != null) {
            return new MethodVisitor(Opcodes.API_VERSION) {
              @Override
              public void visitInvokeDynamicInsn(String indyName,
                                                 String indyDescriptor,
                                                 Handle bootstrapMethodHandle,
                                                 Object... bootstrapMethodArguments) {
                if (indyName.equals(name) && bootstrapMethodHandle.getName().equals("bootstrap") &&
                    bootstrapMethodHandle.getOwner().equals("java/lang/runtime/ObjectMethods")) {
                  syntheticSignatures.add(method);
                }
              }
            };
          }
        }
        return null;
      }

      @Override
      public void visitInnerClass(String name, String outerName, String innerName, int access) {
        if (outerName != null && innerName != null) {
          mapping.put(name, new StringInnerClassEntry(outerName, innerName, isSet(access, Opcodes.ACC_STATIC)));
        }
      }
    }

    FirstPassVisitor visitor = new FirstPassVisitor();
    try {
      reader.accept(visitor, ClsFileImpl.EMPTY_ATTRIBUTES, ClassReader.SKIP_FRAMES);
    }
    catch (Exception ex) {
      LOG.debug(ex);
    }
    String varArgComponent = null;
    if (visitor.isRecord()) {
      visitor.canonicalSignature.append(")V");
      if (visitor.varArgConstructors.contains(visitor.canonicalSignature.toString())) {
        varArgComponent = visitor.lastComponent;
      }
    }
    Set<ObjectMethod> syntheticMethods = visitor.syntheticSignatures == null ? Collections.emptySet() : visitor.syntheticSignatures;
    return new FirstPassData(visitor.mapping, visitor.name, varArgComponent, syntheticMethods, visitor.trustInnerClasses, visitor.sealed);
  }
  
  private enum ObjectMethod {
    EQUALS("equals", "(Ljava/lang/Object;)Z"),
    HASH_CODE("hashCode", "()I"),
    TO_STRING("toString", "()Ljava/lang/String;");

    private final @NotNull String myName;
    private final @NotNull String myDesc;

    ObjectMethod(@NotNull String name, @NotNull String desc) {
      myName = name;
      myDesc = desc;
    }
    
    static @Nullable ObjectMethod from(@NotNull String name, @NotNull String desc) {
      for (ObjectMethod method : values()) {
        if (method.myName.equals(name) && method.myDesc.equals(desc)) {
          return method;
        }
      }
      return null;
    }
  }
}
