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

import { UIElement } from '../../core/element.js';
import { getIcon, whenIconRegistryReady, iconRegistryUnwired } from '../../core/icons.js';

// Upstream finding F1: the zero-config Phosphor glob path can leave the icon
// registry completely unwired under Vite 8 / rolldown (the `import.meta.glob`
// macro doesn't transform from inside a published dependency), so every
// `<icon-ui>` renders blank. The loader layer warns, but the per-icon miss is
// silent (`getIcon → ''`). Fail loud ONCE from the component too, with the
// actionable fix. Deferred a macrotask so an app that calls
// `installIconLoaders()` at entry (the correct path) is never false-flagged.
let _unwiredCheckScheduled = false;
function warnIfIconRegistryUnwired() {
  if (_unwiredCheckScheduled) return;
  _unwiredCheckScheduled = true;
  setTimeout(() => {
    if (!iconRegistryUnwired()) return;   // loaders arrived after all — fine
    // eslint-disable-next-line no-console
    console.error(
      `[icon-ui] No icons are registered and no icon loaders were installed — every <icon-ui> renders blank.\n` +
      `  The zero-config import.meta.glob path does not work from inside a published dependency under Vite 8 / rolldown.\n` +
      `  Fix — from your APP entry (where import.meta.glob is transformed):\n` +
      `    npm i @phosphor-icons/core\n` +
      `    import { installIconLoaders } from '@adia-ai/web-components/core/icons';\n` +
      `    installIconLoaders({ regular: import.meta.glob(\n` +
      `      '/node_modules/@phosphor-icons/core/assets/regular/*.svg',\n` +
      `      { query: '?raw', import: 'default', eager: true }) });\n` +
      `  See USAGE.md § Icons.`,
    );
  }, 0);
}

export class UIIcon extends UIElement {
  static properties = {
    name:   { type: String, default: '',        reflect: true },
    size:   { type: String, default: '',        reflect: true },
    label:  { type: String, default: '',        reflect: true },
    /** regular | thin | light | bold | fill | duotone */
    weight: { type: String, default: 'regular', reflect: true },
  };

  // Incremented when the icon registry becomes ready so the reactive
  // render() re-runs automatically after the loader map arrives.
  // Declared as a private field (not a static property) to keep it out
  // of the attribute reflection loop — it is a purely internal trigger.
  #rev = this.signal(0);

  // Abort controller for the registry-ready subscription. Cancelled on
  // disconnectedCallback so elements removed before the registry resolves
  // do not attempt a render on a detached host.
  #readyCtrl = null;

  connected() {
    this.setAttribute('role', this.label ? 'img' : 'presentation');
    if (!this.label) this.setAttribute('aria-hidden', 'true');

    // Subscribe to the registry-ready promise ONCE per connect cycle.
    // When installIconLoaders() fires (could be before or after connect),
    // bump #rev so the reactive render() reruns and picks up the SVG.
    // Guard: if the registry was already ready (e.g. warm page reload),
    // the .then() fires in a microtask — still safe, just a no-op repaint.
    this.#readyCtrl = new AbortController();
    const ctrl = this.#readyCtrl;
    whenIconRegistryReady.then(() => {
      if (!ctrl.signal.aborted) this.#rev.value++;
    });
  }

  disconnected() {
    this.#readyCtrl?.abort();
    this.#readyCtrl = null;
  }

  render() {
    // Read #rev so this render() is tracked as a dependent of the registry
    // arrival. The value itself is not used — the read is the tracking hook.
    void this.#rev.value;
    if (this.label) this.setAttribute('aria-label', this.label);
    const svg = getIcon(this.name, this.weight || 'regular');
    if (svg && this.innerHTML !== svg) this.innerHTML = svg;
    // A named icon that resolved to nothing AND a never-wired registry is the
    // F1 symptom — schedule the one-shot fail-loud. A plain typo (registry
    // populated, this one name absent) settles `iconRegistryUnwired()` false
    // and is correctly NOT flagged here.
    else if (this.name && !svg) warnIfIconRegistryUnwired();

    // Pixel / rem / em passthrough — `size="48"` → 48px,
    // `size="3.5rem"` → 3.5rem. Named-scale values (xs/sm/md/lg/xl/
    // 2xl/3xl/4xl/fill) are handled by `:scope[size="…"]` rules in
    // icon.css; we only set --icon-size inline for free-form values.
    if (this.size && this.#isFreeFormSize(this.size)) {
      this.style.setProperty('--icon-size', this.#normalizeSize(this.size));
    } else if (this.style.getPropertyValue('--icon-size')) {
      this.style.removeProperty('--icon-size');
    }
  }

  #isFreeFormSize(s) {
    return /^\d+(\.\d+)?(px|rem|em)?$/.test(s);
  }

  #normalizeSize(s) {
    return /^\d+(\.\d+)?$/.test(s) ? `${s}px` : s;
  }
}
