/**
 * Screen size as a measured input, not a guess (FR-9.5, G-4).
 *
 * The desktop layout was built for a 1440×900 window and expressed its type
 * scale in `vw`. On a phone that falls apart twice over: `7vw` of a 412 px
 * viewport is 29 px, well under what is readable from the bars, and five
 * side-by-side readouts do not fit across 412 px at *any* type size. Shrinking
 * is not the answer to a small screen — showing less is.
 *
 * So this module turns a measurement into an explicit plan: type sizes in
 * pixels, column counts, and which sections earn their space. It is a pure
 * function of the measurement, which is what makes the behaviour testable
 * without a browser — see viewport.test.ts. `viewport.svelte.ts` does the
 * measuring and publishes the result to CSS.
 *
 * ## Why the sizes are absolute, not relative
 *
 * A number has to subtend enough visual angle to be read from the riding
 * position. Physical size on screen is `cssPx / pxPerInch`, and the angle is
 * that over the viewing distance. Desktop is ~96 CSS px per inch at about a
 * metre; Android's CSS pixel is the density-independent pixel, ~160 per inch,
 * and a phone clipped to the bars sits at roughly 0.6 m. The two corrections
 * very nearly cancel:
 *
 *     (160/96) × (0.6/1.0) ≈ 1.0
 *
 * which means the *same CSS pixel size* is about as readable on a bar-mounted
 * phone as on a desktop monitor. That is the justification for the floors
 * below being plain pixel numbers with no per-platform correction — and for
 * treating a small screen as a content problem rather than a scaling one.
 */

/** What we actually measure. */
export interface ViewportMeasurement {
  /** Viewport width in CSS pixels. */
  width: number;
  /** Viewport height in CSS pixels. */
  height: number;
  /** `devicePixelRatio` — recorded for diagnostics; the plan does not use it. */
  dpr: number;
  /** Whether the primary input is touch. Drives minimum hit-target size. */
  touch: boolean;
}

/**
 * Width bands, on the Material window-size-class boundaries. They are stated in
 * CSS pixels, which on Android *is* the dp the boundaries were defined in.
 */
export type SizeClass = 'compact' | 'medium' | 'expanded' | 'large';

/**
 * Height bands. The ride screen is a vertical stack of six sections, so height
 * decides what survives far more often than width does.
 */
export type HeightClass = 'short' | 'medium' | 'tall';

export type Orientation = 'portrait' | 'landscape';

export interface LayoutPlan {
  sizeClass: SizeClass;
  heightClass: HeightClass;
  orientation: Orientation;

  /** Type sizes in CSS pixels. */
  type: {
    hero: number;
    big: number;
    mid: number;
    small: number;
    label: number;
    sub: number;
  };

  /** Column counts for the ride screen's three readout grids. */
  columns: {
    primary: number;
    detail: number;
    effort: number;
  };

  /**
   * Sections that are dropped rather than squeezed. Each is a deliberate
   * judgement about what a rider still needs when there is no room.
   */
  show: {
    /** The route profile — the hero of the screen, and the last thing to go. */
    routeChart: boolean;
    /** Elevation / climbing / covered / ascended / elapsed. */
    detailRow: boolean;
    /** Average, normalised, work, burned — post-ride numbers, mid-ride noise. */
    secondaryEffort: boolean;
    /** The rolling power and gradient sparklines. */
    streamCharts: boolean;
    /** The keyboard hints on buttons: meaningless without a keyboard. */
    keyHints: boolean;
  };

  /** Minimum interactive target, in CSS pixels. */
  touchTargetPx: number;
  /** Horizontal page padding. */
  edgePx: number;
  /** Grid gutter. */
  gapPx: number;
  /** Floor for the route chart, below which it is not worth drawing. */
  routeChartMinPx: number;
}

/** The window the desktop layout was designed against. */
const REFERENCE = { width: 1440, height: 900 };

/**
 * Legibility floors, in CSS pixels: the point below which a readout stops
 * doing its job from a riding position. Nothing here scales past them — when
 * the space is not there, `show` gives something up instead.
 */
const FLOOR = { hero: 40, big: 28, mid: 18, small: 14, label: 10, sub: 11 };
const BASE = { hero: 58, big: 42, mid: 26, small: 17, label: 11, sub: 13 };
const CEILING = { hero: 116, big: 78, mid: 45, small: 27, label: 13, sub: 15 };

export function sizeClassFor(width: number): SizeClass {
  if (width < 600) return 'compact';
  if (width < 900) return 'medium';
  if (width < 1280) return 'expanded';
  return 'large';
}

export function heightClassFor(height: number): HeightClass {
  if (height < 480) return 'short';
  if (height < 760) return 'medium';
  return 'tall';
}

const clamp = (lo: number, v: number, hi: number) => Math.min(hi, Math.max(lo, v));

/**
 * Turn a measurement into a layout.
 *
 * Deliberately total: every input, including a zero-sized viewport during the
 * first frame, produces a usable plan rather than a NaN that would propagate
 * into a CSS custom property and blank the screen.
 */
export function planLayout(m: ViewportMeasurement): LayoutPlan {
  const width = Math.max(1, Math.round(m.width));
  const height = Math.max(1, Math.round(m.height));

  const sizeClass = sizeClassFor(width);
  const heightClass = heightClassFor(height);
  const orientation: Orientation = height >= width ? 'portrait' : 'landscape';

  // Scale on whichever axis is tighter: a tall narrow phone is constrained by
  // width, a laptop in a short window by height, and taking the minimum means
  // neither can push type past the space that exists for it.
  const fit = Math.min(width / REFERENCE.width, height / REFERENCE.height);
  const scale = clamp(0.5, fit, 2);

  const sized = (key: keyof typeof BASE) =>
    Math.round(clamp(FLOOR[key], BASE[key] * scale, CEILING[key]));

  const compact = sizeClass === 'compact';
  const portrait = orientation === 'portrait';
  const short = heightClass === 'short';

  // Columns. On compact the primary readouts go two-up: three would put the
  // hero ETA under 40 px to fit, and the floors are not negotiable.
  const primary = compact ? 2 : sizeClass === 'medium' ? 3 : sizeClass === 'expanded' ? 4 : 5;
  const detail = compact ? 3 : sizeClass === 'medium' ? 4 : 5;
  const effort = compact ? 3 : sizeClass === 'medium' ? 4 : 7;

  /*
   * What gets dropped, and in what order. The rule is that anything the rider
   * cannot act on mid-ride goes before anything they can.
   *
   *   - The sparklines are history; the live numbers above them are not. They
   *     go first, and on any short viewport, because a 60 px chart is a smear.
   *   - The secondary effort block (average / normalised / work / burned) is
   *     what the summary screen is for. It goes second.
   *   - The detail row survives longer: "climbing left" is the question a rider
   *     on a hill is actually asking.
   *   - The route chart is the screen's whole point and only goes when there is
   *     genuinely no room — a phone held in landscape, where it would leave
   *     nothing for the numbers.
   *   - Key hints are noise on a touch device: there is no keyboard to press.
   */
  const show = {
    routeChart: !(short && compact),
    detailRow: !short && !(compact && portrait && height < 700),
    secondaryEffort: !compact && !short,
    streamCharts: !compact && !short && height >= 700,
    keyHints: !m.touch,
  };

  return {
    sizeClass,
    heightClass,
    orientation,
    type: {
      hero: sized('hero'),
      big: sized('big'),
      mid: sized('mid'),
      small: sized('small'),
      label: sized('label'),
      sub: sized('sub'),
    },
    columns: { primary, detail, effort },
    show,
    // Android's own accessibility guidance is a 48 dp minimum, and a CSS pixel
    // is a dp there. A rider wearing gloves, out of the saddle, is exactly the
    // case that number exists for.
    touchTargetPx: m.touch ? 48 : 32,
    edgePx: compact ? 14 : Math.round(clamp(16, 44 * scale, 44)),
    gapPx: compact ? 10 : Math.round(clamp(12, 24 * scale, 24)),
    routeChartMinPx: short ? 90 : compact ? 110 : 130,
  };
}

/**
 * The plan as CSS custom properties, for the stylesheets to consume.
 *
 * Publishing the numbers rather than re-deriving them in `@media` rules is the
 * point: a media query cannot see the touch flag, and two rules that disagree
 * about where "compact" starts is a bug waiting to be found on a phone.
 */
export function cssVariables(plan: LayoutPlan): Record<string, string> {
  return {
    '--type-hero': `${plan.type.hero}px`,
    '--type-big': `${plan.type.big}px`,
    '--type-mid': `${plan.type.mid}px`,
    '--type-small': `${plan.type.small}px`,
    '--type-label': `${plan.type.label}px`,
    '--type-sub': `${plan.type.sub}px`,
    '--cols-primary': String(plan.columns.primary),
    '--cols-detail': String(plan.columns.detail),
    '--cols-effort': String(plan.columns.effort),
    '--touch-min': `${plan.touchTargetPx}px`,
    '--edge': `${plan.edgePx}px`,
    '--gap': `${plan.gapPx}px`,
    '--route-min': `${plan.routeChartMinPx}px`,
  };
}
