import { GetRotatorHeading } from '../../wailsjs/go/main/App';

// One poll loop for the antenna heading, shared by everything that shows it.
//
// TWO PROBLEMS THIS FIXES, both of them invisible until you count.
//
// The status bar polled, and the Station Control compass polled, and they polled
// the SAME binding — so with that tab open the rotator was asked twice as often
// as either component believed. Every backend builds a fresh client per call
// (spid.New, gs232.NewSerial, dcu1.NewSerial, rotgenius.New…), so one poll is
// one OPEN and CLOSE of a serial port or a TCP connection, not a read on a link
// already up.
//
// And a fixed interval has no good value. Three seconds moved the needle in
// steps of about thirteen degrees on a turning antenna — a compass that jumps
// rather than sweeps. Seven hundred milliseconds sweeps beautifully and opens
// the controller's port about ten thousand times an hour to watch an antenna
// that has not moved since breakfast.
//
// So: fast while it is turning, slow while it is not. "Turning" is not something
// these controllers report — a SPID answers a position and nothing else — so it
// is inferred from the position changing, and held for a few seconds after the
// last change so the tail of a movement stays smooth.

export type RotorHeading = {
  enabled: boolean; ok: boolean; azimuth: number;
  rotors?: string[]; active?: number; motorized?: boolean; raw?: string;
};

const MOVING_MS = 500;   // while the antenna is turning
const IDLE_MS = 3000;    // while it is parked — the rate everything used before
const SETTLE_MS = 6000;  // stay fast this long after the last movement

const subs = new Set<(h: RotorHeading) => void>();
let timer: number | undefined;
let inFlight = false;
let lastAz: number | null = null;
let lastMoveAt = 0;
// The last heading anyone received, replayed to whoever subscribes next.
//
// Without it, opening Station Control left the compass blank for one or two
// seconds while every other panel filled at once. Nothing was slow: the status
// bar already had the loop running with a tick pending, so a component mounting
// halfway through an idle interval simply waited out the rest of it. The
// heading was known the whole time — it just had nowhere to be read from.
let last: RotorHeading | null = null;

function schedule(delay: number) {
  if (timer !== undefined) window.clearTimeout(timer);
  timer = window.setTimeout(tick, delay);
}

async function tick() {
  // A slow controller must not stack requests behind itself: at 600 baud a SPID
  // reply takes a fifth of a second on the wire alone, and a port that is still
  // open from the last poll cannot be opened again.
  if (inFlight) { schedule(MOVING_MS); return; }
  inFlight = true;
  try {
    const h = (await GetRotatorHeading()) as unknown as RotorHeading;
    if (h?.ok && typeof h.azimuth === 'number') {
      if (lastAz !== null && h.azimuth !== lastAz) lastMoveAt = Date.now();
      lastAz = h.azimuth;
    }
    last = h;
    subs.forEach((fn) => { try { fn(h); } catch { /* a subscriber must not stop the loop */ } });
  } catch {
    // Leave the last heading alone: a single failed poll on a shared serial port
    // is not news, and blanking the compass on one would make it flicker.
  } finally {
    inFlight = false;
  }
  if (subs.size > 0) schedule(Date.now() - lastMoveAt < SETTLE_MS ? MOVING_MS : IDLE_MS);
  else timer = undefined;
}

// subscribeRotorHeading starts the loop if it is not running and returns the
// unsubscribe. The loop stops when the last subscriber leaves.
export function subscribeRotorHeading(fn: (h: RotorHeading) => void): () => void {
  subs.add(fn);
  // Hand over what is already known, at once. An Alpha SPID poll is an open,
  // a read at 600 baud and a close, so even an immediate one takes a moment —
  // the cached heading is what makes the compass appear with the panel rather
  // than after it. In a microtask, so a subscriber is never called back before
  // subscribeRotorHeading has returned to it.
  if (last) { const h = last; queueMicrotask(() => { if (subs.has(fn)) fn(h); }); }
  if (timer === undefined && !inFlight) void tick();
  return () => {
    subs.delete(fn);
    if (subs.size === 0 && timer !== undefined) { window.clearTimeout(timer); timer = undefined; }
  };
}

// pokeRotorHeading polls at once and switches to the fast rate — call it after
// commanding a move, so the needle starts sweeping on the click rather than on
// whatever was left of a three-second tick.
export function pokeRotorHeading(): void {
  lastMoveAt = Date.now();
  if (timer !== undefined) window.clearTimeout(timer);
  timer = undefined;
  void tick();
}
