/**
 * Simple context menu component.
 * Shows a menu at a given position with clickable items.
 */

import { editMenuItems } from "./edit-menu.js";

let activeMenu: HTMLElement | null = null;
let dismissListener: ((e: Event) => void) | null = null;
let escapeListener: ((e: KeyboardEvent) => void) | null = null;

export interface MenuItem {
    label: string;
    action: () => void;
    disabled?: boolean;
    separator?: boolean;
    /** Render bold, as the menu's default/primary action (e.g. "Move here"
     *  in the right-drag menu, vs the secondary "Copy here"). */
    emphasized?: boolean;
    /** Native browser tooltip shown on hover (title attribute). Use it
     *  to explain non-obvious side effects of an action — e.g., "skips
     *  Trash, no undo" so the user can tell two near-identical entries
     *  apart without trial-and-error. */
    tooltip?: string;
    /** Nested items. Rendered with a ▸ affordance; opens on hover (and on
     *  click, so touch works). `action` is ignored when this is set — a
     *  parent row only opens its child menu. One level deep is deliberate:
     *  deeper nesting in a context menu is a usability tax, not a feature. */
    submenu?: MenuItem[];
}

/** Open submenu element, if any. Tracked separately from `activeMenu` so the
 *  outside-click dismisser doesn't treat the child as "outside" the parent. */
let activeSubmenu: HTMLElement | null = null;

function closeSubmenu(): void {
    if (activeSubmenu) { activeSubmenu.remove(); activeSubmenu = null; }
}

/** Close any open context menu and remove dismiss listeners */
export function closeContextMenu(): void {
    closeSubmenu();
    if (activeMenu) {
        activeMenu.remove();
        activeMenu = null;
    }
    if (dismissListener) {
        document.removeEventListener("pointerdown", dismissListener, true);
        dismissListener = null;
    }
    if (escapeListener) {
        document.removeEventListener("keydown", escapeListener, true);
        escapeListener = null;
    }
}

/** Render `items` as a child menu anchored to the right of `parentRow`,
 *  flipping to its left when there isn't room. Shares the .ctx-menu styling
 *  so it can't drift from the parent visually. */
function openSubmenu(parentRow: HTMLElement, items: MenuItem[]): void {
    closeSubmenu();
    const sub = document.createElement("div");
    sub.className = "ctx-menu";
    for (const item of items) {
        if (item.separator) {
            const sep = document.createElement("div");
            sep.className = "ctx-sep";
            sub.appendChild(sep);
            continue;
        }
        const el = document.createElement("div");
        el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
        el.textContent = item.label;
        if (item.tooltip) el.title = item.tooltip;
        if (!item.disabled) {
            el.addEventListener("click", () => {
                closeContextMenu();
                item.action();
            });
        }
        sub.appendChild(el);
    }
    document.body.appendChild(sub);
    const anchor = parentRow.getBoundingClientRect();
    const vw = window.visualViewport?.width ?? window.innerWidth;
    const vh = window.visualViewport?.height ?? window.innerHeight;
    const rect = sub.getBoundingClientRect();
    // -2px overlap keeps the pointer inside a menu while travelling from the
    // parent row into the child; a gap would fire the outside-click dismiss.
    let left = anchor.right - 2;
    if (left + rect.width > vw) left = anchor.left - rect.width + 2;
    let top = anchor.top;
    if (top + rect.height > vh) top = vh - rect.height - 4;
    sub.style.left = `${Math.max(4, left)}px`;
    sub.style.top = `${Math.max(4, top)}px`;
    activeSubmenu = sub;
}

export interface ContextMenuOptions {
    /** The element the right-click landed on. Pass it and the menu grows the
     *  standard Cut / Copy / Paste / Select-all block at the top whenever the
     *  target is editable or has text selected.
     *
     *  Replacing the native menu means inheriting its job: every surface that
     *  calls preventDefault owes the user a clipboard. Passing the target here
     *  is how a surface pays that, without each one re-implementing it
     *  (Bob 2026-08-09: "the menu handling should also share common code for
     *  these functions"). Omit it only where a clipboard genuinely can't
     *  apply. */
    editTarget?: EventTarget | Node | null;
    /** A rich editor (TinyMCE/Quill) whose selection the clipboard items
     *  should act on, instead of the DOM target. */
    editEngine?: any;
    /** Surface a refused clipboard action somewhere the user can see it. */
    onEditError?: (msg: string) => void;
}

/** Show a context menu at the given position */
export function showContextMenu(x: number, y: number, items: MenuItem[], opts: ContextMenuOptions = {}): void {
    closeContextMenu();

    if (opts.editTarget !== undefined || opts.editEngine) {
        const edit = editMenuItems(
            { target: opts.editTarget, engine: opts.editEngine },
            { onError: opts.onEditError },
        );
        // Standard block FIRST — it's what a user reaches for by reflex, and a
        // fixed position means the muscle memory holds across surfaces.
        if (edit.length > 0) {
            items = items.length > 0
                ? [...edit, { label: "", action: () => { /* */ }, separator: true }, ...items]
                : edit;
        }
    }

    const menu = document.createElement("div");
    menu.className = "ctx-menu";

    for (const item of items) {
        if (item.separator) {
            const sep = document.createElement("div");
            sep.className = "ctx-sep";
            menu.appendChild(sep);
            continue;
        }
        const el = document.createElement("div");
        el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "") + (item.emphasized ? " ctx-emphasized" : "");
        if (item.emphasized) el.style.fontWeight = "600";
        el.textContent = item.label;
        if (item.tooltip) el.title = item.tooltip;
        if (item.submenu && item.submenu.length > 0 && !item.disabled) {
            // ▸ pushed to the right edge so parent rows read as parent rows.
            el.style.display = "flex";
            el.style.justifyContent = "space-between";
            el.style.gap = "12px";
            const caret = document.createElement("span");
            caret.textContent = "▸";
            caret.style.opacity = "0.6";
            el.appendChild(caret);
            const open = () => openSubmenu(el, item.submenu!);
            el.addEventListener("mouseenter", open);
            el.addEventListener("click", (e) => { e.stopPropagation(); open(); });
            menu.appendChild(el);
            continue;
        }
        // Any non-parent row is hovered ⇒ whatever submenu was open belongs to
        // a different row and should go.
        el.addEventListener("mouseenter", closeSubmenu);
        if (!item.disabled) {
            el.addEventListener("click", () => {
                closeContextMenu();
                item.action();
            });
        }
        menu.appendChild(el);
    }

    menu.style.left = `${x}px`;
    menu.style.top = `${y}px`;
    document.body.appendChild(menu);

    // Keep the menu fully on-screen — clamp to ALL FOUR edges. The old
    // flip-only logic (x − width) went NEGATIVE when the press was near an
    // edge on a narrow screen, so Android long-press menus rendered clipped
    // mid-word off the left side (Bob 2026-07-13). visualViewport tracks
    // the on-screen keyboard; taller-than-screen menus scroll internally.
    const vw = window.visualViewport?.width ?? window.innerWidth;
    const vh = window.visualViewport?.height ?? window.innerHeight;
    let rect = menu.getBoundingClientRect();
    if (rect.height > vh - 8) {
        menu.style.maxHeight = `${vh - 8}px`;
        menu.style.overflowY = "auto";
        rect = menu.getBoundingClientRect();
    }
    let left = x, top = y;
    if (left + rect.width > vw) left = x - rect.width;   // prefer flipping…
    if (top + rect.height > vh) top = y - rect.height;
    left = Math.max(4, Math.min(left, vw - rect.width - 4));  // …then clamp
    top = Math.max(4, Math.min(top, vh - rect.height - 4));
    menu.style.left = `${left}px`;
    menu.style.top = `${top}px`;

    activeMenu = menu;

    // Dismiss on click/tap outside the menu. Uses pointerdown in capture phase
    // so it fires before any child handler and catches both left- and right-clicks.
    // Deferred by one frame so the opening pointerdown doesn't immediately close it.
    requestAnimationFrame(() => {
        dismissListener = (e: Event) => {
            // A click in the SUBMENU is not a click outside — it lives in its
            // own top-level element, so `activeMenu.contains` says otherwise.
            if (activeSubmenu?.contains(e.target as Node)) return;
            if (activeMenu && !activeMenu.contains(e.target as Node)) {
                closeContextMenu();
            }
        };
        document.addEventListener("pointerdown", dismissListener, true);

        escapeListener = (e: KeyboardEvent) => {
            if (e.key === "Escape") {
                e.preventDefault();
                e.stopPropagation();
                closeContextMenu();
            }
        };
        document.addEventListener("keydown", escapeListener, true);
    });
}

// Scroll anywhere closes the menu (capture phase so nested scrollers trigger it)
document.addEventListener("scroll", closeContextMenu, true);
// A new right-click that opens a different menu goes through showContextMenu→closeContextMenu
document.addEventListener("contextmenu", () => { /* handled by showContextMenu */ });
// Iframe pointerdown forwarded from preview/compose iframes — needed because
// iframe events don't bubble to the parent document, so the dismissListener
// (which hooks document.pointerdown) doesn't see clicks INSIDE iframes. The
// message-viewer's inline iframe script posts {type:"iframePointerDown"} on
// every non-right-click pointerdown.
window.addEventListener("message", (e: MessageEvent) => {
    if (e.data && (e.data as any).type === "iframePointerDown") closeContextMenu();
});
