import { useCallback, useEffect, useRef, useState } from "react";
import type { RawImage } from "./imageConvert.ts";
import type { SyntheticSample } from "./samples.ts";
import { SAMPLES } from "./samples.ts";
import { DEFAULT_SCRIPT } from "./examples.ts";
import { createFilterClient } from "./filterClient.ts";
import type { FilterClient } from "./filterClient.ts";
import type { UploadRecord } from "./imageStore.ts";
import {
  addUpload,
  deleteUpload,
  getUploadImage,
  listUploads,
} from "./imageStore.ts";
import { ImageView } from "./components/ImageView.tsx";
import { SamplePicker } from "./components/SamplePicker.tsx";
import { ScriptPanel } from "./components/ScriptPanel.tsx";

interface Loaded {
  image: RawImage;
  label: string;
}

export default function App() {
  // The worker is created lazily and recreated after teardown. This survives
  // React StrictMode's dev-only mount→unmount→mount, whose cleanup would
  // otherwise terminate a worker held in a ref and never replace it.
  const clientRef = useRef<FilterClient | null>(null);
  const getClient = useCallback((): FilterClient => {
    if (!clientRef.current) clientRef.current = createFilterClient();
    return clientRef.current;
  }, []);

  const [script, setScript] = useState(DEFAULT_SCRIPT);
  const [original, setOriginal] = useState<Loaded | null>(null);
  const [uploads, setUploads] = useState<UploadRecord[]>([]);
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [filtered, setFiltered] = useState<RawImage | null>(null);
  const [running, setRunning] = useState(false);
  const [loadingImage, setLoadingImage] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [logs, setLogs] = useState<string[]>([]);
  const [elapsedMs, setElapsedMs] = useState<number | null>(null);

  const scriptRef = useRef(script);
  scriptRef.current = script;
  const originalRef = useRef(original);
  originalRef.current = original;
  const runIdRef = useRef(0);

  useEffect(() => {
    getClient(); // ensure a live worker exists for this mount
    return () => {
      clientRef.current?.terminate();
      clientRef.current = null; // force a fresh worker on the next mount
    };
  }, [getClient]);

  const run = useCallback(async () => {
    const loaded = originalRef.current;
    if (!loaded) return;
    const myId = ++runIdRef.current;
    setRunning(true);
    try {
      const res = await getClient().run(scriptRef.current, loaded.image);
      if (runIdRef.current !== myId) return;
      setFiltered({ width: res.width, height: res.height, rgba: res.rgba });
      setLogs(res.logs);
      setElapsedMs(res.elapsedMs);
      setError(null);
    } catch (e) {
      if (runIdRef.current !== myId) return;
      const err = e as Error & { logs?: string[] };
      setError(err.message);
      setLogs(err.logs ?? []);
      setElapsedMs(null);
    } finally {
      if (runIdRef.current === myId) setRunning(false);
    }
  }, [getClient]);

  const selectSample = useCallback((sample: SyntheticSample) => {
    setSelectedId(sample.id);
    setError(null);
    setOriginal({ image: sample.generate(), label: sample.name });
  }, []);

  const selectUpload = useCallback(async (rec: UploadRecord) => {
    setSelectedId(rec.id);
    setLoadingImage(true);
    setError(null);
    try {
      const image = await getUploadImage(rec);
      setOriginal({ image, label: rec.name });
    } catch (e) {
      setError((e as Error).message);
    } finally {
      setLoadingImage(false);
    }
  }, []);

  const uploadFile = useCallback(async (file: File) => {
    setLoadingImage(true);
    setError(null);
    try {
      const { record, image } = await addUpload(file);
      setUploads((prev) => [record, ...prev]);
      setSelectedId(record.id);
      setOriginal({ image, label: record.name });
    } catch (e) {
      setError(
        `Could not load or store the image: ${(e as Error).message}`
      );
    } finally {
      setLoadingImage(false);
    }
  }, []);

  const removeUpload = useCallback(
    async (id: string) => {
      try {
        await deleteUpload(id);
      } catch (e) {
        setError((e as Error).message);
        return;
      }
      setUploads((prev) => prev.filter((u) => u.id !== id));
      if (selectedId === id) selectSample(SAMPLES[0]);
    },
    [selectedId, selectSample]
  );

  // Load persisted uploads, then show a default image, on first mount.
  useEffect(() => {
    let cancelled = false;
    listUploads()
      .then((list) => {
        if (!cancelled) setUploads(list);
      })
      .catch(() => {
        /* ignore — uploads list just stays empty */
      });
    selectSample(SAMPLES[0]);
    return () => {
      cancelled = true;
    };
  }, [selectSample]);

  // No auto-run: when a new image is loaded, clear the previous result so the
  // Filtered panel never shows a stale output for a different image. The
  // filter runs only on the Run button (or ⌘/Ctrl+Enter).
  useEffect(() => {
    setFiltered(null);
    setLogs([]);
    setElapsedMs(null);
  }, [original]);

  return (
    <div className="app">
      <header className="app-header">
        <h1>numbl image filter</h1>
        <p>
          Filter an image with a{" "}
          <a href="https://numbl.org" target="_blank" rel="noreferrer">
            numbl
          </a>{" "}
          script (MATLAB syntax), run entirely in your browser.
        </p>
      </header>

      <section className="panel">
        <h2>1 · Choose an image</h2>
        <SamplePicker
          samples={SAMPLES}
          uploads={uploads}
          selectedId={selectedId}
          onSelectSample={selectSample}
          onSelectUpload={selectUpload}
          onUpload={uploadFile}
          onDeleteUpload={removeUpload}
          disabled={loadingImage}
        />
      </section>

      <section className="panel">
        <h2>2 · Write a filter</h2>
        <ScriptPanel
          script={script}
          onChange={setScript}
          onRun={run}
          running={running}
        />
      </section>

      <section className="panel">
        <h2>3 · Result</h2>
        {error && <div className="error-banner">⚠ {error}</div>}
        <div className="viewers">
          <ImageView
            title={`Original${original ? " · " + original.label : ""}`}
            image={original?.image ?? null}
            placeholder={loadingImage ? "Loading…" : "No image"}
          />
          <ImageView
            title="Filtered"
            image={filtered}
            placeholder={running ? "Running…" : "Press Run ▶ to apply the filter"}
            downloadName="filtered.png"
            overlay={
              running ? <div className="running-overlay">Running…</div> : null
            }
          />
        </div>
        <div className="status-row">
          {elapsedMs != null && !error && (
            <span className="timing">Ran in {elapsedMs.toFixed(0)} ms</span>
          )}
          {logs.length > 0 && (
            <pre className="console">{logs.join("")}</pre>
          )}
        </div>
      </section>

      <footer className="app-footer">
        Powered by{" "}
        <a href="https://numbl.org" target="_blank" rel="noreferrer">
          numbl
        </a>{" "}
        · everything runs locally in your browser
      </footer>
    </div>
  );
}
