<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 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 { SchemaStore } from "./state/schema.svelte.ts";
  import { SessionStore } from "./state/session.svelte.ts";
  import type { SubscriptionMode, Tab } from "./state/session.svelte.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 docsOpen = $state(resolvedStorage.get<boolean>("docExplorer") ?? false);
  let historyOpen = $state(resolvedStorage.get<boolean>("historyPanel") ?? false);
  let running = $state(false);

  $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);
  });

  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 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 meta = event.metaKey || event.ctrlKey;

    if (meta && event.key === "Enter") {
      event.preventDefault();
      run();
    }
  }

  function onBeforeUnload() {
    session.persist();
  }
</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 1fr;
    min-height: 0;

    &.history-open {
      grid-template-columns: 260px 1fr 1fr;
    }

    &.docs-open {
      grid-template-columns: 1fr 1fr 320px;
    }

    &.history-open.docs-open {
      grid-template-columns: 260px 1fr 1fr 320px;
    }
  }

  .left {
    border-right: 1px solid var(--graphiql-border, #333);
    display: grid;
    grid-template-rows: 2fr auto 1fr;
    min-height: 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}
    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}>
    {#if historyOpen}
      <HistoryPanel
        entries={history.entries}
        onClear={() => history.clear()}
        onFavorite={(id) => history.favorite(id)}
        onLoad={loadHistory}
        onRemove={(id) => history.remove(id)}/>
    {/if}
    <div class="left">
      <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>
      <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>
    <div class="right">
      <ResultViewer footer={resultFooter} {theme} value={session.active?.result ?? ""}/>
    </div>
    {#if docsOpen && schema.schema}
      <DocExplorer schema={schema.schema}/>
    {/if}
  </div>
  {#if schema.error}
    <div class="status">Schema error: {schema.error}</div>
  {/if}
</div>
