/** * A textarea with syntax highlighting, by overlay. * * A textarea cannot colour its own text, so the highlighted source is rendered * into a
 underneath and the textarea sits on top with transparent text and
 * a visible caret. The two must agree on every metric that affects layout —
 * font, line height, padding, tab size, wrapping — and their scroll offsets are
 * kept in sync, or the colours drift away from the characters.
 */
import { highlightMatlab } from './matlab.ts';

export interface CodeEditorOptions {
  textarea: HTMLTextAreaElement;
  /** The 
 behind it, holding the highlighted copy. */
  overlay: HTMLElement;
  /** Names to mark as host-provided operations. */
  external?: ReadonlySet;
  /** Called on every edit. */
  onInput?: (value: string) => void;
}

export class CodeEditor {
  #textarea: HTMLTextAreaElement;
  #overlay: HTMLElement;
  #external: ReadonlySet;

  constructor(opts: CodeEditorOptions) {
    this.#textarea = opts.textarea;
    this.#overlay = opts.overlay;
    this.#external = opts.external ?? new Set();

    this.#textarea.addEventListener('input', () => {
      this.#repaint();
      opts.onInput?.(this.#textarea.value);
    });
    // Keep the colours under the characters while scrolling.
    this.#textarea.addEventListener('scroll', () => this.#syncScroll());
    // Tab should indent rather than leave the editor.
    this.#textarea.addEventListener('keydown', (e) => this.#onKeyDown(e));
    this.#repaint();
  }

  get value(): string {
    return this.#textarea.value;
  }

  set value(next: string) {
    this.#textarea.value = next;
    this.#repaint();
  }

  focus(): void {
    this.#textarea.focus();
  }

  /** Select a character range, scrolling it into view. */
  select(start: number, end: number): void {
    this.#textarea.focus();
    this.#textarea.setSelectionRange(start, end);
    // setSelectionRange does not always scroll; nudge the line into view.
    const line = this.#textarea.value.slice(0, start).split('\n').length - 1;
    const lineHeight = this.#textarea.scrollHeight / Math.max(1, this.#lineCount());
    const target = line * lineHeight - this.#textarea.clientHeight / 2;
    this.#textarea.scrollTop = Math.max(0, target);
    this.#syncScroll();
  }

  #lineCount(): number {
    return this.#textarea.value.split('\n').length + 1; // +1 for the trailing line
  }

  #onKeyDown(e: KeyboardEvent): void {
    if (e.key !== 'Tab' || e.ctrlKey || e.metaKey || e.altKey) return;
    e.preventDefault();
    const el = this.#textarea;
    const { selectionStart: s, selectionEnd: t, value } = el;
    el.value = `${value.slice(0, s)}  ${value.slice(t)}`;
    el.selectionStart = el.selectionEnd = s + 2;
    // Let the input listener repaint and notify, as for any other edit.
    el.dispatchEvent(new Event('input'));
  }

  #repaint(): void {
    this.#overlay.innerHTML = highlightMatlab(this.#textarea.value, this.#external);
    this.#syncScroll();
  }

  #syncScroll(): void {
    this.#overlay.scrollTop = this.#textarea.scrollTop;
    this.#overlay.scrollLeft = this.#textarea.scrollLeft;
  }
}