// 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.refactoring.extractclass;

import com.intellij.codeInsight.generation.GenerateMembersUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.psi.MethodInheritanceUtils;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

class ExtractedClassBuilder {
  private static final Logger LOGGER = Logger.getInstance(ExtractedClassBuilder.class);

  private String className;
  private String packageName;
  private final List<PsiField> fields = new ArrayList<>(5);
  private final List<PsiMethod> methods = new ArrayList<>(5);
  private final List<PsiClassInitializer> initializers = new ArrayList<>(5);
  private final List<PsiClass> innerClasses = new ArrayList<>(5);
  private final List<PsiClass> innerClassesToMakePublic = new ArrayList<>(5);
  private final List<PsiTypeParameter> typeParams = new ArrayList<>();
  private final List<PsiClass> interfaces = new ArrayList<>();

  private boolean requiresBackPointer;
  private String originalClassName;
  private String backPointerName;
  private Project myProject;
  private JavaCodeStyleManager myJavaCodeStyleManager;
  private Set<PsiField> myFieldsNeedingSetters;
  private Set<PsiField> myFieldsNeedingGetter;
  private List<? extends PsiField> enumConstantFields;
  private PsiType myEnumParameterType;

  public void setClassName(String className) {
    this.className = className;
  }

  public void setPackageName(String packageName) {
    this.packageName = packageName;
  }

  public void setOriginalClassName(String originalClassName) {
    this.originalClassName = originalClassName;
  }

  public void addField(PsiField field) {
    fields.add(field);
  }

  public void addMethod(PsiMethod method) {
    methods.add(method);
  }

  public void addInitializer(PsiClassInitializer initializer) {
    initializers.add(initializer);
  }

  public void addInnerClass(PsiClass innerClass, boolean makePublic) {
    innerClasses.add(innerClass);
    if (makePublic) {
      innerClassesToMakePublic.add(innerClass);
    }
  }

  public void setTypeArguments(List<? extends PsiTypeParameter> typeParams) {
    this.typeParams.clear();
    this.typeParams.addAll(typeParams);
  }

  public void setInterfaces(List<? extends PsiClass> interfaces) {
    this.interfaces.clear();
    this.interfaces.addAll(interfaces);
  }

  public String buildBeanClass(boolean normalizeDeclaration) {
    if (requiresBackPointer) {
      calculateBackpointerName();
    }
    final @NonNls StringBuilder out = new StringBuilder(1024);
    if (!packageName.isEmpty()) out.append("package ").append(packageName).append(";\n");

    out.append("public ");
    fields.removeAll(enumConstantFields);
    out.append(hasEnumConstants() ? "enum " : "class ");
    out.append(className);
    if (!typeParams.isEmpty()) {
      out.append('<');
      boolean first = true;
      for (PsiTypeParameter typeParam : typeParams) {
        if (!first) {
          out.append(',');
        }
        out.append(typeParam.getText());
        first = false;
      }
      out.append('>');
    }
    if (!interfaces.isEmpty()) {
      out.append(" implements ");
      boolean first = true;
      for (PsiClass implemented : interfaces) {
        if (!first) {
          out.append(',');
        }
        out.append(implemented.getQualifiedName());
        first = false;
      }
    }
    out.append("{\n");

    if (requiresBackPointer) {
      out.append("private final ").append(originalClassName);
      if (!typeParams.isEmpty()) {
        out.append('<');
        boolean first = true;
        for (PsiTypeParameter typeParam : typeParams) {
          if (!first) {
            out.append(',');
          }
          out.append(typeParam.getName());
          first = false;
        }
        out.append('>');
      }
      out.append(' ').append(backPointerName).append(";");
    }
    outputFieldsAndInitializers(out, normalizeDeclaration);
    if (hasEnumConstants()) {
      final String fieldName = getValueFieldName();
      out.append("private final ").append(myEnumParameterType.getCanonicalText()).append(" ").append(fieldName).append(";\n");
      outputConstructor(out);
      out.append("public ").append(myEnumParameterType.getCanonicalText()).append(" ")
        .append(getterName()).append("(){\nreturn ").append(fieldName).append(";\n}\n");
    }
    else if (needConstructor() || requiresBackPointer) {
      outputConstructor(out);
    }
    outputMethods(out);
    outputInnerClasses(out);
    out.append("}");
    return out.toString();
  }

  private String getterName() {//todo unique getterName: see also com.intellij.refactoring.extractclass.usageInfo.ReplaceStaticVariableAccess
    return GenerateMembersUtil.suggestGetterName("value", myEnumParameterType, myProject);
  }

  private boolean hasEnumConstants() {
    return !enumConstantFields.isEmpty();
  }

  private String getValueFieldName() {
    final String myValue = myJavaCodeStyleManager.variableNameToPropertyName("value", VariableKind.FIELD);
    return myJavaCodeStyleManager.suggestUniqueVariableName(myValue, enumConstantFields.get(0), true);
  }

  private void calculateBackpointerName() {
    final String baseName;
    if (originalClassName.indexOf('.') == 0) {
      baseName = StringUtil.decapitalize(originalClassName);
    }
    else {
      final String simpleClassName = originalClassName.substring(originalClassName.lastIndexOf('.') + 1);
      baseName = StringUtil.decapitalize(simpleClassName);
    }
    String name = myJavaCodeStyleManager.propertyNameToVariableName(baseName, VariableKind.FIELD);
    if (!existsFieldWithName(name)) {
      backPointerName = name;
      return;
    }
    int counter = 1;
    while (true) {
      name += counter;
      if (!existsFieldWithName(name)) {
        backPointerName = name;
        return;
      }
      counter++;
    }
  }

  private boolean existsFieldWithName(String name) {
    for (PsiField field : fields) {
      final String fieldName = field.getName();
      if (name.equals(fieldName)) {
        return true;
      }
    }
    return false;
  }

  private boolean needConstructor() {
    for ( PsiField field : fields) {
      if (!field.hasModifierProperty(PsiModifier.STATIC)) {
        return true;
      }
    }
    for (PsiMethod method : methods) {
      if (!method.hasModifierProperty(PsiModifier.STATIC)) {
        return true;
      }
    }
    return false;
  }

  private void outputMethods(@NonNls StringBuilder out) {
    for (PsiMethod method : methods) {
      method.accept(new Mutator(out));
    }
  }

  private void outputInnerClasses(@NonNls StringBuilder out) {
    for (PsiClass innerClass : innerClasses) {
      outputMutatedInnerClass(out, innerClass, innerClassesToMakePublic.contains(innerClass));
    }
  }

  private void outputMutatedInnerClass(@NonNls StringBuilder out, PsiClass innerClass, boolean makePublic) {
    if (makePublic) {
      try {
        PsiUtil.setModifierProperty(innerClass, PsiModifier.PUBLIC, false);
      }
      catch (IncorrectOperationException e) {
        LOGGER.error(e);
      }
    }
    innerClass.accept(new Mutator(out));
  }


  private void outputFieldsAndInitializers(@NonNls StringBuilder out, boolean normalizeDeclaration) {
    if (hasEnumConstants()) {
      out.append(StringUtil.join(enumConstantFields, field -> {
        final @NonNls StringBuilder fieldStr = new StringBuilder(field.getName());
        fieldStr.append('(');
        final PsiExpression initializer = field.getInitializer();
        if (initializer != null) {
          initializer.accept(new Mutator(fieldStr));
        }
        fieldStr.append(')');
        return fieldStr.toString();
      }, ", "));
      out.append(";\n");
    }

    final List<PsiClassInitializer> remainingInitializers = new ArrayList<>(initializers);
    for (PsiField field : fields) {
      if (normalizeDeclaration) {
        field.normalizeDeclaration();
      }
      final Iterator<PsiClassInitializer> initializersIterator = remainingInitializers.iterator();
      final int fieldOffset = field.getTextRange().getStartOffset();
      while (initializersIterator.hasNext()) {
        final PsiClassInitializer initializer = initializersIterator.next();
        if (initializer.getTextRange().getStartOffset() < fieldOffset) {
          initializer.accept(new Mutator(out));
          initializersIterator.remove();
        }
      }

      field.accept(new Mutator(out));
      out.append('\n');

      if (myFieldsNeedingGetter != null && myFieldsNeedingGetter.contains(field)) {
        out.append(GenerateMembersUtil.generateGetterPrototype(field).getText());
        out.append("\n");
      }

      if (myFieldsNeedingSetters != null && myFieldsNeedingSetters.contains(field)) {
        out.append(GenerateMembersUtil.generateSetterPrototype(field).getText());
        out.append("\n");
      }
    }
    for (PsiClassInitializer initializer : remainingInitializers) {
      initializer.accept(new Mutator(out));
    }
  }

  private void outputConstructor(@NonNls StringBuilder out) {
    out.append(hasEnumConstants() ? "" : "public ").append(className).append('(');
    if (requiresBackPointer) {
      final String parameterName = myJavaCodeStyleManager.propertyNameToVariableName(backPointerName, VariableKind.PARAMETER);
      out.append(originalClassName);
      if (!typeParams.isEmpty()) {
        out.append('<');
        boolean first = true;
        for (PsiTypeParameter typeParam : typeParams) {
          if (!first) {
            out.append(',');
          }
          out.append(typeParam.getName());
          first = false;
        }
        out.append('>');
      }
      out.append(' ').append(parameterName);
    } else if (hasEnumConstants()) {
      out.append(myEnumParameterType.getCanonicalText()).append(" ").append("value");
    }

    out.append(") {\n");
    if (requiresBackPointer) {
      final String parameterName = myJavaCodeStyleManager.propertyNameToVariableName(backPointerName, VariableKind.PARAMETER);
      if (backPointerName.equals(parameterName)) {
        out.append("this.");
      }
      out.append(backPointerName).append('=').append(parameterName).append(";\n");

    } else if (hasEnumConstants()) {
      final String fieldName = getValueFieldName();
      if (fieldName.equals("value")) {
        out.append("this.");
      }
      out.append(fieldName).append("=value;\n");
    }
    out.append("}\n");
  }

  public void setRequiresBackPointer(boolean requiresBackPointer) {
    this.requiresBackPointer = requiresBackPointer;
  }

  public void setProject(Project project) {
    myProject = project;
    myJavaCodeStyleManager = JavaCodeStyleManager.getInstance(project);
  }

  public void setFieldsNeedingGetters(Set<PsiField> fieldsNeedingGetter) {
    myFieldsNeedingGetter = fieldsNeedingGetter;
  }

  public void setFieldsNeedingSetters(Set<PsiField> fieldsNeedingSetters) {
    myFieldsNeedingSetters = fieldsNeedingSetters;
  }

  private boolean fieldIsExtracted(PsiField field) {
    final ArrayList<PsiField> extractedFields = new ArrayList<>(fields);
    extractedFields.addAll(enumConstantFields);
    if (extractedFields.contains(field)) return true;

    final PsiClass containingClass = field.getContainingClass();
    return innerClasses.contains(containingClass);
  }

  public void setExtractAsEnum(List<? extends PsiField> extractAsEnum) {
    this.enumConstantFields = extractAsEnum;
    if (hasEnumConstants()) {
      myEnumParameterType = enumConstantFields.get(0).getType();
    }
  }

  private final class Mutator extends JavaElementVisitor {
    private final @NonNls StringBuilder out;

    private Mutator(@NonNls StringBuilder out) {
      super();
      this.out = out;
    }

    @Override
    public void visitElement(@NotNull PsiElement element) {

      super.visitElement(element);
      final PsiElement[] children = element.getChildren();
      if (children.length == 0) {
        final String text = element.getText();
        out.append(text);
      }
      else {
        for (PsiElement child : children) {
          child.accept(this);
        }
      }
    }

    @Override
    public void visitReferenceExpression(@NotNull PsiReferenceExpression expression) {
      final JavaResolveResult resolveResult = expression.advancedResolve(true);
      final boolean staticImported = resolveResult.getCurrentFileResolveScope() instanceof PsiImportStaticStatement;
      final PsiElement qualifier = expression.getQualifier();
      if (qualifier == null || qualifier instanceof PsiThisExpression) {
        final PsiElement referent = resolveResult.getElement();
        if (referent instanceof PsiField field) {
          if (fieldIsExtracted(field)) {
            final String name = field.getName();
            if (enumConstantFields.contains(field)) {
              out.append(name).append(".").append(getterName()).append("()");
            } else {
              if (qualifier != null && name.equals(expression.getReferenceName())) {
                out.append("this.");
              }
              out.append(name);
            }
          }
          else {
            if (field.hasModifierProperty(PsiModifier.STATIC)) {
              if (field instanceof PsiEnumConstant) {
                out.append(field.getName());
              } else if (staticImported) {
                final PsiImportStaticStatement importStaticStatement = (PsiImportStaticStatement)resolveResult.getCurrentFileResolveScope();
                final PsiClass targetClass = importStaticStatement.resolveTargetClass();
                out.append(targetClass != null ? targetClass.getQualifiedName() : "").append(".").append(field.getName());
              } else {
                out.append(originalClassName).append('.').append(field.getName());
              }
            }
            else {
              out.append(backPointerName).append('.').append(GenerateMembersUtil.suggestGetterName(field)).append("()");
            }
          }
        }
        else if (referent instanceof PsiClass) {
          String qualifiedName = ((PsiClass)referent).getQualifiedName();
          if (qualifiedName != null) {
            out.append(qualifiedName);
          }
        }
        else {
          visitElement(expression);
        }
      }
      else {
        visitElement(expression);
      }
    }

    @Override
    public void visitAssignmentExpression(@NotNull PsiAssignmentExpression expression) {
      PsiExpression lhs = expression.getLExpression();
      final PsiExpression rhs = expression.getRExpression();

      if (isBackpointerReference(lhs) && rhs != null) {
        while (lhs instanceof PsiParenthesizedExpression) {
          lhs = ((PsiParenthesizedExpression)lhs).getExpression();
        }

        final PsiReferenceExpression reference = (PsiReferenceExpression)lhs;
        assert reference != null;
        final PsiField field = (PsiField)reference.resolve();
        final PsiJavaToken sign = expression.getOperationSign();
        final IElementType tokenType = sign.getTokenType();
        assert field != null;
        if (!field.hasModifierProperty(PsiModifier.STATIC)) {
          delegate(rhs, field, sign, tokenType, backPointerName);
        }
        else {
          visitElement(expression);
        }
      }
      else {
        visitElement(expression);
      }
    }

    private void delegate(PsiExpression rhs, PsiField field, PsiJavaToken sign, IElementType tokenType, String fieldName) {
      final String setterName = GenerateMembersUtil.suggestSetterName(field);
      out.append(fieldName).append('.').append(setterName).append('(');
      if (!tokenType.equals(JavaTokenType.EQ)) {
        final String operator = sign.getText().substring(0, sign.getTextLength() - 1);
        final String getterName = GenerateMembersUtil.suggestGetterName(field);
        out.append(fieldName).append('.').append(getterName).append("()");
        out.append(operator);
      }
      rhs.accept(this);
      out.append(')');
    }

    @Override
    public void visitUnaryExpression(@NotNull PsiUnaryExpression expression) {
      PsiExpression operand = expression.getOperand();
      final IElementType tokenType = expression.getOperationSign().getTokenType();
      if (isBackpointerReference(operand) && (tokenType.equals(JavaTokenType.PLUSPLUS) || tokenType.equals(JavaTokenType.MINUSMINUS))) {
        while (operand instanceof PsiParenthesizedExpression) {
          operand = ((PsiParenthesizedExpression)operand).getExpression();
        }
        final PsiReferenceExpression reference = (PsiReferenceExpression)operand;

        final String operator;
        if (tokenType.equals(JavaTokenType.PLUSPLUS)) {
          operator = "+";
        }
        else {
          operator = "-";
        }
        final PsiField field = (PsiField)reference.resolve();
        assert field != null;
        if (!field.hasModifierProperty(PsiModifier.STATIC)) {
          out.append(backPointerName)
            .append('.').append(GenerateMembersUtil.suggestSetterName(field))
            .append('(')
            .append(backPointerName)
            .append('.').append(GenerateMembersUtil.suggestGetterName(field))
            .append("()")
            .append(operator)
            .append("1)");
        }
        else {
          visitElement(expression);
        }
      }
      else {
        visitElement(expression);
      }
    }

    private boolean isBackpointerReference(PsiExpression expression) {
      return BackpointerUtil.isBackpointerReference(expression, psiField -> !fieldIsExtracted(psiField));
    }

    @Override
    public void visitThisExpression(@NotNull PsiThisExpression expression) {
      out.append(backPointerName);
    }

    @Override
    public void visitMethodCallExpression(@NotNull PsiMethodCallExpression call) {
      final PsiReferenceExpression expression = call.getMethodExpression();
      final JavaResolveResult resolveResult = expression.advancedResolve(false);
      final PsiElement qualifier = expression.getQualifier();
      if (qualifier == null || qualifier instanceof PsiThisExpression) {
        final PsiMethod method = call.resolveMethod();
        if (method != null && !isCompletelyMoved(method)) {
          final String methodName = method.getName();
          if (method.hasModifierProperty(PsiModifier.STATIC)) {
            final PsiElement resolveScope = resolveResult.getCurrentFileResolveScope();
            if (resolveScope instanceof PsiImportStaticStatement) {
              final PsiClass targetClass = ((PsiImportStaticStatement)resolveScope).resolveTargetClass();
              out.append(targetClass != null ? targetClass.getQualifiedName() : "").append('.').append(methodName);
            } else {
              out.append(originalClassName).append('.').append(methodName);
            }
          }
          else {
            out.append(backPointerName).append('.').append(methodName);
          }
          final PsiExpressionList argumentList = call.getArgumentList();
          argumentList.accept(this);
        }
        else {
          visitElement(call);
        }
      }
      else {
        visitElement(call);
      }
    }

    @Override
    public void visitReferenceElement(@NotNull PsiJavaCodeReferenceElement reference) {
      final String referenceText = reference.getCanonicalText();
      out.append(referenceText);
    }

    private boolean isCompletelyMoved(PsiMethod method) {
      return methods.contains(method) && !
        MethodInheritanceUtils.hasSiblingMethods(method);
    }
  }
}

