import "@fontsource/unbounded/700.css";
import "@fontsource/unbounded/800.css";
import "@fontsource/manrope/400.css";
import "@fontsource/manrope/600.css";
import "@fontsource/manrope/700.css";
import "./styles.css";

import { AudioBus } from "./audio/audio";
import { CAMPAIGN, PASSWORDS } from "./game/levels";
import { N, S, W as West } from "./game/pieces";
import { mulberry32, todayKey } from "./game/rng";
import {
  Game,
  makeDailySpec,
  makeEndlessSpec,
  type LevelSpec,
  type Mode,
} from "./game/sim";
import {
  DIFF_MULT,
  loadSave,
  pushScore,
  writeSave,
  type SaveData,
} from "./persist";
import { Renderer, type Assets } from "./render/renderer";

const TUTORIAL = [
  {
    title: "Lay conduit",
    body: "The NEXT pipe is on the left. Click an empty cell to place it. Pieces cannot be rotated — work with what the dispenser gives you.",
  },
  {
    title: "Beat the glow",
    body: "The valve opens after a countdown. LUMINA (the old flooz) then flows. Keep a sealed path until the distance meter fills.",
  },
  {
    title: "Mistakes are metal",
    body: "Replace any unfilled pipe at a cost, or dump a useless piece with X. Open the valve early with Space for a fast-valve bonus.",
  },
  {
    title: "Glory in the works",
    body: "Cross yourself on four-ways. Close loops. Fill cisterns. Unused pipe is a penalty. After every fourth vein, a sliding bonus round.",
  },
];

const $ = <T extends HTMLElement>(sel: string) =>
  document.querySelector(sel) as T;

function show(id: string | null, hot = true): void {
  document.querySelectorAll(".screen").forEach((s) => s.classList.remove("active"));
  const ov = $("#overlay");
  if (!id) {
    ov.classList.remove("hot");
    return;
  }
  document.getElementById(id)?.classList.add("active");
  ov.classList.toggle("hot", hot);
}

function loadImg(src: string): Promise<HTMLImageElement | null> {
  return new Promise((resolve) => {
    const i = new Image();
    i.onload = () => resolve(i);
    i.onerror = () => resolve(null);
    i.src = src;
  });
}

const params = new URLSearchParams(location.search);
const FAST = params.has("fast");

let save: SaveData = loadSave();
let audio = new AudioBus();
let renderer: Renderer;
let game: Game | null = null;
let mode: Mode = "campaign";
let spec: LevelSpec | null = null;
let paused = false;
let running = false;
let last = 0;
let cursor: { x: number; y: number } | null = null;
let settingsFrom: "menu" | "pause" = "menu";
let resultsTimer = 0;
let tutStep = 0;
let campaignIndex = 0;
let campaignRunScore = 0;
let warned = false;

function applySpec(base: LevelSpec): LevelSpec {
  const m = DIFF_MULT[save.settings.difficulty];
  return {
    ...base,
    delay: FAST ? 0.8 : base.delay * m.delay,
    speed: FAST ? base.speed * 1.8 : base.speed * m.speed,
  };
}

function randFor(s: LevelSpec, m: Mode): () => number {
  if (m === "daily") return mulberry32([...todayKey()].reduce((a, ch) => a + ch.charCodeAt(0), 99));
  return Math.random;
}

function startGame(s: LevelSpec, m: Mode, continueCampaign = false): void {
  spec = s;
  mode = m;
  if (!continueCampaign) campaignRunScore = 0;
  const mult = DIFF_MULT[save.settings.difficulty].score;
  const applied = applySpec(s);
  console.log("[lumina] start", {
    name: s.name,
    mode: m,
    delay: applied.delay,
    speed: applied.speed,
    fast: FAST,
    href: location.href,
  });
  game = new Game(applied, m, randFor(s, m), mult);
  paused = false;
  running = true;
  resultsTimer = 0;
  warned = false;
  show(null);
  if (m === "campaign" && !save.seenTutorial && !FAST && !params.get("level")) {
    paused = true;
    tutStep = 0;
    paintTutorial();
    show("sc-tutorial", true);
  }
}

function paintTutorial(): void {
  const t = TUTORIAL[tutStep]!;
  $("#tut-title").textContent = t.title;
  $("#tut-body").textContent = t.body;
}

function paintCampaign(): void {
  const grid = $("#level-grid");
  grid.innerHTML = "";
  for (const lv of CAMPAIGN) {
    const b = document.createElement("button");
    b.className = "lv" + (lv.bonusSlide ? " bonus" : "");
    b.disabled = lv.id > save.unlocked;
    b.innerHTML = `<span class="n">${lv.bonusSlide ? "BONUS" : "VEIN " + String(lv.id).padStart(2, "0")}</span><span class="t">${lv.name}</span>`;
    b.addEventListener("click", () => {
      campaignIndex = CAMPAIGN.indexOf(lv);
      startGame(lv, "campaign");
    });
    grid.appendChild(b);
  }
}

function paintSettings(): void {
  const st = save.settings;
  ($("#s-music") as HTMLInputElement).value = String(Math.round(st.music * 100));
  ($("#s-sfx") as HTMLInputElement).value = String(Math.round(st.sfx * 100));
  ($("#s-diff") as HTMLSelectElement).value = st.difficulty;
  ($("#s-qual") as HTMLSelectElement).value = st.quality;
  ($("#s-lantern") as HTMLInputElement).checked = st.lantern;
  ($("#s-ghost") as HTMLInputElement).checked = st.ghost;
  ($("#s-shake") as HTMLInputElement).checked = st.shake;
  ($("#s-parts") as HTMLInputElement).checked = st.particles;
  ($("#s-cb") as HTMLInputElement).checked = st.colorblind;
  ($("#s-motion") as HTMLInputElement).checked = st.reducedMotion;
}

function readSettings(): void {
  save.settings.music = Number(($("#s-music") as HTMLInputElement).value) / 100;
  save.settings.sfx = Number(($("#s-sfx") as HTMLInputElement).value) / 100;
  save.settings.difficulty = ($("#s-diff") as HTMLSelectElement).value as SaveData["settings"]["difficulty"];
  save.settings.quality = ($("#s-qual") as HTMLSelectElement).value as SaveData["settings"]["quality"];
  save.settings.lantern = ($("#s-lantern") as HTMLInputElement).checked;
  save.settings.ghost = ($("#s-ghost") as HTMLInputElement).checked;
  save.settings.shake = ($("#s-shake") as HTMLInputElement).checked;
  save.settings.particles = ($("#s-parts") as HTMLInputElement).checked;
  save.settings.colorblind = ($("#s-cb") as HTMLInputElement).checked;
  save.settings.reducedMotion = ($("#s-motion") as HTMLInputElement).checked;
  audio.setMusic(save.settings.music);
  audio.setSfx(save.settings.sfx);
  writeSave(save);
}

function paintScores(): void {
  const root = $("#score-cols");
  const cols: ["campaign" | "endless" | "daily", string][] = [
    ["campaign", "CAMPAIGN"],
    ["endless", "ENDLESS"],
    ["daily", "DAILY"],
  ];
  root.innerHTML = cols
    .map(([k, title]) => {
      const rows = save.scores[k];
      const lis =
        rows.length === 0
          ? `<li><span>—</span><span>no traces</span></li>`
          : rows
              .map(
                (r) =>
                  `<li><span>${escapeHtml(r.name)}</span><span>${r.score}${r.extra ? " · " + escapeHtml(r.extra) : ""}</span></li>`,
              )
              .join("");
      return `<div><h3>${title}</h3><ol>${lis}</ol></div>`;
    })
    .join("");
}

function escapeHtml(s: string): string {
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]!);
}

function paintResults(): void {
  if (!game || !spec) return;
  const win = game.wonBy !== "none" && game.phase !== "lost";
  const lost = game.phase === "lost" || game.phase === "results" && game.wonBy === "none";
  const success = game.phase === "won" || (game.phase === "results" && game.wonBy !== "none");
  void lost;
  const ok = success;
  $("#rs-kicker").textContent = ok ? "CONTAINED" : "OVERFLOW";
  $("#rs-kicker").classList.toggle("bad", !ok);
  $("#rs-title").textContent = spec.name;
  $("#rs-score").textContent = String(game.score).padStart(6, "0");
  $("#rs-break").innerHTML = `
    <li><span>Distance</span><span>${game.distance} / ${game.min}</span></li>
    <li><span>Crossings</span><span>${game.crossings}</span></li>
    <li><span>Loops</span><span>${game.loops}</span></li>
    <li><span>Cisterns</span><span>${game.reservoirs}</span></li>
    <li><span>Unused penalty</span><span>−${game.unusedPenalty}</span></li>
    <li><span>Overtime</span><span>+${game.extraBonus}</span></li>
    ${game.perfect ? "<li><span>Clean rig</span><span>+1000</span></li>" : ""}
  `;
  ($("#name-input") as HTMLInputElement).value = save.lastName;
  const next = $("#rs-next") as HTMLButtonElement;
  next.style.display = ok && mode === "campaign" && campaignIndex < CAMPAIGN.length - 1 ? "" : "none";
}

function commitScore(): void {
  if (!game || !spec) return;
  const name = save.lastName || "OPERATOR";
  const row = {
    name,
    score: mode === "campaign" ? campaignRunScore + (game.phase === "lost" ? 0 : game.score) : game.score,
    at: new Date().toISOString(),
    extra: spec.name,
  };
  if (mode === "campaign") {
    save.scores.campaign = pushScore(save.scores.campaign, {
      ...row,
      score: campaignRunScore + game.score,
    });
    save.levelBest[spec.id - 1] = Math.max(save.levelBest[spec.id - 1] ?? 0, game.score);
    if (game.wonBy !== "none") {
      save.unlocked = Math.max(save.unlocked, spec.id + 1);
      campaignRunScore += game.score;
      save.campaignScore = Math.max(save.campaignScore, campaignRunScore);
      save.campaignBest = Math.max(save.campaignBest, campaignRunScore);
    }
  } else if (mode === "endless") {
    save.scores.endless = pushScore(save.scores.endless, row);
    save.endlessBest = Math.max(save.endlessBest, game.score);
  } else if (mode === "daily") {
    save.scores.daily = pushScore(save.scores.daily, { ...row, extra: todayKey() });
    if (!save.dailyBest || save.dailyBest.date !== todayKey() || game.score > save.dailyBest.score) {
      save.dailyBest = { date: todayKey(), score: game.score };
    }
  }
  writeSave(save);
}

function handleEvents(g: Game): void {
  const L = renderer.layout;
  for (const e of g.popEvents()) {
    audio.handle(e);
    if (!L) continue;
    const t = L.tile;
    if (e.r !== undefined && e.c !== undefined) {
      const x = L.bx + e.c * t + t / 2;
      const y = L.by + e.r * t + t / 2;
      if (e.type === "place") {
        renderer.burst(x, y, 10, "#3dffc8", 70);
        renderer.punch(3);
      }
      if (e.type === "replace") {
        renderer.burst(x, y, 14, "#e0b45c", 90);
        renderer.punch(5);
      }
      if (e.type === "loop") {
        renderer.burst(x, y, 22, "#e0b45c", 140, "soft");
        renderer.showBanner("LOOP");
      }
      if (e.type === "cross") {
        renderer.burst(x, y, 18, "#ff5ad5", 120, "soft");
      }
      if (e.type === "spill") {
        renderer.burst(x, y, 40, "#3dffc8", 180, "soft");
        renderer.spillFlash();
      }
    }
    if (e.type === "win") {
      renderer.winFlash();
      if (mode === "endless") {
        renderer.showBanner("WAVE " + g.endlessLevel);
      }
    }
    if (e.type === "lose") renderer.spillFlash();
    if (e.type === "release") renderer.showBanner("VALVE OPEN");
  }
}

function loop(ts: number): void {
  const dt = Math.min(0.05, (ts - last) / 1000 || 0.016);
  last = ts;

  if (running && game && !paused) {
    game.tick(dt);
    handleEvents(game);
    const f = game.forecast();
    audio.tick(dt, game.phase === "flowing", f.danger ? 1 : 0);
    if (f.danger && game.phase === "flowing" && !warned) {
      audio.warn();
      warned = true;
    }
    if (!f.danger) warned = false;

    if (game.phase === "won" || game.phase === "lost") {
      if (mode === "endless" && game.phase === "won") {
        /* endlessAdvance already reset to countdown */
      } else {
        resultsTimer += dt;
        if (resultsTimer > 1.5) {
          game.resultsReady();
          paintResults();
          commitScore();
          show("sc-results", true);
          running = false;
        }
      }
    }
  } else {
    audio.tick(dt, false, 0);
  }

  if (game && renderer) {
    renderer.tick(dt, save.settings);
    renderer.draw(game, save.settings, cursor);
  }
  requestAnimationFrame(loop);
}

function bind(): void {
  document.querySelectorAll("[data-act]").forEach((el) => {
    el.addEventListener("click", () => {
      audio.click();
      act((el as HTMLElement).dataset.act!);
    });
  });

  $("#sc-boot").addEventListener("click", async () => {
    await audio.unlock();
    audio.setMusic(save.settings.music);
    audio.setSfx(save.settings.sfx);
    audio.click();
    show("sc-menu", true);
  });

  $("#pw-form").addEventListener("submit", (e) => {
    e.preventDefault();
    const v = ($("#pw-input") as HTMLInputElement).value.trim().toUpperCase();
    const id = PASSWORDS[v];
    const msg = $("#pw-msg");
    if (id) {
      save.unlocked = Math.max(save.unlocked, id);
      writeSave(save);
      msg.textContent = "Unlocked from " + v;
      paintCampaign();
    } else msg.textContent = "Unknown stamp";
  });

  $("#name-form").addEventListener("submit", (e) => {
    e.preventDefault();
    save.lastName = ($("#name-input") as HTMLInputElement).value.trim().toUpperCase() || "OPERATOR";
    writeSave(save);
    audio.click();
    ($("#name-form") as HTMLElement).style.opacity = "0.5";
  });

  $("#tut-next").addEventListener("click", () => {
    audio.click();
    tutStep++;
    if (tutStep >= TUTORIAL.length) finishTut();
    else paintTutorial();
  });
  $("#tut-skip").addEventListener("click", () => {
    audio.click();
    finishTut();
  });

  for (const id of ["s-music", "s-sfx", "s-diff", "s-qual", "s-lantern", "s-ghost", "s-shake", "s-parts", "s-cb", "s-motion"]) {
    $("#" + id).addEventListener("change", readSettings);
    $("#" + id).addEventListener("input", readSettings);
  }

  const canvas = $("#game") as HTMLCanvasElement;
  canvas.addEventListener("pointermove", (e) => {
    cursor = { x: e.clientX, y: e.clientY };
    if (game && renderer) {
      const h = renderer.hit(e.clientX, e.clientY, game);
      renderer.hover = h.kind === "cell" ? { r: h.r, c: h.c } : null;
    }
  });
  canvas.addEventListener("pointerdown", (e) => {
    if (!game || paused || !running) return;
    audio.unlock();
    const h = renderer.hit(e.clientX, e.clientY, game);
    if (h.kind === "cell") game.place(h.r, h.c);
    if (h.kind === "dump") game.dump();
    if (h.kind === "release") game.release();
    if (h.kind === "pause") act("pause");
  });
  canvas.addEventListener("contextmenu", (e) => {
    e.preventDefault();
    if (game && running && !paused) game.dump();
  });

  window.addEventListener("keydown", (e) => {
    if (e.key === "Escape") {
      if (running && !paused) act("pause");
      else if (paused) act("resume");
      return;
    }
    if (!game || paused || !running) return;
    if (e.key === "x" || e.key === "X") game.dump();
    if (e.key === " " || e.code === "Space") {
      e.preventDefault();
      game.release();
    }
    if (e.key === "p" || e.key === "P") act("pause");
  });
}

function finishTut(): void {
  save.seenTutorial = true;
  writeSave(save);
  paused = false;
  show(null);
}

function act(name: string): void {
  switch (name) {
    case "menu":
      running = false;
      game = null;
      paused = false;
      show("sc-menu", true);
      break;
    case "campaign":
      paintCampaign();
      show("sc-campaign", true);
      break;
    case "endless":
      startGame(makeEndlessSpec(), "endless");
      break;
    case "daily":
      startGame(makeDailySpec(todayKey()), "daily");
      break;
    case "help":
      show("sc-help", true);
      break;
    case "scores":
      paintScores();
      show("sc-scores", true);
      break;
    case "settings":
      settingsFrom = "menu";
      paintSettings();
      show("sc-settings", true);
      break;
    case "settings-ingame":
      settingsFrom = "pause";
      paintSettings();
      show("sc-settings", true);
      break;
    case "pause":
      if (!running || !game) return;
      paused = true;
      show("sc-pause", true);
      break;
    case "resume":
      paused = false;
      show(null);
      break;
    case "settings-back":
      if (settingsFrom === "pause") show("sc-pause", true);
      else show("sc-menu", true);
      break;
    case "abort":
      running = false;
      game = null;
      paused = false;
      show("sc-menu", true);
      break;
    case "retry":
      if (spec) startGame(spec, mode, mode === "campaign");
      break;
    case "next":
      if (mode === "campaign") {
        campaignIndex++;
        const n = CAMPAIGN[campaignIndex];
        if (n) startGame(n, "campaign", true);
        else act("menu");
      }
      break;
    default:
      break;
  }
  if (name === "settings" || name === "settings-ingame") return;
  if (settingsFrom === "pause" && name === "menu") {
    /* abandon handled */
  }
}

/* settings back button uses data-act=menu — fix: intercept when from pause */
function fixSettingsBack(): void {
  const back = document.querySelector("#sc-settings [data-act='menu']") as HTMLButtonElement;
  back.dataset.act = "settings-back";
  back.addEventListener("click", (e) => {
    e.stopImmediatePropagation();
    audio.click();
    if (settingsFrom === "pause") show("sc-pause", true);
    else show("sc-menu", true);
  });
}

async function boot(): Promise<void> {
  const base = import.meta.env.BASE_URL;
  const names = ["hero", "metal", "concrete", "plasma", "logo", "victory", "overflow"];
  const imgs = await Promise.all(names.map((n) => loadImg(`${base}assets/${n}.jpg`)));
  const assets: Assets = {
    hero: imgs[0]!,
    metal: imgs[1]!,
    concrete: imgs[2]!,
    plasma: imgs[3]!,
    logo: imgs[4]!,
    victory: imgs[5]!,
    overflow: imgs[6]!,
  };
  document.querySelectorAll(".logo-mark").forEach((el) => {
    (el as HTMLImageElement).src = `${base}assets/logo.jpg`;
  });
  document.documentElement.style.setProperty(
    "--hero",
    `url('${base}assets/hero.jpg')`,
  );

  const canvas = $("#game") as HTMLCanvasElement;
  renderer = new Renderer(canvas, assets);

  bind();
  fixSettingsBack();
  paintSettings();

  Object.defineProperty(window, "__lumina", {
    get: () => ({ game, renderer, save, startGame, act, CAMPAIGN }),
  });

  requestAnimationFrame(loop);

  const qLevel = Number(params.get("level") || 0);
  if (qLevel) {
    const lv = CAMPAIGN.find((l) => l.id === qLevel) ?? CAMPAIGN[0]!;
    campaignIndex = CAMPAIGN.indexOf(lv);
    void audio.unlock();
    startGame(lv, "campaign");
  } else {
    show("sc-boot", true);
  }
  void N;
  void S;
  void West;
}

boot();
