// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.maven.tasks;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.Shortcut;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.keymap.KeymapManagerListener;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.project.DumbAwareRunnable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.DisposableWrapperList;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.update.MergingQueueUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import org.jetbrains.annotations.*;
import org.jetbrains.idea.maven.project.MavenProject;
import org.jetbrains.idea.maven.project.MavenProjectChanges;
import org.jetbrains.idea.maven.project.MavenProjectsManager;
import org.jetbrains.idea.maven.project.MavenProjectsTree;
import org.jetbrains.idea.maven.utils.MavenMergingUpdateQueue;
import org.jetbrains.idea.maven.utils.MavenUtil;

import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

public final class MavenShortcutsManager implements Disposable {
  private final Project myProject;

  static final String ACTION_ID_PREFIX = "Maven_";

  private final AtomicBoolean isInitialized = new AtomicBoolean();

  private final DisposableWrapperList<Listener> myListeners = new DisposableWrapperList<>();

  public static @NotNull MavenShortcutsManager getInstance(Project project) {
    return project.getService(MavenShortcutsManager.class);
  }

  public static @Nullable MavenShortcutsManager getInstanceIfCreated(@NotNull Project project) {
    return project.getServiceIfCreated(MavenShortcutsManager.class);
  }

  public MavenShortcutsManager(@NotNull Project project) {
    myProject = project;

    if (MavenUtil.isMavenUnitTestModeEnabled() || ApplicationManager.getApplication().isHeadlessEnvironment()) {
      return;
    }

    MavenUtil.runWhenInitialized(myProject, (DumbAwareRunnable)() -> doInit(project));
  }

  @VisibleForTesting
  public void doInit(@NotNull Project project) {
    if (isInitialized.getAndSet(true)) return;

    MyProjectsTreeListener listener = new MyProjectsTreeListener();
    MavenProjectsManager mavenProjectManager = MavenProjectsManager.getInstance(project);
    mavenProjectManager.addManagerListener(listener, this);
    mavenProjectManager.addProjectsTreeListener(listener, this);

    MessageBusConnection busConnection = ApplicationManager.getApplication().getMessageBus().connect(this);
    busConnection.subscribe(KeymapManagerListener.TOPIC, new KeymapManagerListener() {
      @Override
      public void activeKeymapChanged(Keymap keymap) {
        fireShortcutsUpdated();
      }

      @Override
      public void shortcutsChanged(@NotNull Keymap keymap, @NonNls @NotNull Collection<String> actionIds, boolean fromSettings) {
        fireShortcutsUpdated();
      }
    });
  }

  @Override
  public void dispose() {
    if (!isInitialized.getAndSet(false)) {
      return;
    }

    MavenKeymapExtension.clearActions(myProject);
    myListeners.clear();
  }

  public @NotNull String getActionId(@Nullable String projectPath, @Nullable String goal) {
    StringBuilder result = new StringBuilder(ACTION_ID_PREFIX);
    result.append(myProject.getLocationHash());

    if (projectPath != null) {
      String portablePath = FileUtil.toSystemIndependentName(projectPath);

      result.append(Path.of(portablePath).getParent().getFileName());
      result.append(Integer.toHexString(portablePath.hashCode()));

      if (goal != null) result.append(goal);
    }

    return result.toString();
  }

  public String getDescription(MavenProject project, String goal) {
    Shortcut[] shortcuts = getShortcuts(project, goal);
    if (shortcuts.length == 0) return "";
    return KeymapUtil.getShortcutsText(shortcuts);
  }

  boolean hasShortcuts() {
    Keymap activeKeymap = KeymapManager.getInstance().getActiveKeymap();
    return ContainerUtil.exists(activeKeymap.getActionIds(), id -> id.startsWith(ACTION_ID_PREFIX));
  }

  boolean hasShortcuts(MavenProject project, String goal) {
    return getShortcuts(project, goal).length > 0;
  }

  private Shortcut @NotNull [] getShortcuts(MavenProject project, String goal) {
    String actionId = getActionId(project.getPath(), goal);
    Keymap activeKeymap = KeymapManager.getInstance().getActiveKeymap();
    return activeKeymap.getShortcuts(actionId);
  }

  private void fireShortcutsUpdated() {
    for (Listener listener : myListeners) {
      listener.shortcutsUpdated();
    }
  }

  public void addListener(@NotNull Listener l, @NotNull Disposable disposable) {
    myListeners.add(l, disposable);
  }

  @FunctionalInterface
  public interface Listener {
    void shortcutsUpdated();
  }

  private final class MyProjectsTreeListener implements MavenProjectsManager.Listener, MavenProjectsTree.Listener {
    private final Map<MavenProject, Boolean> mySheduledProjects = new HashMap<>();
    private final MergingUpdateQueue myUpdateQueue = new MavenMergingUpdateQueue("MavenShortcutsManager: Keymap Update",
                                                                                 500, true, MavenShortcutsManager.this).usePassThroughInUnitTestMode();

    @Override
    public void activated() {
      scheduleKeymapUpdate(MavenProjectsManager.getInstance(myProject).getNonIgnoredProjects(), true);
    }

    @Override
    public void projectsIgnoredStateChanged(@NotNull List<MavenProject> ignored, @NotNull List<MavenProject> unignored, boolean fromImport) {
      scheduleKeymapUpdate(unignored, true);
      scheduleKeymapUpdate(ignored, false);
    }

    @Override
    public void projectsUpdated(@NotNull List<? extends Pair<MavenProject, MavenProjectChanges>> updated, @NotNull List<MavenProject> deleted) {
      scheduleKeymapUpdate(MavenUtil.collectFirsts(updated), true);
      scheduleKeymapUpdate(deleted, false);
    }

    @Override
    public void projectResolved(@NotNull Pair<MavenProject, MavenProjectChanges> projectWithChanges) {
      scheduleKeymapUpdate(Collections.singletonList(projectWithChanges.first), true);
    }

    @Override
    public void pluginsResolved(@NotNull MavenProject project) {
      scheduleKeymapUpdate(Collections.singletonList(project), true);
    }

    private void scheduleKeymapUpdate(List<MavenProject> mavenProjects, boolean forUpdate) {
      synchronized (mySheduledProjects) {
        for (MavenProject each : mavenProjects) {
          mySheduledProjects.put(each, forUpdate);
        }
      }

      MergingQueueUtil.queueTracked(myUpdateQueue, new Update(MavenShortcutsManager.this) {
        @Override
        public void run() {
          List<MavenProject> projectToUpdate;
          List<MavenProject> projectToDelete;
          synchronized (mySheduledProjects) {
            projectToUpdate = selectScheduledProjects(true);
            projectToDelete = selectScheduledProjects(false);
            mySheduledProjects.clear();
          }

          if (!myProject.isDisposed()) {
            MavenKeymapExtension.clearActions(myProject, projectToDelete);
            MavenKeymapExtension.updateActions(myProject, projectToUpdate);
          }
        }
      });
    }

    private List<MavenProject> selectScheduledProjects(final boolean forUpdate) {
      return ContainerUtil.mapNotNull(mySheduledProjects.entrySet(),
                                      eachEntry -> forUpdate == eachEntry.getValue() ? eachEntry.getKey() : null);
    }
  }
}
