import type { Extension } from '@codemirror/state';
import { EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
import { createLearnitiveMathField } from '@learnitive/latex-mathlive';
import {
  canEditLatexMathWithMathLive,
  findLatexMathAtPosition,
  latexMathRegionToMathLiveValue,
  type LatexMathRegion,
} from '@learnitive/latex-source';

export type LatexMathPreviewMode = 'hover' | 'cursor' | 'hover-cursor' | 'never';

export type LatexMathOpenRequest = {
  region: LatexMathRegion;
  anchor: DOMRect;
};

/**
 * Learnitive CodeMirror MathLive lifecycle. It detects math from the
 * canonical CodeMirror text, shows a read-only hover/cursor preview and hands
 * editing to Learnitive's shared revision-guarded MathLive editor. The extension
 * itself never mutates LaTeX source.
 */
export function createLearnitiveMathLiveExtension(options: {
  previewMode?: LatexMathPreviewMode;
  onOpen: (request: LatexMathOpenRequest) => void;
}): Extension {
  const previewMode = options.previewMode ?? 'hover-cursor';

  return ViewPlugin.fromClass(
    class {
      private overlay?: HTMLDivElement;
      private hoveredRegion?: LatexMathRegion;
      private lastMouse?: MouseEvent;
      private timer?: number;
      private destroyed = false;

      constructor(private readonly view: EditorView) {
        this.view.dom.addEventListener('mousemove', this.onMouseMove);
        this.view.dom.addEventListener('mouseleave', this.onMouseLeave);
        // Do not poll the whole CodeMirror document. Initial preview detection
        // is deferred, then further work is driven only by real cursor/mouse or
        // document activity.
        this.refreshSoon(240);
      }

      update(update: ViewUpdate): void {
        if (update.docChanged) {
          // Complex papers can be very large. A source keystroke must not call
          // doc.toString() + the LaTeX math scanner synchronously (or on the
          // next animation frame). Wait for a genuine typing idle window.
          this.hoveredRegion = undefined;
          this.clearOverlay();
          this.refreshSoon(600);
          return;
        }
        if (update.selectionSet) this.refreshSoon(80);
      }

      destroy(): void {
        this.destroyed = true;
        if (this.timer !== undefined) window.clearTimeout(this.timer);
        this.view.dom.removeEventListener('mousemove', this.onMouseMove);
        this.view.dom.removeEventListener('mouseleave', this.onMouseLeave);
        this.clearOverlay();
      }

      private onMouseMove = (event: MouseEvent) => {
        this.lastMouse = event;
        this.refreshSoon(60);
      };

      private onMouseLeave = () => {
        this.lastMouse = undefined;
      };

      private refreshSoon(delayMs: number): void {
        if (this.destroyed) return;
        if (this.timer !== undefined) window.clearTimeout(this.timer);
        this.timer = window.setTimeout(() => {
          this.timer = undefined;
          this.refresh();
        }, delayMs);
      }

      private regionAtCursor(source: string): { region: LatexMathRegion; rect: DOMRect } | undefined {
        const pos = this.view.state.selection.main.head;
        const region = findLatexMathAtPosition(source, pos);
        const coords = region ? this.view.coordsAtPos(pos) : undefined;
        if (!region || !coords) return undefined;
        return {
          region,
          rect: new DOMRect(coords.left, coords.top, Math.max(1, coords.right - coords.left), Math.max(1, coords.bottom - coords.top)),
        };
      }

      private regionAtMouse(source: string): { region: LatexMathRegion; rect: DOMRect } | undefined {
        const event = this.lastMouse;
        if (!event) return undefined;
        const pos = this.view.posAtCoords({ x: event.clientX, y: event.clientY });
        if (pos === null) return undefined;
        const region = findLatexMathAtPosition(source, pos);
        const coords = region ? this.view.coordsAtPos(pos) : undefined;
        if (!region || !coords) return undefined;
        return {
          region,
          rect: new DOMRect(coords.left, coords.top, Math.max(1, coords.right - coords.left), Math.max(1, coords.bottom - coords.top)),
        };
      }

      private refresh(): void {
        if (this.destroyed || previewMode === 'never') return;
        // Materialize the CodeMirror document at most once for one preview
        // attempt; hover-cursor mode previously allocated/scanned it twice.
        const source = this.view.state.doc.toString();
        const cursor = previewMode === 'cursor' || previewMode === 'hover-cursor'
          ? this.regionAtCursor(source)
          : undefined;
        const mouse = !cursor && (previewMode === 'hover' || previewMode === 'hover-cursor')
          ? this.regionAtMouse(source)
          : undefined;
        const match = cursor ?? mouse;
        if (!match) {
          if (this.overlay && !this.overlay.matches(':hover')) {
            this.hoveredRegion = undefined;
            this.clearOverlay();
          }
          return;
        }
        const same = this.hoveredRegion &&
          this.hoveredRegion.replaceFrom === match.region.replaceFrom &&
          this.hoveredRegion.replaceTo === match.region.replaceTo &&
          this.hoveredRegion.content === match.region.content;
        if (same) return;
        this.hoveredRegion = match.region;
        this.renderPreview(match.region, match.rect);
      }

      private renderPreview(region: LatexMathRegion, anchor: DOMRect): void {
        this.clearOverlay();
        const overlay = document.createElement('div');
        overlay.className = 'interactive-cm-math-preview fixed z-[85] flex max-w-[min(600px,calc(100vw-32px))] items-center gap-2 rounded-lg border bg-background p-2 shadow-xl';
        overlay.dataset.testid = 'interactive-mathlive-preview';
        const fieldHost = document.createElement('button');
        fieldHost.type = 'button';
        fieldHost.className = 'min-w-12 max-w-[520px] overflow-auto rounded bg-transparent px-1 py-0.5 text-left';
        fieldHost.setAttribute('aria-label', 'Edit equation');
        const edit = document.createElement('button');
        edit.type = 'button';
        edit.className = 'rounded-md border px-2 py-1 text-xs hover:bg-muted';
        edit.textContent = 'Edit';
        edit.disabled = !canEditLatexMathWithMathLive(region);
        overlay.append(fieldHost, edit);
        document.body.appendChild(overlay);
        this.overlay = overlay;

        try {
          const field = createLearnitiveMathField({
            parent: fieldHost,
            value: latexMathRegionToMathLiveValue(region),
            readOnly: true,
            ariaLabel: 'Equation preview',
            className: 'pointer-events-none min-w-10 bg-transparent',
          });
          field.style.border = '0';
          field.style.padding = '0';
          field.style.background = 'transparent';
        } catch {
          fieldHost.textContent = region.content;
        }

        const open = (event: Event) => {
          event.preventDefault();
          event.stopPropagation();
          if (!canEditLatexMathWithMathLive(region)) return;
          const rect = overlay.getBoundingClientRect();
          options.onOpen({ region, anchor: rect });
          this.clearOverlay();
        };
        fieldHost.addEventListener('click', open);
        edit.addEventListener('click', open);

        const measured = overlay.getBoundingClientRect();
        const left = Math.min(
          Math.max(8, anchor.left + 18),
          Math.max(8, window.innerWidth - measured.width - 8),
        );
        const top = Math.min(
          Math.max(8, anchor.bottom + 10),
          Math.max(8, window.innerHeight - measured.height - 8),
        );
        overlay.style.left = `${left}px`;
        overlay.style.top = `${top}px`;
      }

      private clearOverlay(): void {
        this.overlay?.remove();
        this.overlay = undefined;
      }
    },
  );
}
