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

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.Nullable;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

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

  /**
   * @param member  nonstatic class member to search for class references in
   * @return Set<PsiMember> in result map may be null in case no member is needed, but class itself is.
   */
  public static Map<PsiClass, Set<PsiMember>> getThisClassesToMembers(final PsiMember member) {
    Map<PsiClass, Set<PsiMember>> map = new LinkedHashMap<>();
    getThisClassesToMembers (member, map, member);
    return map;
  }

  private static void getThisClassesToMembers(final PsiElement scope, final Map<PsiClass, Set<PsiMember>> map, final PsiMember refMember) {
    if (scope instanceof PsiExpression expression &&
        (!(scope instanceof PsiReferenceExpression) || !((PsiReferenceExpression)scope).isReferenceTo(refMember))) {
      final Pair<PsiMember, PsiClass> pair = getMemberAndClassReferencedByThis(expression);
      if (pair != null) {
        PsiClass refClass = pair.getSecond();
        PsiMember member = pair.getFirst();
        if (refClass != null) {
          boolean inherited = false;
          PsiClass parentClass = PsiTreeUtil.getParentOfType(scope, PsiClass.class, true);
          while (parentClass != null && PsiTreeUtil.isAncestor(refMember, parentClass, false)) {
            if (parentClass == refClass || parentClass.isInheritor(refClass, true)) {
              inherited = true;
              break;
            }
            parentClass = PsiTreeUtil.getParentOfType(parentClass, PsiClass.class, true);
          }
          if (!inherited && !PsiTreeUtil.isAncestor(refMember, member, false)) {
            addReferencedMember(map, refClass, member);
          }
        }
      }

      if (expression instanceof PsiThisExpression thisExpression) {
        final PsiJavaCodeReferenceElement thisQualifier = thisExpression.getQualifier();
        PsiClass thisClass =
          thisQualifier == null ? PsiTreeUtil.getParentOfType(expression, PsiClass.class, true) : ((PsiClass)thisQualifier.resolve());
        if (thisClass != null && !PsiTreeUtil.isAncestor(refMember, thisClass, false)) {
          addReferencedMember(map, thisClass, null);
        }
      }
    }

    final PsiElement[] children = scope.getChildren();
    for (PsiElement child : children) {
      getThisClassesToMembers(child, map, refMember);
    }
  }

  private static void addReferencedMember(final Map<PsiClass, Set<PsiMember>> map, final PsiClass classReferenced, final PsiMember member) {
    Set<PsiMember> members = map.get(classReferenced);
    if (members == null) {
      members = new HashSet<>();
      map.put(classReferenced, members);
    }
    members.add(member);
  }

  private static @Nullable Pair<PsiMember, PsiClass> getMemberAndClassReferencedByThis(final PsiExpression expression) {
    if (expression instanceof PsiReferenceExpression) {
      final PsiExpression qualifier = ((PsiReferenceExpression)expression).getQualifierExpression();
      if (qualifier == null || qualifier instanceof PsiThisExpression) {
        final PsiElement resolved = ((PsiReferenceExpression)expression).resolve();
        if (resolved instanceof PsiMember && !((PsiMember)resolved).hasModifierProperty(PsiModifier.STATIC)) {
          PsiClass referencedClass = getReferencedClass((PsiMember)resolved, qualifier, expression);
          return Pair.create((PsiMember)resolved, referencedClass);
        }
      }
    } else if (expression instanceof PsiNewExpression newExpression) {
      final PsiExpression qualifier = newExpression.getQualifier();
      if (qualifier == null || qualifier instanceof PsiThisExpression) {
        PsiJavaCodeReferenceElement classReference = newExpression.getClassOrAnonymousClassReference();
        if (classReference != null) {
          final PsiClass resolved = (PsiClass)classReference.resolve();
          if (resolved != null && !resolved.hasModifierProperty(PsiModifier.STATIC)) {
            PsiClass referencedClass = getReferencedClass(resolved, qualifier, expression);
            return new Pair<>(resolved, referencedClass);
          }
        }
      }
    }

    return null;
  }

  private static @Nullable PsiClass getReferencedClass(final PsiMember member, final PsiExpression exprQualifier, final PsiExpression expression) {
    if (exprQualifier != null) {
      final PsiType type = exprQualifier.getType();
      if (type instanceof PsiClassType) {
        return ((PsiClassType)type).resolve();
      }
      return null;
    } else {
      PsiClass referencedClass = member.getContainingClass();
      if (referencedClass == null) return null;
      final PsiClass parentClass = PsiTreeUtil.getParentOfType(expression, PsiClass.class);
      assert parentClass != null;
      if (InheritanceUtil.isInheritorOrSelf(parentClass, referencedClass, false)) {
        referencedClass = parentClass;
      }
      return referencedClass;
    }
  }

  public static @Nullable PsiClass getClassReferencedByThis(final PsiExpression expression) {
    PsiClass enclosingClass = PsiTreeUtil.getParentOfType(expression, PsiClass.class);
    if (enclosingClass == null) return null;
    final Pair<PsiMember, PsiClass> pair = getMemberAndClassReferencedByThis(expression);
    if (pair != null) return pair.getSecond();

    if (expression instanceof PsiThisExpression) {
      final PsiJavaCodeReferenceElement thisQualifier = ((PsiThisExpression)expression).getQualifier();
      if (thisQualifier == null) {
        return enclosingClass;
      }
      else {
        return (PsiClass)thisQualifier.resolve();
      }
    }
    return null;
  }

  public static void moveInitializerToConstructor(PsiElementFactory factory, PsiMethod constructor, PsiField field) {
    final PsiExpression initializer = field.getInitializer();
    PsiExpression initializerCopy = (PsiExpression)initializer.copy();
    final PsiCodeBlock body = constructor.getBody();
    if (body != null) {
      try {
        String fieldName = field.getName();
        final PsiReferenceExpression refExpr = (PsiReferenceExpression)factory.createExpressionFromText(fieldName, body);
        if (refExpr.resolve() != null) fieldName = "this." + fieldName;
        PsiExpressionStatement statement = (PsiExpressionStatement)factory.createStatementFromText(fieldName + "= y;", null);
        if (initializerCopy instanceof PsiArrayInitializerExpression) {
          PsiType type = initializer.getType();
          PsiNewExpression newExpression =
            (PsiNewExpression)factory.createExpressionFromText("new " + type.getCanonicalText() + "{}", body);
          newExpression.getArrayInitializer().replace(initializerCopy);
          initializerCopy = newExpression;
        }
        ((PsiAssignmentExpression)statement.getExpression()).getRExpression().replace(initializerCopy);
        statement = (PsiExpressionStatement)CodeStyleManager.getInstance(field.getManager().getProject()).reformat(statement);
        body.add(statement);
        initializer.delete();
      }
      catch (IncorrectOperationException e) {
        LOG.error(e);
      }
    }
  }
}
