import { adopt, define, sheet } from "@nibling/web/internals"; import type * as CodeModule from "../code/element.ts"; import type { Files } from "../files.ts"; import { languageOf, type Language } from "./keys.ts"; import type { Unsubscribe } from "../shell/workbench.ts"; import type { CaretMove, EditingTarget } from "./editing.ts"; /** * Structural CSS only; theme values come from @nibling/web's semantic tokens. `--nib-code-padding` * cascades into ``'s shadow root (custom properties inherit through shadow boundaries), * so the `
` shown before the chunk loads and the editor shown after it share one padding rule
 * and there is no layout shift between them.
 */
const editorSheet = sheet(`
  :host {
    display: flex;
    container: nib-editor / size;
    --nib-code-padding: var(--nib-space-sm) 0;
  }
  nib-code { flex: 1; min-width: 0; min-height: 0; }
  pre {
    flex: 1;
    min-width: 0;
    min-height: 0;
    margin: 0;
    overflow: auto;
    padding: var(--nib-code-padding);
    background: var(--nib-color-canvas);
    color: var(--nib-color-text);
    font: var(--nib-font-code);
    white-space: pre;
  }
  @container nib-editor (max-width: 220px) {
    :host { --nib-code-padding: var(--nib-space-xs) 0; }
  }
`);

type DocType = import("../collab/doc.ts").Doc;
type ConnectFn = typeof import("../collab/sync.ts").connect;

type CodeChunk = {
  NibCodeElement: typeof CodeModule.NibCodeElement;
  fileDocs: typeof import("../collab/file-docs.ts").fileDocs;
  Doc: typeof import("../collab/doc.ts").Doc;
  connect: ConnectFn;
};

/** Loaded once, shared by every `` that mounts after the first. */
let loading: Promise | undefined;

function loadCode(): Promise {
  return (loading ??= Promise.all([
    import("../code/element.ts"),
    import("../collab/file-docs.ts"),
    import("../collab/doc.ts"),
    import("../collab/sync.ts"),
  ]).then(([code, docs, doc, sync]) => ({ NibCodeElement: code.NibCodeElement, fileDocs: docs.fileDocs, Doc: doc.Doc, connect: sync.connect })));
}

/**
 * Every live editor replica for a path, so a newly opened panel can mesh into the ones already
 * there. `connect`'s `onUpdate` only forwards a doc's own local edits (D16), not updates it merely
 * relayed in from elsewhere; a hub that just forwarded everyone's edits through itself would never
 * pass one spoke's edit on to another. Connecting every replica directly to every other one (and to
 * the path's shared hub Doc, which only Files reads and writes) sidesteps that: each edit is local
 * to the replica that made it, so ordinary two-party `connect` forwarding reaches every peer.
 */
const liveReplicas = new WeakMap>>();

/** Connects `replica` to `hub` and to every other replica already open on `path`, then joins the set. */
function mesh(files: Files, path: string, hub: DocType, replica: DocType, connect: ConnectFn): Unsubscribe {
  let byPath = liveReplicas.get(files);
  if (!byPath) liveReplicas.set(files, (byPath = new Map()));
  let peers = byPath.get(path);
  if (!peers) byPath.set(path, (peers = new Set()));

  const stops: Unsubscribe[] = [];
  for (const other of [hub, ...peers]) {
    const channel = new MessageChannel();
    stops.push(connect(other, channel.port1), connect(replica, channel.port2), () => {
      channel.port1.close();
      channel.port2.close();
    });
  }
  peers.add(replica);

  return () => {
    peers!.delete(replica);
    for (const stop of stops) stop();
  };
}

/**
 * ``: a lazy host over `` (CodeMirror + Loro), so CodeMirror never enters the
 * initial bundle. Until the chunk loads, a non-interactive `
` mirrors the file so there's no
 * blank flash; it tracks `files` directly, since the shared `Doc` (D16) doesn't exist yet.
 */
export class EditorElement extends HTMLElement implements EditingTarget {
  readonly #root: ShadowRoot;
  #pre: HTMLPreElement | undefined;
  #code: CodeModule.NibCodeElement | undefined;
  #commands: typeof import("@codemirror/commands") | undefined;
  #undo: typeof import("loro-codemirror") | undefined;
  #language: Language = "text";
  #disposed = false;
  #disposeSync: Unsubscribe | undefined;

  constructor() {
    super();
    this.#root = this.attachShadow({ mode: "open", delegatesFocus: true });
    adopt(this.#root, editorSheet);
  }

  override focus(): void {
    this.#code?.view?.focus();
  }

  override blur(): void {
    this.#code?.view?.contentDOM.blur();
  }

  get language(): Language {
    return this.#language;
  }

  insert(text: string, caret = text.length): void {
    const view = this.#code?.view;
    if (!view) return;
    const { from, to } = view.state.selection.main;
    view.dispatch({ changes: { from, to, insert: text }, selection: { anchor: from + caret }, scrollIntoView: true });
    view.focus();
  }

  move(direction: CaretMove): void {
    const view = this.#code?.view;
    const commands = this.#commands;
    if (!view || !commands) return;
    const run = {
      left: commands.cursorCharLeft,
      right: commands.cursorCharRight,
      up: commands.cursorLineUp,
      down: commands.cursorLineDown,
    }[direction];
    run(view);
  }

  indent(): void {
    const view = this.#code?.view;
    if (view && this.#commands) this.#commands.indentMore(view);
  }

  undo(): void {
    const view = this.#code?.view;
    if (view && this.#undo) this.#undo.undo(view);
  }

  redo(): void {
    const view = this.#code?.view;
    if (view && this.#undo) this.#undo.redo(view);
  }

  /** Binds to `path` in `files`. The accessible name is the path, set by `` once it mounts. */
  connect(files: Files, path: string): Unsubscribe {
    this.#language = languageOf(path);

    const pre = document.createElement("pre");
    pre.textContent = files.read(path) ?? "";
    pre.setAttribute("aria-hidden", "true");
    this.#pre = pre;
    this.#root.append(pre);
    const unwatchPre = files.watch((changed) => {
      if (changed === path) pre.textContent = files.read(path) ?? "";
    });

    loadCode().then(({ NibCodeElement, fileDocs, Doc, connect }) => {
      if (this.#disposed) return;
      pre.remove();
      unwatchPre();
      this.#pre = undefined;

      // CodeMirror's Loro binding tells its own edits apart from a peer's by Loro's local/remote
      // event flag, which is meaningless if two editor instances share one LoroDoc directly (every
      // edit looks "local" to both, so neither ever sees the other's). Each panel instead gets its
      // own replica, seeded from the shared per-path doc (D16) and meshed with the hub and every
      // other open replica on this path, the same connect() a human, an agent, or the kernel syncs
      // through (collab/sync.ts).
      const hub = fileDocs(files).open(path);
      const replica = Doc.load(hub.export());
      this.#disposeSync = mesh(files, path, hub, replica, connect);

      const code = new NibCodeElement();
      code.filename = path;
      this.#code = code;
      this.#root.append(code);
      code.doc = replica;

      Promise.all([import("@codemirror/commands"), import("loro-codemirror")]).then(([commands, loroCodemirror]) => {
        if (this.#disposed) return;
        this.#commands = commands;
        this.#undo = loroCodemirror;
      });
    });

    return () => {
      this.#disposed = true;
      this.#pre?.remove();
      unwatchPre();
      this.#code?.remove();
      this.#disposeSync?.();
    };
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "nib-editor": EditorElement;
  }
}

define("nib-editor", EditorElement);