// browser.ts - Mithril entry point
// Built by: bun build src/browser.ts --target browser --outfile public/mono-display.js

import m from "mithril";
import {
  ElementType,
  ElementTypeToString,
  MonoDisplayDriver,
  MonoDisplayFile,
  MonoDisplayParser,
  MonoDisplayRenderer,
  SectionType,
  SectionTypeToString,
  StringToElementType,
  StringToSectionType,
  cycleTheme,
  type ElementTypeName,
  type MonoFormatAnimation,
  type MonoFormatElement,
  type MonoFormatElementsAlways,
  type MonoFormatElementsTimespan,
  type MonoFormatImage2D,
  type MonoFormatPixelImage,
  type MonoFormatSection,
  type SectionTypeName
} from "libmonoformat";


let W = 120, H = 60;

// --- State -------------------------------------------------------------------
let file: MonoDisplayFile = new MonoDisplayFile([]);
let activeSecIndex: number | null = null;
let activeElIndex: number | null = null;
let currentFilename = "untitled";
let isDirty = false;

// --- Element metadata --------------------------------------------------------
type ElFieldMeta = { key: string; label: string; type: string; default: any; full?: boolean };
type ElFlagMeta = { key: string; label: string; default?: boolean };

const EL_FIELDS: Partial<Record<ElementTypeName, ElFieldMeta[]>> = {
  Image2D: [
    { key: "xOffset", label: "X offset", type: "number", default: 0 },
    { key: "yOffset", label: "Y offset", type: "number", default: 0 },
    { key: "width", label: "Width", type: "number", default: W },
    { key: "height", label: "Height", type: "number", default: H },
  ],
  Animation: [
    { key: "xOffset", label: "X offset", type: "number", default: 0 },
    { key: "yOffset", label: "Y offset", type: "number", default: 0 },
    { key: "width", label: "Width", type: "number", default: W },
    { key: "height", label: "Height", type: "number", default: H },
    { key: "updateInterval", label: "Update interval", type: "number", default: 12 },
    { key: "frames", label: "Frames", type: "list", default: [] },
  ],
  ClippedText: [
    { key: "text", label: "Text", type: "text", default: "Hello, World!", full: true },
    { key: "xOffset", label: "X offset", type: "number", default: 0 },
    { key: "yOffset", label: "Y offset", type: "number", default: 10 },
    { key: "width", label: "Width", type: "number", default: 30 },
    { key: "height", label: "Height", type: "number", default: 10 },
    { key: "fontIndex", label: "Font", type: "font", default: 0 },
  ],
  HScrollText: [
    { key: "text", label: "Text", type: "text", default: "Scrolling text - ", full: true },
    { key: "xOffset", label: "X offset", type: "number", default: 0 },
    { key: "yOffset", label: "Y offset", type: "number", default: 10 },
    { key: "width", label: "Width", type: "number", default: 30 },
    { key: "height", label: "Height", type: "number", default: 10 },
    { key: "scrollSpeed", label: "Scroll speed", type: "number", default: 50 },
    { key: "fontIndex", label: "Font", type: "font", default: 0 },
  ],
  VScrollText: [
    { key: "text", label: "Text", type: "text", default: "Scrolling text - ", full: true },
    { key: "xOffset", label: "X offset", type: "number", default: 0 },
    { key: "yOffset", label: "Y offset", type: "number", default: 32 },
    { key: "width", label: "Width", type: "number", default: 30 },
    { key: "height", label: "Height", type: "number", default: 10 },
    { key: "scrollSpeed", label: "Scroll speed", type: "number", default: 50 },
    { key: "fontIndex", label: "Font", type: "font", default: 0 },
  ],
  Line: [
    { key: "xOrigin", label: "X offset", type: "number", default: 0 },
    { key: "yOrigin", label: "Y offset", type: "number", default: 32 },
    { key: "xTarget", label: "X offset", type: "number", default: 0 },
    { key: "yTarget", label: "Y offset", type: "number", default: 32 },
    { key: "linestyle", label: "Lintylee S", type: "number", default: 0 },
  ],
  CurrentTime: [
    { key: "xOffset", label: "X offset", type: "number", default: 0 },
    { key: "yOffset", label: "Y offset", type: "number", default: 8 },
    { key: "width", label: "Width", type: "number", default: W },
    { key: "height", label: "Height", type: "number", default: H },
    { key: "utcOffsetMinutes", label: "UTC offset (min)", type: "number", default: 120 },
    { key: "fontIndex", label: "Font", type: "font", default: 0 },
  ],
};

const EL_FLAGS: Partial<Record<ElementTypeName, ElFlagMeta[]>> = {
  HScrollText: [
    { key: "endless", label: "Endless" },
    { key: "invertDirection", label: "Invert direction" },
    { key: "padBefore", label: "Pad Before" },
    { key: "padAfter", label: "Pad After" },
  ],
  CurrentTime: [
    { key: "clock12h", label: "12h mode", default: false },
    { key: "showHours", label: "Show hours", default: true },
    { key: "showSeconds", label: "Show seconds", default: true },
  ],
};

const EL_TYPES: ElementType[] = Object.keys(EL_FIELDS).map(
  (x) => StringToElementType[x as ElementTypeName]
);

const DEFAULT_FONTS = Object.keys(MonoDisplayRenderer.builtinFonts);

// --- Confirm dialog ----------------------------------------------------------
function showConfirm(
  msg: string,
  buttons: { label: string; primary?: boolean; action: boolean }[]
): Promise<boolean> {
  return new Promise(resolve => {
    const overlay = document.getElementById("confirm-overlay");
    const msgEl = document.getElementById("confirm-msg");
    const btnsEl = document.getElementById("confirm-btns");
    if (msgEl) msgEl.textContent = msg;
    if (btnsEl) btnsEl.innerHTML = "";
    buttons.forEach(b => {
      const btn = document.createElement("button");
      btn.className = "cb" + (b.primary ? " primary" : "");
      btn.textContent = b.label;
      btn.onclick = () => { overlay?.classList.remove("show"); resolve(b.action); };
      btnsEl?.appendChild(btn);
    });
    overlay?.classList.add("show");
  });
}

// --- Persistence -------------------------------------------------------------
const STORAGE_KEY = "monodisplay_autosave";

function saveToStorage() {
  try {
    const buf = file.toBuffer();
    const b64 = btoa(String.fromCharCode(...buf));
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ filename: currentFilename, data: b64 }));
  } catch { }
}

function loadFromStorage() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return;
    const { filename, data } = JSON.parse(raw);
    const bytes = Uint8Array.from(atob(data), c => c.charCodeAt(0));
    const parsed = new MonoDisplayParser().parse(bytes.buffer);
    file = new MonoDisplayFile(parsed.sections);
    currentFilename = filename;
    const filenameEl = document.getElementById("filename");
    if (filenameEl) filenameEl.textContent = filename;
    isDirty = true;
    document.getElementById("filename")?.classList.add("dirty");
    m.redraw();
  } catch { }
}

// --- Dirty tracking ----------------------------------------------------------
function markDirty() {
  isDirty = true;
  document.getElementById("filename")?.classList.add("dirty");
  saveToStorage();
}
function markClean() {
  isDirty = false;
  document.getElementById("filename")?.classList.remove("dirty");
}
async function guardDirty(): Promise<boolean> {
  if (!isDirty || !file.sections.length) return true;
  return showConfirm(
    "You have unsaved changes. Loading a demo will replace the current editor state.",
    [{ label: "Cancel", action: false }, { label: "Load anyway", primary: true, action: true }]
  );
}

function changeDisplayLayout() {
  const sel = document.querySelector('.pv-label select') as HTMLSelectElement;
  const [w, h] = sel.value.split('×').map(s => parseInt(s.trim()));
  W = w as number;
  H = h as number;
  ((window as any)._mdDriver as MonoDisplayDriver).setSize(W, H);
  ((window as any)._mdDriver as MonoDisplayDriver).load(() => Promise.resolve(file.toBuffer()));
}

// --- Helpers -----------------------------------------------------------------
function newSec(): MonoFormatElementsAlways {
  return {
    sectionType: SectionType.ElementsAlways, elements: [], flags: {
      clearBuffer: true,
      drawBack: true,
      drawFront: true,
    }
  };
}

function createNewElement(type: ElementType): MonoFormatElement {
  const fields: Record<string, any> = {};
  (EL_FIELDS[ElementTypeToString[type]] as ElFieldMeta[] || [])
    .forEach(f => (fields[f.key] = f.default));
  const flags: Record<string, any> = {};
  (EL_FLAGS[ElementTypeToString[type]] as ElFlagMeta[] || [])
    .forEach(f => (flags[f.key] = f.default ?? false));
  const el: any = { type, ...fields, flags };
  if (type === ElementType.Image2D) {
    el.image = { pixels: new Uint8Array(el.width * el.height), width: el.width, height: el.height };
  }
  if (type === ElementType.Animation) {
    el.frames = [{ pixels: new Uint8Array(el.width * el.height), width: el.width, height: el.height }];
  }
  return el as MonoFormatElement;
}

function getSectionByIndex(i: number | null): MonoFormatSection | undefined {
  return i !== null ? file.sections[i] : undefined;
}

function getElementByIndex(si: number, ei: number): MonoFormatElement | undefined {
  const s = getSectionByIndex(si);
  return s && "elements" in s ? s.elements[ei] : undefined;
}

function getCustomFonts(): { fontname: string; index: number }[] {
  return file.sections
    .filter(s => s.sectionType === SectionType.CustomFont)
    .map((_, i) => ({ fontname: `CustomFont ${i}`, index: 0x8000 + i }));
}

function elSummary(el: MonoFormatElement): string {
  switch (el.type) {
    case ElementType.ClippedText:
    case ElementType.HScrollText:
      return el.text.slice(0, 24) + (el.text.length > 24 ? "..." : "");
    case ElementType.CurrentTime:
    case ElementType.Image2D:
    case ElementType.HorizontalScroll:
    case ElementType.VerticalScroll:
      return `${(el as any).xOffset ?? 0}, ${(el as any).yOffset ?? 0}`;
    default:
      return el.type.toString();
  }
}

// --- Mutations ----------------------------------------------------------------
function addSection() {
  file.sections.push(newSec());
  activeSecIndex = file.sections.length - 1;
  activeElIndex = null;
  markDirty(); triggerPreview(); m.redraw();
}

function removeSection(si: number) {
  file.sections.splice(si, 1);
  if (activeSecIndex === si) {
    activeSecIndex = file.sections.length ? file.sections.length - 1 : null;
    activeElIndex = null;
  }
  markDirty(); triggerPreview();
}

function toggleSection(si: number) {
  activeSecIndex = si;
}

function setSectionFlag(flag: string, val: boolean) {
  if (activeSecIndex === null) return;
  const s = getSectionByIndex(activeSecIndex);
  if (!s) return;
  (s as any).flags[flag] = val;
  markDirty(); triggerPreview();
}

function setSectionField(si: number, key: string, val: any) {
  const s = getSectionByIndex(si);
  if (!s) return;
  (s as any)[key] = val;
  markDirty(); triggerPreview();
}

function setSectionType(si: number, sectionType: string) {
  const sec = getSectionByIndex(si);
  if (!sec) return;
  const wasElementsSection = sec.sectionType !== SectionType.CustomFont;
  (sec as any).sectionType = StringToSectionType[sectionType as SectionTypeName];
  if ((sec as any).sectionType === SectionType.CustomFont) {
    (sec as any).fontData = new Uint8Array();
    delete (sec as any).elements;
    delete (sec as any).flags;
  } else {
    if (!wasElementsSection) {
      (sec as any).elements = [];
      (sec as any).flags = {};
    }
    delete (sec as any).fontData;
    if ((sec as any).sectionType === SectionType.ElementsTimespan) {
      const now = BigInt(Math.floor(Date.now() / 1000));
      (sec as any).startTimestamp = now;
      (sec as any).endTimestamp = now + 3600n;
    } else {
      delete (sec as any).startTimestamp;
      delete (sec as any).endTimestamp;
    }
  }
  markDirty(); triggerPreview();
}

function addElement(si: number) {
  const s = getSectionByIndex(si);
  if (!s || !("elements" in s)) return;
  s.elements.push(createNewElement(ElementType.HScrollText));
  activeElIndex = s.elements.length - 1;
  activeSecIndex = si;
  markDirty(); triggerPreview();
}

function removeElement(si: number, ei: number) {
  const s = getSectionByIndex(si);
  if (!s || !("elements" in s)) return;
  s.elements.splice(ei, 1);
  activeElIndex = null;
  markDirty(); triggerPreview();
}

function selectElement(si: number, ei: number) {
  if (activeSecIndex === si && activeElIndex === ei) {
    activeElIndex = null;
  } else {
    activeSecIndex = si;
    activeElIndex = ei;
  }
}

function changeElType(si: number, ei: number, typeString: ElementTypeName) {
  const el = getElementByIndex(si, ei);
  if (!el) return;
  const fresh = createNewElement(StringToElementType[typeString]);
  Object.assign(el, fresh);
  markDirty(); triggerPreview();
}

function setElField(si: number, ei: number, key: string, val: any) {
  const el = getElementByIndex(si, ei);
  if (!el) return;
  (el as any)[key] = val;
  markDirty(); triggerPreview();
}

function setElFlag(si: number, ei: number, key: string, val: any) {
  const el = getElementByIndex(si, ei);
  if (!el) return;
  (el as any).flags[key] = val;
  markDirty(); triggerPreview();
}

// --- DEMO --------------------------------------------------------------------
const DEMO: Record<string, () => MonoDisplayFile> = {
  Checkerboard() {
    const pixels = new Uint8Array(W * H);
    for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) pixels[y * W + x] = (x + y) % 2;
    return new MonoDisplayFile([{
      sectionType: SectionType.ElementsAlways,
      elements: [{ type: ElementType.Image2D, image: { pixels, height: H, width: W }, xOffset: 0, yOffset: 0 }],
      flags: { clearBuffer: true, drawFront: true, drawBack: true },
    }]);
  },
  Blink() {
    return new MonoDisplayFile([{
      sectionType: SectionType.ElementsAlways,
      elements: [{
        type: ElementType.Animation, width: W, height: H, updateInterval: 12, xOffset: 0, yOffset: 0,
        frames: [
          { pixels: new Uint8Array(W * H).fill(1), width: 0, height: 0 },
          { pixels: new Uint8Array(W * H).fill(0), width: 0, height: 0 },
        ],
      }],
      flags: { clearBuffer: true, drawFront: true, drawBack: true },
    }]);
  },
  Text() {
    return new MonoDisplayFile([{
      sectionType: SectionType.ElementsAlways,
      elements: [{ type: ElementType.ClippedText, fontIndex: 0, text: "Hello, World!", xOffset: 0, yOffset: 16, width: 60, height: 10 }],
      flags: { clearBuffer: true, drawFront: true, drawBack: true },
    }]);
  },
  Scrolltext() {
    return new MonoDisplayFile([{
      sectionType: SectionType.ElementsAlways,
      elements: [{
        type: ElementType.HScrollText, fontIndex: 0,
        text: "MONO DISPLAY - scrolling ticker - 🚀 ",
        xOffset: 0, yOffset: 32, width: W, height: 16, scrollSpeed: 50,
        flags: { endless: true, invertDirection: false, padStart: false, padEnd: false },
      }],
      flags: { clearBuffer: true, drawFront: true, drawBack: true },
    }]);
  },
  Time() {
    return new MonoDisplayFile([{
      sectionType: SectionType.ElementsAlways,
      elements: [
        { type: ElementType.CurrentTime, fontIndex: 0, flags: {}, xOffset: 0, yOffset: 8, width: W, height: 16, utcOffsetMinutes: 120 },
        { type: ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true }, xOffset: 40, yOffset: 16, width: W, height: 16, utcOffsetMinutes: 120 },
        { type: ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true, showHours: true }, xOffset: 0, yOffset: 24, width: W, height: 16, utcOffsetMinutes: 120 },
        { type: ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true, showHours: false }, xOffset: 40, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 },
        { type: ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true, showSeconds: true }, xOffset: 80, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 },
      ],
      flags: { clearBuffer: true, drawFront: true, drawBack: true },
    }]);
  },
};

// --- Preview -----------------------------------------------------------------
let previewTimer: ReturnType<typeof setTimeout> | null = null;

function triggerPreview() {
  if (previewTimer) clearTimeout(previewTimer);
  previewTimer = setTimeout(() => {
    try { buildPreview(); } catch (e) { console.warn("preview", e); }
  }, 150);
}

function buildPreview() {
  if (!(window as any)._mdDriver)
    (window as any)._mdDriver = new MonoDisplayDriver("canvas_root", { onColor: "#EC0", offColor: "#000", fps: 25 });
  changeDisplayLayout();
}

// --- Load / Export -----------------------------------------------------------
function loadBin(input: HTMLInputElement) {
  const binFile = input.files?.[0];
  if (!binFile) return;
  currentFilename = binFile.name;
  const filenameEl = document.getElementById("filename");
  if (filenameEl) filenameEl.textContent = binFile.name;
  const reader = new FileReader();
  reader.onload = e => {
    try {
      const parsed = new MonoDisplayParser().parse(e.target!.result as ArrayBuffer);
      file = new MonoDisplayFile(parsed.sections);
      activeSecIndex = null;
      activeElIndex = null;
      triggerPreview();
      m.redraw();
    } catch (err) {
      console.warn("Could not parse sections from bin:", err);
    }
  };
  reader.readAsArrayBuffer(binFile);
  input.value = "";
  markClean();
}

function exportBin() {
  // if (!window.MonoDisplay) { alert("MonoDisplay library not loaded."); return; }
  const blob = new Blob([file.toBuffer() as any], { type: "application/octet-stream" });
  const a = document.createElement("a");
  a.href = URL.createObjectURL(blob);
  a.download = currentFilename.replace(/\.bin$/, "") + ".bin";
  a.click();
  markClean();
}

// --- Mithril Components -------------------------------------------------------

function FieldInput(si: number, ei: number, field: ElFieldMeta, el: MonoFormatElement): m.Vnode {
  const val = (el as any)[field.key] ?? field.default;
  switch (field.type) {
    case "text":
      return m("textarea", {
        onchange: (e: Event) => setElField(si, ei, field.key, (e.target as HTMLTextAreaElement).value),
      }, val);
    case "font":
      return m("select", {
        onchange: (e: Event) => setElField(si, ei, field.key, +(e.target as HTMLSelectElement).value),
      }, [
        ...DEFAULT_FONTS.map((name, i) => m("option", { value: i, selected: val === i }, name)),
        ...getCustomFonts().map(cf => m("option", { value: cf.index, selected: val === cf.index }, cf.fontname)),
      ]);
    case "list":
      return m("span"); // rendered by dedicated editor
    default:
      return m("input[type=number]", {
        value: val,
        onchange: (e: Event) => setElField(si, ei, field.key, +(e.target as HTMLInputElement).value),
      });
  }
}

// --- Shared pixel canvas ------------------------------------------------------

const PIXEL_SCALE = 4;

function getPixelIndex(img: MonoFormatPixelImage, x: number, y: number): number {
  return y * img.width + x;
}

function drawPixelCanvas(canvas: HTMLCanvasElement, img: MonoFormatPixelImage) {
  const ctx = canvas.getContext("2d")!;
  ctx.fillStyle = "#000";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "#EC0";
  for (let y = 0; y < img.height; y++) {
    for (let x = 0; x < img.width; x++) {
      if (img.pixels[getPixelIndex(img, x, y)]) {
        ctx.fillRect(x * PIXEL_SCALE, y * PIXEL_SCALE, PIXEL_SCALE, PIXEL_SCALE);
      }
    }
  }
}

interface PixelCanvasState { drawing: boolean; drawValue: number; }

const PixelCanvas: m.Component<{ img: MonoFormatPixelImage; onpaint: () => void }, PixelCanvasState> = {
  oninit(vnode) {
    vnode.state.drawing = false;
    vnode.state.drawValue = 1;
  },
  view({ attrs: { img, onpaint }, state: s }) {
    function pixelFromCoords(canvas: HTMLCanvasElement, clientX: number, clientY: number) {
      const rect = canvas.getBoundingClientRect();
      const scaleX = canvas.width / rect.width;
      const scaleY = canvas.height / rect.height;
      const x = Math.floor((clientX - rect.left) * scaleX / PIXEL_SCALE);
      const y = Math.floor((clientY - rect.top) * scaleY / PIXEL_SCALE);
      if (x < 0 || x >= img.width || y < 0 || y >= img.height) return null;
      return { x, y };
    }

    function paintAt(canvas: HTMLCanvasElement, clientX: number, clientY: number) {
      const p = pixelFromCoords(canvas, clientX, clientY);
      if (!p) return;
      img.pixels[getPixelIndex(img, p.x, p.y)] = s.drawValue;
      onpaint();
      drawPixelCanvas(canvas, img);
    }

    function paint(e: MouseEvent) {
      paintAt(e.currentTarget as HTMLCanvasElement, e.clientX, e.clientY);
    }

    function attachTouch(canvas: HTMLCanvasElement) {
      canvas.addEventListener("touchstart", (e: TouchEvent) => {
        e.preventDefault();
        s.drawing = true;
        const touch = e.changedTouches[0];
        const p = pixelFromCoords(canvas, touch.clientX, touch.clientY);
        if (p) s.drawValue = img.pixels[getPixelIndex(img, p.x, p.y)] ? 0 : 1;
        paintAt(canvas, touch.clientX, touch.clientY);
      }, { passive: false });

      canvas.addEventListener("touchmove", (e: TouchEvent) => {
        e.preventDefault();
        if (!s.drawing) return;
        const touch = e.changedTouches[0];
        paintAt(canvas, touch.clientX, touch.clientY);
      }, { passive: false });

      canvas.addEventListener("touchend", (e) => { e.preventDefault(); s.drawing = false; }, { passive: false });
      canvas.addEventListener("touchcancel", (e) => { e.preventDefault(); s.drawing = false; }, { passive: false });
    }

    return m("canvas.pixel-editor", {
      width: img.width * PIXEL_SCALE,
      height: img.height * PIXEL_SCALE,
      oncreate: ({ dom }: m.VnodeDOM) => {
        const canvas = dom as HTMLCanvasElement;
        drawPixelCanvas(canvas, img);
        attachTouch(canvas);
      },
      onupdate: ({ dom }: m.VnodeDOM) => drawPixelCanvas(dom as HTMLCanvasElement, img),

      onmousedown: (e: MouseEvent) => {
        s.drawing = true;
        const p = pixelFromCoords(e.currentTarget as HTMLCanvasElement, e.clientX, e.clientY);
        if (p) s.drawValue = img.pixels[getPixelIndex(img, p.x, p.y)] ? 0 : 1;
        paint(e);
      },
      onmousemove: (e: MouseEvent) => { if (s.drawing) paint(e); },
      onmouseup: () => { s.drawing = false; },
      onmouseleave: () => { s.drawing = false; },

      ondragover: (e: DragEvent) => {
        e.preventDefault();
        e.dataTransfer!.dropEffect = "copy";
      },
      ondrop: (e: DragEvent) => {
        e.preventDefault();
        const file = e.dataTransfer?.files[0];
        if (!file || !file.type.startsWith("image/")) return;
        const url = URL.createObjectURL(file);
        const htmlImg = new Image();
        htmlImg.onload = () => {
          const offscreen = document.createElement("canvas");
          const ctx = offscreen.getContext("2d")!;
          const scale = Math.min(1, 120 / htmlImg.width, 60 / htmlImg.height);
          const w = Math.round(htmlImg.width * scale);
          const h = Math.round(htmlImg.height * scale);
          offscreen.width = w;
          offscreen.height = h;
          ctx.drawImage(htmlImg, 0, 0, w, h);
          const imageData = ctx.getImageData(0, 0, w, h);
          img.width = w;
          img.height = h;
          img.pixels = new Uint8Array(w * h).map((_, i) => {
            const r = imageData.data[i * 4];
            const g = imageData.data[i * 4 + 1];
            const b = imageData.data[i * 4 + 2];
            const a = imageData.data[i * 4 + 3];
            return a > 128 && (r * 299 + g * 587 + b * 114) < 128_000 ? 0 : 1;
          });
          URL.revokeObjectURL(url);
          m.redraw();
        };
        htmlImg.src = url;
      },
    });
  },
};

// --- Image2D editor -----------------------------------------------------------

const Image2DEditor: m.Component<{ si: number; ei: number; el: MonoFormatImage2D }> = {
  view({ attrs: { si, ei, el } }) {
    if (!el.image) {
      const w = (el as any).width || W;
      const h = (el as any).height || H;
      el.image = { pixels: new Uint8Array(w * h), width: w, height: h };
    }
    return m(PixelCanvas, {
      img: el.image,
      onpaint: () => { markDirty(); triggerPreview(); },
    });
  },
};

// --- Animation editor ---------------------------------------------------------

interface AnimationEditorState { activeFrame: number; }

const AnimationEditor: m.Component<{ si: number; ei: number; el: MonoFormatAnimation }, AnimationEditorState> = {
  oninit(vnode) {
    vnode.state.activeFrame = 0;
  },
  view({ attrs: { si, ei, el }, state }) {
    const w = el.width || W;
    const h = el.height || H;
    if (!el.frames) el.frames = [];
    if (!el.frames.length) {
      el.frames.push({ pixels: new Uint8Array(w * h), width: w, height: h });
    }
    state.activeFrame = Math.min(state.activeFrame, el.frames.length - 1);
    const frame = el.frames[state.activeFrame];

    function addFrame() {
      el.frames.push({ pixels: new Uint8Array(w * h), width: w, height: h });
      state.activeFrame = el.frames.length - 1;
      markDirty(); triggerPreview(); m.redraw();
    }

    function removeFrame(fi: number) {
      el.frames.splice(fi, 1);
      if (!el.frames.length)
        el.frames.push({ pixels: new Uint8Array(w * h), width: w, height: h });
      state.activeFrame = Math.min(state.activeFrame, el.frames.length - 1);
      markDirty(); triggerPreview();
    }

    return m(".anim-editor",
      m(".anim-frame-tabs", { key: "tabs" },
        el.frames.map((_, fi) =>
          m(".anim-frame-tab", {
            class: fi === state.activeFrame ? "active" : "",
            onclick: () => { state.activeFrame = fi; },
          },
            m("span", fi + 1),
            el.frames.length > 1
              ? m("button.x-btn-sm", {
                onclick: (e: Event) => { e.stopPropagation(); removeFrame(fi); },
              }, "×")
              : null,
          )
        ),
        m("button.add-el", { onclick: addFrame }, "+ frame"),
      ),
      m("div", { key: state.activeFrame },
        m(PixelCanvas as any, {
          img: frame,
          onpaint: () => { markDirty(); triggerPreview(); },
        }),
      ),
    );
  },
};

const ElementItem: m.Component<{ si: number; ei: number; el: MonoFormatElement }> = {
  view({ attrs: { si, ei, el } }) {
    const isActive = activeSecIndex === si && activeElIndex === ei;
    const typeStr = ElementTypeToString[el.type];
    const fields = EL_FIELDS[typeStr] || [];
    const flags = EL_FLAGS[typeStr] || [];

    return m(".el-item", { class: isActive ? "active" : "" },
      m(".el-item-hdr", { onclick: () => selectElement(si, ei) },
        m("span.el-type", typeStr),
        m("span.el-name", elSummary(el)),
        m("button.x-btn", {
          title: "Remove element",
          onclick: (e: Event) => { e.stopPropagation(); removeElement(si, ei); },
        }, "X"),
      ),
      isActive && m(".el-fields",
        m(".field.full",
          m("label", "Type"),
          m("select", {
            onchange: (e: Event) => changeElType(si, ei, (e.target as HTMLSelectElement).value as ElementTypeName),
          },
            EL_TYPES.map(t =>
              m("option", { value: ElementTypeToString[t], selected: t === el.type }, ElementTypeToString[t])
            )
          ),
        ),
        m(".fields-grid",
          fields.map(field =>
            m(`.field${field.full ? ".full" : ""}`,
              m("label", field.label),
              FieldInput(si, ei, field, el),
            )
          )
        ),
        flags.length ? m(".field.full",
          m("label", "Flags"),
          m(".flags-row",
            flags.map(flag =>
              m("label.flag-check",
                m("input[type=checkbox]", {
                  checked: !!(el as any).flags?.[flag.key],
                  onchange: (e: Event) => setElFlag(si, ei, flag.key, (e.target as HTMLInputElement).checked),
                }),
                " " + flag.label,
              )
            )
          ),
        ) : null,
        el.type === ElementType.Image2D
          ? m(".field.full",
            m("label", "Pixels"),
            m(Image2DEditor, { si, ei, el: el as MonoFormatImage2D }),
          )
          : el.type === ElementType.Animation
            ? m(".field.full",
              m("label", "Frames"),
              m(AnimationEditor, { si, ei, el: el as MonoFormatAnimation }),
            )
            : null,
      ),
    );
  },
};

const SectionCard: m.Component<{ si: number }> = {
  view({ attrs: { si } }) {
    const section = file.sections[si];
    if (!section) return null;
    const isActive = activeSecIndex === si;
    const typeStr = SectionTypeToString[section.sectionType];

    const hdr = m(`.sec-hdr${isActive ? ".active" : ""}`, { onclick: () => toggleSection(si) },
      m("span.sec-arrow", "▶"),
      m("span.sec-label", typeStr),
      m("button.x-btn", {
        title: "Remove section",
        onclick: (e: Event) => { e.stopPropagation(); removeSection(si); },
      }, "X"),
    );

    if (section.sectionType === SectionType.CustomFont) {
      return m(`.sec-card.open${isActive ? ".active" : ""}`,
        m(`.sec-hdr${isActive ? ".active" : ""}`, { onclick: () => toggleSection(si) },
          m("span.sec-arrow", "▶"),
          m("span.sec-label", "Custom Font"),
          m("span.sec-badge", "1 el"),
          m("button.x-btn", {
            title: "Remove section",
            onclick: (e: Event) => { e.stopPropagation(); removeSection(si); },
          }, "X"),
        ),
        m(".el-list", "?"),
      );
    }

    const elSection = section as MonoFormatElementsAlways | MonoFormatElementsTimespan;

    return m(`.sec-card.open${isActive ? ".active" : ""}`,
      m(`.sec-hdr${isActive ? ".active" : ""}`, { onclick: () => toggleSection(si) },
        m("span.sec-arrow", "▶"),
        m("span.sec-label", typeStr),
        m("span.sec-badge", `${elSection.elements.length} el`),
        m("button.x-btn", {
          title: "Remove section",
          onclick: (e: Event) => { e.stopPropagation(); removeSection(si); },
        }, "X"),
      ),
      section.sectionType === SectionType.ElementsTimespan
        ? m(".el-list",
          ([["Start Time", "startTimestamp"], ["Stop Time", "endTimestamp"]] as [string, string][])
            .map(([label, key]) => {
              let posix: bigint = (section as any)[key];
              if (!posix) {
                posix = BigInt(Math.floor(Date.now() / 1000));
                setSectionField(si, key, posix);
              }
              const dtLocal = new Date(Number(posix) * 1000).toISOString().slice(0, 16);
              return m(".field",
                m("label", label),
                m("input[type=datetime-local]", {
                  value: dtLocal,
                  onchange: (e: Event) => {
                    const ms = new Date((e.target as HTMLInputElement).value).getTime();
                    setSectionField(si, key, BigInt(Math.floor(ms / 1000)));
                  },
                }),
              );
            })
        )
        : null,
      m(".el-list",
        elSection.elements.map((el, ei) => m(ElementItem, { key: String(ei), si, ei, el })),
        m("button.add-el", { onclick: () => addElement(si) }, "+ add element"),
      ),
    );
  },
};

const SectionsList: m.Component = {
  view() {
    if (!file.sections.length)
      return m(".empty-state", m.trust("No sections yet.<br/>Use <b>Add section</b> to get started."));
    if (activeSecIndex === null && file.sections.length === 1) activeSecIndex = 0;
    return m("", file.sections.map((_, si) => m(SectionCard, { key: String(si), si })));
  },
};

const MetaPanel: m.Component = {
  view() {
    const section = getSectionByIndex(activeSecIndex);
    return m("",
      m(".meta-row",
        m("label", "Selected section"),
        section
          ? m("select.meta-val.green", {
            value: SectionTypeToString[section.sectionType],
            onchange: (e: Event) =>
              activeSecIndex !== null && setSectionType(activeSecIndex, (e.target as HTMLSelectElement).value),
          },
            Object.values(SectionTypeToString).map(name =>
              m("option", { value: name, selected: name === SectionTypeToString[section.sectionType] }, name)
            )
          )
          : m("span.meta-val.green", "-"),
      ),
      m(".meta-row",
        m("label", "Elements"),
        m("span.meta-val", section && "elements" in section ? `${section.elements.length} element(s)` : "-"),
      ),
      m(".meta-row",
        m("label", "Section flags"),
        (["drawFront", "drawBack", "clearBuffer"] as const).map((flag, i) =>
          m("label.flag-check", { style: i > 0 ? "margin-left:-8px" : "" },
            m("input[type=checkbox]", {
              checked: section ? !!(section as any).flags?.[flag] : false,
              onchange: (e: Event) => setSectionFlag(flag, (e.target as HTMLInputElement).checked),
            }),
            " " + flag,
          )
        ),
      ),
    );
  },
};

let activeDemoName: string | null = null;

const DemoButtons: m.Component = {
  view() {
    return Object.entries(DEMO).map(([name, fn]) =>
      m("button.tb-btn", {
        style: activeDemoName === name ? "color:#33ff66" : "",
        onclick: async () => {
          const ok = await guardDirty();
          if (!ok) return;
          file = fn();
          activeSecIndex = null;
          activeElIndex = null;
          currentFilename = name.toLowerCase();
          const filenameEl = document.getElementById("filename");
          if (filenameEl) filenameEl.textContent = currentFilename;
          activeDemoName = name;
          markClean();
          triggerPreview();
          m.redraw();
        },
      }, name)
    );
  },
};

// --- Mount --------------------------------------------------------------------
m.mount(document.getElementById("sections-wrap")!, SectionsList);
m.mount(document.getElementById("demo-btns")!, DemoButtons);
m.mount(document.getElementById("sec-meta")!, MetaPanel);

async function clearAll() {
  if (isDirty && file.sections.length) {
    const ok = await showConfirm("Clear all sections?", [
      { label: "Cancel", action: false },
      { label: "Clear", primary: true, action: true },
    ]);
    if (!ok) return;
  }
  file = new MonoDisplayFile([]);
  activeSecIndex = null;
  activeElIndex = null;
  currentFilename = "untitled";
  const filenameEl = document.getElementById("filename");
  if (filenameEl) filenameEl.textContent = "untitled";
  localStorage.removeItem(STORAGE_KEY);
  markClean();
  triggerPreview();
  m.redraw();
}

// Expose globals referenced by topbar inline handlers
Object.assign(window, { loadBin, exportBin, addSection, clearAll, cycleTheme, changeDisplayLayout });

// window.addEventListener("beforeunload", (e) => {
//   if (isDirty) e.preventDefault();
// });

loadFromStorage();
triggerPreview();
