// A single, persistent banner rotation engine. The rotator and scheduler live here at module
// scope, so the rotation keeps running across screen navigations: each screen mounts its own
// AdBanner view, which only attaches as the DOM host. This makes the banner continuous and
// independent of route transitions (the cycle is not restarted on navigation).

import { createBannerRotator, type BannerHost, type Rotator } from './banner';
import type { BannerCampaign, BannerColors, BannerTimings } from './model';

const noColors: BannerColors = { all: null, dark: null };

let rotator: Rotator | null = null;
let mounted: BannerHost | null = null;
let current = '';
let currentColors: BannerColors = noColors;
let key = '';
// The in-flight horizontal scroll of the current message, so a view mounted by navigation can
// resume it from the same offset instead of restarting at the left. Null when not scrolling.
let activeScroll: { toPx: number; dur: number; start: number } | null = null;

// proxy is the rotator's host: it records the current message + scroll (so a freshly-mounted view
// can resume them) and forwards every effect to the currently-attached DOM host, if any.
const proxy: BannerHost = {
  show(md, colors) {
    current = md;
    currentColors = colors;
    activeScroll = null;
    mounted?.show(md, colors);
  },
  resetScroll() {
    activeScroll = null;
    mounted?.resetScroll();
  },
  hide(durationMs) {
    mounted?.hide(durationMs);
  },
  overflowPx() {
    return mounted?.overflowPx() ?? 0;
  },
  scrollTo(toPx, durationMs) {
    activeScroll = { toPx, dur: durationMs, start: Date.now() };
    mounted?.scrollTo(toPx, durationMs);
  },
  resumeScroll(fromTx, toPx, durationMs) {
    mounted?.resumeScroll(fromTx, toPx, durationMs);
  },
};

// bannerKey identifies a campaigns+timings set so configureBanner restarts the cycle only on a
// real change, not on every (re)mount.
function bannerKey(campaigns: BannerCampaign[], timings: BannerTimings): string {
  return JSON.stringify({ campaigns, timings });
}

/**
 * configureBanner (re)starts the rotation for the given campaigns and timings, or leaves the
 * running rotation untouched when they are unchanged — so navigating between screens (which
 * remounts the view with the same data) continues the cycle rather than restarting it.
 */
export function configureBanner(campaigns: BannerCampaign[], timings: BannerTimings): void {
  const k = bannerKey(campaigns, timings);
  if (k === key && rotator) return;
  key = k;
  rotator?.stop();
  current = '';
  currentColors = noColors;
  rotator = createBannerRotator(campaigns, proxy, timings);
  rotator.start();
}

/**
 * bannerCurrent returns the message the engine is currently displaying (empty before the first
 * one). A freshly-mounted view reads it to render the live message immediately, without a fade —
 * so navigating between screens does not visibly restart the cycle.
 */
export function bannerCurrent(): string {
  return current;
}

/**
 * bannerCurrentColors returns the colour override of the campaign whose message is currently
 * displayed (neutral tokens — both sets null — before the first message). A freshly-mounted view
 * reads it alongside bannerCurrent so the strip paints the right colours immediately on mount.
 */
export function bannerCurrentColors(): BannerColors {
  return currentColors;
}

/**
 * attachBannerHost connects a freshly-mounted view as the DOM host. It does NOT re-show the
 * current message (the view renders it itself from bannerCurrent on mount): re-showing here would
 * replay the fade-in on every navigation, which looks like the cycle restarting. The engine's own
 * timer keeps driving show/hide for real message changes through this host.
 */
export function attachBannerHost(host: BannerHost): void {
  mounted = host;
  // Resume the current message's scroll from where it had reached, so navigation does not restart
  // a long message at the left. Only while a scroll is still in flight; a finished scroll is left
  // at its end and the rotator's own loop takes over.
  if (activeScroll) {
    const elapsed = Date.now() - activeScroll.start;
    if (elapsed < activeScroll.dur) {
      const progress = elapsed / activeScroll.dur;
      host.resumeScroll(-activeScroll.toPx * progress, activeScroll.toPx, activeScroll.dur - elapsed);
    }
  }
}

/**
 * remeasureBanner re-presents the current message (re-measuring overflow and restarting its
 * scroll), for when the viewport size changed (e.g. a portrait↔landscape rotation) and a message
 * that fit may now overflow, or vice versa.
 */
export function remeasureBanner(): void {
  rotator?.restart();
}

/**
 * detachBannerHost disconnects a view on unmount without stopping the engine. It clears the host
 * only when it is still the current one, so an outgoing view leaving after the incoming view has
 * already attached does not detach the new host (the transition briefly double-mounts).
 */
export function detachBannerHost(host: BannerHost): void {
  if (mounted === host) mounted = null;
}
