import type { Extension } from '@codemirror/state';
import { EditorView, ViewPlugin } from '@codemirror/view';
import { getLatexEditorCommandDefinition } from '@learnitive/latex-source';

export type LatexToolbarEntry =
  | { key: string; label: string }
  | { type: 'split' }
  | { type: 'space' };

export type LatexEditorCommand = {
  id: string;
  label: string;
  toolbar: boolean;
  slash: readonly string[];
  executeSource: (view: EditorView) => boolean;
};

function focusAndDispatch(
  view: EditorView,
  changes: { from: number; to: number; insert: string } | readonly { from: number; to: number; insert: string }[],
  anchor?: number,
): boolean {
  view.dispatch({
    changes,
    ...(anchor === undefined ? {} : { selection: { anchor } }),
    scrollIntoView: true,
  });
  view.focus();
  return true;
}

function wrapSelection(view: EditorView, prefix: string, suffix: string): boolean {
  // Adapted from Texlyre's toolbar/helpers.ts. Focus first, then perform one
  // atomic content+selection transaction. Focusing *after* a second selection
  // transaction can let the DOM selection overwrite CodeMirror's state in
  // jsdom and some browser focus transitions, which was the source of the
  // cursor/selection jump regression.
  const selection = view.state.selection.main;
  const selected = view.state.sliceDoc(selection.from, selection.to);
  const anchor = selection.from + prefix.length;
  view.focus();
  view.dispatch({
    changes: {
      from: selection.from,
      to: selection.to,
      insert: `${prefix}${selected}${suffix}`,
    },
    selection: {
      anchor,
      head: anchor + selected.length,
    },
    scrollIntoView: true,
  });
  return true;
}

function insertText(view: EditorView, text: string, cursorOffset = 0): boolean {
  // Adapted from Texlyre's toolbar/helpers.ts.
  const selection = view.state.selection.main;
  return focusAndDispatch(
    view,
    { from: selection.from, to: selection.to, insert: text },
    selection.from + text.length + cursorOffset,
  );
}

function selectedText(view: EditorView): string {
  const selection = view.state.selection.main;
  return view.state.sliceDoc(selection.from, selection.to);
}

const command = (
  id: string,
  fallbackLabel: string,
  executeSource: (view: EditorView) => boolean,
  fallbackSlash: readonly string[] = [id],
): LatexEditorCommand => {
  const semantic = getLatexEditorCommandDefinition(id);
  return {
    id,
    label: semantic?.label ?? fallbackLabel,
    toolbar: semantic?.toolbar ?? true,
    slash: semantic?.slash ?? fallbackSlash,
    executeSource,
  };
};

export const LATEX_EDITOR_COMMANDS: readonly LatexEditorCommand[] = [
  command('bold', 'Bold', (view) => wrapSelection(view, '\\textbf{', '}')),
  command('italic', 'Italic', (view) => wrapSelection(view, '\\textit{', '}')),
  command('underline', 'Underline', (view) => wrapSelection(view, '\\underline{', '}')),
  command('strikethrough', 'Strikethrough', (view) => wrapSelection(view, '\\sout{', '}'), ['strike', 'strikethrough']),
  command('emphasis', 'Emphasis', (view) => wrapSelection(view, '\\emph{', '}'), ['emphasis', 'emph']),
  command('typewriter', 'Typewriter', (view) => wrapSelection(view, '\\texttt{', '}'), ['typewriter', 'monospace']),
  command('superscript', 'Superscript', (view) => wrapSelection(view, '\\textsuperscript{', '}')),
  command('subscript', 'Subscript', (view) => wrapSelection(view, '\\textsubscript{', '}')),
  command('section', 'Section', (view) => {
    const text = selectedText(view);
    return insertText(view, `\\section{${text}}\n`, -(text.length + 2));
  }),
  command('subsection', 'Subsection', (view) => {
    const text = selectedText(view);
    return insertText(view, `\\subsection{${text}}\n`, -(text.length + 2));
  }),
  command('subsubsection', 'Subsubsection', (view) => {
    const text = selectedText(view);
    return insertText(view, `\\subsubsection{${text}}\n`, -(text.length + 2));
  }),
  command('itemize', 'Itemize', (view) =>
    insertText(view, '\\begin{itemize}\n\t\\item \n\\end{itemize}', -14)),
  command('enumerate', 'Enumerate', (view) =>
    insertText(view, '\\begin{enumerate}\n\t\\item \n\\end{enumerate}', -16)),
  command('inline-math', 'Inline Math', (view) => wrapSelection(view, '$', '$'), ['math', 'inline-math']),
  command('display-math', 'Display Math', (view) => {
    const text = selectedText(view);
    const value = `\\[\n\t${text}\n\\]`;
    return insertText(view, value, text ? -(text.length + 3) : -3);
  }, ['display-math', 'math-block']),
  command('equation', 'Equation', (view) =>
    insertText(view, '\\begin{equation}\n\t\n\\end{equation}', -15)),
  command('figure', 'Figure', (view) =>
    insertText(
      view,
      '\\begin{figure}[h]\n\t\\centering\n\t\\includegraphics[width=0.8\\textwidth]{}\n\t\\caption{}\n\t\\label{fig:}\n\\end{figure}',
      -40,
    )),
  command('table', 'Table', (view) =>
    insertText(
      view,
      '\\begin{table}[h]\n\t\\centering\n\t\\begin{tabular}{|c|c|}\n\t\t\\hline\n\t\tHeader 1 & Header 2 \\\\\n\t\t\\hline\n\t\t &  \\\\\n\t\t\\hline\n\t\\end{tabular}\n\t\\caption{}\n\t\\label{tab:}\n\\end{table}',
      0,
    )),
  command('verbatim', 'Verbatim', (view) => {
    const text = selectedText(view);
    const value = `\\begin{verbatim}\n${text}\n\\end{verbatim}`;
    return insertText(view, value, text ? -(text.length + 13) : -13);
  }),
  command('listing', 'Code Listing', (view) =>
    insertText(view, '\\begin{lstlisting}\n\t\n\\end{lstlisting}', -18), ['listing', 'code']),
  command('hyperlink', 'Hyperlink', (view) => {
    const text = selectedText(view);
    return text
      ? insertText(view, `\\href{}{${text}}`, -(text.length + 2))
      : insertText(view, '\\href{}{}', -3);
  }, ['link', 'hyperlink']),
  command('citation', 'Citation', (view) => wrapSelection(view, '\\cite{', '}'), ['cite', 'citation']),
  command('reference', 'Reference', (view) => wrapSelection(view, '\\ref{', '}'), ['ref', 'reference']),
  command('label', 'Label', (view) => wrapSelection(view, '\\label{', '}')),
  command('footnote', 'Footnote', (view) => wrapSelection(view, '\\footnote{', '}')),
  command('text-colour', 'Text colour', (view) => wrapSelection(view, '\\textcolor{red}{', '}'), ['color', 'colour', 'text-colour']),
  command('highlight', 'Highlight', (view) => wrapSelection(view, '\\colorbox{yellow}{', '}')),
];

const COMMANDS_BY_ID = new Map(LATEX_EDITOR_COMMANDS.map((item) => [item.id, item]));

function findTabularScope(view: EditorView): { start: number; end: number; columns: number } | undefined {
  const text = view.state.doc.toString();
  const pos = view.state.selection.main.head;
  const before = text.slice(0, pos);
  const beginPattern = /\\begin\{(?:tabular\*?|tabularx|longtable|array)\}(?:\[[^\]]*\])?\{([^}]*)\}/g;
  let latest: RegExpExecArray | undefined;
  for (const match of before.matchAll(beginPattern)) latest = match;
  if (!latest || latest.index === undefined) return undefined;
  const beginText = latest[0];
  const env = /\\begin\{([^}]+)\}/.exec(beginText)?.[1];
  if (!env) return undefined;
  const endToken = `\\end{${env}}`;
  const end = text.indexOf(endToken, latest.index + beginText.length);
  if (end < pos) return undefined;
  const closerBefore = before.lastIndexOf(endToken);
  if (closerBefore > latest.index) return undefined;
  const spec = latest[1] ?? '';
  const columns = Math.max(1, (spec.match(/[lcrpmbX]/g) ?? []).length);
  return { start: latest.index + beginText.length, end, columns };
}

function currentTableLine(view: EditorView, scope: { start: number; end: number }): { from: number; to: number; text: string } {
  const pos = Math.max(scope.start, Math.min(view.state.selection.main.head, scope.end));
  const line = view.state.doc.lineAt(pos);
  return { from: Math.max(scope.start, line.from), to: Math.min(scope.end, line.to), text: line.text };
}

function tableCommand(action: 'row-before' | 'row-after' | 'row-remove' | 'col-before' | 'col-after' | 'col-remove') {
  return (view: EditorView): boolean => {
    const scope = findTabularScope(view);
    if (!scope) return false;
    const line = currentTableLine(view, scope);
    if (action.startsWith('row-')) {
      if (action === 'row-remove') {
        const to = Math.min(scope.end, line.to + (view.state.doc.sliceString(line.to, line.to + 1) === '\n' ? 1 : 0));
        return focusAndDispatch(view, { from: line.from, to, insert: '' }, Math.min(line.from, view.state.doc.length));
      }
      const row = `${' & '.repeat(scope.columns - 1)} \\\\\n`;
      const at = action === 'row-before' ? line.from : Math.min(scope.end, line.to + 1);
      return focusAndDispatch(view, { from: at, to: at, insert: row }, at);
    }

    const changes: { from: number; to: number; insert: string }[] = [];
    const body = view.state.doc.sliceString(scope.start, scope.end);
    let bodyOffset = scope.start;
    for (const rawLine of body.split('\n')) {
      const lineStart = bodyOffset;
      const lineEnd = lineStart + rawLine.length;
      bodyOffset = lineEnd + 1;
      if (!rawLine.includes('\\\\')) continue;
      const ampersands = [...rawLine.matchAll(/&/g)].map((m) => lineStart + (m.index ?? 0));
      if (action === 'col-remove') {
        if (ampersands.length === 0) continue;
        const amp = ampersands.at(-1);
        if (amp === undefined) continue;
        changes.push({ from: Math.max(lineStart, amp - 1), to: Math.min(lineEnd, amp + 2), insert: '' });
      } else if (action === 'col-before') {
        changes.push({ from: lineStart, to: lineStart, insert: ' & ' });
      } else {
        const slash = rawLine.lastIndexOf('\\\\');
        const at = slash >= 0 ? lineStart + slash : lineEnd;
        changes.push({ from: at, to: at, insert: ' & ' });
      }
    }
    if (changes.length === 0) return false;
    return focusAndDispatch(view, changes.sort((a, b) => b.from - a.from));
  };
}

const TABLE_COMMANDS: readonly LatexEditorCommand[] = [
  command('table-row-before', 'Add row before', tableCommand('row-before')),
  command('table-row-after', 'Add row after', tableCommand('row-after')),
  command('table-row-remove', 'Remove row', tableCommand('row-remove')),
  command('table-col-before', 'Add column before', tableCommand('col-before')),
  command('table-col-after', 'Add column after', tableCommand('col-after')),
  command('table-col-remove', 'Remove column', tableCommand('col-remove')),
];
for (const item of TABLE_COMMANDS) COMMANDS_BY_ID.set(item.id, item);

const split: LatexToolbarEntry = { type: 'split' };
const space: LatexToolbarEntry = { type: 'space' };

function baseToolbarEntries(): LatexToolbarEntry[] {
  const entry = (id: string): LatexToolbarEntry => {
    const item = COMMANDS_BY_ID.get(id);
    if (!item) throw new Error(`Unknown LaTeX editor command: ${id}`);
    return { key: item.id, label: item.label };
  };
  return [
    entry('bold'), entry('italic'), entry('underline'), entry('strikethrough'), entry('emphasis'), entry('typewriter'), split,
    entry('superscript'), entry('subscript'), split,
    entry('section'), entry('subsection'), entry('subsubsection'), split,
    entry('itemize'), entry('enumerate'), split,
    { key: 'inline-math', label: 'Equation editor' }, split,
    entry('figure'), entry('table'), split,
    entry('verbatim'), entry('listing'), split,
    entry('hyperlink'), split,
    entry('citation'), entry('reference'), entry('label'), entry('footnote'), split,
    entry('text-colour'), entry('highlight'),
  ];
}

export interface LatexToolbarController {
  extension: Extension;
  subscribe(listener: () => void): () => void;
  getItems(): LatexToolbarEntry[];
  run(key: string): boolean;
}

export function createLatexToolbarController(): LatexToolbarController {
  let view: EditorView | undefined;
  let inTable = false;
  let items = baseToolbarEntries();
  const listeners = new Set<() => void>();

  const rebuild = () => {
    const next = baseToolbarEntries();
    if (inTable) {
      next.push(
        split,
        ...TABLE_COMMANDS.map((item) => ({ key: item.id, label: item.label } as const)),
      );
    }
    next.push(space);
    items = next;
    for (const listener of listeners) listener();
  };

  const extension = ViewPlugin.fromClass(class {
    constructor(v: EditorView) {
      view = v;
      inTable = Boolean(findTabularScope(v));
      rebuild();
    }
    update(update: { view: EditorView; selectionSet: boolean; docChanged: boolean }) {
      if (!update.selectionSet && !update.docChanged) return;
      const next = Boolean(findTabularScope(update.view));
      if (next !== inTable) {
        inTable = next;
        rebuild();
      }
    }
    destroy() {
      view = undefined;
    }
  });

  return {
    extension,
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    getItems: () => items,
    run(key) {
      const item = COMMANDS_BY_ID.get(key);
      return Boolean(view && item?.executeSource(view));
    },
  };
}

export function getLatexEditorCommand(id: string): LatexEditorCommand | undefined {
  return COMMANDS_BY_ID.get(id);
}
