import { ModalElement, type UIModal, type UIModalStyle } from "./UIModal"
import { Presentable } from "./presentable"
import { device } from "../runtime/device"
import { buildUI, type BoundingClientRect, type ChildrenFn, type UIChildArg, type UINodeChild } from "./UINode"

export type UIPopoverStyle = UIModalStyle

/** What a popover can anchor to: any element (it reads `getBoundingClientRect()`), or a raw
 *  `{x, y}` point — a long-press context menu anchors to the touch position. */
export type UIPopoverAnchor = { getBoundingClientRect(): BoundingClientRect | null } | { x: number, y: number }

const POPOVER_ANIM_MS = 120
const ANCHOR_GAP = 4       // between the anchor's edge and the popover
const VIEWPORT_MARGIN = 8  // minimum distance kept to every screen edge

/**
 * An anchored menu: a `UIModal` whose scrim is transparent (invisible but still intercepting —
 * an outside tap dismisses, and nothing underneath can scroll while it's open) and whose
 * position comes from the anchor passed to `show(anchor)`: below the anchor's left edge,
 * flipped above when there's no room, clamped into the viewport. Position is computed once per
 * show — anchored popovers don't follow their anchor. Backs dropdowns, context menus, and
 * tooltips; create it **once** at module scope and reuse it.
 */
export interface UIPopover extends UIModal {
  /** Position next to `anchor` (an element or an `{x, y}` point), attach to
   *  `Presentable.current` (unless an owner was set via `attachTo`), and play the entrance
   *  transition. Without an anchor the popover shows wherever its own style puts it. */
  show(anchor?: UIPopoverAnchor): void,
}

export class PopoverElement extends ModalElement {
  private _anchorRect?: { left: number, top: number, right: number, bottom: number }
  private _placedLeft?: number
  private _placedTop?: number

  constructor(style: UIPopoverStyle, children: UINodeChild[] | ChildrenFn) {
    super({ overlayColor: "transparent", ...style }, children)
    this.transition({ opacity: 0, duration: POPOVER_ANIM_MS })
    // The flip/clamp decision needs the popover's measured size, which exists only after the
    // mount lays it out — so placement is corrected on layout, inside the entrance fade.
    this.onLayout(({ width, height }) => this._place(width, height))
  }

  show(anchor?: UIPopoverAnchor): void {
    if (anchor) {
      const r = "getBoundingClientRect" in anchor
        ? anchor.getBoundingClientRect()
        : { left: anchor.x, top: anchor.y, right: anchor.x, bottom: anchor.y }
      if (r) {
        this._anchorRect = { left: r.left, top: r.top, right: r.right, bottom: r.bottom }
        this._placedLeft = undefined
        this._placedTop = undefined
        // Preliminary spot (below, left-aligned) so the first paint is already close; _place
        // refines it once the size is known.
        this.style({ left: r.left, top: r.bottom + ANCHOR_GAP })
      }
    } else {
      this._anchorRect = undefined   // unanchored show: the user's own style owns the position
    }
    // A menu belongs to the page it opened on: it hides with it and rides its transition. An
    // explicit attachTo() owner (e.g. a scene HUD's popover) is respected.
    if (!this._owner) this.attachTo(Presentable.current)
    super.show()
  }

  private _place(width: number, height: number): void {
    const anchor = this._anchorRect
    if (!anchor || !this._open) return
    const displayWidth = device.width, displayHeight = device.height   // 0 on hosts without a size
    let left = anchor.left
    let top = anchor.bottom + ANCHOR_GAP
    if (displayWidth > 0) {
      left = Math.min(left, displayWidth - width - VIEWPORT_MARGIN)
      left = Math.max(left, VIEWPORT_MARGIN)
    }
    if (displayHeight > 0 && top + height > displayHeight - VIEWPORT_MARGIN) {
      const above = anchor.top - ANCHOR_GAP - height
      top = above >= VIEWPORT_MARGIN ? above : Math.max(VIEWPORT_MARGIN, displayHeight - VIEWPORT_MARGIN - height)
    }
    // Idempotence guard: hosts re-fire onLayout on recalc, and writing the same position would
    // trigger another recalc — only write when the target actually moved.
    if (this._placedLeft !== undefined && Math.abs(left - this._placedLeft) < 0.5
      && this._placedTop !== undefined && Math.abs(top - this._placedTop) < 0.5) return
    this._placedLeft = left
    this._placedTop = top
    this.style({ left, top })
  }
}

const makePopover = (style: any, children: any) => new PopoverElement(style, children)

/** Create an anchored popover (see {@link UIPopover}) — create once at module scope, then
 *  `show(anchor)` next to an element or `{x, y}` point. Same argument forms as `UIColumn`. */
export function UIPopover(...children: UIChildArg[]): UIPopover;
export function UIPopover(children: UINodeChild[] | ChildrenFn): UIPopover;
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
export function UIPopover(style: UIPopoverStyle, children?: UINodeChild[] | ChildrenFn): UIPopover;
export function UIPopover(...args: any[]): UIPopover {
  return buildUI(args, null, makePopover)
}
