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

import com.intellij.codeInsight.folding.JavaCodeFoldingSettings;
import com.intellij.codeInsight.generation.OverrideImplementExploreUtil;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.FoldingGroup;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

final class ClosureFolding {
  private final @NotNull PsiAnonymousClass myAnonymousClass;
  private final @NotNull PsiNewExpression myNewExpression;
  private final @Nullable PsiClass myBaseClass;
  private final @NotNull JavaFoldingBuilderBase myBuilder;
  private final @NotNull PsiMethod myMethod;
  final @NotNull PsiCodeBlock methodBody;
  private final boolean myQuick;

  private ClosureFolding(@NotNull PsiAnonymousClass anonymousClass,
                         @NotNull PsiNewExpression newExpression,
                         boolean quick,
                         @Nullable PsiClass baseClass,
                         @NotNull JavaFoldingBuilderBase builder,
                         @NotNull PsiMethod method,
                         @NotNull PsiCodeBlock methodBody) {
    myAnonymousClass = anonymousClass;
    myNewExpression = newExpression;
    myQuick = quick;
    myBaseClass = baseClass;
    myBuilder = builder;
    myMethod = method;
    this.methodBody = methodBody;
  }

  @Nullable
  List<FoldingDescriptor> process(@NotNull Document document) {
    PsiJavaToken lbrace = methodBody.getLBrace();
    PsiJavaToken rbrace = methodBody.getRBrace();
    PsiElement classRBrace = myAnonymousClass.getRBrace();
    if (lbrace == null || rbrace == null || classRBrace == null) return null;

    CharSequence seq = document.getCharsSequence();
    int rangeStart = lbrace.getTextRange().getEndOffset();
    int rangeEnd = getContentRangeEnd(document, rbrace, classRBrace);

    String contents = getClosureContents(rangeStart, rangeEnd, seq);
    if (contents == null) return null;

    String header = getFoldingHeader();
    if (showSingleLineFolding(document, contents, header)) {
      return createDescriptors(classRBrace, trimStartSpaces(seq, rangeStart), trimTailSpaces(seq, rangeEnd), header + " ", " }");
    }

    return createDescriptors(classRBrace, rangeStart, rangeEnd, header, "}");
  }

  private static int trimStartSpaces(@NotNull CharSequence seq, int rangeStart) {
    return CharArrayUtil.shiftForward(seq, rangeStart, " \n\t");
  }

  private static int trimTailSpaces(@NotNull CharSequence seq, int rangeEnd) {
    return CharArrayUtil.shiftBackward(seq, rangeEnd - 1, " \n\t") + 1;
  }

  private static int getContentRangeEnd(@NotNull Document document, @NotNull PsiJavaToken rbrace, @NotNull PsiElement classRBrace) {
    CharSequence seq = document.getCharsSequence();
    int rangeEnd = rbrace.getTextRange().getStartOffset();

    int methodEndLine = document.getLineNumber(rangeEnd);
    int methodEndLineStart = document.getLineStartOffset(methodEndLine);
    if ("}".equals(seq.subSequence(methodEndLineStart, document.getLineEndOffset(methodEndLine)).toString().trim())) {
      int classEndStart = classRBrace.getTextRange().getStartOffset();
      int classEndCol = classEndStart - document.getLineStartOffset(document.getLineNumber(classEndStart));
      return classEndCol + methodEndLineStart;
    }
    return rangeEnd;
  }

  private boolean showSingleLineFolding(@NotNull Document document, @NotNull String contents, @NotNull String header) {
    return contents.indexOf('\n') < 0 &&
                      myBuilder.fitsRightMargin(myAnonymousClass, document, getClosureStartOffset(), getClosureEndOffset(), header.length() + contents.length() + 5);
  }

  private int getClosureEndOffset() {
    return myNewExpression.getTextRange().getEndOffset();
  }

  private int getClosureStartOffset() {
    return myNewExpression.getTextRange().getStartOffset();
  }

  private @Nullable List<FoldingDescriptor> createDescriptors(@NotNull PsiElement classRBrace,
                                                              int rangeStart,
                                                              int rangeEnd,
                                                              @NotNull String header,
                                                              @NotNull String footer) {
    if (rangeStart >= rangeEnd) return null;

    FoldingGroup group = FoldingGroup.newGroup("lambda");
    List<FoldingDescriptor> foldElements = new ArrayList<>();
    foldElements.add(new FoldingDescriptor(myNewExpression.getNode(), new TextRange(getClosureStartOffset(), rangeStart), group, header,
                                           JavaCodeFoldingSettings.getInstance().isCollapseLambdas(), Collections.emptySet()));
    if (rangeEnd + 1 < getClosureEndOffset()) {
      foldElements.add(new FoldingDescriptor(classRBrace.getNode(), new TextRange(rangeEnd, getClosureEndOffset()), group, footer,
                                             JavaCodeFoldingSettings.getInstance().isCollapseLambdas(), Collections.emptySet()));
    }
    return foldElements;
  }

  private static @Nullable String getClosureContents(int rangeStart, int rangeEnd, @NotNull CharSequence seq) {
    int firstLineStart = CharArrayUtil.shiftForward(seq, rangeStart, " \t");
    if (firstLineStart < seq.length() - 1 && seq.charAt(firstLineStart) == '\n') firstLineStart++;

    int lastLineEnd = CharArrayUtil.shiftBackward(seq, rangeEnd - 1, " \t");
    if (lastLineEnd > 0 && seq.charAt(lastLineEnd) == '\n') lastLineEnd--;
    if (lastLineEnd < firstLineStart) return null;
    return seq.subSequence(firstLineStart, lastLineEnd).toString();
  }

  private @NotNull String getFoldingHeader() {
    String methodName = shouldShowMethodName() ? myMethod.getName() : "";
    String type = myQuick ? "" : getOptionalLambdaType();
    String params = StringUtil.join(myMethod.getParameterList().getParameters(), psiParameter -> psiParameter.getName(), ", ");
    return type + methodName + "(" + params + ") " + myBuilder.rightArrow() + " {";
  }

  static @Nullable ClosureFolding prepare(@NotNull PsiAnonymousClass anonymousClass, boolean quick, @NotNull JavaFoldingBuilderBase builder) {
    PsiElement parent = anonymousClass.getParent();
    if (parent instanceof PsiNewExpression && hasNoArguments((PsiNewExpression)parent)) {
      PsiClass baseClass = quick ? null : anonymousClass.getBaseClassType().resolve();
      if (hasOnlyOneLambdaMethod(anonymousClass, !quick) && (quick || seemsLikeLambda(baseClass, anonymousClass))) {
        PsiMethod method = anonymousClass.getMethods()[0];
        PsiCodeBlock body = method.getBody();
        if (body != null) {
          return new ClosureFolding(anonymousClass, (PsiNewExpression)parent, quick, baseClass, builder, method, body);
        }
      }
    }
    return null;
  }

  private static boolean hasNoArguments(@NotNull PsiNewExpression expression) {
    PsiExpressionList argumentList = expression.getArgumentList();
    return argumentList != null && argumentList.isEmpty();
  }

  private static boolean hasOnlyOneLambdaMethod(@NotNull PsiAnonymousClass anonymousClass, boolean checkResolve) {
    PsiField[] fields = anonymousClass.getFields();
    if (fields.length != 0 && (fields.length != 1 || !CommonClassNames.SERIAL_VERSION_UID_FIELD_NAME.equals(fields[0].getName()) ||
                               !fields[0].hasModifierProperty(PsiModifier.STATIC))) {
      return false;
    }
    if (anonymousClass.getInitializers().length != 0 ||
        anonymousClass.getInnerClasses().length != 0 ||
        anonymousClass.getMethods().length != 1) {
      return false;
    }

    PsiMethod method = anonymousClass.getMethods()[0];
    if (method.hasModifierProperty(PsiModifier.SYNCHRONIZED)) {
      return false;
    }

    if (checkResolve) {
      for (PsiClassType type : method.getThrowsList().getReferencedTypes()) {
        if (type.resolve() == null) {
          return false;
        }
      }
    }

    return true;
  }

  static boolean seemsLikeLambda(@Nullable PsiClass baseClass, @NotNull PsiElement context) {
    if (baseClass == null || !PsiUtil.hasDefaultConstructor(baseClass, true)) return false;

    return !PsiUtil.isAvailable(JavaFeature.LAMBDA_EXPRESSIONS, context) || !LambdaUtil.isFunctionalClass(baseClass);
  }

  private @NotNull String getOptionalLambdaType() {
    if (myBuilder.shouldShowExplicitLambdaType(myAnonymousClass, myNewExpression)) {
      String baseClassName = Objects.requireNonNull(myAnonymousClass.getBaseClassType().resolve()).getName();
      if (baseClassName != null) {
        return "(" + baseClassName + ") ";
      }
    }
    return "";
  }

  private boolean shouldShowMethodName() {
    if (myBaseClass == null || !myBaseClass.hasModifierProperty(PsiModifier.ABSTRACT)) return true;

    for (PsiMethod method : myBaseClass.getMethods()) {
      if (method.hasModifierProperty(PsiModifier.ABSTRACT)) {
        return false;
      }
    }

    try {
      return OverrideImplementExploreUtil.getMethodSignaturesToImplement(myBaseClass).isEmpty();
    }
    catch (IndexNotReadyException e) {
      return true;
    }
  }
}
