/**
 * The measuring half of the screen-size work; `viewport.ts` holds the rules.
 *
 * One store, mounted once from `main.ts`, that watches the viewport and
 * publishes the resulting plan two ways: as reactive state for the components
 * that need to branch in markup, and as CSS custom properties plus data
 * attributes on `<html>` for everything that can be expressed in a stylesheet.
 */
import { cssVariables, planLayout, type LayoutPlan, type ViewportMeasurement } from './viewport';

/**
 * Read the viewport.
 *
 * `visualViewport` rather than `innerWidth`/`innerHeight` where it exists,
 * because on Android those two lie: they report the layout viewport, which
 * ignores the on-screen keyboard and the collapsing URL bar. The rider never
 * sees a keyboard mid-ride, but they do see the bar, and a layout planned
 * against a height 60 px taller than the one being painted puts the control
 * bar under the edge of the screen.
 */
export function measure(): ViewportMeasurement {
  const vv = typeof window !== 'undefined' ? window.visualViewport : null;
  return {
    width: vv?.width ?? window.innerWidth,
    height: vv?.height ?? window.innerHeight,
    dpr: window.devicePixelRatio || 1,
    // `pointer: coarse` is the honest question — "is the primary input a
    // finger" — where a user-agent sniff is a guess. A touchscreen laptop
    // reports `fine`, correctly: it has a keyboard and a trackpad.
    touch: window.matchMedia?.('(pointer: coarse)').matches ?? false,
  };
}

class ViewportStore {
  /** The last measurement taken, published for diagnostics and tests. */
  measurement = $state<ViewportMeasurement>({ width: 1440, height: 900, dpr: 1, touch: false });
  /** The plan derived from it. Components read this; nothing writes it. */
  plan = $state<LayoutPlan>(planLayout({ width: 1440, height: 900, dpr: 1, touch: false }));

  private detach: (() => void) | null = null;

  /**
   * Start watching. Returns a teardown so a test (or a future multi-window
   * shell) can stop it; the app itself never does.
   */
  observe(): () => void {
    this.detach?.();
    this.apply();

    // A frame's grace after a rotation: Android reports the new orientation
    // before the webview has been resized to match, so measuring on the event
    // itself yields the *old* dimensions with the new orientation — briefly a
    // portrait layout on a landscape screen.
    let frame = 0;
    const schedule = () => {
      cancelAnimationFrame(frame);
      frame = requestAnimationFrame(() => this.apply());
    };

    window.addEventListener('resize', schedule);
    window.addEventListener('orientationchange', schedule);
    window.visualViewport?.addEventListener('resize', schedule);

    this.detach = () => {
      cancelAnimationFrame(frame);
      window.removeEventListener('resize', schedule);
      window.removeEventListener('orientationchange', schedule);
      window.visualViewport?.removeEventListener('resize', schedule);
      this.detach = null;
    };
    return this.detach;
  }

  /** Measure, plan, publish. */
  apply(): void {
    const m = measure();
    const plan = planLayout(m);
    this.measurement = m;
    this.plan = plan;

    const root = document.documentElement;
    for (const [name, value] of Object.entries(cssVariables(plan))) {
      root.style.setProperty(name, value);
    }
    // Data attributes rather than classes: a stylesheet can then say
    // `[data-size='compact']` and read like the plan it came from.
    root.dataset.size = plan.sizeClass;
    root.dataset.height = plan.heightClass;
    root.dataset.orientation = plan.orientation;
    root.dataset.input = m.touch ? 'touch' : 'pointer';
  }
}

export const viewport = new ViewportStore();
