/**
 * Bounded, self-decimating chart history (NFR-3).
 *
 * A two-hour ride at 4 Hz is 28 800 samples per series. Keeping them all is
 * both a memory leak and a rendering cost that grows through the ride — exactly
 * what NFR-3 forbids. Instead the buffer has a hard capacity: when it fills, it
 * averages adjacent pairs in place, halving the point count and doubling the
 * time each point represents. Later samples are then averaged in groups of that
 * same stride before being stored.
 *
 * The result is constant memory and a constant point count for any ride
 * length, with resolution degrading gracefully: full 250 ms detail for the
 * first ~15 minutes, 2-second buckets by two hours. Typed arrays throughout so
 * uPlot can consume them without a copy.
 */

const CAPACITY = 3600;

export class History {
  readonly capacity: number;
  readonly seriesCount: number;
  /** Seconds since ride start. */
  readonly x: Float64Array;
  readonly y: Float64Array[];
  /** Number of populated points. */
  length = 0;
  /** Raw samples currently folded into one stored point. */
  stride = 1;

  private pendingX = 0;
  private pendingY: Float64Array;
  private pendingN = 0;

  constructor(seriesCount: number, capacity = CAPACITY) {
    this.capacity = capacity;
    this.seriesCount = seriesCount;
    this.x = new Float64Array(capacity);
    this.y = Array.from({ length: seriesCount }, () => new Float64Array(capacity));
    this.pendingY = new Float64Array(seriesCount);
  }

  push(x: number, values: number[]): void {
    this.pendingX += x;
    for (let s = 0; s < this.seriesCount; s++) this.pendingY[s] += values[s] ?? 0;
    this.pendingN++;
    if (this.pendingN < this.stride) return;

    if (this.length >= this.capacity) this.compact();

    const i = this.length++;
    this.x[i] = this.pendingX / this.pendingN;
    for (let s = 0; s < this.seriesCount; s++) this.y[s][i] = this.pendingY[s] / this.pendingN;

    this.pendingX = 0;
    this.pendingY.fill(0);
    this.pendingN = 0;
  }

  /** Halve the resolution in place. O(capacity), amortised to O(1) per sample. */
  private compact(): void {
    const half = this.length >> 1;
    for (let i = 0; i < half; i++) {
      const a = i * 2;
      const b = a + 1;
      this.x[i] = (this.x[a] + this.x[b]) / 2;
      for (let s = 0; s < this.seriesCount; s++) {
        this.y[s][i] = (this.y[s][a] + this.y[s][b]) / 2;
      }
    }
    this.length = half;
    this.stride *= 2;
  }

  clear(): void {
    this.length = 0;
    this.stride = 1;
    this.pendingX = 0;
    this.pendingY.fill(0);
    this.pendingN = 0;
  }

  /** Views sized to the populated region, ready for `uPlot.setData`. */
  view(): Float64Array[] {
    return [this.x.subarray(0, this.length), ...this.y.map((a) => a.subarray(0, this.length))];
  }
}
