package org.jetbrains.research.intellijdeodorant.ide.refactoring;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.psi.*;
import com.intellij.refactoring.move.moveInstanceMethod.MoveInstanceMethodDialog;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.research.intellijdeodorant.ide.fus.collectors.IntelliJDeodorantCounterCollector;
import org.jetbrains.research.intellijdeodorant.ide.refactoring.moveMethod.MoveMethodRefactoring;
import org.jetbrains.research.intellijdeodorant.utils.PsiUtils;

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

public class RefactoringsApplier {
    private RefactoringsApplier() {
    }

    public static void moveRefactoring(final @NotNull List<MoveMethodRefactoring> refactorings) {
        if (!checkValid(refactorings)) {
            throw new IllegalArgumentException("Methods in refactorings list must be unique!");
        }

        final Map<PsiClass, List<MoveMethodRefactoring>> groupedRefactorings = prepareRefactorings(refactorings);

        ApplicationManager.getApplication().runReadAction(() -> {
            for (Map.Entry<PsiClass, List<MoveMethodRefactoring>> refactoring : groupedRefactorings.entrySet()) {
                final PsiClass target = refactoring.getKey();
                refactoring.getValue().forEach(r -> {
                    if (canMoveInstanceMethod(r.getMethod(), target)) {
                        moveInstanceMethod(r, target);
                        if (!r.getOptionalMethod().isPresent()) {
                            IntelliJDeodorantCounterCollector.getInstance().moveMethodRefactoringApplied(target.getProject(),
                                    r.getSourceAccessedMembers(), r.getTargetAccessedMembers(),
                                    r.getMethodLength(), r.getMethodParametersCount());
                        }
                    }
                });
            }
        });
    }

    private static boolean checkValid(Collection<MoveMethodRefactoring> refactorings) {
        final long uniqueUnits = refactorings.stream()
                .map(MoveMethodRefactoring::getMethod)
                .distinct()
                .count();
        return uniqueUnits == refactorings.size();
    }

    private static @NotNull
    Map<PsiClass, List<MoveMethodRefactoring>> prepareRefactorings(
            final @NotNull List<MoveMethodRefactoring> refactorings
    ) {
        return refactorings.stream().collect(
                Collectors.groupingBy(MoveMethodRefactoring::getTargetClass, Collectors.toList())
        );
    }

    private static void moveInstanceMethod(@NotNull MoveMethodRefactoring refactoring, PsiClass target) {
        PsiMethod methodToMove = refactoring.getMethod();
        PsiVariable[] available = getAvailableVariables(methodToMove, target);
        if (available.length == 0) {
            throw new IllegalStateException("Cannot move instance method");
        }
        MoveInstanceMethodDialog dialog = new MoveInstanceMethodDialog(methodToMove, available);
        dialog.setTitle("Move Instance Method " + PsiUtils.calculateSignature(methodToMove));
        ApplicationManager.getApplication().invokeAndWait(dialog::show);
    }

    private static PsiVariable[] getAvailableVariables(@NotNull PsiMethod method, @NotNull PsiClass target) {
        final PsiClass psiClass = method.getContainingClass();
        Stream<PsiVariable> parameters = Arrays.stream(method.getParameterList().getParameters());
        Stream<PsiVariable> fields = psiClass == null ? Stream.empty() : Arrays.stream(psiClass.getFields());
        return Stream.concat(parameters, fields)
                .filter(Objects::nonNull)
                .filter(p -> p.getType() instanceof PsiClassType && target.equals(((PsiClassType) p.getType()).resolve()))
                .toArray(PsiVariable[]::new);
    }

    private static boolean canMoveInstanceMethod(@NotNull PsiMethod method, PsiClass target) {
        PsiVariable[] available = getAvailableVariables(method, target);
        return available.length != 0;
    }
}