/**
 * Non-side-effect class export for `<toolbar-ui>` (+ co-located primitives).
 *
 * Importing this file gives you the class(es) without auto-registering the tag.
 * Useful for test isolation, subclassing with tag-name override, or selective
 * composition.
 *
 * The auto-register path stays at `@adia-ai/web-components/components/toolbar`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * <toolbar-ui>
 *   <toolbar-group-ui>
 *     <button-ui icon="text-bold" variant="ghost" size="sm"></button-ui>
 *     <button-ui icon="text-italic" variant="ghost" size="sm"></button-ui>
 *   </toolbar-group-ui>
 *   <divider-ui vertical></divider-ui>
 *   <toolbar-group-ui>
 *     <button-ui icon="text-align-left" variant="ghost" size="sm"></button-ui>
 *   </toolbar-group-ui>
 * </toolbar-ui>
 *
 * Horizontal action bar with automatic overflow:
 *   • ResizeObserver measures available width.
 *   • Items that don't fit are moved (from the end) into a spillover popover.
 *   • `toolbar-group-ui` wraps siblings so they move as an atomic unit
 *     (no half-group split between visible bar and overflow menu).
 *   • Trailing dividers are trimmed. Dividers are not moved to the menu.
 *
 * Platform: Popover API (top-layer) + CSS Anchor Positioning via @core/anchor.js.
 */

import { UIElement } from '../../core/element.js';
import { anchorPopover } from '../../core/anchor.js';

const SPILLOVER_ATTR = 'data-toolbar-spillover';
const SPILLOVER_MENU_ATTR = 'data-toolbar-spillover-menu';

/* Headroom kept free at the right edge before an item starts spilling over.
   A non-zero value makes overflow more responsive — items move to the menu
   while there's still breathing room, instead of waiting until they actually
   touch the toolbar's right edge. */
const OVERFLOW_BUFFER = 8;

const SPILLOVER_LABEL_FLAG = 'data-toolbar-spillover-label';

function isDivider(el) {
  return el?.tagName && el.tagName.toLowerCase() === 'divider-ui';
}

function humanizeIcon(name) {
  if (!name) return '';
  return name
    .split('-')
    .filter(Boolean)
    .map(w => w[0].toUpperCase() + w.slice(1))
    .join(' ');
}

function applySpilloverLabels(root) {
  const buttons = root.matches?.('button-ui') ? [root] : Array.from(root.querySelectorAll?.('button-ui') || []);
  for (const btn of buttons) {
    if (btn.hasAttribute(SPILLOVER_LABEL_FLAG)) continue;
    if (btn.getAttribute('text')) continue;
    const label = btn.getAttribute('aria-label') || humanizeIcon(btn.getAttribute('icon'));
    if (!label) continue;
    btn.setAttribute(SPILLOVER_LABEL_FLAG, '');
    btn.setAttribute('text', label);
  }
}

function stripSpilloverLabels(root) {
  const buttons = root.matches?.('button-ui') ? [root] : Array.from(root.querySelectorAll?.('button-ui') || []);
  for (const btn of buttons) {
    if (!btn.hasAttribute(SPILLOVER_LABEL_FLAG)) continue;
    btn.removeAttribute(SPILLOVER_LABEL_FLAG);
    btn.removeAttribute('text');
  }
}

function outerWidth(el) {
  const r = el.getBoundingClientRect();
  const cs = getComputedStyle(el);
  const ml = parseFloat(cs.marginLeft) || 0;
  const mr = parseFloat(cs.marginRight) || 0;
  return r.width + ml + mr;
}

export class UIToolbar extends UIElement {
  static properties = {
    gap:      { type: String,  default: 'sm', reflect: true },
    align:    { type: String,  default: 'start', reflect: true },
    overflow: { type: String,  default: 'menu', reflect: true },
    // yaml documented reflect:true since v1 — closing the gap so
    // el.bordered = true also updates [bordered] CSS state.
    bordered: { type: Boolean, default: false, reflect: true },
  };

  static template = () => null;

  #ro = null;
  #mo = null;
  #trigger = null;
  #popover = null;
  #spilloverCleanup = null;
  #measuring = false;
  #reflowQueued = false;
  #reflowRaf = null;
  #rafId = null;

  connected() {
    this.setAttribute('role', 'toolbar');
    this.#ensureSpillover();
    this.#ro = new ResizeObserver(() => this.#queueReflow());
    this.#ro.observe(this);
    this.#mo = new MutationObserver(() => {
      if (this.#measuring) return;
      this.#queueReflow();
    });
    this.#moObserve();
    this.#queueReflow();
  }

  disconnected() {
    if (this.#reflowRaf != null) {
      cancelAnimationFrame(this.#reflowRaf);
      this.#reflowRaf = null;
    }
    this.#reflowQueued = false;
    this.#ro?.disconnect();
    this.#mo?.disconnect();
    this.#closeSpillover();
    this.#ro = null;
    this.#mo = null;
    this.#trigger = null;
    this.#popover = null;
  }

  // ── Spillover infrastructure ──

  #ensureSpillover() {
    if (this.#trigger) return;

    const trigger = document.createElement('button-ui');
    trigger.setAttribute('icon', 'dots-three');
    trigger.setAttribute('variant', 'ghost');
    trigger.setAttribute('size', 'sm');
    trigger.setAttribute(SPILLOVER_ATTR, '');
    trigger.setAttribute('aria-label', 'More actions');
    trigger.setAttribute('aria-haspopup', 'menu');
    trigger.hidden = true;

    const popover = document.createElement('div');
    popover.setAttribute(SPILLOVER_MENU_ATTR, '');
    popover.setAttribute('popover', 'manual');
    popover.setAttribute('role', 'menu');

    trigger.addEventListener('click', this.#onTriggerClick);
    popover.addEventListener('click', this.#onMenuClick);
    popover.addEventListener('toggle', this.#onToggle);

    this.appendChild(trigger);
    this.appendChild(popover);

    this.#trigger = trigger;
    this.#popover = popover;
  }

  #openSpillover() {
    const menu = this.#popover;
    if (!menu || menu.matches(':popover-open')) return;
    menu.showPopover?.();
    this.#spilloverCleanup?.();
    this.#spilloverCleanup = anchorPopover(this.#trigger, menu, {
      placement: 'bottom-end',
      gap: 4,
    });
    this.#rafId = requestAnimationFrame(() => {
      this.#rafId = null;
      document.addEventListener('pointerdown', this.#onOutside, true);
      document.addEventListener('keydown', this.#onKey, true);
    });
  }

  #closeSpillover() {
    this.#spilloverCleanup?.();
    this.#spilloverCleanup = null;
    if (this.#popover?.matches(':popover-open')) this.#popover.hidePopover?.();
    if (this.#rafId != null) {
      cancelAnimationFrame(this.#rafId);
      this.#rafId = null;
    }
    document.removeEventListener('pointerdown', this.#onOutside, true);
    document.removeEventListener('keydown', this.#onKey, true);
  }

  #onTriggerClick = (e) => {
    e.stopPropagation();
    if (this.#popover?.matches(':popover-open')) this.#closeSpillover();
    else this.#openSpillover();
  };

  #onMenuClick = (e) => {
    // Only auto-close after a "terminal" action (button-ui press). Composite
    // controls inside the menu — select-ui, segmented-ui, etc. — manage their
    // own state; clicking them must not dismiss the spillover.
    const btn = e.target.closest('button-ui');
    if (!btn) return;
    if (btn.matches(`[${SPILLOVER_ATTR}]`)) return;
    queueMicrotask(() => this.#closeSpillover());
  };

  #onOutside = (e) => {
    if (this.#trigger?.contains(e.target)) return;
    if (this.#popover?.contains(e.target)) return;
    // Ignore clicks inside any other open popover (e.g., a select-ui dropdown
    // launched from a select inside our spillover, which lives in the top
    // layer outside the spillover popover's DOM subtree).
    const path = e.composedPath?.() || [];
    for (const node of path) {
      if (node === document) break;
      if (node?.nodeType === 1 && node.hasAttribute?.('popover')) return;
    }
    this.#closeSpillover();
  };

  #onKey = (e) => {
    if (e.key === 'Escape') {
      e.stopPropagation();
      this.#closeSpillover();
      this.#trigger?.focus?.({ preventScroll: true });
    }
  };

  #onToggle = (e) => {
    if (e.newState === 'closed') {
      // Keep listeners cleaned up if closed externally.
      this.#spilloverCleanup?.();
      this.#spilloverCleanup = null;
      if (this.#rafId != null) {
        cancelAnimationFrame(this.#rafId);
        this.#rafId = null;
      }
      document.removeEventListener('pointerdown', this.#onOutside, true);
      document.removeEventListener('keydown', this.#onKey, true);
    }
  };

  // ── Reflow / overflow ──

  #queueReflow() {
    if (this.#reflowQueued) return;
    this.#reflowQueued = true;
    this.#reflowRaf = requestAnimationFrame(() => {
      this.#reflowRaf = null;
      this.#reflowQueued = false;
      if (!this.isConnected) return;
      this.#reflow();
    });
  }

  #moObserve() {
    this.#mo?.observe(this, { childList: true });
  }

  #moPause() {
    // MutationObserver delivers records asynchronously, so the #measuring
    // flag alone doesn't stop self-triggered reflows — by the time the
    // observer's microtask fires, the flag has already been cleared. We must
    // physically disconnect during reflow, flush any pending records the
    // observer had already queued, then reconnect on the next microtask.
    if (!this.#mo) return;
    this.#mo.disconnect();
    this.#mo.takeRecords();
  }

  #reflow() {
    if (!this.isConnected) return;
    const trigger = this.#trigger;
    const popover = this.#popover;
    if (!trigger || !popover) return;

    this.#measuring = true;
    this.#moPause();
    try {
      // 1. Move everything from the popover back into the toolbar (before the trigger).
      //    Strip injected spillover labels so the buttons measure as icon-only again.
      while (popover.firstChild) {
        stripSpilloverLabels(popover.firstChild);
        this.insertBefore(popover.firstChild, trigger);
      }

      // Stamp the original DOM order on each item the first time we see it.
      // (Items moved to the popover and back must restore their original slot —
      // otherwise dividers end up clumped next to dividers and groups split.)
      let idx = 0;
      // wrapper-trap-ok: stamps original DOM order on toolbar items for overflow tracking; toolbar children are always static literal buttons/groups (dynamic toolbars not documented)
      for (const el of this.children) {
        if (el === trigger || el === popover) continue;
        if (el.dataset.toolbarOrder == null) el.dataset.toolbarOrder = String(idx);
        idx += 1;
      }

      // Sort items back into their original order before measurement.
      const ordered = Array.from(this.children)
        .filter(c => c !== trigger && c !== popover)
        .sort((a, b) => Number(a.dataset.toolbarOrder) - Number(b.dataset.toolbarOrder));
      for (const el of ordered) this.insertBefore(el, trigger);

      const all = ordered;
      for (const el of all) el.hidden = false;
      trigger.hidden = true;

      // 2. Measure.
      const style = getComputedStyle(this);
      const padL = parseFloat(style.paddingLeft) || 0;
      const padR = parseFloat(style.paddingRight) || 0;
      const gap = parseFloat(style.columnGap || style.gap) || 0;
      const available = this.clientWidth - padL - padR;

      // Include horizontal margins — components like divider-ui add their own
      // margin-inline which getBoundingClientRect() does NOT include but flex
      // layout DOES count toward the row width.
      const widths = all.map(el => outerWidth(el));
      const total = widths.reduce((a, w) => a + w, 0) + Math.max(0, all.length - 1) * gap;

      // Reserve OVERFLOW_BUFFER worth of headroom — start spilling sooner
      // rather than waiting until items literally touch the right edge.
      const fitsCleanly = total <= available - OVERFLOW_BUFFER;

      if (this.overflow === 'none' || fitsCleanly) {
        return;
      }

      // 3. Need to move some items out. Reserve trigger width + buffer.
      trigger.hidden = false;
      const triggerW = outerWidth(trigger);

      let fitIdx = all.length;
      let running = total;
      // Drop items from the end (and subtract their width + preceding gap)
      // until visible items + trigger fit with buffer headroom.
      while (fitIdx > 0) {
        const wWithTrigger = running + gap + triggerW;
        if (wWithTrigger <= available - OVERFLOW_BUFFER) break;
        fitIdx -= 1;
        running -= widths[fitIdx];
        if (fitIdx > 0) running -= gap;
      }

      // 4. Trim trailing dividers from visible section.
      while (fitIdx > 0 && isDivider(all[fitIdx - 1])) fitIdx -= 1;

      // 5. Move overflowed items into popover (skip dividers entirely).
      const toMove = all.slice(fitIdx);
      for (const el of toMove) {
        if (isDivider(el)) {
          el.hidden = true;
          continue;
        }
        // Inject derived labels on icon-only buttons so the spillover row
        // shows "Bold" instead of just an unlabeled icon.
        applySpilloverLabels(el);
        popover.appendChild(el);
      }

      // 6. If nothing actually ended up in the popover (e.g. only a divider overflowed),
      //    hide the trigger again.
      if (!popover.firstElementChild) {
        trigger.hidden = true;
      }
    } finally {
      this.#measuring = false;
      // Drop any mutation records our reflow produced before reconnecting,
      // otherwise they'd fire as soon as we observe() again and loop forever.
      queueMicrotask(() => {
        this.#mo?.takeRecords();
        this.#moObserve();
      });
    }
  }
}

export class UIToolbarGroup extends UIElement {
  static template = () => null;
  connected() {
    this.setAttribute('role', 'group');
  }
}
