/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * 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.intellij.psi.impl.compiled;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.PsiNameHelper;
import com.intellij.psi.PsiReferenceList;
import com.intellij.psi.impl.cache.ModifierFlags;
import com.intellij.psi.impl.cache.TypeInfo;
import com.intellij.psi.impl.java.stubs.*;
import com.intellij.psi.impl.java.stubs.impl.*;
import com.intellij.psi.stubs.PsiFileStub;
import com.intellij.psi.stubs.StubElement;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.cls.ClsFormatException;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.StringRef;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.org.objectweb.asm.*;

import java.lang.reflect.Array;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.intellij.openapi.util.Pair.pair;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_ANNOTATION_ANNOTATION;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_STRING;
import static com.intellij.psi.impl.compiled.ClsFileImpl.EMPTY_ATTRIBUTES;
import static com.intellij.util.BitUtil.isSet;

/**
 * @author max
 */
public class StubBuildingVisitor<T> extends ClassVisitor {
  private static final Logger LOG = Logger.getInstance(StubBuildingVisitor.class);

  private static final String DOUBLE_POSITIVE_INF = "1.0 / 0.0";
  private static final String DOUBLE_NEGATIVE_INF = "-1.0 / 0.0";
  private static final String DOUBLE_NAN = "0.0d / 0.0";
  private static final String FLOAT_POSITIVE_INF = "1.0f / 0.0";
  private static final String FLOAT_NEGATIVE_INF = "-1.0f / 0.0";
  private static final String FLOAT_NAN = "0.0f / 0.0";
  private static final String SYNTHETIC_CLASS_INIT_METHOD = "<clinit>";
  private static final String SYNTHETIC_INIT_METHOD = "<init>";

  private static final int ASM_API = Opcodes.ASM5;

  private final T mySource;
  private final InnerClassSourceStrategy<T> myInnersStrategy;
  private final StubElement myParent;
  private final int myAccess;
  private final String myShortName;
  private final Function<String, String> myMapping;
  private String myInternalName;
  private PsiClassStub myResult;
  private PsiModifierListStub myModList;

  public StubBuildingVisitor(T classSource, InnerClassSourceStrategy<T> innersStrategy, StubElement parent, int access, String shortName) {
    super(ASM_API);
    mySource = classSource;
    myInnersStrategy = innersStrategy;
    myParent = parent;
    myAccess = access;
    myShortName = shortName;
    myMapping = createMapping(classSource);
  }

  public PsiClassStub<?> getResult() {
    return myResult;
  }

  @Override
  public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
    myInternalName = name;
    String parentName = myParent instanceof PsiClassStub ? ((PsiClassStub)myParent).getQualifiedName() :
                        myParent instanceof PsiJavaFileStub ? ((PsiJavaFileStub)myParent).getPackageName() :
                        null;
    String fqn = getFqn(name, myShortName, parentName);
    String shortName = myShortName != null && name.endsWith(myShortName) ? myShortName : PsiNameHelper.getShortClassName(fqn);

    int flags = myAccess | access;
    boolean isDeprecated = isSet(flags, Opcodes.ACC_DEPRECATED);
    boolean isInterface = isSet(flags, Opcodes.ACC_INTERFACE);
    boolean isEnum = isSet(flags, Opcodes.ACC_ENUM);
    boolean isAnnotationType = isSet(flags, Opcodes.ACC_ANNOTATION);
    byte stubFlags = PsiClassStubImpl.packFlags(isDeprecated, isInterface, isEnum, false, false, isAnnotationType, false, false);
    myResult = new PsiClassStubImpl(JavaStubElementTypes.CLASS, myParent, fqn, shortName, null, stubFlags);

    LanguageLevel languageLevel = ClsParsingUtil.getLanguageLevelByVersion(version);
    if (languageLevel == null) languageLevel = LanguageLevel.HIGHEST;
    ((PsiClassStubImpl)myResult).setLanguageLevel(languageLevel);

    myModList = new PsiModifierListStubImpl(myResult, packClassFlags(flags));

    ClassInfo info = null;
    if (signature != null) {
      try {
        info = parseClassSignature(signature);
      }
      catch (ClsFormatException e) {
        if (LOG.isDebugEnabled()) LOG.debug("source=" + mySource + " signature=" + signature, e);
      }
    }
    if (info == null) {
      info = parseClassDescription(superName, interfaces);
    }

    PsiTypeParameterListStub typeParameterList = new PsiTypeParameterListStubImpl(myResult);
    for (Pair<String, String[]> parameter : info.typeParameters) {
      PsiTypeParameterStub parameterStub = new PsiTypeParameterStubImpl(typeParameterList, StringRef.fromString(parameter.first));
      newReferenceList(JavaStubElementTypes.EXTENDS_BOUND_LIST, parameterStub, parameter.second);
    }

    if (myResult.isInterface()) {
      if (info.interfaceNames != null && myResult.isAnnotationType()) {
        info.interfaceNames.remove(JAVA_LANG_ANNOTATION_ANNOTATION);
      }
      newReferenceList(JavaStubElementTypes.EXTENDS_LIST, myResult, ArrayUtil.toStringArray(info.interfaceNames));
      newReferenceList(JavaStubElementTypes.IMPLEMENTS_LIST, myResult, ArrayUtil.EMPTY_STRING_ARRAY);
    }
    else {
      if (info.superName == null || "java/lang/Object".equals(superName) || myResult.isEnum() && "java/lang/Enum".equals(superName)) {
        newReferenceList(JavaStubElementTypes.EXTENDS_LIST, myResult, ArrayUtil.EMPTY_STRING_ARRAY);
      }
      else {
        newReferenceList(JavaStubElementTypes.EXTENDS_LIST, myResult, new String[]{info.superName});
      }
      newReferenceList(JavaStubElementTypes.IMPLEMENTS_LIST, myResult, ArrayUtil.toStringArray(info.interfaceNames));
    }
  }

  private String getFqn(@NotNull String internalName, @Nullable String shortName, @Nullable String parentName) {
    if (shortName == null || !internalName.endsWith(shortName)) {
      return myMapping.fun(internalName);
    }
    if (internalName.length() == shortName.length()) {
      return shortName;
    }
    if (parentName == null) {
      parentName = myMapping.fun(internalName.substring(0, internalName.length() - shortName.length() - 1));
    }
    return parentName + '.' + shortName;
  }

  private ClassInfo parseClassSignature(String signature) throws ClsFormatException {
    ClassInfo result = new ClassInfo();
    CharacterIterator iterator = new StringCharacterIterator(signature);
    result.typeParameters = SignatureParsing.parseTypeParametersDeclaration(iterator, myMapping);
    result.superName = SignatureParsing.parseTopLevelClassRefSignature(iterator, myMapping);
    while (iterator.current() != CharacterIterator.DONE) {
      String name = SignatureParsing.parseTopLevelClassRefSignature(iterator, myMapping);
      if (name == null) throw new ClsFormatException();
      if (result.interfaceNames == null) result.interfaceNames = ContainerUtil.newSmartList();
      result.interfaceNames.add(name);
    }
    return result;
  }

  private ClassInfo parseClassDescription(String superClass, String[] superInterfaces) {
    ClassInfo result = new ClassInfo();
    result.typeParameters = ContainerUtil.emptyList();
    result.superName = superClass != null ? myMapping.fun(superClass) : null;
    result.interfaceNames = superInterfaces == null ? null : ContainerUtil.map(superInterfaces, new Function<String, String>() {
      @Override
      public String fun(String name) {
        return myMapping.fun(name);
      }
    });
    return result;
  }

  private static void newReferenceList(JavaClassReferenceListElementType type, StubElement parent, String[] types) {
    PsiReferenceList.Role role;

    if (type == JavaStubElementTypes.EXTENDS_LIST) role = PsiReferenceList.Role.EXTENDS_LIST;
    else if (type == JavaStubElementTypes.IMPLEMENTS_LIST) role = PsiReferenceList.Role.IMPLEMENTS_LIST;
    else if (type == JavaStubElementTypes.THROWS_LIST) role = PsiReferenceList.Role.THROWS_LIST;
    else if (type == JavaStubElementTypes.EXTENDS_BOUND_LIST) role = PsiReferenceList.Role.EXTENDS_BOUNDS_LIST;
    else throw new IllegalArgumentException("Unknown type: " + type);

    new PsiClassReferenceListStubImpl(type, parent, types, role);
  }

  private static int packCommonFlags(int access) {
    int flags = 0;

    if (isSet(access, Opcodes.ACC_PRIVATE)) flags |= ModifierFlags.PRIVATE_MASK;
    else if (isSet(access, Opcodes.ACC_PROTECTED)) flags |= ModifierFlags.PROTECTED_MASK;
    else if (isSet(access, Opcodes.ACC_PUBLIC)) flags |= ModifierFlags.PUBLIC_MASK;
    else flags |= ModifierFlags.PACKAGE_LOCAL_MASK;

    if (isSet(access, Opcodes.ACC_STATIC)) flags |= ModifierFlags.STATIC_MASK;
    if (isSet(access, Opcodes.ACC_FINAL)) flags |= ModifierFlags.FINAL_MASK;

    return flags;
  }

  private static int packClassFlags(int access) {
    int flags = packCommonFlags(access);
    if (isSet(access, Opcodes.ACC_ABSTRACT)) flags |= ModifierFlags.ABSTRACT_MASK;
    return flags;
  }

  private static int packFieldFlags(int access) {
    int flags = packCommonFlags(access);
    if (isSet(access, Opcodes.ACC_VOLATILE)) flags |= ModifierFlags.VOLATILE_MASK;
    if (isSet(access, Opcodes.ACC_TRANSIENT)) flags |= ModifierFlags.TRANSIENT_MASK;
    return flags;
  }

  private static int packMethodFlags(int access, boolean isInterface) {
    int flags = packCommonFlags(access);

    if (isSet(access, Opcodes.ACC_SYNCHRONIZED)) flags |= ModifierFlags.SYNCHRONIZED_MASK;
    if (isSet(access, Opcodes.ACC_NATIVE)) flags |= ModifierFlags.NATIVE_MASK;
    if (isSet(access, Opcodes.ACC_STRICT)) flags |= ModifierFlags.STRICTFP_MASK;

    if (isSet(access, Opcodes.ACC_ABSTRACT)) flags |= ModifierFlags.ABSTRACT_MASK;
    else if (isInterface && !isSet(access, Opcodes.ACC_STATIC)) flags |= ModifierFlags.DEFENDER_MASK;

    return flags;
  }

  @Override
  public void visitSource(String source, String debug) {
    ((PsiClassStubImpl)myResult).setSourceFileName(source);
  }

  @Override
  public void visitOuterClass(String owner, String name, String desc) {
    if (myParent instanceof PsiFileStub) {
      throw new OutOfOrderInnerClassException();
    }
  }

  @Override
  public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
    return new AnnotationTextCollector(desc, myMapping, new Consumer<String>() {
      @Override
      public void consume(String text) {
        new PsiAnnotationStubImpl(myModList, text);
      }
    });
  }

  @Override
  public void visitInnerClass(String name, String outerName, String innerName, int access) {
    if (isSet(access, Opcodes.ACC_SYNTHETIC)) return;
    if (innerName == null || outerName == null) return;

    if (myParent instanceof PsiFileStub && myInternalName.equals(name)) {
      throw new OutOfOrderInnerClassException();  // our result is inner class
    }

    if (myInternalName.equals(outerName)) {
      T innerClass = myInnersStrategy.findInnerClass(innerName, mySource);
      if (innerClass != null) {
        myInnersStrategy.accept(innerClass, new StubBuildingVisitor<T>(innerClass, myInnersStrategy, myResult, access, innerName));
      }
    }
  }

  @Override
  @Nullable
  public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
    if (isSet(access, Opcodes.ACC_SYNTHETIC)) return null;
    if (name == null) return null;

    byte flags = PsiFieldStubImpl.packFlags(isSet(access, Opcodes.ACC_ENUM), isSet(access, Opcodes.ACC_DEPRECATED), false, false);
    TypeInfo type = fieldType(desc, signature);
    String initializer = constToString(value, type.text, false, myMapping);
    PsiFieldStub stub = new PsiFieldStubImpl(myResult, name, type, initializer, flags);
    PsiModifierListStub modList = new PsiModifierListStubImpl(stub, packFieldFlags(access));
    return new FieldAnnotationCollectingVisitor(modList, myMapping);
  }

  private TypeInfo fieldType(String desc, String signature) {
    String type = null;
    if (signature != null) {
      try {
        type = SignatureParsing.parseTypeString(new StringCharacterIterator(signature), myMapping);
      }
      catch (ClsFormatException e) {
        if (LOG.isDebugEnabled()) LOG.debug("source=" + mySource + " signature=" + signature, e);
      }
    }
    if (type == null) {
      type = toJavaType(Type.getType(desc), myMapping);
    }
    return TypeInfo.fromString(type, false);
  }

  private static final String[] parameterNames = {"p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9"};

  @Override
  @Nullable
  public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    // JLS 13.1 says: Any constructs introduced by the compiler that do not have a corresponding construct in the source code
    // must be marked as synthetic, except for default constructors and the class initialization method.
    // However Scala compiler erroneously generates ACC_BRIDGE instead of ACC_SYNTHETIC flag for in-trait implementation delegation.
    // See IDEA-78649
    if (isSet(access, Opcodes.ACC_SYNTHETIC)) return null;
    if (name == null) return null;
    if (SYNTHETIC_CLASS_INIT_METHOD.equals(name)) return null;

    // skip semi-synthetic enum methods
    boolean isEnum = myResult.isEnum();
    if (isEnum) {
      if ("values".equals(name) && desc.startsWith("()")) return null;
      //noinspection SpellCheckingInspection
      if ("valueOf".equals(name) && desc.startsWith("(Ljava/lang/String;)")) return null;
    }

    boolean isConstructor = SYNTHETIC_INIT_METHOD.equals(name);
    boolean isDeprecated = isSet(access, Opcodes.ACC_DEPRECATED);
    boolean isVarargs = isSet(access, Opcodes.ACC_VARARGS);
    boolean isStatic = isSet(access, Opcodes.ACC_STATIC);
    boolean isAnnotationMethod = myResult.isAnnotationType();

    byte flags = PsiMethodStubImpl.packFlags(isConstructor, isAnnotationMethod, isVarargs, isDeprecated, false, false);

    String canonicalMethodName = isConstructor ? myResult.getName() : name;

    MethodInfo info = null;
    boolean generic = false;
    if (signature != null) {
      try {
        info = parseMethodSignature(signature, exceptions);
        generic = true;
      }
      catch (ClsFormatException e) {
        if (LOG.isDebugEnabled()) LOG.debug("source=" + mySource + " signature=" + signature, e);
      }
    }
    if (info == null) {
      info = parseMethodDescription(desc, exceptions);
    }

    PsiMethodStubImpl stub = new PsiMethodStubImpl(myResult, canonicalMethodName, TypeInfo.fromString(info.returnType, false), flags, null);

    PsiModifierListStub modList = new PsiModifierListStubImpl(stub, packMethodFlags(access, myResult.isInterface()));

    PsiTypeParameterListStub list = new PsiTypeParameterListStubImpl(stub);
    for (Pair<String, String[]> parameter : info.typeParameters) {
      PsiTypeParameterStub parameterStub = new PsiTypeParameterStubImpl(list, StringRef.fromString(parameter.first));
      newReferenceList(JavaStubElementTypes.EXTENDS_BOUND_LIST, parameterStub, parameter.second);
    }

    boolean isEnumConstructor = isEnum && isConstructor;
    boolean isInnerClassConstructor = isConstructor &&
                                      !(myParent instanceof PsiFileStub) &&
                                      !isSet(myModList.getModifiersMask(), Opcodes.ACC_STATIC);

    List<String> args = info.argTypes;
    if (!generic && isEnumConstructor && args.size() >= 2 && JAVA_LANG_STRING.equals(args.get(0)) && "int".equals(args.get(1))) {
      // omit synthetic enum constructor parameters
      args = args.subList(2, args.size());
    }

    PsiParameterListStubImpl parameterList = new PsiParameterListStubImpl(stub);
    int paramCount = args.size();
    PsiParameterStubImpl[] paramStubs = new PsiParameterStubImpl[paramCount];
    for (int i = 0; i < paramCount; i++) {
      // omit synthetic inner class constructor parameter
      if (i == 0 && !generic && isInnerClassConstructor) continue;

      String arg = args.get(i);
      boolean isEllipsisParam = isVarargs && i == paramCount - 1;
      TypeInfo typeInfo = TypeInfo.fromString(arg, isEllipsisParam);

      String paramName = i < parameterNames.length ? parameterNames[i] : "p" + (i + 1);
      PsiParameterStubImpl parameterStub = new PsiParameterStubImpl(parameterList, paramName, typeInfo, isEllipsisParam, true);
      paramStubs[i] = parameterStub;
      new PsiModifierListStubImpl(parameterStub, 0);
    }

    newReferenceList(JavaStubElementTypes.THROWS_LIST, stub, ArrayUtil.toStringArray(info.throwTypes));

    int localVarIgnoreCount = isStatic ? 0 : isEnumConstructor ? 3 : 1;
    int paramIgnoreCount = isEnumConstructor ? 2 : isInnerClassConstructor ? 1 : 0;
    return new MethodAnnotationCollectingVisitor(stub, modList, localVarIgnoreCount, paramIgnoreCount, paramCount, paramStubs, myMapping);
  }

  private MethodInfo parseMethodSignature(String signature, String[] exceptions) throws ClsFormatException {
    MethodInfo result = new MethodInfo();
    CharacterIterator iterator = new StringCharacterIterator(signature);

    result.typeParameters = SignatureParsing.parseTypeParametersDeclaration(iterator, myMapping);

    if (iterator.current() != '(') throw new ClsFormatException();
    iterator.next();
    if (iterator.current() == ')') {
      result.argTypes = ContainerUtil.emptyList();
    }
    else {
      result.argTypes = ContainerUtil.newSmartList();
      while (iterator.current() != ')' && iterator.current() != CharacterIterator.DONE) {
        result.argTypes.add(SignatureParsing.parseTypeString(iterator, myMapping));
      }
      if (iterator.current() != ')') throw new ClsFormatException();
    }
    iterator.next();

    result.returnType = SignatureParsing.parseTypeString(iterator, myMapping);

    result.throwTypes = null;
    while (iterator.current() == '^') {
      iterator.next();
      if (result.throwTypes == null) result.throwTypes = ContainerUtil.newSmartList();
      result.throwTypes.add(SignatureParsing.parseTypeString(iterator, myMapping));
    }
    if (exceptions != null && (result.throwTypes == null || exceptions.length > result.throwTypes.size())) {
      // a signature may be inconsistent with exception list - in this case, the more complete list takes precedence
      result.throwTypes = ContainerUtil.map(exceptions, new Function<String, String>() {
        @Override
        public String fun(String name) {
          return myMapping.fun(name);
        }
      });
    }

    return result;
  }

  private MethodInfo parseMethodDescription(String desc, String[] exceptions) {
    MethodInfo result = new MethodInfo();
    result.typeParameters = ContainerUtil.emptyList();
    result.returnType = toJavaType(Type.getReturnType(desc), myMapping);
    result.argTypes = ContainerUtil.map(Type.getArgumentTypes(desc), new Function<Type, String>() {
      @Override
      public String fun(Type type) {
        return toJavaType(type, myMapping);
      }
    });
    result.throwTypes = exceptions == null ? null : ContainerUtil.map(exceptions, new Function<String, String>() {
      @Override
      public String fun(String name) {
        return myMapping.fun(name);
      }
    });
    return result;
  }


  private static class ClassInfo {
    private List<Pair<String, String[]>> typeParameters;
    private String superName;
    private List<String> interfaceNames;
  }

  private static class MethodInfo {
    private List<Pair<String, String[]>> typeParameters;
    private String returnType;
    private List<String> argTypes;
    private List<String> throwTypes;
  }

  private static class AnnotationTextCollector extends AnnotationVisitor {
    private final StringBuilder myBuilder = new StringBuilder();
    private final Function<String, String> myMapping;
    private final Consumer<String> myCallback;
    private boolean hasPrefix;
    private boolean hasParams;

    public AnnotationTextCollector(@Nullable String desc, Function<String, String> mapping, Consumer<String> callback) {
      super(ASM_API);
      myMapping = mapping;
      myCallback = callback;

      if (desc != null) {
        hasPrefix = true;
        myBuilder.append('@').append(toJavaType(Type.getType(desc), myMapping));
      }
    }

    @Override
    public void visit(String name, Object value) {
      valuePairPrefix(name);
      myBuilder.append(constToString(value, null, true, myMapping));
    }

    @Override
    public void visitEnum(String name, String desc, String value) {
      valuePairPrefix(name);
      myBuilder.append(toJavaType(Type.getType(desc), myMapping)).append('.').append(value);
    }

    private void valuePairPrefix(String name) {
      if (!hasParams) {
        hasParams = true;
        if (hasPrefix) {
          myBuilder.append('(');
        }
      }
      else {
        myBuilder.append(',');
      }

      if (name != null) {
        myBuilder.append(name).append('=');
      }
    }

    @Override
    public AnnotationVisitor visitAnnotation(String name, String desc) {
      valuePairPrefix(name);
      return new AnnotationTextCollector(desc, myMapping, new Consumer<String>() {
        @Override
        public void consume(String text) {
          myBuilder.append(text);
        }
      });
    }

    @Override
    public AnnotationVisitor visitArray(String name) {
      valuePairPrefix(name);
      myBuilder.append('{');
      return new AnnotationTextCollector(null, myMapping, new Consumer<String>() {
        @Override
        public void consume(String text) {
          myBuilder.append(text).append('}');
        }
      });
    }

    @Override
    public void visitEnd() {
      if (hasPrefix && hasParams) {
        myBuilder.append(')');
      }
      myCallback.consume(myBuilder.toString());
    }
  }

  private static class FieldAnnotationCollectingVisitor extends FieldVisitor {
    private final PsiModifierListStub myModList;
    private final Function<String, String> myMapping;
    private Set<String> myFilter;

    private FieldAnnotationCollectingVisitor(PsiModifierListStub modList, Function<String, String> mapping) {
      super(ASM_API);
      myModList = modList;
      myMapping = mapping;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
      return new AnnotationTextCollector(desc, myMapping, new Consumer<String>() {
        @Override
        public void consume(String text) {
          if (myFilter == null) myFilter = ContainerUtil.newTroveSet();
          myFilter.add(text);
          new PsiAnnotationStubImpl(myModList, text);
        }
      });
    }

    @Override
    public AnnotationVisitor visitTypeAnnotation(int typeRef, final TypePath typePath, String desc, boolean visible) {
      return new AnnotationTextCollector(desc, myMapping, new Consumer<String>() {
        @Override
        public void consume(String text) {
          if (typePath == null && (myFilter == null || !myFilter.contains(text))) {
            new PsiAnnotationStubImpl(myModList, text);
          }
        }
      });
    }
  }

  private static class MethodAnnotationCollectingVisitor extends MethodVisitor {
    private final PsiMethodStub myOwner;
    private final PsiModifierListStub myModList;
    private final int myIgnoreCount;
    private final int myParamIgnoreCount;
    private final int myParamCount;
    private final PsiParameterStubImpl[] myParamStubs;
    private final Function<String, String> myMapping;
    private int myUsedParamSize;
    private int myUsedParamCount;
    private List<Set<String>> myFilters;

    private MethodAnnotationCollectingVisitor(PsiMethodStub owner,
                                              PsiModifierListStub modList,
                                              int ignoreCount,
                                              int paramIgnoreCount,
                                              int paramCount,
                                              PsiParameterStubImpl[] paramStubs,
                                              Function<String, String> mapping) {
      super(ASM_API);
      myOwner = owner;
      myModList = modList;
      myIgnoreCount = ignoreCount;
      myParamIgnoreCount = paramIgnoreCount;
      myParamCount = paramCount;
      myParamStubs = paramStubs;
      myMapping = mapping;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
      return new AnnotationTextCollector(desc, myMapping, new Consumer<String>() {
        @Override
        public void consume(String text) {
          filter(0, text);
          new PsiAnnotationStubImpl(myModList, text);
        }
      });
    }

    @Override
    @Nullable
    public AnnotationVisitor visitParameterAnnotation(final int parameter, String desc, boolean visible) {
      return parameter < myParamIgnoreCount ? null : new AnnotationTextCollector(desc, myMapping, new Consumer<String>() {
        @Override
        public void consume(String text) {
          int idx = parameter - myParamIgnoreCount;
          filter(idx + 1, text);
          new PsiAnnotationStubImpl(myOwner.findParameter(idx).getModList(), text);
        }
      });
    }

    @Override
    public AnnotationVisitor visitTypeAnnotation(int typeRef, final TypePath typePath, String desc, boolean visible) {
      final TypeReference ref = new TypeReference(typeRef);
      return new AnnotationTextCollector(desc, myMapping, new Consumer<String>() {
        @Override
        public void consume(String text) {
          if (ref.getSort() == TypeReference.METHOD_RETURN && typePath == null && !filtered(0, text)) {
            new PsiAnnotationStubImpl(myModList, text);
          }
          else if (ref.getSort() == TypeReference.METHOD_FORMAL_PARAMETER && typePath == null) {
            int idx = ref.getFormalParameterIndex() - myParamIgnoreCount;
            if (!filtered(idx + 1, text)) {
              new PsiAnnotationStubImpl(myOwner.findParameter(idx).getModList(), text);
            }
          }
        }
      });
    }

    @Override
    public AnnotationVisitor visitAnnotationDefault() {
      return new AnnotationTextCollector(null, myMapping, new Consumer<String>() {
        @Override
        public void consume(String text) {
          ((PsiMethodStubImpl)myOwner).setDefaultValueText(text);
        }
      });
    }

    @Override
    public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
      if (index >= myIgnoreCount) {
        // long and double variables increase the index by 2, not by 1
        int paramIndex = (index - myIgnoreCount == myUsedParamSize) ? myUsedParamCount : index - myIgnoreCount;
        if (paramIndex >= myParamCount) return;

        if (ClsParsingUtil.isJavaIdentifier(name, LanguageLevel.HIGHEST)) {
          PsiParameterStubImpl parameterStub = myParamStubs[paramIndex];
          if (parameterStub != null) {
            parameterStub.setName(name);
          }
        }

        myUsedParamCount = paramIndex + 1;
        myUsedParamSize += "D".equals(desc) || "J".equals(desc) ? 2 : 1;
      }
    }

    private void filter(int index, String text) {
      if (myFilters == null) {
        myFilters = ContainerUtil.newArrayListWithCapacity(myParamCount + 1);
        for (int i = 0; i < myParamCount + 1; i++) myFilters.add(null);
      }
      Set<String> filter = myFilters.get(index);
      if (filter == null) myFilters.set(index, (filter = ContainerUtil.newTroveSet()));
      filter.add(text);
    }

    private boolean filtered(int index, String text) {
      return myFilters != null && myFilters.get(index) != null && myFilters.get(index).contains(text);
    }
  }

  @Nullable
  private static String constToString(@Nullable Object value, @Nullable String type, boolean anno, Function<String, String> mapping) {
    if (value == null) return null;

    if (value instanceof String) {
      return "\"" + StringUtil.escapeStringCharacters((String)value) + "\"";
    }

    if (value instanceof Boolean || value instanceof Short || value instanceof Byte) {
      return value.toString();
    }

    if (value instanceof Character) {
      return "'" + StringUtil.escapeCharCharacters(value.toString()) + "'";
    }

    if (value instanceof Long) {
      return value.toString() + 'L';
    }

    if (value instanceof Integer) {
      if ("boolean".equals(type)) {
        if (value.equals(0)) return "false";
        if (value.equals(1)) return "true";
      }
      if ("char".equals(type)) {
        char ch = (char)((Integer)value).intValue();
        return "'" + StringUtil.escapeCharCharacters(String.valueOf(ch)) + "'";
      }
      return value.toString();
    }

    if (value instanceof Double) {
      double d = (Double)value;
      if (Double.isInfinite(d)) {
        return d > 0 ? DOUBLE_POSITIVE_INF : DOUBLE_NEGATIVE_INF;
      }
      else if (Double.isNaN(d)) {
        return DOUBLE_NAN;
      }
      return Double.toString(d);
    }

    if (value instanceof Float) {
      float v = (Float)value;

      if (Float.isInfinite(v)) {
        return v > 0 ? FLOAT_POSITIVE_INF : FLOAT_NEGATIVE_INF;
      }
      else if (Float.isNaN(v)) {
        return FLOAT_NAN;
      }
      else {
        return Float.toString(v) + 'f';
      }
    }

    if (value.getClass().isArray()) {
      StringBuilder buffer = new StringBuilder();
      buffer.append('{');
      for (int i = 0, length = Array.getLength(value); i < length; i++) {
        if (i > 0) buffer.append(", ");
        buffer.append(constToString(Array.get(value, i), type, anno, mapping));
      }
      buffer.append('}');
      return buffer.toString();
    }

    if (anno && value instanceof Type) {
      return toJavaType(((Type)value), mapping) + ".class";
    }

    return null;
  }

  private static String toJavaType(Type type, Function<String, String> mapping) {
    int dimensions = 0;
    if (type.getSort() == Type.ARRAY) {
      dimensions = type.getDimensions();
      type = type.getElementType();
    }
    String text = type.getSort() == Type.OBJECT ? mapping.fun(type.getInternalName()) : type.getClassName();
    if (dimensions > 0) text += StringUtil.repeat("[]", dimensions);
    return text;
  }

  private static Function<String, String> createMapping(Object classSource) {
    if (classSource instanceof VirtualFile) {
      final Map<String, Pair<String, String>> mapping = ContainerUtil.newHashMap();

      try {
        byte[] bytes = ((VirtualFile)classSource).contentsToByteArray(false);
        new ClassReader(bytes).accept(new ClassVisitor(ASM_API) {
          @Override
          public void visitInnerClass(String name, String outerName, String innerName, int access) {
            if (outerName != null && innerName != null) {
              mapping.put(name, pair(outerName, innerName));
            }
          }
        }, EMPTY_ATTRIBUTES, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
      }
      catch (Exception ignored) { }

      if (!mapping.isEmpty()) {
        return new Function<String, String>() {
          @Override
          public String fun(String internalName) {
            String className = internalName;

            if (className.indexOf('$') >= 0) {
              Pair<String, String> p = mapping.get(className);
              if (p == null) {
                return GUESSING_MAPPER.fun(className);
              }
              className = p.first;
              if (p.second != null) {
                className = fun(p.first) + '.' + p.second;
                mapping.put(className, pair(className, (String)null));
              }
            }

            return className.replace('/', '.');
          }
        };
      }
    }

    return GUESSING_MAPPER;
  }

  public static final Function<String, String> GUESSING_MAPPER = new Function<String, String>() {
    @Override
    public String fun(String internalName) {
      String canonicalText = internalName;

      if (canonicalText.indexOf('$') >= 0) {
        StringBuilder sb = new StringBuilder(canonicalText);
        boolean updated = false;
        for (int p = 0; p < sb.length(); p++) {
          char c = sb.charAt(p);
          if (c == '$' && p > 0 && sb.charAt(p - 1) != '/' && p < sb.length() - 1 && sb.charAt(p + 1) != '$') {
            sb.setCharAt(p, '.');
            updated = true;
          }
        }
        if (updated) {
          canonicalText = sb.toString();
        }
      }

      return canonicalText.replace('/', '.');
    }
  };
}