<script setup>
import { Compartment, EditorState } from "@codemirror/state";
import { EditorView, keymap, placeholder as cmPlaceholder } from "@codemirror/view";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { onBeforeUnmount, onMounted, useTemplateRef, watch } from "vue";
import { cn } from "../../../utils/cn";
import { celHighlighting } from "./cel-language";
import { scopeChips, scopeLookupFacet } from "./chip-extension";
import { selectionChips, selectionLookupFacet } from "./selection-chip-extension";
import { stepChips, stepLookupFacet } from "./step-chip-extension";
defineOptions({ name: "UiCodeMirrorInput" });
const props = defineProps({
  class: { type: [Boolean, null, String, Object, Array], required: false, skipCheck: true },
  defaultValue: { type: String, required: false },
  modelValue: { type: String, required: false },
  multiline: { type: Boolean, required: false },
  placeholder: { type: String, required: false },
  scopeLookup: { type: Map, required: true },
  selectionLookup: { type: Map, required: true },
  stepLookup: { type: Map, required: true }
});
const emits = defineEmits(["update:modelValue"]);
const host = useTemplateRef("host");
let view = null;
const lookupCompartment = new Compartment();
const selectionLookupCompartment = new Compartment();
const stepLookupCompartment = new Compartment();
const modeCompartment = new Compartment();
let applyingExternal = false;
const editorTheme = EditorView.theme({
  "&": {
    fontSize: "0.875rem",
    color: "var(--color-zinc-700)",
    backgroundColor: "transparent"
  },
  "&.cm-focused": {
    outline: "none"
  },
  ".cm-content": {
    fontFamily: "var(--font-mono)",
    padding: "7px 0",
    lineHeight: "1.5",
    caretColor: "var(--color-zinc-700)"
  },
  ".cm-placeholder": {
    color: "var(--color-zinc-200)"
  },
  ".cm-scroller": {
    fontFamily: "var(--font-mono)",
    lineHeight: "1.5",
    // Behave like a native <input>: a single line that outgrows a narrow editor
    // scrolls horizontally with the caret, but never surfaces a scrollbar —
    // whose track would otherwise read as a stray line under the field.
    scrollbarWidth: "none"
  },
  ".cm-scroller::-webkit-scrollbar": {
    display: "none"
  },
  ".cm-line": {
    padding: "0"
  }
});
function modeExtension(multiline) {
  if (multiline) return EditorView.lineWrapping;
  return EditorState.transactionFilter.of((tr) => {
    if (!tr.docChanged) return tr;
    let hasNewline = false;
    tr.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => {
      if (inserted.lines > 1) hasNewline = true;
    });
    return hasNewline ? [] : tr;
  });
}
function makeState(doc) {
  return EditorState.create({
    doc,
    extensions: [
      history(),
      keymap.of([...defaultKeymap, ...historyKeymap]),
      cmPlaceholder(props.placeholder ?? ""),
      lookupCompartment.of(scopeLookupFacet.of(props.scopeLookup)),
      selectionLookupCompartment.of(selectionLookupFacet.of(props.selectionLookup)),
      stepLookupCompartment.of(stepLookupFacet.of(props.stepLookup)),
      modeCompartment.of(modeExtension(!!props.multiline)),
      editorTheme,
      // CEL syntax colours sit *under* the chips: the chip plugin's
      // `Decoration.replace` widgets hide the `__scopes__[...]` spans, so the
      // marks here only show through on ordinary expression text.
      celHighlighting(),
      scopeChips(),
      selectionChips(),
      stepChips(),
      EditorView.contentAttributes.of({ "aria-label": props.placeholder ?? "Expression" }),
      EditorView.updateListener.of((update) => {
        if (!update.docChanged || applyingExternal) return;
        emits("update:modelValue", update.state.doc.toString());
      })
    ]
  });
}
onMounted(() => {
  if (!host.value) return;
  view = new EditorView({
    state: makeState(props.modelValue ?? props.defaultValue ?? ""),
    parent: host.value
  });
});
onBeforeUnmount(() => {
  view?.destroy();
  view = null;
});
watch(() => props.modelValue, (next) => {
  if (!view) return;
  const incoming = next ?? "";
  if (incoming === view.state.doc.toString()) return;
  applyingExternal = true;
  view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: incoming } });
  applyingExternal = false;
});
watch(() => props.scopeLookup, (lookup) => {
  view?.dispatch({ effects: lookupCompartment.reconfigure(scopeLookupFacet.of(lookup)) });
});
watch(() => props.selectionLookup, (lookup) => {
  view?.dispatch({ effects: selectionLookupCompartment.reconfigure(selectionLookupFacet.of(lookup)) });
});
watch(() => props.stepLookup, (lookup) => {
  view?.dispatch({ effects: stepLookupCompartment.reconfigure(stepLookupFacet.of(lookup)) });
});
watch(() => props.multiline, (multiline) => {
  view?.dispatch({ effects: modeCompartment.reconfigure(modeExtension(!!multiline)) });
});
function insertAtSelection(text) {
  if (!view) return;
  const { from, to } = view.state.selection.main;
  view.dispatch({
    changes: { from, to, insert: text },
    selection: { anchor: from + text.length }
  });
  view.focus();
}
function focus() {
  view?.focus();
}
defineExpose({ insertAtSelection, focus });
</script>

<template>
  <div
    ref="host"
    data-slot="input-group-control"
    :class="cn(
  'flex-1 min-w-0 self-stretch overflow-x-auto px-3',
  // Single-line: `overflow-x-auto` on its own makes the browser promote
  // `overflow-y` from `visible` to `auto` (CSS spec quirk), so the 1px gap
  // between the 35px content box and the 34px `h-9` host surfaces a phantom
  // vertical scrollbar. Pin it hidden — one line never scrolls vertically.
  props.multiline ? '[&_.cm-content]:min-h-16' : 'overflow-y-hidden [&_.cm-scroller]:items-center',
  props.class
)"
  />
</template>

<style scoped>
[data-slot=input-group-control]{scrollbar-width:none}[data-slot=input-group-control]::-webkit-scrollbar{display:none}
</style>
