import type { LatexToolbarEntry } from '@learnitive/latex-codemirror';
import {
  Baseline,
  Bold,
  BookOpen,
  Braces,
  CaseSensitive,
  Code2,
  Columns3,
  FileCode2,
  Footprints,
  Heading1,
  Heading2,
  Heading3,
  Highlighter,
  Image,
  Italic,
  Link2,
  List,
  ListOrdered,
  MoreHorizontal,
  Palette,
  Pilcrow,
  Quote,
  RemoveFormatting,
  Sigma,
  SquareFunction,
  Strikethrough,
  Subscript,
  Superscript,
  Table2,
  TextCursorInput,
  Underline,
} from 'lucide-react';
import React from 'react';

type ToolbarButton = Extract<LatexToolbarEntry, { key: string }>;

const DEFAULT_ITEM_WIDTH = 34;
const DEFAULT_SPLIT_WIDTH = 9;
const FIT_PADDING = 54;
const OVERFLOW_KEY = 'toolbar-overflow';

function splitGroups(entries: readonly LatexToolbarEntry[]) {
  let tailStart = entries.length;
  for (let index = entries.length - 1; index >= 0; index -= 1) {
    const entry = entries[index];
    if (entry && 'type' in entry && entry.type === 'space') {
      tailStart = index;
      break;
    }
  }
  const groups: ToolbarButton[][] = [];
  let current: ToolbarButton[] = [];
  for (const entry of entries.slice(0, tailStart)) {
    if ('type' in entry) {
      if (current.length > 0) groups.push(current);
      current = [];
    } else {
      current.push(entry);
    }
  }
  if (current.length > 0) groups.push(current);
  return { groups, tail: entries.slice(tailStart) };
}

function iconFor(key: string) {
  if (key === 'bold') return Bold;
  if (key === 'italic' || key === 'emphasis') return Italic;
  if (key === 'underline') return Underline;
  if (key === 'strikethrough') return Strikethrough;
  if (key === 'typewriter') return CaseSensitive;
  if (key === 'superscript') return Superscript;
  if (key === 'subscript') return Subscript;
  if (key === 'section') return Heading1;
  if (key === 'subsection') return Heading2;
  if (key === 'subsubsection') return Heading3;
  if (key === 'itemize') return List;
  if (key === 'enumerate') return ListOrdered;
  if (key === 'inline-math') return Sigma;
  if (key === 'display-math' || key === 'equation') return SquareFunction;
  if (key === 'figure') return Image;
  if (key === 'table') return Table2;
  if (key === 'verbatim') return Braces;
  if (key === 'listing') return FileCode2;
  if (key === 'hyperlink') return Link2;
  if (key === 'citation') return BookOpen;
  if (key === 'reference') return Quote;
  if (key === 'label') return Pilcrow;
  if (key === 'footnote') return Footprints;
  if (key === 'text-colour') return Palette;
  if (key === 'highlight') return Highlighter;
  if (key.includes('remove')) return RemoveFormatting;
  if (key.includes('row')) return TextCursorInput;
  if (key.includes('col')) return Columns3;
  return Code2;
}

export function LearnitivePluginToolbar({
  items,
  onRun,
  disabled = false,
  protectedTailGroups = 1,
}: {
  items: readonly LatexToolbarEntry[];
  onRun: (key: string) => void;
  disabled?: boolean;
  protectedTailGroups?: number;
}) {
  const toolbarRef = React.useRef<HTMLDivElement>(null);
  const overflowMenuRef = React.useRef<HTMLDivElement>(null);
  const baselineRef = React.useRef(true);
  const widthCacheRef = React.useRef<{
    byKey: Map<string, number>;
    split: number;
    overflow: number;
  } | undefined>(undefined);
  const [collapsed, setCollapsed] = React.useState<Set<number>>(() => new Set());
  const [overflowOpen, setOverflowOpen] = React.useState(false);

  const measure = React.useCallback(() => {
    const root = toolbarRef.current;
    if (!root || root.clientWidth <= 0) return;

    if (baselineRef.current) {
      const byKey = new Map<string, number>();
      for (const node of root.querySelectorAll<HTMLElement>('[data-toolbar-item]')) {
        const key = node.dataset.toolbarItem;
        if (key) byKey.set(key, node.offsetWidth);
      }
      widthCacheRef.current = {
        byKey,
        split: root.querySelector<HTMLElement>('[data-toolbar-split]')?.offsetWidth ?? DEFAULT_SPLIT_WIDTH,
        overflow: byKey.get(OVERFLOW_KEY) ?? DEFAULT_ITEM_WIDTH,
      };
    }

    const cache = widthCacheRef.current;
    if (!cache) return;
    const { groups, tail } = splitGroups(items);
    const widthOf = (key: string) => cache.byKey.get(key) ?? DEFAULT_ITEM_WIDTH;
    const groupWidths = groups.map((group) => group.reduce((sum, item) => sum + widthOf(item.key), 0));
    const tailWidth = tail.reduce((sum, entry) => (
      'type' in entry
        ? sum + (entry.type === 'split' ? cache.split : 0)
        : sum + widthOf(entry.key)
    ), 0);
    let total = groupWidths.reduce((sum, width) => sum + width, 0)
      + Math.max(0, groups.length - 1) * cache.split
      + tailWidth;
    const fitLimit = Math.max(0, root.clientWidth - cache.overflow - FIT_PADDING);
    const next = new Set<number>();
    const protectedCount = Math.min(protectedTailGroups, groups.length);

    const collapseAt = (index: number) => {
      total -= groupWidths[index] ?? 0;
      if (next.size === 0) total += cache.overflow;
      else total -= cache.split;
      next.add(index);
    };

    if (total > fitLimit && protectedCount > 0) {
      for (let index = groups.length - protectedCount - 1; index >= 0 && total > fitLimit; index -= 1) {
        collapseAt(index);
      }
    }
    for (let index = groups.length - 1; index >= 0 && total > fitLimit; index -= 1) {
      if (!next.has(index)) collapseAt(index);
    }

    baselineRef.current = next.size === 0;
    setCollapsed((previous) => (
      previous.size === next.size && [...previous].every((index) => next.has(index))
        ? previous
        : next
    ));
  }, [items, protectedTailGroups]);

  React.useLayoutEffect(() => {
    // Item changes invalidate the measured responsive-toolbar cache.
    void items;
    widthCacheRef.current = undefined;
    baselineRef.current = true;
    setCollapsed(new Set());
  }, [items]);

  React.useEffect(() => {
    const root = toolbarRef.current;
    if (!root) return;
    if (typeof ResizeObserver === 'undefined') {
      measure();
      return;
    }
    const observer = new ResizeObserver(measure);
    observer.observe(root);
    measure();
    return () => observer.disconnect();
  }, [measure]);

  React.useEffect(() => {
    if (!overflowOpen) return;
    const close = (event: MouseEvent) => {
      const target = event.target as Node;
      if (
        !toolbarRef.current?.contains(target)
        && !overflowMenuRef.current?.contains(target)
      ) setOverflowOpen(false);
    };
    document.addEventListener('mousedown', close);
    return () => document.removeEventListener('mousedown', close);
  }, [overflowOpen]);

  const { groups } = splitGroups(items);
  const visibleGroups = groups.filter((_group, index) => !collapsed.has(index));
  const collapsedGroups = groups.filter((_group, index) => collapsed.has(index));

  const button = (item: ToolbarButton, overflow = false) => {
    const Icon = iconFor(item.key);
    return (
      <button
        key={item.key}
        type="button"
        title={item.label}
        aria-label={item.label}
        data-toolbar-item={item.key}
        className={overflow
          ? 'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm hover:bg-muted'
          : 'inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40'}
        disabled={disabled}
        onMouseDown={(event) => event.preventDefault()}
        onClick={() => {
          setOverflowOpen(false);
          onRun(item.key);
        }}
      >
        <Icon className="size-4 shrink-0" />
        {overflow && <span>{item.label}</span>}
      </button>
    );
  };

  return (
    <div
      ref={toolbarRef}
      className="relative flex min-h-10 shrink-0 items-center gap-0.5 border-b bg-background px-2 py-1"
      data-testid="latex-plugin-toolbar"
    >
      {visibleGroups.map((group, index) => (
        <React.Fragment key={group.map((item) => item.key).join(':')}>
          {index > 0 && <span data-toolbar-split className="mx-1 h-5 w-px shrink-0 bg-border" />}
          {group.map((item) => button(item))}
        </React.Fragment>
      ))}

      {collapsedGroups.length > 0 && (
        <>
          {visibleGroups.length > 0 && <span data-toolbar-split className="mx-1 h-5 w-px shrink-0 bg-border" />}
          <button
            type="button"
            title="More editor commands"
            aria-label="More editor commands"
            data-toolbar-item={OVERFLOW_KEY}
            className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground"
            onMouseDown={(event) => event.preventDefault()}
            onClick={(event) => {
              event.stopPropagation();
              setOverflowOpen((open) => !open);
            }}
          >
            <MoreHorizontal className="size-4" />
          </button>
        </>
      )}

      <span className="flex-1" />

      {overflowOpen && collapsedGroups.length > 0 && (
        <div
          ref={overflowMenuRef}
          className="absolute top-full right-2 z-50 mt-1 max-h-[50vh] min-w-48 overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-lg"
          role="menu"
        >
          {collapsedGroups.map((group, groupIndex) => (
            <React.Fragment key={group.map((item) => item.key).join(':')}>
              {groupIndex > 0 && <div className="my-1 h-px bg-border" />}
              {group.map((item) => button(item, true))}
            </React.Fragment>
          ))}
        </div>
      )}
    </div>
  );
}
