/** @author peter */
class TypeCorrector extends PsiTypeMapper {
  private final Map<PsiClassType, PsiClassType> myResultMap = ContainerUtil.newIdentityHashMap();
  private final GlobalSearchScope myResolveScope;

  TypeCorrector(GlobalSearchScope resolveScope) {
    myResolveScope = resolveScope;
  }

  @Override
  public PsiType visitType(PsiType type) {
    if (type instanceof PsiLambdaParameterType
        || type instanceof PsiLambdaExpressionType
        || type instanceof PsiMethodReferenceType) {
      return type;
    }
    return super.visitType(type);
  }

  @Nullable
  public <T extends PsiType> T correctType(@NotNull T type) {
    if (type instanceof PsiClassType) {
      PsiClassType classType = (PsiClassType) type;
      if (classType.getParameterCount() == 0) {
        final PsiClassType.ClassResolveResult classResolveResult = classType.resolveGenerics();
        final PsiClass psiClass = classResolveResult.getElement();
        if (psiClass != null && classResolveResult.getSubstitutor() == PsiSubstitutor.EMPTY) {
          final PsiClass mappedClass = mapClass(psiClass);
          if (mappedClass == null || mappedClass == psiClass) return (T) classType;
        }
      }
    }

    return (T) type.accept(this);
  }

  @Override
  public PsiType visitClassType(final PsiClassType classType) {
    PsiClassType alreadyComputed = myResultMap.get(classType);
    if (alreadyComputed != null) {
      return alreadyComputed;
    }

    final PsiClassType.ClassResolveResult classResolveResult = classType.resolveGenerics();
    final PsiClass psiClass = classResolveResult.getElement();
    final PsiSubstitutor substitutor = classResolveResult.getSubstitutor();
    if (psiClass == null) return classType;

    PsiUtilCore.ensureValid(psiClass);

    final PsiClass mappedClass = mapClass(psiClass);
    if (mappedClass == null) return classType;

    PsiClassType mappedType =
        new PsiCorrectedClassType(
            classType.getLanguageLevel(),
            classType,
            new CorrectedResolveResult(psiClass, mappedClass, substitutor, classResolveResult));
    myResultMap.put(classType, mappedType);
    return mappedType;
  }

  @Nullable
  private PsiClass mapClass(@NotNull PsiClass psiClass) {
    String qualifiedName = psiClass.getQualifiedName();
    if (qualifiedName == null) {
      return psiClass;
    }

    PsiFile file = psiClass.getContainingFile();
    if (file == null || !file.getViewProvider().isPhysical()) {
      return psiClass;
    }

    final VirtualFile vFile = file.getVirtualFile();
    if (vFile == null) {
      return psiClass;
    }

    final FileIndexFacade index = FileIndexFacade.getInstance(file.getProject());
    if (!index.isInSource(vFile)
        && !index.isInLibrarySource(vFile)
        && !index.isInLibraryClasses(vFile)) {
      return psiClass;
    }

    return JavaPsiFacade.getInstance(psiClass.getProject())
        .findClass(qualifiedName, myResolveScope);
  }

  @NotNull
  private PsiSubstitutor mapSubstitutor(
      PsiClass originalClass, PsiClass mappedClass, PsiSubstitutor substitutor) {
    PsiTypeParameter[] typeParameters = mappedClass.getTypeParameters();
    PsiTypeParameter[] originalTypeParameters = originalClass.getTypeParameters();
    if (typeParameters.length != originalTypeParameters.length) {
      if (originalTypeParameters.length == 0) {
        return JavaPsiFacade.getElementFactory(mappedClass.getProject())
            .createRawSubstitutor(mappedClass);
      }
      return substitutor;
    }

    Map<PsiTypeParameter, PsiType> substitutionMap = substitutor.getSubstitutionMap();

    PsiSubstitutor mappedSubstitutor = PsiSubstitutor.EMPTY;
    for (int i = 0; i < originalTypeParameters.length; i++) {
      if (!substitutionMap.containsKey(originalTypeParameters[i])) continue;

      PsiType originalSubstitute = substitutor.substitute(originalTypeParameters[i]);
      if (originalSubstitute != null) {
        PsiType substitute = mapType(originalSubstitute);
        if (substitute == null) return substitutor;

        mappedSubstitutor = mappedSubstitutor.put(typeParameters[i], substitute);
      } else {
        mappedSubstitutor = mappedSubstitutor.put(typeParameters[i], null);
      }
    }

    if (mappedClass.hasModifierProperty(PsiModifier.STATIC)) {
      return mappedSubstitutor;
    }
    PsiClass mappedContaining = mappedClass.getContainingClass();
    PsiClass originalContaining = originalClass.getContainingClass();
    //noinspection DoubleNegation
    if ((mappedContaining != null) != (originalContaining != null)) {
      return substitutor;
    }

    if (mappedContaining != null) {
      return mappedSubstitutor.putAll(
          mapSubstitutor(originalContaining, mappedContaining, substitutor));
    }

    return mappedSubstitutor;
  }

  private class PsiCorrectedClassType extends PsiClassType.Stub {
    private final PsiClassType myDelegate;
    private final CorrectedResolveResult myResolveResult;

    public PsiCorrectedClassType(
        LanguageLevel languageLevel, PsiClassType delegate, CorrectedResolveResult resolveResult) {
      super(languageLevel, delegate.getAnnotationProvider());
      myDelegate = delegate;
      myResolveResult = resolveResult;
    }

    @NotNull
    @Override
    public String getCanonicalText(boolean annotated) {
      return myDelegate.getCanonicalText();
    }

    @NotNull
    @Override
    public PsiClass resolve() {
      return myResolveResult.myMappedClass;
    }

    @Override
    public String getClassName() {
      return myDelegate.getClassName();
    }

    @NotNull
    @Override
    public PsiType[] getParameters() {
      return ContainerUtil.map2Array(
          myDelegate.getParameters(),
          PsiType.class,
          new Function<PsiType, PsiType>() {
            @Override
            public PsiType fun(PsiType type) {
              if (type == null) {
                LOG.error(
                    myDelegate
                        + " of "
                        + myDelegate.getClass()
                        + "; substitutor="
                        + myDelegate.resolveGenerics().getSubstitutor());
                return null;
              }
              return mapType(type);
            }
          });
    }

    @Override
    public int getParameterCount() {
      return myDelegate.getParameters().length;
    }

    @NotNull
    @Override
    public ClassResolveResult resolveGenerics() {
      return myResolveResult;
    }

    @NotNull
    @Override
    public PsiClassType rawType() {
      PsiClass psiClass = resolve();
      PsiElementFactory factory = JavaPsiFacade.getElementFactory(psiClass.getProject());
      return factory.createType(psiClass, factory.createRawSubstitutor(psiClass));
    }

    @NotNull
    @Override
    public GlobalSearchScope getResolveScope() {
      return myResolveScope;
    }

    @NotNull
    @Override
    public LanguageLevel getLanguageLevel() {
      return myLanguageLevel;
    }

    @NotNull
    @Override
    public PsiClassType setLanguageLevel(@NotNull LanguageLevel languageLevel) {
      return new PsiCorrectedClassType(languageLevel, myDelegate, myResolveResult);
    }

    @NotNull
    @Override
    public String getPresentableText() {
      return myDelegate.getPresentableText();
    }

    @NotNull
    @Override
    public String getInternalCanonicalText() {
      return myDelegate.getInternalCanonicalText();
    }

    @Override
    public boolean isValid() {
      return myDelegate.isValid() && resolve().isValid();
    }

    @Override
    public boolean equalsToText(@NotNull @NonNls String text) {
      return myDelegate.equalsToText(text);
    }
  }

  private class CorrectedResolveResult implements PsiClassType.ClassResolveResult {
    private final PsiClass myPsiClass;
    private final PsiClass myMappedClass;
    private final PsiSubstitutor mySubstitutor;
    private final PsiClassType.ClassResolveResult myClassResolveResult;
    private volatile PsiSubstitutor myLazySubstitutor;

    public CorrectedResolveResult(
        PsiClass psiClass,
        PsiClass mappedClass,
        PsiSubstitutor substitutor,
        PsiClassType.ClassResolveResult classResolveResult) {
      myPsiClass = psiClass;
      myMappedClass = mappedClass;
      mySubstitutor = substitutor;
      myClassResolveResult = classResolveResult;
    }

    @NotNull
    @Override
    public PsiSubstitutor getSubstitutor() {
      PsiSubstitutor result = myLazySubstitutor;
      if (result == null) {
        myLazySubstitutor = result = mapSubstitutor(myPsiClass, myMappedClass, mySubstitutor);
      }
      return result;
    }

    @Override
    public PsiClass getElement() {
      return myMappedClass;
    }

    @Override
    public boolean isPackagePrefixPackageReference() {
      return myClassResolveResult.isPackagePrefixPackageReference();
    }

    @Override
    public boolean isAccessible() {
      return myClassResolveResult.isAccessible();
    }

    @Override
    public boolean isStaticsScopeCorrect() {
      return myClassResolveResult.isStaticsScopeCorrect();
    }

    @Override
    public PsiElement getCurrentFileResolveScope() {
      return myClassResolveResult.getCurrentFileResolveScope();
    }

    @Override
    public boolean isValidResult() {
      return myClassResolveResult.isValidResult();
    }
  }
}
/**
 * A service to hold a state of plugin changes in a current session (i.e. before the changes are
 * applied on restart).
 */
public class InstalledPluginsState {
  @Nullable
  public static InstalledPluginsState getInstanceIfLoaded() {
    return IdeaApplication.isLoaded() ? getInstance() : null;
  }

  public static InstalledPluginsState getInstance() {
    return ServiceManager.getService(InstalledPluginsState.class);
  }

  private final Object myLock = new Object();
  private final Map<PluginId, IdeaPluginDescriptor> myInstalledPlugins =
      ContainerUtil.newIdentityHashMap();
  private final Map<PluginId, IdeaPluginDescriptor> myUpdatedPlugins =
      ContainerUtil.newIdentityHashMap();
  private final Set<String> myOutdatedPlugins = ContainerUtil.newHashSet();

  @NotNull
  public Collection<IdeaPluginDescriptor> getInstalledPlugins() {
    synchronized (myLock) {
      return Collections.unmodifiableCollection(myInstalledPlugins.values());
    }
  }

  public boolean hasNewerVersion(@NotNull PluginId id) {
    synchronized (myLock) {
      return !wasUpdated(id) && myOutdatedPlugins.contains(id.getIdString());
    }
  }

  public boolean wasInstalled(@NotNull PluginId id) {
    synchronized (myLock) {
      return myInstalledPlugins.containsKey(id);
    }
  }

  public boolean wasUpdated(@NotNull PluginId id) {
    synchronized (myLock) {
      return myUpdatedPlugins.containsKey(id);
    }
  }

  /**
   * Should be called whenever a list of plugins is loaded from a repository to check if there is an
   * updated version.
   */
  public void onDescriptorDownload(@NotNull IdeaPluginDescriptor descriptor) {
    PluginId id = descriptor.getPluginId();
    IdeaPluginDescriptor existing = PluginManager.getPlugin(id);
    if (existing == null
        || (existing.isBundled() && !existing.allowBundledUpdate())
        || wasUpdated(id)) {
      return;
    }

    boolean supersedes =
        !PluginManagerCore.isIncompatible(descriptor)
            && (PluginDownloader.compareVersionsSkipBroken(existing, descriptor.getVersion()) > 0
                || PluginManagerCore.isIncompatible(existing));

    String idString = id.getIdString();

    synchronized (myLock) {
      if (supersedes) {
        myOutdatedPlugins.add(idString);
      } else {
        myOutdatedPlugins.remove(idString);
      }
    }
  }

  /** Should be called whenever a new plugin is installed or an existing one is updated. */
  public void onPluginInstall(@NotNull IdeaPluginDescriptor descriptor) {
    PluginId id = descriptor.getPluginId();
    boolean existing = PluginManager.isPluginInstalled(id);

    synchronized (myLock) {
      myOutdatedPlugins.remove(id.getIdString());
      if (existing) {
        myUpdatedPlugins.put(id, descriptor);
      } else {
        myInstalledPlugins.put(id, descriptor);
      }
    }
  }
}