/**
 * Non-side-effect class export for `<popover-ui>`.
 *
 * 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/popover`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * <popover-ui trigger="click" placement="bottom-start">
 *   <button-ui slot="trigger" text="Open"></button-ui>
 *   <div slot="content">Popover content here</div>
 * </popover-ui>
 *
 * Built on the web platform:
 *   • Popover API — content has popover="manual"; showPopover() promotes it
 *     to the top layer (no z-index, no overflow clipping, ESC bubbling handled).
 *   • CSS Anchor Positioning — used natively when supported, JS fallback otherwise
 *     (see @core/anchor.js).
 *
 * Triggers: click | hover | manual. ESC closes click/manual popovers.
 */

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

const HOVER_CLOSE_DELAY = 120;

export class UIPopover extends UIElement {
  static properties = {
    placement: { type: String,  default: 'bottom', reflect: true },
    gap:       { type: Number,  default: 4, reflect: true },
    open:      { type: Boolean, default: false, reflect: true },
    trigger:   { type: String,  default: 'click', reflect: true },
  };

  static template = () => null;

  #anchorCleanup = null;
  #bound = false;
  #hoverTimer = null;
  #content = null;
  #rafId = null;

  connected() {
    const content = this.querySelector('[slot="content"]');
    if (content) {
      if (!content.hasAttribute('popover')) content.setAttribute('popover', 'manual');
      // Sync state when the browser toggles the popover (e.g. light-dismiss, ESC on auto, programmatic).
      content.addEventListener('toggle', this.#onToggle);
      this.#content = content;
    }

    if (!this.#bound) {
      this.#bound = true;

      if (this.trigger === 'click') {
        this.addEventListener('click', this.#onClick);
      }

      if (this.trigger === 'hover') {
        this.addEventListener('pointerenter', this.#onEnter);
        this.addEventListener('pointerleave', this.#onLeave);
        // Also let the pointer cross into the content without closing.
        if (content) {
          content.addEventListener('pointerenter', this.#onEnter);
          content.addEventListener('pointerleave', this.#onLeave);
        }
      }
    }
  }

  render() {
    if (this.open) this.#show();
    else this.#hide();
  }

  // ── Public API ──
  show()   { this.open = true; }
  hide()   { this.open = false; }
  toggle() { this.open = !this.open; }

  // ── Internal ──

  #show() {
    const trigger = this.querySelector('[slot="trigger"]');
    const content = this.#content ?? this.querySelector('[slot="content"]');
    if (!trigger || !content) return;

    if (!content.matches(':popover-open')) {
      // try/catch covers test envs (happy-dom) without the API. Known iOS
      // Safari quirks at the supported floor (light-dismiss broken on
      // 17.0–18.2, virtual-keyboard-on-input persists past 18.3) are
      // documented in docs/BROWSER-COMPAT.md §3a.
      try { content.showPopover(); } catch { /* popover API unavailable — anchor positioning still runs */ }
    }

    this.#anchorCleanup?.();
    this.#anchorCleanup = anchorPopover(trigger, content, {
      placement: this.placement,
      gap: this.gap,
    });

    if (this.trigger === 'click' || this.trigger === 'manual') {
      // Defer to next frame so the opening click doesn't trigger outside-dismiss.
      this.#rafId = requestAnimationFrame(() => {
        this.#rafId = null;
        document.addEventListener('pointerdown', this.#onOutside);
        document.addEventListener('keydown', this.#onKey);
      });
    }
  }

  #hide() {
    this.#anchorCleanup?.();
    this.#anchorCleanup = null;

    const content = this.#content ?? this.querySelector('[slot="content"]');
    if (content?.matches(':popover-open')) {
      try { content.hidePopover(); } catch { /* popover API unavailable */ }
    }

    if (this.#rafId != null) {
      cancelAnimationFrame(this.#rafId);
      this.#rafId = null;
    }
    document.removeEventListener('pointerdown', this.#onOutside);
    document.removeEventListener('keydown', this.#onKey);
  }

  // ── Events ──

  #onClick = (e) => {
    const trigger = this.querySelector('[slot="trigger"]');
    if (trigger && (e.target === trigger || trigger.contains(e.target))) {
      this.open = !this.open;
    }
  };

  #onOutside = (e) => {
    if (!this.open) return;
    const content = this.#content;
    // Click inside host or inside the (top-layer) content: keep open.
    if (this.contains(e.target) || content?.contains(e.target)) return;
    this.open = false;
  };

  #onKey = (e) => {
    if (e.key === 'Escape' && this.open) {
      e.stopPropagation();
      this.open = false;
    }
  };

  #onEnter = () => {
    clearTimeout(this.#hoverTimer);
    this.open = true;
  };

  #onLeave = () => {
    clearTimeout(this.#hoverTimer);
    this.#hoverTimer = setTimeout(() => { this.open = false; }, HOVER_CLOSE_DELAY);
  };

  #onToggle = (e) => {
    // Reflect browser-driven state changes (e.g. popover-auto light-dismiss).
    const nowOpen = e.newState === 'open';
    if (nowOpen !== this.open) this.open = nowOpen;
  };

  disconnected() {
    clearTimeout(this.#hoverTimer);
    this.#hide();
    if (this.trigger === 'click') {
      this.removeEventListener('click', this.#onClick);
    }
    if (this.trigger === 'hover') {
      this.removeEventListener('pointerenter', this.#onEnter);
      this.removeEventListener('pointerleave', this.#onLeave);
      if (this.#content) {
        this.#content.removeEventListener('pointerenter', this.#onEnter);
        this.#content.removeEventListener('pointerleave', this.#onLeave);
      }
    }
    this.#content?.removeEventListener('toggle', this.#onToggle);
    this.#content = null;
    this.#bound = false;
  }
}
