import { DebugProtocol } from "@vscode/debugprotocol";
import * as vscode from "vscode";

/** A helper type for mapping event types to their corresponding data type. */
// prettier-ignore
interface EventMap {
  "module": DebugProtocol.ModuleEvent;
}

/** A type assertion to check if a ProtocolMessage is an event or if it is a specific event. */
function isEvent(
  message: DebugProtocol.ProtocolMessage,
): message is DebugProtocol.Event;
function isEvent<K extends keyof EventMap>(
  message: DebugProtocol.ProtocolMessage,
  event: K,
): message is EventMap[K];
function isEvent(
  message: DebugProtocol.ProtocolMessage,
  event?: string,
): boolean {
  return (
    message.type === "event" &&
    (!event || (message as DebugProtocol.Event).event === event)
  );
}

/** Tracks lldb-dap sessions for data visualizers. */
export class DebugSessionTracker
  implements vscode.DebugAdapterTrackerFactory, vscode.Disposable
{
  /**
   * Tracks active modules for each debug sessions.
   *
   * The modules are kept in an array to maintain the load order of the modules.
   */
  private modules = new Map<vscode.DebugSession, DebugProtocol.Module[]>();
  private modulesChanged = new vscode.EventEmitter<
    vscode.DebugSession | undefined
  >();

  /**
   * Fired when modules are changed for any active debug session.
   *
   * Use `debugSessionModules` to retieve the active modules for a given debug session.
   */
  onDidChangeModules: vscode.Event<vscode.DebugSession | undefined> =
    this.modulesChanged.event;

  constructor() {
    this.onDidChangeModules(this.moduleChangedListener, this);
    vscode.debug.onDidChangeActiveDebugSession((session) =>
      this.modulesChanged.fire(session),
    );
  }

  dispose() {
    this.modules.clear();
    this.modulesChanged.dispose();
  }

  createDebugAdapterTracker(
    session: vscode.DebugSession,
  ): vscode.ProviderResult<vscode.DebugAdapterTracker> {
    return {
      onDidSendMessage: (message) => this.onDidSendMessage(session, message),
      onExit: () => this.onExit(session),
    };
  }

  /**
   * Retrieves the modules for the given debug session.
   *
   * Modules are returned in load order.
   */
  debugSessionModules(session: vscode.DebugSession): DebugProtocol.Module[] {
    return this.modules.get(session) ?? [];
  }

  /** Clear information from the active session. */
  private onExit(session: vscode.DebugSession) {
    this.modules.delete(session);
    this.modulesChanged.fire(undefined);
  }

  private showModulesTreeView(showModules: boolean) {
    vscode.commands.executeCommand(
      "setContext",
      "lldb-dap.showModules",
      showModules,
    );
  }

  private moduleChangedListener(session: vscode.DebugSession | undefined) {
    if (!session) {
      this.showModulesTreeView(false);
      return;
    }

    if (session == vscode.debug.activeDebugSession) {
      const sessionHasModules = this.modules.get(session) != undefined;
      this.showModulesTreeView(sessionHasModules);
    }
  }

  private onDidSendMessage(
    session: vscode.DebugSession,
    message: DebugProtocol.ProtocolMessage,
  ) {
    if (isEvent(message, "module")) {
      const { module, reason } = message.body;
      const modules = this.modules.get(session) ?? [];
      switch (reason) {
        case "new":
        case "changed": {
          const index = modules.findIndex((m) => m.id === module.id);
          if (index !== -1) {
            modules[index] = module;
          } else {
            modules.push(module);
          }
          break;
        }
        case "removed": {
          const index = modules.findIndex((m) => m.id === module.id);
          if (index !== -1) {
            modules.splice(index, 1);
          }
          break;
        }
        default:
          console.error("unexpected module event reason");
          break;
      }
      this.modules.set(session, modules);
      this.modulesChanged.fire(session);
    }
  }
}
