// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.impl.source.resolve.reference.impl;

import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.util.TextRange;
import com.intellij.patterns.ElementPattern;
import com.intellij.patterns.PatternCondition;
import com.intellij.psi.*;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiConsumer;

import static com.intellij.codeInsight.completion.JavaCompletionContributor.isInJavaContext;
import static com.intellij.patterns.PsiJavaPatterns.*;
import static com.intellij.psi.impl.source.resolve.reference.impl.JavaReflectionReferenceUtil.*;

public final class JavaReflectionCompletionContributor extends CompletionContributor implements DumbAware {
  private static final String CONSTRUCTOR = "getConstructor";
  private static final String DECLARED_CONSTRUCTOR = "getDeclaredConstructor";
  private static final String ANNOTATION = "getAnnotation";
  private static final String DECLARED_ANNOTATION = "getDeclaredAnnotation";
  private static final String ANNOTATIONS_BY_TYPE = "getAnnotationsByType";
  private static final String DECLARED_ANNOTATIONS_BY_TYPE = "getDeclaredAnnotationsByType";
  private static final String ANNOTATED_ELEMENT = "java.lang.reflect.AnnotatedElement";

  private static final Set<String> DECLARED_NAMES =
    Set.of(DECLARED_CONSTRUCTOR, DECLARED_ANNOTATION, DECLARED_ANNOTATIONS_BY_TYPE);
  private static final ElementPattern<? extends PsiElement> CONSTRUCTOR_ARGUMENTS = psiElement(PsiExpressionList.class)
    .withParent(psiExpression().methodCall(
      psiMethod()
        .withName(CONSTRUCTOR, DECLARED_CONSTRUCTOR)
        .definedInClass(CommonClassNames.JAVA_LANG_CLASS)));

  private static final ElementPattern<? extends PsiElement> ANNOTATION_ARGUMENTS = psiElement(PsiExpressionList.class)
    .withParent(psiExpression().methodCall(
      psiMethod()
        .withName(ANNOTATION, DECLARED_ANNOTATION, ANNOTATIONS_BY_TYPE, DECLARED_ANNOTATIONS_BY_TYPE)
        .with(new MethodDefinedInInterfacePatternCondition(ANNOTATED_ELEMENT))));

  private static final ElementPattern<PsiElement> BEGINNING_OF_CONSTRUCTOR_ARGUMENTS = beginningOfArguments(CONSTRUCTOR_ARGUMENTS);

  private static final ElementPattern<PsiElement> BEGINNING_OF_ANNOTATION_ARGUMENTS = beginningOfArguments(ANNOTATION_ARGUMENTS);

  private static ElementPattern<PsiElement> beginningOfArguments(ElementPattern<? extends PsiElement> argumentsPattern) {
    return psiElement().afterLeaf("(").withParent(
      or(psiExpression().withParent(argumentsPattern),
         psiElement().withParent(PsiTypeElement.class) // special case for getConstructor(int.class) because 'int' is a keyword
           .withSuperParent(2, PsiClassObjectAccessExpression.class)
           .withSuperParent(3, argumentsPattern)));
  }

  @Override
  public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
    if (parameters.getCompletionType() != CompletionType.BASIC) {
      return;
    }

    final PsiElement position = parameters.getPosition();
    if (!isInJavaContext(position)) {
      return;
    }

    if (BEGINNING_OF_ANNOTATION_ARGUMENTS.accepts(position)) {
      addVariants(position, (psiClass, isDeclared) -> addAnnotationClasses(psiClass, isDeclared, result));
      //TODO handle annotations on fields and methods
    }
    else if (BEGINNING_OF_CONSTRUCTOR_ARGUMENTS.accepts(position)) {
      addVariants(position, (psiClass, isDeclared) -> addConstructorParameterTypes(psiClass, isDeclared, result));
    }
    else if (JavaReflectionReferenceContributor.Holder.CLASS_PATTERN.accepts(position.getParent())) {
      JavaClassNameCompletionContributor.addAllClasses(parameters, parameters.getInvocationCount() <= 1, result.getPrefixMatcher(), result);
    }
  }

  private static void addVariants(PsiElement position, BiConsumer<? super PsiClass, ? super Boolean> variantAdder) {
    PsiMethodCallExpression methodCall = PsiTreeUtil.getParentOfType(position, PsiMethodCallExpression.class);
    if (methodCall != null) {
      ReflectiveClass ownerClass = getReflectiveClass(methodCall.getMethodExpression().getQualifierExpression());
      if (ownerClass != null) {
        String methodName = methodCall.getMethodExpression().getReferenceName();
        if (methodName != null) {
          variantAdder.accept(ownerClass.getPsiClass(), DECLARED_NAMES.contains(methodName));
        }
      }
    }
  }

  private static void addAnnotationClasses(@NotNull PsiClass psiClass, boolean isDeclared, @NotNull CompletionResultSet result) {
    Set<PsiAnnotation> declaredAnnotations =
      isDeclared ? Collections.unmodifiableSet(new HashSet<>(Arrays.asList(AnnotationUtil.getAllAnnotations(psiClass, false, null, false)))) : null;

    PsiAnnotation[] annotations = AnnotationUtil.getAllAnnotations(psiClass, true, null, false);
    for (PsiAnnotation annotation : annotations) {
      PsiJavaCodeReferenceElement referenceElement = annotation.getNameReferenceElement();
      if (referenceElement != null) {
        PsiElement resolved = referenceElement.resolve();
        if (resolved instanceof PsiClass annotationClass) {
          String className = annotationClass.getName();
          if (className != null) {
            LookupElement lookupElement = LookupElementBuilder.createWithIcon(annotationClass)
              .withPresentableText(className + ".class")
              .withInsertHandler(JavaReflectionCompletionContributor::handleAnnotationClassInsertion);
            if (isDeclared) {
              lookupElement = withPriority(lookupElement, declaredAnnotations.contains(annotation));
            }
            result.addElement(lookupElement);
          }
        }
      }
    }
  }

  private static void addConstructorParameterTypes(@NotNull PsiClass psiClass, boolean isDeclared, @NotNull CompletionResultSet result) {
    PsiMethod[] constructors = psiClass.getConstructors();
    for (PsiMethod constructor : constructors) {
      LookupElement lookupElement = JavaLookupElementBuilder.forMethod(constructor, PsiSubstitutor.EMPTY)
        .withInsertHandler(JavaReflectionCompletionContributor::handleConstructorSignatureInsertion);
      if (isDeclared) {
        lookupElement = withPriority(lookupElement, isPublic(constructor));
      }
      result.addElement(lookupElement);
    }
  }

  private static void handleAnnotationClassInsertion(@NotNull InsertionContext context, @NotNull LookupElement item) {
    Object object = item.getObject();
    if (object instanceof PsiClass) {
      String className = ((PsiClass)object).getQualifiedName();
      if (className != null) {
        handleParametersInsertion(context, className + ".class");
      }
    }
  }

  private static void handleConstructorSignatureInsertion(@NotNull InsertionContext context, @NotNull LookupElement item) {
    Object object = item.getObject();
    if (object instanceof PsiMethod) {
      String text = getParameterTypesText((PsiMethod)object);
      if (text != null) {
        handleParametersInsertion(context, text);
      }
    }
  }

  private static void handleParametersInsertion(@NotNull InsertionContext context, @NotNull String text) {
    PsiElement newElement = PsiUtilCore.getElementAtOffset(context.getFile(), context.getStartOffset());
    PsiExpressionList parameterList = PsiTreeUtil.getParentOfType(newElement, PsiExpressionList.class);
    if (parameterList != null) {
      final TextRange range = parameterList.getTextRange();
      context.getDocument().replaceString(range.getStartOffset(), range.getEndOffset(), "(" + text + ")");
      context.commitDocument();
      shortenArgumentsClassReferences(context);
    }
  }

  private static class MethodDefinedInInterfacePatternCondition extends PatternCondition<PsiMethod> {
    private final String myInterfaceName;

    MethodDefinedInInterfacePatternCondition(@NotNull String interfaceName) {
      super("definedInInterface");
      myInterfaceName = interfaceName;
    }

    @Override
    public boolean accepts(@NotNull PsiMethod method, ProcessingContext context) {
      PsiClass containingClass = method.getContainingClass();
      return InheritanceUtil.isInheritor(containingClass, false, myInterfaceName);
    }
  }
}
