import GUI from "lil-gui";
import type { CameraMode, SceneMode } from "@/types/debug/debug";
import type { HandTrackingSource } from "@/types/handTracking/handTracking";
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";

const DEBUG_CONTROLS_STORAGE_KEY = "la-fabrik-debug-controls";

interface StoredDebugControls {
  cameraMode: CameraMode;
  sceneMode: SceneMode;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function isCameraMode(value: unknown): value is CameraMode {
  return value === "player" || value === "debug";
}

function isSceneMode(value: unknown): value is SceneMode {
  return value === "game" || value === "physics";
}

function getStoredDebugControls(): Partial<StoredDebugControls> {
  try {
    const rawValue = window.localStorage.getItem(DEBUG_CONTROLS_STORAGE_KEY);
    if (!rawValue) return {};

    const parsedValue: unknown = JSON.parse(rawValue);
    if (!isRecord(parsedValue)) return {};

    return {
      ...(isCameraMode(parsedValue.cameraMode)
        ? { cameraMode: parsedValue.cameraMode }
        : {}),
      ...(isSceneMode(parsedValue.sceneMode)
        ? { sceneMode: parsedValue.sceneMode }
        : {}),
    };
  } catch {
    return {};
  }
}

export class Debug {
  private static instance: Debug | null = null;

  public readonly active: boolean;
  private readonly gui: GUI | null;
  private readonly folders = new Map<string, GUI>();
  private readonly folderRefCounts = new Map<string, number>();
  private readonly listeners = new Set<() => void>();
  private readonly controls: {
    cameraMode: CameraMode;
    handTrackingSource: HandTrackingSource;
    showDebugOverlay: boolean;
    showHandTrackingSvg: boolean;
    showInteractionSpheres: boolean;
    showPerf: boolean;
    sceneMode: SceneMode;
  };

  static getInstance(): Debug {
    if (!Debug.instance) {
      Debug.instance = new Debug();
    }

    return Debug.instance;
  }

  private constructor() {
    this.active = isDebugEnabled();
    const storedControls = getStoredDebugControls();

    this.controls = {
      cameraMode: storedControls.cameraMode ?? "player",
      handTrackingSource: "backend",
      showDebugOverlay: true,
      showHandTrackingSvg: false,
      showInteractionSpheres: false,
      showPerf: true,
      sceneMode: storedControls.sceneMode ?? "game",
    };

    this.gui = this.active ? new GUI({ title: "La-Fabrik Debug" }) : null;

    if (this.gui) {
      const folder = this.createFolder("Debug");

      if (!folder) return;

      folder
        .add(this.controls, "cameraMode", { Player: "player", Debug: "debug" })
        .name("Camera Mode")
        .onChange((value: CameraMode) => {
          this.controls.cameraMode = value;
          this.saveAndEmit();
        });

      folder
        .add(this.controls, "sceneMode", { Game: "game", Physics: "physics" })
        .name("Scene")
        .onChange((value: SceneMode) => {
          this.controls.sceneMode = value;
          this.saveAndEmit();
        });

      folder
        .add(this.controls, "showPerf")
        .name("R3F Perf")
        .onChange((value: boolean) => {
          this.controls.showPerf = value;
          this.emit();
        });

      folder
        .add(this.controls, "showDebugOverlay")
        .name("Debug Overlay")
        .onChange((value: boolean) => {
          this.controls.showDebugOverlay = value;
          this.emit();
        });

      const handTrackingFolder = this.createFolder("Hand Tracking");

      handTrackingFolder
        ?.add(this.controls, "showHandTrackingSvg")
        .name("Show SVG")
        .onChange((value: boolean) => {
          this.controls.showHandTrackingSvg = value;
          this.emit();
        });

      handTrackingFolder
        ?.add(this.controls, "handTrackingSource", {
          Backend: "backend",
          "Browser JS": "browser",
        })
        .name("Source")
        .onChange((value: HandTrackingSource) => {
          this.controls.handTrackingSource = value;
          this.emit();
        });
    }
  }

  /**
   * Acquires a named GUI folder. Returns the folder on first acquisition and null
   * on subsequent acquisitions so callers only register controls once.
   */
  createFolder(name: string): GUI | null {
    if (!this.gui) return null;

    const existing = this.folders.get(name);

    if (existing) {
      this.folderRefCounts.set(name, (this.folderRefCounts.get(name) ?? 0) + 1);
      return null;
    }

    const folder = this.gui.addFolder(name);
    this.folders.set(name, folder);
    this.folderRefCounts.set(name, 1);

    return folder;
  }

  destroyFolder(name: string): void {
    const folder = this.folders.get(name);
    const refCount = this.folderRefCounts.get(name);
    if (!folder || refCount === undefined) return;

    if (refCount > 1) {
      this.folderRefCounts.set(name, refCount - 1);
      return;
    }

    folder.destroy();
    this.folders.delete(name);
    this.folderRefCounts.delete(name);
  }

  subscribe(listener: () => void): () => void {
    this.listeners.add(listener);

    return () => {
      this.listeners.delete(listener);
    };
  }

  getCameraMode(): CameraMode {
    return this.controls.cameraMode;
  }

  getSceneMode(): SceneMode {
    return this.controls.sceneMode;
  }

  getShowDebugOverlay(): boolean {
    return this.active && this.controls.showDebugOverlay;
  }

  getHandTrackingSource(): HandTrackingSource {
    return this.controls.handTrackingSource;
  }

  getShowInteractionSpheres(): boolean {
    return this.controls.showInteractionSpheres;
  }

  getShowHandTrackingSvg(): boolean {
    return this.controls.showHandTrackingSvg;
  }

  setShowHandTrackingSvg(value: boolean): void {
    this.controls.showHandTrackingSvg = value;
    this.emit();
  }

  setShowInteractionSpheres(value: boolean): void {
    this.controls.showInteractionSpheres = value;
    this.emit();
  }

  getShowPerf(): boolean {
    return this.active && this.controls.showPerf;
  }

  private emit(): void {
    this.listeners.forEach((listener) => listener());
  }

  private saveAndEmit(): void {
    try {
      window.localStorage.setItem(
        DEBUG_CONTROLS_STORAGE_KEY,
        JSON.stringify({
          cameraMode: this.controls.cameraMode,
          sceneMode: this.controls.sceneMode,
        }),
      );
    } catch {
      // Debug persistence is optional; controls still work if storage is blocked.
    }

    this.emit();
  }
}
