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

  import { onMount } from "svelte";
  import type { Extension } from "@codemirror/state";
  import type { EditorView } from "@codemirror/view";
  import type { GraphQLSchema } from "graphql";

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

  type Props = {
    language?: "graphql" | "json";
    onChange: (value: string) => void;
    readOnly?: boolean;
    schema?: string;
    theme?: Extension;
    value: string;
  };

  let {
    language = "graphql",
    onChange,
    readOnly = false,
    schema,
    theme,
    value
  }: Props = $props();

  let buildSchemaFn = $state<((sdl: string) => GraphQLSchema) | null>(null);
  let container: HTMLDivElement;
  let updateSchemaFn = $state<((v: EditorView, s: GraphQLSchema) => void) | null>(null);
  let view = $state<EditorView | null>(null);

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

    let disposed = false;

    (async () => {
      const [
        { EditorView: EV, keymap, lineNumbers, highlightActiveLine },
        { EditorState },
        { defaultKeymap, history, historyKeymap },
        { syntaxHighlighting, defaultHighlightStyle, bracketMatching, indentOnInput },
        { closeBrackets, closeBracketsKeymap },
        { graphql, updateSchema },
        { json },
        { buildSchema }
      ] = await Promise.all([
        import("@codemirror/view"),
        import("@codemirror/state"),
        import("@codemirror/commands"),
        import("@codemirror/language"),
        import("@codemirror/autocomplete"),
        import("cm6-graphql"),
        import("@codemirror/lang-json"),
        import("graphql")
      ]);

      if (disposed)
        return;

      const themeExt: Extension = theme ?? (await import("@codemirror/theme-one-dark")).oneDark;

      const languageExt = language === "graphql" ?
        graphql(schema ? buildSchema(schema) : undefined) :
        json();

      const instance = new EV({
        parent: container,
        state: EditorState.create({
          doc: value,
          extensions: [
            lineNumbers(),
            highlightActiveLine(),
            history(),
            bracketMatching(),
            closeBrackets(),
            indentOnInput(),
            syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
            keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...historyKeymap]),
            languageExt,
            themeExt,
            EV.editable.of(!readOnly),
            EV.updateListener.of((u) => {
              if (u.docChanged)
                onChange(u.state.doc.toString());
            })
          ]
        })
      });

      view = instance;
      updateSchemaFn = updateSchema;
      buildSchemaFn = buildSchema;
    })();

    return () => {
      disposed = true;
      view?.destroy();
    };
  });

  $effect(() => {
    if (!view || !updateSchemaFn || !buildSchemaFn)
      return;

    if (language !== "graphql" || !schema)
      return;

    try {
      updateSchemaFn(view, buildSchemaFn(schema));
    } catch {
      // Invalid SDL — silently skip; editor keeps working without schema awareness
    }
  });

  $effect(() => {
    if (!view)
      return;

    if (view.state.doc.toString() === value)
      return;

    view.dispatch({
      changes: { from: 0, insert: value, to: view.state.doc.length }
    });
  });
</script>

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

    :global(.cm-editor) {
      height: 100%;
    }

    :global(.cm-scroller) {
      font-family: inherit;
    }
  }
</style>

<div bind:this={container} class="editor"></div>
