// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.completion;

import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementDecorator;
import com.intellij.codeInsight.lookup.LookupElementPresentation;
import com.intellij.codeInsight.lookup.TypedLookupItem;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.ClassConditionKey;
import com.intellij.openapi.util.Key;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Objects;
import java.util.Set;

public class JavaChainLookupElement extends LookupElementDecorator<LookupElement> implements TypedLookupItem {
  public static final Key<Boolean> CHAIN_QUALIFIER = Key.create("CHAIN_QUALIFIER");
  public static final ClassConditionKey<JavaChainLookupElement> CLASS_CONDITION_KEY = ClassConditionKey.create(JavaChainLookupElement.class);
  private final LookupElement myQualifier;
  private final String mySeparator;

  public JavaChainLookupElement(LookupElement qualifier, LookupElement main) {
    this(qualifier, main, ".");
  }
  
  public JavaChainLookupElement(LookupElement qualifier, LookupElement main, String separator) {
    super(main);
    myQualifier = qualifier;
    mySeparator = separator;
  }

  @Override
  public @NotNull String getLookupString() {
    return maybeAddParentheses(myQualifier.getLookupString()) + mySeparator + getDelegate().getLookupString();
  }

  public LookupElement getQualifier() {
    return myQualifier;
  }

  @Override
  public @NotNull Set<String> getAllLookupStrings() {
    return Set.of(getLookupString());
  }

  @Override
  public @NotNull String toString() {
    return maybeAddParentheses(myQualifier.toString()) + mySeparator + getDelegate();
  }

  private String maybeAddParentheses(String s) {
    return getQualifierObject() instanceof PsiMethod ? s + "()" : s;
  }

  private @Nullable Object getQualifierObject() {
    Object qObject = myQualifier.getObject();
    if (qObject instanceof ResolveResult) {
      qObject = ((ResolveResult)qObject).getElement();
    }
    return qObject;
  }

  @Override
  public boolean isValid() {
    return super.isValid() && myQualifier.isValid();
  }

  @Override
  public void renderElement(@NotNull LookupElementPresentation presentation) {
    super.renderElement(presentation);
    final LookupElementPresentation qualifierPresentation = new LookupElementPresentation();
    myQualifier.renderElement(qualifierPresentation);
    String name = maybeAddParentheses(qualifierPresentation.getItemText());
    final String qualifierText = myQualifier.as(CastingLookupElementDecorator.CLASS_CONDITION_KEY) != null ? "(" + name + ")" : name;
    presentation.setItemText(qualifierText + mySeparator + presentation.getItemText());

    if (myQualifier instanceof JavaPsiClassReferenceElement) {
      presentation.appendTailText(((JavaPsiClassReferenceElement)myQualifier).getLocationString(), false);
    }
    if (qualifierPresentation.isStrikeout()) {
      presentation.setStrikeout(true);
    }
  }

  @Override
  public void handleInsert(@NotNull InsertionContext context) {
    final Document document = context.getEditor().getDocument();
    document.replaceString(context.getStartOffset(), context.getTailOffset(), ";");
    myQualifier.putUserData(CHAIN_QUALIFIER, true);
    final InsertionContext qualifierContext = CompletionUtil.emulateInsertion(context, context.getStartOffset(), myQualifier);
    OffsetKey oldStart = context.trackOffset(context.getStartOffset(), false);

    PsiDocumentManager.getInstance(context.getProject()).doPostponedOperationsAndUnblockDocument(document);

    int start = CharArrayUtil.shiftForward(context.getDocument().getCharsSequence(), context.getStartOffset(), " \t\n");
    if (shouldParenthesizeQualifier(context.getFile(), start, qualifierContext.getTailOffset())) {
      final String space = CodeStyle.getLanguageSettings(context.getFile()).SPACE_WITHIN_PARENTHESES ? " " : "";
      document.insertString(start, "(" + space);
      document.insertString(qualifierContext.getTailOffset(), space + ")");
    }

    char atTail = document.getCharsSequence().charAt(qualifierContext.getTailOffset());
    if (atTail != ';') {
      return;
    }
    document.replaceString(qualifierContext.getTailOffset(), qualifierContext.getTailOffset() + 1, mySeparator);

    CompletionUtil.emulateInsertion(getDelegate(), qualifierContext.getTailOffset() + mySeparator.length(), context);
    context.commitDocument();

    int formatStart = context.getOffset(oldStart);
    int formatEnd = context.getTailOffset();
    if (formatStart >= 0 && formatEnd >= 0) {
      CodeStyleManager.getInstance(context.getProject()).reformatText(context.getFile(), formatStart, formatEnd);
    }
  }

  protected boolean shouldParenthesizeQualifier(final PsiFile file, final int startOffset, final int endOffset) {
    PsiElement element = file.findElementAt(startOffset);
    if (element == null) {
      return false;
    }

    PsiElement last = element;
    while (element != null &&
           element.getTextRange().getStartOffset() >= startOffset && element.getTextRange().getEndOffset() <= endOffset &&
           !(element instanceof PsiExpressionStatement)) {
      last = element;
      element = element.getParent();
    }
    PsiExpression expr = PsiTreeUtil.getParentOfType(last, PsiExpression.class, false, PsiClass.class);
    if (expr == null) {
      return false;
    }
    if (expr.getTextRange().getEndOffset() > endOffset) {
      return true;
    }

    if (expr instanceof PsiJavaCodeReferenceElement ||
        expr instanceof PsiMethodCallExpression ||
        expr instanceof PsiNewExpression ||
        expr instanceof PsiArrayAccessExpression) {
      return false;
    }

    return true;
  }

  private @Nullable LookupElement getComparableQualifier() {
    final CastingLookupElementDecorator casting = myQualifier.as(CastingLookupElementDecorator.CLASS_CONDITION_KEY);
    LookupElement qualifier = casting == null ? myQualifier : casting.getDelegate();
    if (qualifier.getObject() instanceof PsiClass) return null;
    return qualifier;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;
    if (!mySeparator.equals(((JavaChainLookupElement)o).mySeparator)) return false;
    return Objects.equals(getComparableQualifier(), ((JavaChainLookupElement)o).getComparableQualifier());
  }

  @Override
  public int hashCode() {
    return 31 * super.hashCode() + Objects.hashCode(getComparableQualifier());
  }

  @Override
  public PsiType getType() {
    final Object object = getObject();
    if (object instanceof PsiMember) {
      return JavaCompletionUtil.getQualifiedMemberReferenceType(JavaCompletionUtil.getLookupElementType(myQualifier), (PsiMember)object);
    }
    return ((PsiVariable) object).getType();
  }

  /**
   * @param base base item to create a chain
   * @param item nested item
   * @return false if the chain looks redundant, and it's better not to suggest it.
   */
  static boolean isReasonableChain(LookupElement base, LookupElement item) {
    PsiElement baseElement = base.getPsiElement();
    PsiElement itemElement = item.getPsiElement();
    if (baseElement == null || itemElement == null) return true;
    if (baseElement.equals(itemElement)) return false;
    if (itemElement instanceof PsiMember member) {
      PsiClass itemClass = member.getContainingClass();
      if (itemClass == null || itemClass.equals(baseElement)) return true;
      if (PsiTreeUtil.isAncestor(itemClass, baseElement, true)) return false;
    }
    return true;
  }
}
