// deno-lint-ignore-file

import {
  Decoration,
  DecorationSet,
  EditorView,
  highlightActiveLine,
  highlightActiveLineGutter,
  hoverTooltip,
  keymap,
  lineNumbers,
} from "npm:@codemirror/view";

import { EditorState, RangeSetBuilder, StateEffect, StateField, Text } from "npm:@codemirror/state";
import {
  defaultKeymap,
  history,
  historyKeymap,
} from "npm:@codemirror/commands";
import { bracketMatching, indentOnInput } from "npm:@codemirror/language";
import { closeBrackets } from "npm:@codemirror/autocomplete";

import wasm from "./wasm.ts";

import { Share } from "./share.ts";
import { examples } from "./examples.ts";
import { bus } from "./bus.ts";
import { current, Highlight, HighlightKind } from "./highlight.ts";

function tokenize(text: string): wasm.Tok[] {
  try {
    return wasm.lex(text);
  } catch (e) {
    console.log(e);
    return [];
  }
}

function compile(
  text: string,
): { log: wasm.CompileLog[]; ansi_log: string; machine: string | undefined } {
  try {
    return wasm.compile(text);
  } catch (e) {
    console.log(e);
    return { log: [], ansi_log: "", machine: "" };
  }
}


function decoForKind(kind: HighlightKind) {
  // Use a class per kind so each gets a distinct color via CSS
  return Decoration.mark({ class: `cm-highlight cm-highlight-${kind}` });
}

bus.on("highlight/update", _ => {
  const arr = current.values().toArray().sort((a, b) => a.span[0]-b.span[0]);
  editor.dispatch({ effects: setHighlights.of(arr) });
});
export const setHighlights = StateEffect.define<Highlight[]>();
export const highlightsField = StateField.define<DecorationSet>({
  create() {
    return Decoration.none;
  },

  update(highlights, tr) {
    // Keep highlights aligned with document edits
    highlights = highlights.map(tr.changes);

    for (const e of tr.effects) {
      if (e.is(setHighlights)) {
        const spans = e.value;

        const builder = new RangeSetBuilder<Decoration>();
        for (const s of spans) {
          
          const from = Math.max(0, Math.min(s.span[0], tr.state.doc.length));
          const to = Math.max(0, Math.min(s.span[1], tr.state.doc.length));
          if (to > from) builder.add(from, to, decoForKind(s.kind));
        }
        highlights = builder.finish();
      }
    }

    return highlights;
  },

  provide: (f) => EditorView.decorations.from(f),
});


const eventBusConnection = StateField.define({
  create(state) {
    const text = state.doc.toString();
    bus.emit("editor/change", { text, doc: state.doc });
    return buildAnalysis(text, state.doc);
  },
  update(value, tr) {
    if (!tr.docChanged) return value;
    const text = tr.state.doc.toString();
    bus.emit("editor/change", { text, doc: state.doc });
    return buildAnalysis(text, tr.state.doc);
  },
  provide: (f) => EditorView.decorations.from(f, (v) => v.deco),
});

function buildAnalysis(text: string, doc: Text) {
  save(text);
  const tokens = tokenize(text);
  const { log, ansi_log, machine } = compile(text);

  bus.emit("compiled", { log, ansi_log, machine });

  const marks = [];
  const docLen = doc.length;

  for (const tok of tokens) {
    const start = Math.max(0, Math.min(docLen, tok.start));
    const end = Math.max(start, Math.min(docLen, tok.end));
    let tc = tokenClass(tok.kind);
    if (tc === "rb-") {
      tc += tok.scope_level.toString();
    }
    if (end > start) {
      marks.push(Decoration.mark({ class: tc }).range(start, end));
    }
  }

  for (const d of log) {
    if (d.start === undefined || d.end === undefined) continue;
    const start = Math.max(0, Math.min(docLen, d.start));
    const endRaw = d.end == null ? d.start : d.end;
    const end = Math.max(start, Math.min(docLen, endRaw));
    const cls = severityClass(d.level);
    if (end > start) {
      marks.push(Decoration.mark({ class: cls }).range(start, end));
    } else {
      const end = Math.min(docLen, start + 1);
      if (end > start) {
        marks.push(Decoration.mark({ class: cls }).range(start, end));
      }
    }
  }

  const deco = Decoration.set(marks, true);
  return { tokens, log, ansi_log, deco };
}

const tokenClass = (t: string) => ({
  comment: "tok-comment",
  keyword: "tok-keyword",
  error: "tok-error",
  ident: "tok-ident",
  punc: "tok-punc",
  string: "tok-string",
  lpar: "rb-",
  lbrace: "rb-",
  lbracket: "rb-",

  rpar: "rb-",
  rbrace: "rb-",
  rbracket: "rb-",
}[t] || "tok-ident");

function severityClass(sev: string) {
  const s = (sev || "error").toLowerCase();
  if (s === "warning") return "cm-diag-warning";
  if (s === "info") return "cm-diag-info";
  return "cm-diag-error";
}
function sevRank(sev: string) {
  if (sev === "error") return 3;
  if (sev === "warning") return 2;
  return 1;
}

// ===================== Hover tooltip (uses cached diags) =====================
const diagHover = hoverTooltip((view, pos) => {
  const { log } = view.state.field(eventBusConnection);
  const hits = log.filter((d) =>
    d.start !== undefined && d.end !== undefined && pos >= d.start &&
    pos <= d.end
  );
  if (hits.length === 0) return null;

  const top = hits.reduce(
    (a, b) => (sevRank(b.level) > sevRank(a.level) ? b : a),
    hits[0],
  );

  return {
    pos,
    end: pos,
    above: true,
    create() {
      const dom = document.createElement("div");
      dom.className = "cm-tooltip cm-tooltip-hover";

      const title = document.createElement("div");
      title.className = `tipTitle ${top.level}`;
      title.textContent = hits.length === 1
        ? top.level.toUpperCase()
        : `${top.level.toUpperCase()} (${hits.length})`;

      const body = document.createElement("div");
      body.className = "tipBody";
      body.textContent = hits
        .slice()
        .sort((a, b) => sevRank(b.level) - sevRank(a.level))
        .map((h) => `[${h.level.toUpperCase()}] ${h.message}`)
        .join("\n");

      dom.appendChild(title);
      dom.appendChild(body);
      return { dom };
    },
  };
});

function save(text: string) {
  globalThis.localStorage.save = text;
}

function getSaved(): string | undefined {
  return globalThis.localStorage.save;
}

function defaultText(): string {
  return Share.sharedText() ?? getSaved() ?? examples[0].machine;
}

const state = EditorState.create({
  doc: defaultText(),
  extensions: [
    lineNumbers(),
    highlightActiveLineGutter(),
    history(),
    indentOnInput(),
    bracketMatching(),
    highlightActiveLine(),
    closeBrackets(),
    keymap.of([...defaultKeymap, ...historyKeymap]),

    eventBusConnection,
    highlightsField,
    diagHover,

    EditorView.lineWrapping,
  ],
});

const editor = new EditorView({
  state,
  parent: document.getElementById("editor")!,
});

bus.on(
  "begin",
  (_) => bus.emit("controls/editor/set_text", defaultText()),
);

bus.on("controls/editor/set_text", text => {
  editor.dispatch({
    changes: { from: 0, to: editor.state.doc.length, insert: text },
  });
});

bus.on("example/selected", example => {
  bus.emit("controls/editor/set_text", example.machine);
});
