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

/**
 * <spinner-ui size="md" variant="arc" tone="current" label="Loading"></spinner-ui>
 * <spinner-ui variant="dots" tone="subtle" label="Assistant is typing"></spinner-ui>
 * <spinner-ui paused size="lg"></spinner-ui>
 *
 * Circular animated indicator for INDETERMINATE loading. The visual is
 * rendered entirely via `::before` (and per-variant peers) on the host;
 * the component stamps no internal DOM (`static template = () => null`).
 *
 * Props (attributes):
 *   size     — sm | md (default) | lg
 *   variant  — arc (default) | ring | dots
 *   tone     — current (default) | subtle | accent | inverse
 *   paused   — freeze the animation in place (default: false)
 *   label    — accessible operation name (default: "Loading")
 *
 * ARIA: role="progressbar" + aria-busy="true" + aria-valuetext=<label>.
 * Reduced motion: when prefers-reduced-motion: reduce is active, the
 * spinning animation is replaced with a static "…" ellipsis via CSS
 * only — no JS observer required. WCAG 2.3.3.
 *
 * Lifecycle: pure-CSS animation. No timers, no observers. connected()
 * sets the three ARIA attributes once; render() keeps aria-valuetext in
 * sync with the [label] prop.
 *
 * @see SPEC-001 (docs/specs/implementation-ready/SPEC-001-spinner-loader.md)
 */

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

export class UISpinner extends UIElement {
  static properties = {
    size:    { type: String,  default: 'md',      reflect: true },
    variant: { type: String,  default: 'arc',     reflect: true },
    tone:    { type: String,  default: 'current', reflect: true },
    paused:  { type: Boolean, default: false,     reflect: true },
    label:   { type: String,  default: 'Loading', reflect: false },
  };

  // Arc / ring / knight variants are pure ::before paint — no children.
  // Dots variant stamps three <span data-spinner-dot=N> children in
  // #syncDots() so each dot can carry an independent animation-delay.
  // Pre-fix: ::before painted dot1, ::after painted dot3, and a
  // box-shadow on ::before painted the middle dot. The pseudo-element
  // approach broke spacing (the box-shadow dot doesn't participate in
  // flex-gap math; the middle dot can't animate; the outer dots picked
  // up scale from keyframes but the middle stayed static). Three real
  // children let flex-gap distribute the row evenly AND give each dot
  // its own animation phase.
  static template = () => null;

  connected() {
    // ARIA wiring — fixed for the lifetime of the element.
    if (!this.hasAttribute('role')) this.setAttribute('role', 'progressbar');
    if (!this.hasAttribute('aria-busy')) this.setAttribute('aria-busy', 'true');
    this.setAttribute('aria-valuetext', this.label || 'Loading');
    this.#syncDots();
  }

  render() {
    // Keep aria-valuetext in sync if `label` changes after connect.
    this.setAttribute('aria-valuetext', this.label || 'Loading');
    this.#syncDots();
  }

  #syncDots() {
    const isDots = this.variant === 'dots';
    const existing = this.querySelectorAll(':scope > [data-spinner-dot]');
    if (isDots) {
      if (existing.length === 3) return; // already stamped
      // Wipe any stale dots from a previous variant + stamp fresh.
      existing.forEach((n) => n.remove());
      const frag = document.createDocumentFragment();
      for (let i = 1; i <= 3; i++) {
        const span = document.createElement('span');
        span.setAttribute('data-spinner-dot', String(i));
        span.setAttribute('aria-hidden', 'true');
        frag.appendChild(span);
      }
      this.appendChild(frag);
    } else if (existing.length) {
      // Variant changed away from dots — drop the stamped children.
      existing.forEach((n) => n.remove());
    }
  }
}
