<script lang="ts">
  /*** IMPORT ------------------------------------------- ***/
  import "@inc/uchu/css";
  import { onMount, type Snippet } from "svelte";
  import type { Extension } from "@codemirror/state";

  /*** UTILITY ------------------------------------------ ***/
  import DocExplorer from "./components/DocExplorer.svelte";
  import Editor from "./components/Editor.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 { createLocalStorage, createMemoryStorage } from "./state/storage.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 { validateSessionExport } from "./state/session-io.ts";

  import type { Fetcher } from "./fetcher/types.ts";
  import type { Storage } from "./state/storage.ts";
  import type { SubscriptionMode, Tab } from "./state/session.svelte.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;
  };

  const MINIMUM_HISTORY_WIDTH = 300;
  const PERSIST_DEBOUNCE_MS = 300;

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

  // svelte-ignore state_referenced_locally
  const resolvedStorage = storage ??
    (typeof globalThis.localStorage !== "undefined" ?
      // svelte-ignore state_referenced_locally
      createLocalStorage(namespace) :
      createMemoryStorage());

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

  // svelte-ignore state_referenced_locally
  if (initialQuery && session.active && session.active.query === "") {
    // svelte-ignore state_referenced_locally
    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 dragStartBottomHeight = 0;
  let dragStartBottomHeightPx = 0;
  let dragStartDocsWidth = 0;
  let dragStartHistoryWidth = 0;
  let dragStartLeftWidth = 0;
  let dragStartLeftWidthPx = 0;
  let historyNotice = $state<string | null>(null);
  let historyOpen = $state(resolvedStorage.get<boolean>("historyPanel") ?? false);
  let historyWidth = $state(resolvedStorage.get<number>("layout.historyWidth") ?? MINIMUM_HISTORY_WIDTH);
  let leftEl = $state<HTMLDivElement | null>(null);
  let leftWidth = $state(resolvedStorage.get<number>("layout.leftWidth") ?? 50);
  let runAbort: AbortController | null = null;
  let running = $state(false);
  let windowWidth = $state(0);
  let windowHeight = $state(0);
  let windowResizeTimeout: ReturnType<typeof setTimeout>;





  $effect(() => {
    const updateSize = () => {
      clearTimeout(windowResizeTimeout);

      windowResizeTimeout = setTimeout(() => {
        windowWidth = globalThis.innerWidth;
        windowHeight = globalThis.innerHeight;
      }, 100);
    };

    updateSize();
    globalThis.addEventListener("resize", updateSize);

    return () => {
      globalThis.removeEventListener("resize", updateSize);
    };
  });

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

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

  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 onBeforeUnload() {
    session.persist();
  }

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

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

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

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

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

  function onDocsDragStart() {
    dragStartDocsWidth = docsWidth;
  }

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

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

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

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

  function onHistoryDragStart() {
    dragStartHistoryWidth = historyWidth;
  }

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

  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 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 onLeftDrag(dx: number) {
    if (dragStartLeftWidthPx === 0)
      return;

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

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

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

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

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

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

    background-color: var(--uchu-yang);
    color: var(--uchu-yin-9);
    font-family: "Berkeley Mono", ui-monospace, SFMono-Regular, monospace;
    font-size: 16px;
    overflow: hidden;
    overscroll-behavior: none;

    @media (min-width: 1025px) {
      display: grid;
      grid-template-rows: auto auto 1fr;
    }

    @media (max-width: 1024px) {
      display: flex;
      flex-direction: column;
    }
  }

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

    @media (max-width: 1024px) {
      height: stretch;
    }

    &.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,
  .left {
    margin-right: -6px;
    min-height: 0;
    min-width: 0;
  }

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

    @media (max-width: 1024px) {
      display: flex;
      flex-direction: column;
    }

    .query {
      flex: 2;
      margin-bottom: -6px;
      min-height: 0;
    }

    .bottom {
      flex: 1;
      min-height: 0;
    }

    .left {
      display: flex;
      flex-direction: column;
      overflow-x: hidden;

      @media (max-width: 1024px) {
        flex: 2;
      }
    }

    .right {
      min-height: 0;

      @media (max-width: 1024px) {
        border-top: 2px solid var(--uchu-gray-3);
        flex: 1;
      }
    }
  }

  .switcher {
    background-color: var(--uchu-gray-2);
    display: flex;
    flex-direction: row;

    .switch {
      background: none;
      border: none;
      cursor: pointer;
      font-family: "Berkeley Mono", ui-monospace, SFMono-Regular, monospace;
      font-size: 0.75rem;
      font-weight: 600;
      letter-spacing: 0.05rem;
      padding: 0.375rem 0.75rem;
      text-transform: uppercase;

      &:not(.active) {
        color: var(--uchu-yin-5);
      }

      &.active {
        background-color: var(--uchu-gray-3);
      }
    }
  }

  .status {
    background-color: var(--uchu-yellow-2);
    border-top: 1px solid var(--uchu-yellow-3);
    color: var(--uchu-yellow-9);
    font-size: 0.75rem;
    padding: 0.25rem 0.75rem;

    span {
      text-transform: uppercase;
    }
  }
</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>

        <Splitter
          onDrag={onBottomDrag}
          onDragStart={onBottomDragStart}
          onKeyAdjust={onBottomKeyAdjust}
          orientation="vertical"/>

        <div class="bottom">
          <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>

          {#if bottomPane === "variables"}
            <Editor
              language="json"
              onChange={onVariablesChange}
              {theme}
              value={session.active?.variables ?? "{}"}/>
          {:else}
            <Editor
              language="json"
              onChange={onHeadersChange}
              {theme}
              value={session.active?.headers ?? "{}"}/>
          {/if}
        </div>
      </div>

      {#if windowWidth >= 1025}
        <Splitter
          onDrag={onLeftDrag}
          onDragStart={onLeftDragStart}
          onKeyAdjust={onLeftKeyAdjust}
          orientation="horizontal"/>
      {/if}

      <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"><span>Schema error</span> {schema.error}</div>
  {/if}
</div>
