// shared/context-menu.js — generic context-menu framework exposed on
// window.zddc.menu. Built so every ZDDC tool can drop a right-click
// menu (or any programmatically-opened menu) onto its UI without
// shipping its own implementation.
//
// API:
//   window.zddc.menu.open({ x, y, items, context })
//   window.zddc.menu.close()
//
// `items` is an array (or a function returning an array, evaluated
// against `context` at open-time). Each entry is one of:
//   { label, action, icon?, accel?, disabled?, visible?, danger?, tooltip? }
//       — a normal menu item; `action(ctx)` fires on click/Enter.
//       `tooltip` (string or fn(ctx)) sets the row's title attribute —
//       useful for explaining WHY a disabled item is unavailable
//       ("You don't have write access here", etc.).
//   { label, checked, action, ... }
//       — toggle item; `checked` may be a bool or a fn(ctx). Renders
//         a ✓ in the gutter when truthy.
//   { label, items, ... }
//       — submenu; `items` may itself be an array or fn(ctx).
//   { separator: true }
//       — horizontal divider. Leading/trailing/duplicate separators
//         are collapsed automatically so callers can build items
//         conditionally without managing dividers.
//
// Any of `label`, `checked`, `visible`, `disabled`, `tooltip`, and
// `items` may be a function — each is invoked with the context object
// so callers can render fully context-aware menus from a single
// declarative config.
//
// Keyboard: ArrowUp/Down move within a menu, ArrowRight opens a
// submenu, ArrowLeft / Escape backs up one level (or closes if
// already at the root), Enter / Space activates. Click-outside,
// window blur, scroll, and resize all dismiss.
(function () {
    'use strict';

    if (!window.zddc) window.zddc = {};
    if (window.zddc.menu) return;

    var SUBMENU_HOVER_MS = 180;

    // Open menu stack — index 0 is the root, deeper entries are
    // nested submenus. Each frame: { el, depth, parentRow? }.
    var stack = [];
    var rootContext = null;
    var submenuTimer = null;

    function resolve(val, ctx) {
        return typeof val === 'function' ? val(ctx) : val;
    }

    function close() {
        if (submenuTimer) { clearTimeout(submenuTimer); submenuTimer = null; }
        for (var i = 0; i < stack.length; i++) {
            var fr = stack[i];
            if (fr.el && fr.el.parentNode) fr.el.parentNode.removeChild(fr.el);
        }
        stack = [];
        rootContext = null;
        document.removeEventListener('mousedown', onDocMouseDown, true);
        document.removeEventListener('keydown', onDocKeyDown, true);
        // blur is bound WITHOUT capture so we only react to the window
        // itself losing focus — capturing would also fire when any
        // inner element blurs (which happens every time the user moves
        // the mouse between menu rows, since hover focuses the row).
        window.removeEventListener('blur', close);
        window.removeEventListener('resize', close, true);
        window.removeEventListener('scroll', onDocScroll, true);
    }

    function open(opts) {
        opts = opts || {};
        close();
        rootContext = opts.context || {};
        var items = resolve(opts.items, rootContext) || [];
        var el = buildMenu(items, rootContext, 0);
        document.body.appendChild(el);
        position(el, opts.x || 0, opts.y || 0, null);
        stack.push({ el: el, depth: 0 });

        document.addEventListener('mousedown', onDocMouseDown, true);
        document.addEventListener('keydown', onDocKeyDown, true);
        window.addEventListener('blur', close);
        window.addEventListener('resize', close, true);
        window.addEventListener('scroll', onDocScroll, true);

        focusFirst(el);
    }

    // ── Building ─────────────────────────────────────────────────────────

    function collapseSeparators(items) {
        var out = [];
        for (var i = 0; i < items.length; i++) {
            var it = items[i];
            if (it && it.separator) {
                if (out.length === 0) continue;
                if (out[out.length - 1].separator) continue;
                out.push(it);
            } else if (it) {
                out.push(it);
            }
        }
        while (out.length && out[out.length - 1].separator) out.pop();
        return out;
    }

    function buildMenu(items, ctx, depth) {
        var menu = document.createElement('div');
        menu.className = 'zddc-menu';
        menu.setAttribute('role', 'menu');
        menu.dataset.depth = String(depth);
        // Suppress the native context menu over our own menu.
        menu.addEventListener('contextmenu', function (e) { e.preventDefault(); });

        var filtered = items.filter(function (it) {
            if (!it) return false;
            if (it.separator) return true;
            if ('visible' in it && !resolve(it.visible, ctx)) return false;
            return true;
        });
        var pruned = collapseSeparators(filtered);

        for (var i = 0; i < pruned.length; i++) {
            menu.appendChild(buildRow(pruned[i], ctx, depth));
        }
        return menu;
    }

    function buildRow(item, ctx, depth) {
        if (item.separator) {
            var sep = document.createElement('div');
            sep.className = 'zddc-menu__sep';
            sep.setAttribute('role', 'separator');
            return sep;
        }

        var hasSub = !!item.items;
        var isToggle = ('checked' in item);
        var disabled = 'disabled' in item ? !!resolve(item.disabled, ctx) : false;

        var row = document.createElement('div');
        row.className = 'zddc-menu__item';
        if (item.danger) row.classList.add('zddc-menu__item--danger');
        if (hasSub) row.classList.add('zddc-menu__item--has-sub');
        if (disabled) {
            row.classList.add('is-disabled');
            row.setAttribute('aria-disabled', 'true');
        }
        if ('tooltip' in item) {
            var tip = resolve(item.tooltip, ctx);
            if (tip) row.title = String(tip);
        }
        row.setAttribute('role',
            hasSub ? 'menuitem'
                   : (isToggle ? 'menuitemcheckbox' : 'menuitem'));
        row.tabIndex = -1;

        // Check gutter — present on every row so columns align.
        var check = document.createElement('span');
        check.className = 'zddc-menu__check';
        if (isToggle) {
            var on = !!resolve(item.checked, ctx);
            if (on) {
                check.textContent = '✓';
                row.classList.add('is-checked');
                row.setAttribute('aria-checked', 'true');
            } else {
                row.setAttribute('aria-checked', 'false');
            }
        }
        row.appendChild(check);

        // Icon column.
        var icon = document.createElement('span');
        icon.className = 'zddc-menu__icon';
        if (item.icon) icon.textContent = item.icon;
        row.appendChild(icon);

        // Label.
        var label = document.createElement('span');
        label.className = 'zddc-menu__label';
        label.textContent = String(resolve(item.label, ctx) || '');
        row.appendChild(label);

        // Accelerator hint (visual only; no binding).
        var accel = document.createElement('span');
        accel.className = 'zddc-menu__accel';
        if (item.accel) accel.textContent = item.accel;
        row.appendChild(accel);

        // Submenu arrow.
        var arrow = document.createElement('span');
        arrow.className = 'zddc-menu__arrow';
        if (hasSub) arrow.textContent = '▸';
        row.appendChild(arrow);

        if (!disabled) {
            row.addEventListener('mouseenter', function () {
                // Hovering any row in a menu collapses deeper menus
                // (so traversing siblings closes a previously-opened
                // submenu) and re-focuses this row for keyboard nav.
                closeBelow(depth);
                if (submenuTimer) { clearTimeout(submenuTimer); submenuTimer = null; }
                if (hasSub) {
                    submenuTimer = setTimeout(function () {
                        openSubmenu(row, item, ctx, depth + 1, false);
                    }, SUBMENU_HOVER_MS);
                }
                try { row.focus({ preventScroll: true }); } catch (_e) { row.focus(); }
            });
            row.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                if (submenuTimer) { clearTimeout(submenuTimer); submenuTimer = null; }
                if (hasSub) {
                    openSubmenu(row, item, ctx, depth + 1, true);
                    return;
                }
                activate(item, ctx);
            });
        }
        return row;
    }

    function activate(item, ctx) {
        try {
            if (typeof item.action === 'function') item.action(ctx);
        } finally {
            close();
        }
    }

    function openSubmenu(parentRow, parentItem, ctx, depth, takeFocus) {
        closeBelow(depth - 1);
        var items = resolve(parentItem.items, ctx) || [];
        var el = buildMenu(items, ctx, depth);
        document.body.appendChild(el);
        var rect = parentRow.getBoundingClientRect();
        // Slight overlap so pointer-cross feels continuous.
        position(el, rect.right - 2, rect.top - 4, parentRow);
        stack.push({ el: el, depth: depth, parentRow: parentRow });
        if (takeFocus) focusFirst(el);
    }

    function closeBelow(depth) {
        while (stack.length && stack[stack.length - 1].depth > depth) {
            var fr = stack.pop();
            if (fr.el && fr.el.parentNode) fr.el.parentNode.removeChild(fr.el);
        }
    }

    // ── Positioning ──────────────────────────────────────────────────────

    function position(el, x, y, parentRow) {
        // Fixed so we ignore document scroll; measure after layout.
        el.style.position = 'fixed';
        el.style.left = '0px';
        el.style.top = '0px';
        el.style.visibility = 'hidden';
        var rect = el.getBoundingClientRect();
        var w = rect.width;
        var h = rect.height;
        var vw = window.innerWidth;
        var vh = window.innerHeight;

        var leftX = x;
        if (leftX + w > vw - 4) {
            if (parentRow) {
                var pr = parentRow.getBoundingClientRect();
                leftX = pr.left - w + 2;     // flip submenu to the left
            } else {
                leftX = Math.max(4, x - w);  // flip root menu left of cursor
            }
        }
        if (leftX < 4) leftX = 4;

        var topY = y;
        if (topY + h > vh - 4) topY = Math.max(4, vh - h - 4);
        if (topY < 4) topY = 4;

        el.style.left = leftX + 'px';
        el.style.top = topY + 'px';
        el.style.visibility = '';
    }

    // ── Focus + keyboard ─────────────────────────────────────────────────

    function focusable(menuEl) {
        return Array.prototype.slice.call(
            menuEl.querySelectorAll('.zddc-menu__item:not(.is-disabled)'));
    }

    function focusFirst(menuEl) {
        var items = focusable(menuEl);
        if (items.length) {
            try { items[0].focus({ preventScroll: true }); }
            catch (_e) { items[0].focus(); }
        }
    }

    function onDocMouseDown(e) {
        for (var i = 0; i < stack.length; i++) {
            if (stack[i].el.contains(e.target)) return;
        }
        close();
    }

    // Scroll listener uses capture so scrolls inside any element (the
    // tree pane, the document, etc.) dismiss the menu — its position
    // is fixed and would otherwise hang over stale content. Scrolls
    // that originate inside the menu itself (a future tall submenu)
    // are ignored.
    function onDocScroll(e) {
        var t = e.target;
        for (var i = 0; i < stack.length; i++) {
            if (stack[i].el === t || (t && t.nodeType === 1 && stack[i].el.contains(t))) {
                return;
            }
        }
        close();
    }

    function onDocKeyDown(e) {
        if (!stack.length) return;
        var top = stack[stack.length - 1];
        var items = focusable(top.el);
        var active = document.activeElement;
        var idx = items.indexOf(active);

        switch (e.key) {
            case 'Escape':
                e.preventDefault();
                if (stack.length > 1) {
                    var fr = stack.pop();
                    if (fr.el.parentNode) fr.el.parentNode.removeChild(fr.el);
                    if (fr.parentRow) fr.parentRow.focus();
                } else {
                    close();
                }
                return;
            case 'ArrowDown':
                e.preventDefault();
                if (!items.length) return;
                items[idx < 0 ? 0 : (idx + 1) % items.length].focus();
                return;
            case 'ArrowUp':
                e.preventDefault();
                if (!items.length) return;
                items[idx < 0 ? items.length - 1
                              : (idx - 1 + items.length) % items.length].focus();
                return;
            case 'Home':
                e.preventDefault();
                if (items.length) items[0].focus();
                return;
            case 'End':
                e.preventDefault();
                if (items.length) items[items.length - 1].focus();
                return;
            case 'ArrowRight':
                if (active && active.classList.contains('zddc-menu__item--has-sub')) {
                    e.preventDefault();
                    active.click();
                }
                return;
            case 'ArrowLeft':
                if (stack.length > 1) {
                    e.preventDefault();
                    var fr2 = stack.pop();
                    if (fr2.el.parentNode) fr2.el.parentNode.removeChild(fr2.el);
                    if (fr2.parentRow) fr2.parentRow.focus();
                }
                return;
            case 'Enter':
            case ' ':
                if (active) {
                    e.preventDefault();
                    active.click();
                }
                return;
        }
    }

    window.zddc.menu = { open: open, close: close };
})();
