// 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.generation;

import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.AnnotationTargetUtil;
import com.intellij.codeInsight.ExceptionUtil;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInsight.daemon.impl.quickfix.CreateFromUsageUtils;
import com.intellij.codeInsight.intention.AddAnnotationPsiFix;
import com.intellij.lang.ASTNode;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.*;
import com.intellij.psi.impl.light.LightElement;
import com.intellij.psi.impl.light.LightMethod;
import com.intellij.psi.impl.light.LightTypeElement;
import com.intellij.psi.impl.source.codeStyle.JavaCodeStyleManagerImpl;
import com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.*;
import com.intellij.refactoring.util.ModifierListUtil;
import com.intellij.util.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.UniqueNameGenerator;
import com.siyeh.ig.psiutils.CommentTracker;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.java.generate.GenerationUtil;
import org.jetbrains.java.generate.exception.GenerateCodeException;
import org.jetbrains.java.generate.template.TemplatesManager;

import java.util.*;
import java.util.stream.Collectors;

public final class GenerateMembersUtil {
  private static final Logger LOG = Logger.getInstance(GenerateMembersUtil.class);

  private GenerateMembersUtil() {
  }

  public static @NotNull <T extends GenerationInfo> List<T> insertMembersAtOffset(@NotNull PsiFile file,
                                                                                  int offset,
                                                                                  @NotNull List<T> memberPrototypes) throws IncorrectOperationException {
    return insertMembersAtOffset(file, offset, memberPrototypes, leaf -> findClassAtOffset(file, leaf));
  }

  public static @NotNull <T extends GenerationInfo> List<T> insertMembersAtOffset(@NotNull PsiClass psiClass,
                                                                                  int offset,
                                                                                  @NotNull List<T> memberPrototypes) throws IncorrectOperationException {
    return insertMembersAtOffset(psiClass.getContainingFile(), offset, memberPrototypes, leaf -> psiClass);
  }

  private static @NotNull <T extends GenerationInfo> List<T> insertMembersAtOffset(@NotNull PsiFile file,
                                                                                   int offset,
                                                                                   @NotNull List<T> memberPrototypes,
                                                                                   @NotNull Function<? super PsiElement, ? extends PsiClass> aClassFunction) throws IncorrectOperationException {
    if (memberPrototypes.isEmpty()) return memberPrototypes;
    final PsiElement leaf = file.findElementAt(offset);
    PsiElement anchor;
    PsiClass aClass;
    if (leaf != null){
      aClass = aClassFunction.fun(leaf);
      if (aClass == null) return Collections.emptyList();
      anchor = memberPrototypes.get(0).findInsertionAnchor(aClass, leaf);
    }
    else if (file instanceof PsiJavaFile javaFile &&
             javaFile.getClasses().length == 1 &&
             javaFile.getClasses()[0] instanceof PsiImplicitClass implicitClass) {
      anchor = null;
      aClass = implicitClass;
    }
    else {
      return Collections.emptyList();
    }

    if (anchor instanceof PsiWhiteSpace) {
      final ASTNode spaceNode = anchor.getNode();
      anchor = anchor.getNextSibling();

      assert spaceNode != null;
      if (spaceNode.getStartOffset() <= offset && spaceNode.getStartOffset() + spaceNode.getTextLength() >= offset) {
        String whiteSpace = spaceNode.getText().substring(0, offset - spaceNode.getStartOffset());
        if (!StringUtil.containsLineBreak(whiteSpace)) {
          // There is a possible case that the caret is located at the end of the line that already contains expression, say, we
          // want to override particular method while caret is located after the field.
          // Example - consider that we want to override toString() method at the class below:
          //     class Test {
          //         int i;<caret>
          //     }
          // We want to add line feed then in order to avoid situation like below:
          //     class Test {
          //         int i;@Override String toString() {
          //             super.toString();
          //         }
          //     }
          whiteSpace += "\n";
        }
        final PsiParserFacade parserFacade = PsiParserFacade.getInstance(file.getProject());
        final ASTNode singleNewLineWhitespace = parserFacade.createWhiteSpaceFromText(whiteSpace).getNode();
        if (singleNewLineWhitespace != null) {
          spaceNode.getTreeParent().replaceChild(spaceNode, singleNewLineWhitespace); // See http://jetbrains.net/jira/browse/IDEADEV-12837
        }
      }
    }

    // Q: shouldn't it be somewhere in PSI?
    PsiElement element = anchor;
    while (true) {
      if (element == null) break;
      if (element instanceof PsiField || element instanceof PsiMethod || element instanceof PsiClassInitializer) break;
      element = element.getNextSibling();
    }
    if (element instanceof PsiField field) {
      PsiTypeElement typeElement = field.getTypeElement();
      if (typeElement != null && !field.equals(typeElement.getParent())) {
        field.normalizeDeclaration();
        anchor = field;
      }
    }

    return insertMembersBeforeAnchor(aClass, anchor, memberPrototypes);
  }

  public static @NotNull <T extends GenerationInfo> List<T> insertMembersBeforeAnchor(@NotNull PsiClass aClass, @Nullable PsiElement anchor, @NotNull List<T> memberPrototypes) throws IncorrectOperationException {
    boolean before = true;
    for (T memberPrototype : memberPrototypes) {
      memberPrototype.insert(aClass, anchor, before);
      before = false;
      anchor = memberPrototype.getPsiMember();
    }
    return memberPrototypes;
  }

  /**
   * @see GenerationInfo#positionCaret(Editor, boolean)
   */
  public static void positionCaret(@NotNull Editor editor, @NotNull PsiElement firstMember, boolean toEditMethodBody) {
    LOG.assertTrue(firstMember.isValid());
    Project project = firstMember.getProject();

    if (toEditMethodBody) {
      PsiMethod method = (PsiMethod)firstMember;
      PsiCodeBlock body = method.getBody();
      if (body != null) {
        PositionInfo info = getPositionInfo(body);
        int start = info.start();
        int end = info.end();
        boolean adjustLineIndent = info.adjustLineIndent();

        editor.getCaretModel().moveToOffset(Math.min(start, end));
        editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
        if (start < end) {
          //Not an empty body
          editor.getSelectionModel().setSelection(start, end);
        } else if (adjustLineIndent) {
          Document document = editor.getDocument();
          RangeMarker marker = document.createRangeMarker(start, start);
          PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document);
          if (marker.isValid()) {
            CodeStyleManager.getInstance(project).adjustLineIndent(document, marker.getStartOffset());
          }
        }
        return;
      }
    }

    int offset = getOffsetInMethod(firstMember);

    editor.getCaretModel().moveToOffset(offset);
    editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
    editor.getSelectionModel().removeSelection();
  }

  public static void positionCaret(@NotNull ModNavigator updater, @NotNull PsiElement firstMember, boolean toEditMethodBody) {
    LOG.assertTrue(firstMember.isValid());
    Project project = firstMember.getProject();

    if (toEditMethodBody) {
      PsiMethod method = (PsiMethod)firstMember;
      PsiCodeBlock body = method.getBody();
      if (body != null) {
        PositionInfo info = getPositionInfo(body);

        updater.moveCaretTo(Math.min(info.start(), info.end()));
        if (info.start() < info.end()) {
          //Not an empty body
          updater.select(TextRange.create(info.start(), info.end()));
        } else if (info.adjustLineIndent()) {
          Document document = firstMember.getContainingFile().getViewProvider().getDocument();
          RangeMarker marker = document.createRangeMarker(info.start(), info.start());
          PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document);
          if (marker.isValid()) {
            CodeStyleManager.getInstance(project).adjustLineIndent(document, marker.getStartOffset());
          }
        }
        return;
      }
    }

    int offset = getOffsetInMethod(firstMember);
    updater.moveCaretTo(offset);
  }

  private static @NotNull PositionInfo getPositionInfo(PsiCodeBlock body) {
    PsiElement firstBodyElement = body.getFirstBodyElement();
    PsiElement l = firstBodyElement;
    while (l instanceof PsiWhiteSpace) l = l.getNextSibling();
    if (l == null) l = body;
    PsiElement lastBodyElement = body.getLastBodyElement();
    PsiElement r = lastBodyElement;
    while (r instanceof PsiWhiteSpace) r = r.getPrevSibling();
    if (r == null) r = body;

    int start = l.getTextRange().getStartOffset();
    int end = r.getTextRange().getEndOffset();

    boolean adjustLineIndent = false;

    // body is whitespace
    if (start > end &&
        firstBodyElement == lastBodyElement &&
        firstBodyElement instanceof PsiWhiteSpaceImpl
    ) {
      CharSequence chars = ((PsiWhiteSpaceImpl)firstBodyElement).getChars();
      if (chars.length() > 1 && chars.charAt(0) == '\n' && chars.charAt(1) == '\n') {
        start = end = firstBodyElement.getTextRange().getStartOffset() + 1;
        adjustLineIndent = true;
      }
    }
    return new PositionInfo(start, end, adjustLineIndent);
  }

  private record PositionInfo(int start, int end, boolean adjustLineIndent) {
  }

  private static int getOffsetInMethod(@NotNull PsiElement member) {
    if (member instanceof PsiMethod method) {
      PsiCodeBlock body = method.getBody();
      if (body == null) {
        return method.getTextRange().getStartOffset();
      }
      PsiJavaToken lBrace = body.getLBrace();
      assert lBrace != null : member.getText();
      return lBrace.getTextRange().getEndOffset();
    }
    return member.getTextRange().getStartOffset();
  }

  public static PsiElement insert(@NotNull PsiClass aClass, @NotNull PsiMember member, @Nullable PsiElement anchor, boolean before) throws IncorrectOperationException {
    if (member instanceof PsiMethod) {
      if (!aClass.isInterface()) {
        final PsiParameter[] parameters = ((PsiMethod)member).getParameterList().getParameters();
        final boolean generateFinals = JavaCodeStyleSettings.getInstance(aClass.getContainingFile()).GENERATE_FINAL_PARAMETERS;
        for (final PsiParameter parameter : parameters) {
          PsiUtil.setModifierProperty(parameter, PsiModifier.FINAL, generateFinals);
        }
      }
    }

    if (anchor != null) {
      return before ? aClass.addBefore(member, anchor) : aClass.addAfter(member, anchor);
    }
    else {
      return aClass.add(member);
    }
  }

  private static @Nullable PsiClass findClassAtOffset(@NotNull PsiFile file, PsiElement leaf) {
    PsiElement element = leaf;
    while (element != null && !(element instanceof PsiFile)) {
      if (element instanceof PsiClass psiClass && !(element instanceof PsiTypeParameter)) {
        if (psiClass.isEnum()) {
          PsiElement lastChild = null;
          for (PsiElement child = psiClass.getFirstChild(); child != null; child = child.getNextSibling()) {
            if (child instanceof PsiJavaToken && ";".equals(child.getText())) {
              lastChild = child;
              break;
            }
            else if (child instanceof PsiJavaToken && ",".equals(child.getText()) || child instanceof PsiEnumConstant) {
              lastChild = child;
            }
          }
          if (lastChild != null) {
            int adjustedOffset = lastChild.getTextRange().getEndOffset();
            if (leaf.getTextRange().getEndOffset() <= adjustedOffset) return findClassAtOffset(file, file.findElementAt(adjustedOffset));
          }
        }
        return psiClass;
      }
      element = element.getParent();
    }
    return null;
  }

  public static @NotNull PsiMethod substituteGenericMethod(@NotNull PsiMethod method, @NotNull PsiSubstitutor substitutor) {
    return substituteGenericMethod(method, substitutor, null);
  }

  public static @NotNull PsiMethod substituteGenericMethod(@NotNull PsiMethod sourceMethod,
                                                           @NotNull PsiSubstitutor substitutor,
                                                           @Nullable PsiElement target) {
    final Project project = sourceMethod.getProject();
    final JVMElementFactory factory = getFactory(sourceMethod.getProject(), target);
    final JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(project);

    try {
      PsiMethod resultMethod = createMethod(factory, sourceMethod, target);
      copyModifiers(sourceMethod.getModifierList(), resultMethod.getModifierList());
      PsiSubstitutor collisionResolvedSubstitutor =
        substituteTypeParameters(factory, target, sourceMethod.getTypeParameterList(), resultMethod.getTypeParameterList(), substitutor, sourceMethod);
      substituteReturnType(PsiManager.getInstance(project), resultMethod, sourceMethod.getReturnType(), collisionResolvedSubstitutor);
      PsiTypeElement typeElement = resultMethod.getReturnTypeElement();
      if (typeElement != null && typeElement.getText().startsWith("@")) {
        // If return type is annotated, substituteReturnType will add the annotation into type element,
        // so the method should be reparsed to move it to the modifier list
        resultMethod = factory.createMethodFromText(resultMethod.getText(), target);
      }
      substituteParameters(factory, codeStyleManager, sourceMethod.getParameterList(), resultMethod.getParameterList(), collisionResolvedSubstitutor, target);
      copyDocComment(sourceMethod, resultMethod, factory);
      GlobalSearchScope scope = sourceMethod.getResolveScope();
      List<PsiClassType> thrownTypes = ExceptionUtil.collectSubstituted(collisionResolvedSubstitutor, sourceMethod.getThrowsList().getReferencedTypes(), scope);
      if (target instanceof PsiClass) {
        for (PsiMethod psiMethod : ((PsiClass)target).findMethodsBySignature(sourceMethod, true)) {
          if (psiMethod != null && psiMethod != sourceMethod && !MethodSignatureUtil.isSuperMethod(psiMethod, sourceMethod)) {
            PsiClass aSuper = psiMethod.getContainingClass();
            if (aSuper != null && aSuper != target) {
              PsiSubstitutor superClassSubstitutor = TypeConversionUtil.getSuperClassSubstitutor(aSuper, (PsiClass)target, PsiSubstitutor.EMPTY);
              ExceptionUtil.retainExceptions(thrownTypes, ExceptionUtil.collectSubstituted(superClassSubstitutor, psiMethod.getThrowsList().getReferencedTypes(), scope));
            }
          }
        }
      }
      substituteThrows(factory, resultMethod.getThrowsList(), collisionResolvedSubstitutor, sourceMethod, thrownTypes);
      sortModifiers(resultMethod, sourceMethod);
      return resultMethod;
    }
    catch (IncorrectOperationException e) {
      LOG.error(e);
      return sourceMethod;
    }
  }

  /**
   * Sorts the modifiers of the source method based on the order of the modifiers in the target method.
   *
   * @param targetMethod The target method whose modifier order will be used.
   * @param sourceMethod The source method whose modifiers will be sorted.
   */
  public static void sortModifiers(@NotNull PsiMethod targetMethod, @Nullable PsiMethod sourceMethod) {
    if (targetMethod instanceof LightMethod ||
        !targetMethod.isWritable() ||
        targetMethod.getLanguage() != JavaLanguage.INSTANCE) {
      return;
    }
    PsiModifierList newList = targetMethod.getModifierList();
    Map<@NotNull String, @NotNull PsiAnnotation> oldAnnotations = new HashMap<>();
    if (sourceMethod != null) {
      PsiModifierList oldList = sourceMethod.getModifierList();
      for (@NotNull PsiElement child : oldList.getChildren()) {
        if (child instanceof PsiAnnotation psiAnnotation) {
          String annotationQualifiedName = psiAnnotation.getQualifiedName();
          if (annotationQualifiedName != null) {
            oldAnnotations.put(annotationQualifiedName, psiAnnotation);
          }
        }
      }
    }
    Comparator<PsiAnnotation> comparator = Comparator.comparingInt(a -> {
      String q = a.getQualifiedName();
      if (q == null) return -1;
      PsiAnnotation old = oldAnnotations.get(q);
      //Probably, it is better to have it at the bottom if it is new.
      //For example, it preserves the current behavior for @Override
      if (old == null) return Integer.MAX_VALUE;
      return old.getTextRange().getStartOffset();
    });
    PsiModifierList newList2 = ModifierListUtil.createSortedModifierList(newList, comparator, false);
    if (newList2 != null) {
      new CommentTracker().replace(newList, newList2);
    }
  }

  private static void copyModifiers(@NotNull PsiModifierList sourceModifierList,
                                    @NotNull PsiModifierList targetModifierList) {
    VisibilityUtil.setVisibility(targetModifierList, VisibilityUtil.getVisibilityModifier(sourceModifierList));
  }

  private static @NotNull PsiSubstitutor substituteTypeParameters(@NotNull JVMElementFactory factory,
                                                                  @Nullable PsiElement target,
                                                                  @Nullable PsiTypeParameterList sourceTypeParameterList,
                                                                  @Nullable PsiTypeParameterList targetTypeParameterList,
                                                                  @NotNull PsiSubstitutor substitutor,
                                                                  @NotNull PsiMethod sourceMethod) {
    if (sourceTypeParameterList == null || targetTypeParameterList == null || PsiUtil.isRawSubstitutor(sourceMethod, substitutor)) {
      return substitutor;
    }

    final Map<PsiTypeParameter, PsiType> substitutionMap = new HashMap<>(substitutor.getSubstitutionMap());
    for (PsiTypeParameter typeParam : sourceTypeParameterList.getTypeParameters()) {
      final PsiTypeParameter substitutedTypeParam = substituteTypeParameter(factory, typeParam, substitutor, sourceMethod);

      final PsiTypeParameter resolvedTypeParam = resolveTypeParametersCollision(factory, sourceTypeParameterList, target,
                                                                                substitutedTypeParam, substitutor);
      targetTypeParameterList.add(resolvedTypeParam);
      if (substitutedTypeParam != resolvedTypeParam) {
        substitutionMap.put(typeParam, factory.createType(resolvedTypeParam));
      }
    }
    return substitutionMap.isEmpty() ? substitutor : factory.createSubstitutor(substitutionMap);
  }

  private static @NotNull PsiTypeParameter resolveTypeParametersCollision(@NotNull JVMElementFactory factory,
                                                                          @NotNull PsiTypeParameterList sourceTypeParameterList,
                                                                          @Nullable PsiElement target,
                                                                          @NotNull PsiTypeParameter typeParam,
                                                                          @NotNull PsiSubstitutor substitutor) {
    String typeParamName = typeParam.getName();
    for (PsiType type : substitutor.getSubstitutionMap().values()) {
      if (type != null && Objects.equals(type.getCanonicalText(), typeParamName)) {
        final String newName = suggestUniqueTypeParameterName(typeParamName, sourceTypeParameterList, PsiTreeUtil.getParentOfType(target, PsiClass.class, false));
        return factory.createTypeParameter(newName, typeParam.getSuperTypes());
      }
    }
    return factory.createTypeParameter(typeParamName, typeParam.getSuperTypes());
  }

  private static @NotNull String suggestUniqueTypeParameterName(@NotNull String baseName, @NotNull PsiTypeParameterList typeParameterList, @Nullable PsiClass targetClass) {
    int i = 0;
    while (true) {
      final String newName = baseName + ++i;
      if (checkUniqueTypeParameterName(newName, typeParameterList) && (targetClass == null || checkUniqueTypeParameterName(newName, targetClass.getTypeParameterList()))) {
        return newName;
      }
    }
  }


  private static boolean checkUniqueTypeParameterName(@NotNull String baseName, @Nullable PsiTypeParameterList typeParameterList) {
    if (typeParameterList == null) return true;

    for (PsiTypeParameter typeParameter : typeParameterList.getTypeParameters()) {
      if (Objects.equals(typeParameter.getName(), baseName)) {
        return false;
      }
    }
    return true;
  }


  private static @NotNull PsiTypeParameter substituteTypeParameter(@NotNull JVMElementFactory factory,
                                                                   @NotNull PsiTypeParameter typeParameter,
                                                                   @NotNull PsiSubstitutor substitutor,
                                                                   @NotNull PsiMethod sourceMethod) {
    if (typeParameter instanceof LightElement) {
      List<PsiClassType> substitutedSupers = ContainerUtil.map(typeParameter.getSuperTypes(), t -> ObjectUtils.notNull(toClassType(substitutor.substitute(t)), t));
      return factory.createTypeParameter(Objects.requireNonNull(typeParameter.getName()), substitutedSupers.toArray(PsiClassType.EMPTY_ARRAY));
    }
    final PsiElement copy = ObjectUtils.notNull(typeParameter instanceof PsiCompiledElement ? ((PsiCompiledElement)typeParameter).getMirror() : typeParameter, typeParameter).copy();
    LOG.assertTrue(copy != null, typeParameter);
    final Map<PsiElement, PsiElement> replacementMap = new HashMap<>();
    copy.accept(new JavaRecursiveElementVisitor() {
      @Override
      public void visitReferenceElement(@NotNull PsiJavaCodeReferenceElement reference) {
        super.visitReferenceElement(reference);
        final PsiElement resolve = reference.resolve();
        if (resolve instanceof PsiTypeParameter) {
          final PsiType type = factory.createType((PsiTypeParameter)resolve);
          replacementMap.put(reference, factory.createReferenceElementByType((PsiClassType)substituteType(substitutor, type, sourceMethod, null)));
        }
      }
    });
    return (PsiTypeParameter)CommonJavaRefactoringUtil.replaceElementsWithMap(copy, replacementMap);
  }

  private static PsiClassType toClassType(PsiType type) {
    if (type instanceof PsiClassType) {
      return (PsiClassType)type;
    }
    if (type instanceof PsiCapturedWildcardType) {
      return toClassType(((PsiCapturedWildcardType)type).getUpperBound());
    }
    if (type instanceof PsiWildcardType) {
      return toClassType(((PsiWildcardType)type).getBound());
    }
    return null;
  }

  private static void substituteParameters(@NotNull JVMElementFactory factory,
                                           @NotNull JavaCodeStyleManager codeStyleManager,
                                           @NotNull PsiParameterList sourceParameterList,
                                           @NotNull PsiParameterList targetParameterList,
                                           @NotNull PsiSubstitutor substitutor, PsiElement target) {
    final PsiParameter[] parameters = sourceParameterList.getParameters();
    final PsiParameter[] newParameters = overriddenParameters(parameters, factory, codeStyleManager, substitutor, target);
    for (int i = 0; i < newParameters.length; i++) {
      final PsiParameter newParameter = newParameters[i];
      copyOrReplaceModifierList(parameters[i], target, newParameter);
      targetParameterList.add(newParameter);
    }
  }

  public static PsiParameter @NotNull [] overriddenParameters(PsiParameter @NotNull [] parameters,
                                                              @NotNull JVMElementFactory factory,
                                                              @NotNull JavaCodeStyleManager codeStyleManager,
                                                              @NotNull PsiSubstitutor substitutor,
                                                              @Nullable PsiElement target) {
    PsiParameter[] result = new PsiParameter[parameters.length];
    UniqueNameGenerator generator = new UniqueNameGenerator();

    for (int i = 0; i < parameters.length; i++) {
      PsiParameter parameter = parameters[i];
      final PsiType parameterType = parameter.getType();
      PsiElement declarationScope = parameter.getDeclarationScope();
      PsiType substituted = declarationScope instanceof PsiTypeParameterListOwner ? substituteType(substitutor, parameterType, (PsiTypeParameterListOwner)declarationScope, parameter.getModifierList())
                                                                                  : parameterType;
      String paramName = parameter.getName();
      boolean isBaseNameGenerated = true;
      final boolean isSubstituted = substituted.equals(parameterType);
      if (!isSubstituted && isBaseNameGenerated(codeStyleManager, TypeConversionUtil.erasure(parameterType), paramName)) {
        isBaseNameGenerated = false;
      }

      if (isBaseNameGenerated && !isSubstituted && isBaseNameGenerated(codeStyleManager, parameterType, paramName) ||
          !factory.isValidParameterName(paramName)) {
        String[] names = codeStyleManager.suggestVariableName(VariableKind.PARAMETER, null, null, substituted).names;
        if (names.length > 0) {
          paramName = generator.generateUniqueName(names[0]);
        }
        else {
          paramName = generator.generateUniqueName("p");
        }
      }
      else if (!generator.value(paramName)) {
        paramName = generator.generateUniqueName(paramName);
      }
      generator.addExistingName(paramName);
      PsiType expressionType = GenericsUtil.getVariableTypeByExpressionType(substituted);
      if (expressionType instanceof PsiArrayType && substituted instanceof PsiEllipsisType) {
        expressionType = new PsiEllipsisType(((PsiArrayType)expressionType).getComponentType())
          .annotate(expressionType.getAnnotationProvider());
      }
      result[i] = factory.createParameter(paramName, expressionType, target);
    }
    return result;
  }

  private static void substituteThrows(@NotNull JVMElementFactory factory,
                                       @NotNull PsiReferenceList targetThrowsList,
                                       @NotNull PsiSubstitutor substitutor,
                                       @NotNull PsiMethod sourceMethod,
                                       @NotNull List<? extends PsiClassType> thrownTypes) {
    for (PsiClassType thrownType : thrownTypes) {
      targetThrowsList.add(factory.createReferenceElementByType((PsiClassType)substituteType(substitutor, thrownType, sourceMethod, null)));
    }
  }

  private static void copyDocComment(@NotNull PsiMethod source, @NotNull PsiMethod target, @NotNull JVMElementFactory factory) {
    final PsiElement navigationElement = source.getNavigationElement();
    if (navigationElement instanceof PsiDocCommentOwner) {
      final PsiDocComment docComment = ((PsiDocCommentOwner)navigationElement).getDocComment();
      if (docComment != null) {
        target.addAfter(factory.createDocCommentFromText(docComment.getText()), null);
      }
    }
    final PsiParameter[] sourceParameters = source.getParameterList().getParameters();
    final PsiParameterList targetParameterList = target.getParameterList();
    CommonJavaRefactoringUtil.fixJavadocsForParams(target, Set.of(targetParameterList.getParameters()), pair -> {
      final int parameterIndex = targetParameterList.getParameterIndex(pair.first);
      if (parameterIndex >= 0 && parameterIndex < sourceParameters.length) {
        return Comparing.strEqual(pair.second, sourceParameters[parameterIndex].getName());
      }
      return false;
    });
  }

  private static @NotNull PsiMethod createMethod(@NotNull JVMElementFactory factory,
                                                 @NotNull PsiMethod method, PsiElement target) {
    if (method.isConstructor()) {
      return factory.createConstructor(method.getName(), target);
    }
    return factory.createMethod(method.getName(), PsiTypes.voidType(), target);
  }

  private static void substituteReturnType(@NotNull PsiManager manager,
                                           @NotNull PsiMethod method,
                                           @Nullable PsiType returnType,
                                           @NotNull PsiSubstitutor substitutor) {
    final PsiTypeElement returnTypeElement = method.getReturnTypeElement();
    if (returnTypeElement == null || returnType == null) {
      return;
    }
    final PsiType substitutedReturnType = substituteType(substitutor, returnType, method, method.getModifierList());

    returnTypeElement.replace(new LightTypeElement(manager, substitutedReturnType instanceof PsiWildcardType ? TypeConversionUtil.erasure(substitutedReturnType) : substitutedReturnType));
  }

  private static @NotNull JVMElementFactory getFactory(@NotNull Project p, @Nullable PsiElement target) {
    return target == null ? JavaPsiFacade.getElementFactory(p) : JVMElementFactories.requireFactory(target.getLanguage(), p);
  }

  private static boolean isBaseNameGenerated(@NotNull JavaCodeStyleManager csManager, @NotNull PsiType parameterType, @NotNull String paramName) {
    if (Arrays.asList(csManager.suggestVariableName(VariableKind.PARAMETER, null, null, parameterType).names).contains(paramName)) {
      return true;
    }
    final String typeName = JavaCodeStyleManagerImpl.getTypeName(parameterType);
    return typeName != null &&
           NameUtil.getSuggestionsByName(typeName, "", "", false, false, parameterType instanceof PsiArrayType).contains(paramName);
  }

  private static PsiType substituteType(final PsiSubstitutor substitutor, final PsiType type, @NotNull PsiTypeParameterListOwner owner, PsiModifierList modifierList) {
    PsiType substitutedType = PsiUtil.isRawSubstitutor(owner, substitutor)
                    ? TypeConversionUtil.erasure(type)
                    : GenericsUtil.eliminateWildcards(substitutor.substitute(type), false, true);
    return substitutedType != null ? AnnotationTargetUtil.keepStrictlyTypeUseAnnotations(modifierList, substitutedType) : null;
  }

  public static boolean isChildInRange(PsiElement child, PsiElement first, PsiElement last) {
    if (child.equals(first)) return true;
    while (true) {
      if (child.equals(first)) return false; // before first
      if (child.equals(last)) return true;
      child = child.getNextSibling();
      if (child == null) return false;
    }
  }

  /**
   * May add @Override, body according to the override template and align throws list according to the super method
   */
  public static void setupGeneratedMethod(PsiMethod method) {
    PsiClass containingClass = method.getContainingClass();
    PsiClass base = containingClass == null ? null : containingClass.getSuperClass();
    PsiMethod overridden = base == null ? null : base.findMethodBySignature(method, true);

    boolean emptyTemplate = true;
    PsiCodeBlock body = method.getBody();
    if (body != null) {
      PsiJavaToken lBrace = body.getLBrace();
      int left = lBrace != null ? lBrace.getStartOffsetInParent() + 1 : 0;
