// glyph-app.jsx — the Glyph Forge page.
//
// Left: the specimen — the mark under trim marks, its printed spec, and the
// same mark in the three places it actually has to survive. Right: the panel.
// Everything is live; state persists to localStorage.

const DEFAULTS = {
  mode: "icon",
  iconUrl: "https://lucide.dev/icons/chart-no-axes-gantt",
  text: "A",
  font: "display",
  bold: true,
  italic: false,
  contentColor: "#ffffff",
  bg: "#000000",
  sides: 6,
  orientation: "flat",
  rotation: 0,
  cornerRadius: 22,
  strokeWidth: 2,
  contentSize: 128,
  size: 280,
  format: "svg",
  pngSize: 512,
};

const DownloadIcon = () => (
  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
       strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d="M12 3v12" /><path d="m7 10 5 5 5-5" /><path d="M5 21h14" />
  </svg>
);
const WarnIcon = () => (
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
       strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d="M12 9v4" /><path d="M12 17h.01" />
    <path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />
  </svg>
);
const SavedIcon = () => (
  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
       strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d="m5 12 5 5L20 7" />
  </svg>
);

function GlyphForge() {
  const [t, set] = useSettings(DEFAULTS);
  const [theme, setTheme] = useTheme();
  const { icon, status, error } = useLucideIcon(t.iconUrl);
  const isText = t.mode === "text";

  // Shared by the specimen, every plate, and the export — one object, so the
  // download can never drift from what is on screen.
  const glyphProps = {
    bg: t.bg, contentColor: t.contentColor,
    sides: t.sides, orientation: t.orientation, rotation: t.rotation,
    cornerRadius: t.cornerRadius, strokeWidth: t.strokeWidth, contentSize: t.contentSize,
    mode: t.mode, iconBody: icon.body,
    text: t.text || " ", font: t.font, bold: t.bold, italic: t.italic,
  };

  // Undo rather than a confirm dialog: people click through confirmations
  // without reading them, and reset is cheap to reverse if we keep the old
  // state. Holding the snapshot also means no dialog to trap focus.
  const [undoSnapshot, setUndoSnapshot] = React.useState(null);
  React.useEffect(() => {
    if (!undoSnapshot) return;
    const id = setTimeout(() => setUndoSnapshot(null), 8000);
    return () => clearTimeout(id);
  }, [undoSnapshot]);

  const reset = () => { setUndoSnapshot(t); set({ ...DEFAULTS }); };
  const undoReset = () => { set(undoSnapshot); setUndoSnapshot(null); };

  const rotationMax = Math.round(360 / t.sides);
  // Rotation is bounded by side count, so changing sides has to bring it along
  // or it silently keeps a value the slider can no longer reach.
  const setSides = (v) =>
    set({ sides: v, rotation: Math.min(t.rotation, Math.round(360 / v)) });

  // The browser saves the file with no visible sign it happened, so the button
  // has to be the receipt.
  const [saved, setSaved] = React.useState(false);
  React.useEffect(() => {
    if (!saved) return;
    const id = setTimeout(() => setSaved(false), 2200);
    return () => clearTimeout(id);
  }, [saved]);

  const isPng = t.format === "png";
  const [exportError, setExportError] = React.useState(null);

  const download = async () => {
    const stem = isText ? `text-${(t.text || "glyph").trim().replace(/\s+/g, "-")}` : icon.slug;
    setExportError(null);
    if (isPng) {
      // Rasterise from a canvas at the chosen pixel size, not the preview size.
      const svg = buildSvgString({ size: t.pngSize, ...glyphProps });
      try {
        saveBlob(`${stem}-${t.sides}gon-${t.pngSize}.png`, await buildPngBlob(svg, t.pngSize));
        setSaved(true);
      } catch (e) {
        setExportError("Couldn't build the PNG. The SVG download still works.");
      }
    } else {
      downloadSvg(`${stem}-${t.sides}gon-${t.size}.svg`, buildSvgString({ size: t.size, ...glyphProps }));
      setSaved(true);
    }
  };

  // Warn, never block. The tool states the problem and lets you decide — the
  // same way it reports a failed icon fetch without clearing the stage.
  const ratio = contrastRatio(t.contentColor, t.bg);
  const faint = ratio != null && ratio < MIN_MARK_CONTRAST;

  const label = describeGlyph({ ...t, iconSlug: icon.slug });
  const payloadName = isText
    ? (t.text || "").trim() ? `“${t.text}”` : "no content"
    : icon.slug;

  return (
    <div className="tool">
      <header className="bar">
        <div className="bar-id">
          <h1 className="wordmark">Glyph<WordmarkMark />Forge</h1>
          <p className="tagline">Set a mark. Prove it at 16px.</p>
        </div>
        <CSeg label="Theme" hideLabel mini value={theme} onChange={setTheme}
          options={[
            { value: "system", label: "Auto" },
            { value: "light", label: "Light" },
            { value: "dark", label: "Dark" },
          ]} />
      </header>

      <div className="main">
        {/* ═══ SPECIMEN ═══════════════════════════════════════════════ */}
        <main className="stage">
          <div className="specimen">
            <div className="frame" style={{ width: t.size + 36 }}>
              <i className="tick tl" /><i className="tick tr" />
              <i className="tick bl" /><i className="tick br" />
              <Glyph size={t.size} title={label} {...glyphProps} />
            </div>
            <p className="spec">
              <b>{POLY_NAMES[t.sides] || `${t.sides}-gon`}</b>
              <span className="sep">·</span>{t.orientation === "pointy" ? "Point up" : "Flat top"}
              <span className="sep">·</span>{t.rotation}°
              <span className="sep">·</span>r{t.cornerRadius}
              <br />
              <span className="slug">{payloadName}</span>
              <span className="sep">·</span>
              <span className="keep">{colorName(t.contentColor)} on {colorName(t.bg)}</span>
              <span className="sep">·</span>
              {/* States the file you would actually get, so it tracks Format. */}
              <span className="keep">
                {isPng ? `png ${t.pngSize}×${t.pngSize}` : `svg ${t.size}×${t.size}`}
              </span>
            </p>
            {faint && (
              <p className="warn" role="status" aria-live="polite">
                <WarnIcon />
                <span>
                  {ratio < 1.2
                    ? "This mark is invisible against its own background."
                    : "This mark will be hard to see."}{" "}
                  {ratio.toFixed(1)}:1 between {isText ? "letters" : "icon"} and fill — a
                  shape needs {MIN_MARK_CONTRAST}:1.
                </span>
              </p>
            )}
          </div>

          {/* The whole point of the tool: does it survive being small? */}
          <div className="plates">
            <div className="plate">
              <div className="chrome">
                <div className="dots"><i /><i /><i /></div>
                <div className="tab">
                  <Glyph size={16} {...glyphProps} />
                  {/* Set dressing. A screen reader announcing a fake domain
                      would be reading a prop as if it were information. */}
                  <span className="url" aria-hidden="true">glyphforge.app</span>
                </div>
              </div>
              <span className="cap">Browser tab · 16px</span>
            </div>
            <div className="plate">
              <div className="tile"><Glyph size={66} {...glyphProps} /></div>
              <span className="cap">App tile</span>
            </div>
            <div className="plate">
              <div className="round"><Glyph size={62} {...glyphProps} /></div>
              <span className="cap">Avatar</span>
            </div>
          </div>

          <div className="tail">
            <div className="about">
              <h2>About this tool</h2>
              <p>
                Glyph Forge builds a square mark from a Lucide icon or a few letters, set
                inside a polygon you control — sides, rotation, corner radius, colour. The
                result exports as a plain SVG that scales to any size, with no account and
                nothing stored on a server.
              </p>
            </div>
            <div className="ad" role="complementary" aria-label="Advertisement">
              <span>Advertisement</span>
            </div>
          </div>
        </main>

        {/* ═══ PANEL ══════════════════════════════════════════════════ */}
        <aside className="panel" aria-label="Glyph settings">
          <div className="panel-scroll">
            <CSection label="Content">
              <CSeg label="Inside the shape" value={t.mode} onChange={(v) => set("mode", v)}
                options={[{ value: "icon", label: "Icon" }, { value: "text", label: "Letters" }]} />

              {!isText && (
                <CText label="Which icon" value={t.iconUrl}
                  placeholder="Type a name, e.g. star"
                  hint={status === "loading" ? "loading…" : status === "error" ? error : icon.slug}
                  hintBad={status === "error"} describedBy="icon-status"
                  options={SUGGESTED_ICONS}
                  after={
                    <a className="sidenote" href="https://lucide.dev/icons/"
                       target="_blank" rel="noopener noreferrer">
                      Browse all 2,000 icons ↗
                    </a>
                  }
                  onChange={(v) => set("iconUrl", v)} />
              )}

              {isText && (
                <>
                  <CText label="Letters" value={t.text} placeholder="A, Æ, ⌘…"
                    onChange={(v) => set("text", v)} />
                  <CSeg label="Font" value={t.font} onChange={(v) => set("font", v)}
                    options={[
                      { value: "display", label: "Display" }, { value: "sans", label: "Sans" },
                      { value: "serif", label: "Serif" }, { value: "mono", label: "Mono" },
                    ]} />
                  <CFormat bold={t.bold} italic={t.italic}
                    onBold={(v) => set("bold", v)} onItalic={(v) => set("italic", v)} />
                </>
              )}
            </CSection>

            <CSection label="Shape">
              <CSlider label="Sides" value={t.sides} min={3} max={12} onChange={setSides} />
              <CSeg label="Orientation" value={t.orientation} onChange={(v) => set("orientation", v)}
                options={[{ value: "flat", label: "Flat top" }, { value: "pointy", label: "Point up" }]} />
              <CSlider label="Rotation" value={t.rotation} min={0} max={rotationMax} unit="°"
                onChange={(v) => set("rotation", v)} />
              <CSlider label="Corner radius" value={t.cornerRadius} min={0} max={48} unit="px"
                onChange={(v) => set("cornerRadius", v)} />
            </CSection>

            {/* Both colours together: the pair is what determines legibility,
                so the two controls that decide it belong side by side. */}
            <CSection label="Colour">
              <CColor label={isText ? "Letter colour" : "Icon colour"} value={t.contentColor}
                onChange={(v) => set("contentColor", v)} />
              <CColor label="Fill colour" value={t.bg} onChange={(v) => set("bg", v)} />
              {faint && (
                <p className="caveat is-bad">
                  {ratio.toFixed(1)}:1 between them — below the {MIN_MARK_CONTRAST}:1 a shape needs.
                </p>
              )}
            </CSection>

            <CSection label="Size">
              <CSlider label={isText ? "Letter size" : "Icon size"} value={t.contentSize}
                min={24} max={240} step={2} unit="px" onChange={(v) => set("contentSize", v)} />
              {!isText && (
                <CSlider label="Stroke width" value={t.strokeWidth} min={1} max={3.5} step={0.1}
                  onChange={(v) => set("strokeWidth", v)} />
              )}
            </CSection>

            <CSection label="Export">
              <CSeg label="Format" value={t.format} onChange={(v) => set("format", v)}
                options={[{ value: "svg", label: "SVG" }, { value: "png", label: "PNG" }]} />

              {isPng ? (
                <CSeg label="Pixel size" value={String(t.pngSize)}
                  onChange={(v) => set("pngSize", Number(v))}
                  options={PNG_SIZES.map((s) => ({ value: String(s), label: String(s) }))} />
              ) : (
                <CSlider label="Canvas size" value={t.size} min={120} max={420} step={4} unit="px"
                  onChange={(v) => set("size", v)} />
              )}

              {!isPng && isText && (
                <p className="caveat">
                  Letters export as live text referencing the font by name. Convert to outlines
                  before sharing the file, or it falls back elsewhere.
                </p>
              )}
              {isPng && (
                <p className="caveat">
                  {isText
                    ? "PNG rasterises in isolation, so letters render in a fallback face, not the one shown. Use SVG to keep these letterforms."
                    : "Fixed-resolution bitmap. Use SVG where the mark needs to scale."}
                </p>
              )}
            </CSection>
          </div>

          {/* Only the two actions are pinned. Format and size are set once; the
              button is what must never scroll out of reach. */}
          <div className="dock">
            <button className="btn" data-saved={saved ? "1" : "0"} onClick={download}>
              {saved ? <SavedIcon /> : <DownloadIcon />}{" "}
              {saved ? "Saved to downloads" : `Download ${isPng ? "PNG" : "SVG"}`}
            </button>

            {undoSnapshot ? (
              <button className="btn-ghost" onClick={undoReset}>Undo reset</button>
            ) : (
              <button className="btn-ghost" onClick={reset}>Reset to defaults</button>
            )}

            {exportError && <p className="caveat is-bad">{exportError}</p>}
          </div>
        </aside>
      </div>

      {/* Announced, not just shown — the hint text beside the field is silent
          to a screen reader once focus has moved on. */}
      <p id="icon-status" className="vh" role="status" aria-live="polite">
        {saved ? "SVG saved to downloads"
          : status === "loading" ? "Loading icon"
          : status === "error" ? error
          : `Icon ${icon.slug} loaded`}
      </p>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<GlyphForge />);
