<script lang="ts">
  /*** IMPORT ------------------------------------------- ***/

  import { onMount, type Snippet } from "svelte";

  /*** UTILITY ------------------------------------------ ***/

  import DocExplorer from "./components/DocExplorer.svelte";
  import Editor from "./components/Editor.svelte";
  import HeadersEditor from "./components/HeadersEditor.svelte";
  import HistoryPanel from "./components/HistoryPanel.svelte";
  import ResultViewer from "./components/ResultViewer.svelte";
  import Splitter from "./components/Splitter.svelte";
  import TabBar from "./components/TabBar.svelte";
  import Toolbar from "./components/Toolbar.svelte";
  import type { Extension } from "@codemirror/state";
  import type { Fetcher } from "./fetcher/types.ts";
  import { HistoryStore } from "./state/history.svelte.ts";
  import { matchShortcut } from "./state/keyboard.ts";
  import { SchemaStore } from "./state/schema.svelte.ts";
  import { SessionStore } from "./state/session.svelte.ts";
  import type { SubscriptionMode, Tab } from "./state/session.svelte.ts";
  import { validateSessionExport } from "./state/session-io.ts";
  import { createLocalStorage, createMemoryStorage } from "./state/storage.ts";
  import type { Storage } from "./state/storage.ts";

  type Props = {
    fetcher: Fetcher;
    initialQuery?: string;
    namespace?: string;
    resultFooter?: Snippet<[{ result: string }]>;
    storage?: Storage;
    subscriptionMode?: SubscriptionMode;
    tabExtras?: Snippet<[{ tab: Tab }]>;
    theme?: Extension;
    toolbarExtras?: Snippet;
  };

  let {
    fetcher,
    initialQuery = "",
    namespace = "eol-graphiql",
    resultFooter,
    storage,
    subscriptionMode = "append",
    tabExtras,
    theme,
    toolbarExtras
  }: Props = $props();

  const resolvedStorage = storage ??
    (typeof globalThis.localStorage !== "undefined" ?
      createLocalStorage(namespace) :
      createMemoryStorage());

  const PERSIST_DEBOUNCE_MS = 300;
  const history = new HistoryStore(resolvedStorage);
  const schema = new SchemaStore();
  const session = new SessionStore(resolvedStorage);

  if (initialQuery && session.active && session.active.query === "")
    session.updateQuery(session.active.id, initialQuery);

  let bottomPane = $state<"variables" | "headers">("variables");
  let bottomHeight = $state(resolvedStorage.get<number>("layout.bottomHeight") ?? 35);
  let centerEl = $state<HTMLDivElement | null>(null);
  let docsOpen = $state(resolvedStorage.get<boolean>("docExplorer") ?? false);
  let docsWidth = $state(resolvedStorage.get<number>("layout.docsWidth") ?? 320);
  let historyNotice = $state<string | null>(null);
  let historyOpen = $state(resolvedStorage.get<boolean>("historyPanel") ?? false);
  let historyWidth = $state(resolvedStorage.get<number>("layout.historyWidth") ?? 260);
  let leftEl = $state<HTMLDivElement | null>(null);
  let leftWidth = $state(resolvedStorage.get<number>("layout.leftWidth") ?? 50);
  let running = $state(false);

  let dragStartHistoryWidth = 0;
  let dragStartDocsWidth = 0;
  let dragStartLeftWidth = 0;
  let dragStartLeftWidthPx = 0;
  let dragStartBottomHeight = 0;
  let dragStartBottomHeightPx = 0;

  function clamp(value: number, min: number, max: number): number {
    return Math.min(Math.max(value, min), max);
  }

  $effect(() => {
    void session.tabs;
    void session.activeId;

    const timer = setTimeout(() => {
      session.persist();
    }, PERSIST_DEBOUNCE_MS);

    return () => clearTimeout(timer);
  });

  $effect(() => {
    void history.entries;

    const timer = setTimeout(() => {
      history.persist();
    }, PERSIST_DEBOUNCE_MS);

    return () => clearTimeout(timer);
  });

  $effect(() => {
    resolvedStorage.set("docExplorer", docsOpen);
  });

  $effect(() => {
    resolvedStorage.set("historyPanel", historyOpen);
  });

  $effect(() => {
    resolvedStorage.set("layout.bottomHeight", bottomHeight);
  });

  $effect(() => {
    resolvedStorage.set("layout.docsWidth", docsWidth);
  });

  $effect(() => {
    resolvedStorage.set("layout.historyWidth", historyWidth);
  });

  $effect(() => {
    resolvedStorage.set("layout.leftWidth", leftWidth);
  });

  onMount(() => {
    schema.introspect(fetcher);
  });

  let runAbort: AbortController | null = null;

  async function run() {
    if (running) {
      runAbort?.abort();
      return;
    }

    running = true;
    runAbort = new AbortController();

    try {
      const tab = session.active;
      const ok = await session.run(fetcher, {
        signal: runAbort.signal,
        subscriptionMode
      });

      if (ok && tab) {
        history.add({
          headers: tab.headers,
          operationName: tab.operationName,
          query: tab.query,
          title: tab.title,
          variables: tab.variables
        });
      }
    } finally {
      runAbort = null;
      running = false;
    }
  }

  function loadHistory(id: string, inNewTab: boolean) {
    const entry = history.entries.find((e) => e.id === id);

    if (!entry)
      return;

    const seed = {
      headers: entry.headers,
      operationName: entry.operationName,
      query: entry.query,
      title: entry.title,
      variables: entry.variables
    };

    if (inNewTab)
      session.addTab(seed);
    else
      session.overwriteActive(seed);
  }

  function onExportSession() {
    if (typeof globalThis.URL === "undefined")
      return;

    if (typeof globalThis.document === "undefined")
      return;

    const data = session.exportAll();
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = globalThis.document.createElement("a");
    a.download = `graphiql-session-${data.exportedAt}.json`;
    a.href = url;
    a.click();
    URL.revokeObjectURL(url);
  }

  async function onImportSession(file: File) {
    let text: string;

    try {
      text = await file.text();
    } catch (err) {
      historyNotice = `Import failed: ${String(err)}`;
      return;
    }

    let parsed: unknown;

    try {
      parsed = JSON.parse(text);
    } catch (err) {
      historyNotice = `Import failed: invalid JSON (${String(err)})`;
      return;
    }

    const valid = validateSessionExport(parsed);

    if ("error" in valid) {
      historyNotice = `Import failed: ${valid.error}`;
      return;
    }

    const result = session.importTabs(valid, { mode: "append" });
    const parts = [`Imported ${result.added} tab${result.added === 1 ? "" : "s"}`];

    if (result.skipped > 0)
      parts.push(`skipped ${result.skipped}`);

    if (result.errors.length > 0)
      parts.push(`${result.errors.length} error${result.errors.length === 1 ? "" : "s"}`);

    historyNotice = parts.join(", ");
  }

  function onQueryChange(value: string) {
    if (session.active)
      session.updateQuery(session.active.id, value);
  }

  function onVariablesChange(value: string) {
    if (session.active)
      session.active.variables = value;
  }

  function onHeadersChange(value: string) {
    if (session.active)
      session.active.headers = value;
  }

  function onKeydown(event: KeyboardEvent) {
    const action = matchShortcut(event);

    if (!action)
      return;

    event.preventDefault();

    switch (action.type) {
      case "closeTab":
        session.closeTab(session.activeId);
        break;
      case "format":
        session.formatActive();
        break;
      case "newTab":
        session.addTab();
        break;
      case "nextTab":
        session.nextTab();
        break;
      case "prevTab":
        session.prevTab();
        break;
      case "run":
        run();
        break;
    }
  }

  function onBeforeUnload() {
    session.persist();
  }

  function onHistoryDragStart() {
    dragStartHistoryWidth = historyWidth;
  }

  function onHistoryDrag(dx: number) {
    historyWidth = clamp(dragStartHistoryWidth + dx, 200, 500);
  }

  function onHistoryKeyAdjust(delta: number) {
    historyWidth = clamp(historyWidth + delta, 200, 500);
  }

  function onDocsDragStart() {
    dragStartDocsWidth = docsWidth;
  }

  function onDocsDrag(dx: number) {
    docsWidth = clamp(dragStartDocsWidth - dx, 240, 600);
  }

  function onDocsKeyAdjust(delta: number) {
    docsWidth = clamp(docsWidth - delta, 240, 600);
  }

  function onLeftDragStart() {
    dragStartLeftWidth = leftWidth;
    dragStartLeftWidthPx = centerEl?.getBoundingClientRect().width ?? 0;
  }

  function onLeftDrag(dx: number) {
    if (dragStartLeftWidthPx === 0)
      return;

    const percentDelta = (dx / dragStartLeftWidthPx) * 100;
    leftWidth = clamp(dragStartLeftWidth + percentDelta, 20, 80);
  }

  function onLeftKeyAdjust(delta: number) {
    leftWidth = clamp(leftWidth + delta, 20, 80);
  }

  function onBottomDragStart() {
    dragStartBottomHeight = bottomHeight;
    dragStartBottomHeightPx = leftEl?.getBoundingClientRect().height ?? 0;
  }

  function onBottomDrag(_dx: number, dy: number) {
    if (dragStartBottomHeightPx === 0)
      return;

    const percentDelta = (dy / dragStartBottomHeightPx) * 100;
    bottomHeight = clamp(dragStartBottomHeight - percentDelta, 15, 70);
  }

  function onBottomKeyAdjust(delta: number) {
    bottomHeight = clamp(bottomHeight - delta, 15, 70);
  }
</script>

<style lang="scss">
  .graphiql {
    width: 100%; height: 100%;

    background-color: var(--graphiql-bg, #1e1e1e);
    color: var(--graphiql-fg, #d4d4d4);
    display: grid;
    font-family: var(--graphiql-font, ui-monospace, SFMono-Regular, monospace);
    grid-template-rows: auto auto 1fr;
  }

  .panes {
    display: grid;
    grid-template-columns: 1fr;
    min-height: 0;

    &.history-open {
      grid-template-columns: var(--graphiql-history-width) 6px 1fr;
    }

    &.docs-open {
      grid-template-columns: 1fr 6px var(--graphiql-docs-width);
    }

    &.history-open.docs-open {
      grid-template-columns: var(--graphiql-history-width) 6px 1fr 6px var(--graphiql-docs-width);
    }
  }

  .center {
    display: grid;
    grid-template-columns: var(--graphiql-left-width) 6px calc(100% - var(--graphiql-left-width) - 6px);
    min-height: 0;
    min-width: 0;
  }

  .left {
    display: grid;
    grid-template-rows: 1fr auto 6px var(--graphiql-bottom-height);
    min-height: 0;
    min-width: 0;
  }

  .query {
    min-height: 0;
  }

  .switcher {
    background-color: var(--graphiql-panel, #252526);
    border-top: 1px solid var(--graphiql-border, #333);
    display: flex;
  }

  .switch {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    padding: 0.375rem 0.75rem;
    text-transform: uppercase;

    &:not(.active) {
      color: var(--graphiql-muted, #858585);
    }

    &.active {
      color: var(--graphiql-fg, #d4d4d4);
    }
  }

  .bottom {
    min-height: 0;
  }

  .right {
    min-height: 0;
  }

  .status {
    background-color: var(--graphiql-panel, #252526);
    border-top: 1px solid var(--graphiql-border, #333);
    font-size: 0.75rem;
    padding: 0.25rem 0.75rem;
  }
</style>

<svelte:window onbeforeunload={onBeforeUnload} onkeydown={onKeydown}/>

<div class="graphiql">
  <Toolbar
    disabled={running || !session.active}
    docsAvailable={schema.schema !== null}
    {docsOpen}
    extras={toolbarExtras}
    {historyOpen}
    onFormat={() => session.formatActive()}
    onRun={run}
    onSelectOperation={(name) => {
      if (session.active)
        session.selectOperation(session.active.id, name);
    }}
    onToggleDocs={() => (docsOpen = !docsOpen)}
    onToggleHistory={() => (historyOpen = !historyOpen)}
    operationName={session.active?.operationName ?? null}
    operations={session.active?.operations ?? []}
    {running}
    schemaLoading={schema.loading}/>
  <TabBar
    activeId={session.activeId}
    extras={tabExtras}
    onAdd={() => session.addTab()}
    onClose={(id) => session.closeTab(id)}
    onRename={(id, title) => session.renameTab(id, title)}
    onSelect={(id) => session.selectTab(id)}
    tabs={session.tabs}/>
  <div
    class="panes"
    class:docs-open={docsOpen && schema.schema}
    class:history-open={historyOpen}
    style="--graphiql-history-width: {historyWidth}px; --graphiql-docs-width: {docsWidth}px;"
  >
    {#if historyOpen}
      <HistoryPanel
        entries={history.entries}
        notice={historyNotice}
        onClear={() => history.clear()}
        onDismissNotice={() => (historyNotice = null)}
        onExport={onExportSession}
        onFavorite={(id) => history.favorite(id)}
        onImport={onImportSession}
        onLoad={loadHistory}
        onRemove={(id) => history.remove(id)}/>
      <Splitter
        onDrag={onHistoryDrag}
        onDragStart={onHistoryDragStart}
        onKeyAdjust={onHistoryKeyAdjust}
        orientation="horizontal"/>
    {/if}
    <div
      bind:this={centerEl}
      class="center"
      style="--graphiql-left-width: {leftWidth}%;"
    >
      <div bind:this={leftEl} class="left" style="--graphiql-bottom-height: {bottomHeight}%;">
        <div class="query">
          <Editor
            language="graphql"
            onChange={onQueryChange}
            schema={schema.sdl}
            {theme}
            value={session.active?.query ?? ""}/>
        </div>
        <div class="switcher">
          <button
            class="switch"
            class:active={bottomPane === "variables"}
            onclick={() => (bottomPane = "variables")}>Variables</button>
          <button
            class="switch"
            class:active={bottomPane === "headers"}
            onclick={() => (bottomPane = "headers")}>Headers</button>
        </div>
        <Splitter
          onDrag={onBottomDrag}
          onDragStart={onBottomDragStart}
          onKeyAdjust={onBottomKeyAdjust}
          orientation="vertical"/>
        <div class="bottom">
          {#if bottomPane === "variables"}
            <Editor
              language="json"
              onChange={onVariablesChange}
              {theme}
              value={session.active?.variables ?? "{}"}/>
          {:else}
            <HeadersEditor
              onChange={onHeadersChange}
              {theme}
              value={session.active?.headers ?? "{}"}/>
          {/if}
        </div>
      </div>
      <Splitter
        onDrag={onLeftDrag}
        onDragStart={onLeftDragStart}
        onKeyAdjust={onLeftKeyAdjust}
        orientation="horizontal"/>
      <div class="right">
        <ResultViewer
          footer={resultFooter}
          streamIntervals={session.active?.streamIntervals ?? []}
          {theme}
          timing={session.active?.timing ?? null}
          value={session.active?.result ?? ""}/>
      </div>
    </div>
    {#if docsOpen && schema.schema}
      <Splitter
        onDrag={onDocsDrag}
        onDragStart={onDocsDragStart}
        onKeyAdjust={onDocsKeyAdjust}
        orientation="horizontal"/>
      <DocExplorer schema={schema.schema}/>
    {/if}
  </div>
  {#if schema.error}
    <div class="status">Schema error: {schema.error}</div>
  {/if}
</div>
