import { DEFAULT_TIMELINE_BUFFER_SIZE } from './types.js';
/**
 * TimelineBuffer — fixed-capacity ring buffer of TimelineEvent snapshots.
 *
 * Indices are stable within a session (they increment monotonically via
 * the `seq` field on each event). The cursor is an offset into the live
 * ring: 0 = oldest retained, size-1 = newest, size = live.
 */
export class TimelineBuffer {
    _maxSize;
    _ring;
    _head = 0; // next write position
    _totalAppended = 0;
    _cursor; // live by default
    _nextSeq = 1;
    /**
     * @param maxSize — Maximum events to retain. Must be >= 2.
     * @default DEFAULT_TIMELINE_BUFFER_SIZE
     */
    constructor(maxSize = DEFAULT_TIMELINE_BUFFER_SIZE) {
        if (maxSize < 2)
            throw new RangeError(`maxSize must be >= 2, got ${maxSize}`);
        this._maxSize = maxSize;
        this._ring = new Array(maxSize).fill(undefined);
        this._cursor = 0; // starts at live (size === 0 initially)
    }
    // ── Capacity ──────────────────────────────────────────────────────────────
    /** Maximum events retained. */
    get maxSize() {
        return this._maxSize;
    }
    /** Current number of events retained (capped at maxSize). */
    get size() {
        return Math.min(this._totalAppended, this._maxSize);
    }
    /** Total events ever appended (monotonically increasing). */
    get totalAppended() {
        return this._totalAppended;
    }
    // ── Cursor state ─────────────────────────────────────────────────────────
    /**
     * Whether the cursor is at the live position (past the newest event).
     * When live, `getCurrentSnapshot()` returns undefined.
     */
    get isLive() {
        return this._cursor >= this.size;
    }
    /** Current cursor state for display. */
    get cursorState() {
        const s = this.size;
        return {
            index: this._cursor,
            total: s,
            isLive: this.isLive,
        };
    }
    // ── Append ────────────────────────────────────────────────────────────────
    /**
     * Append a new timeline event.
     *
     * If the cursor is live it advances with the tail (stays live).
     * If the cursor is pinned (time-travel mode), it stays pinned.
     *
     * @param event — Event without `seq` (assigned here).
     * @returns The stored TimelineEvent with its assigned `seq`.
     */
    append(event) {
        const wasLive = this.isLive;
        const stored = { ...event, seq: this._nextSeq++ };
        this._ring[this._head] = stored;
        this._head = (this._head + 1) % this._maxSize;
        this._totalAppended++;
        // Advance cursor only when live
        if (wasLive) {
            this._cursor = this.size; // stay at live
        }
        return stored;
    }
    // ── Random access ─────────────────────────────────────────────────────────
    /**
     * Return all retained events in chronological order.
     *
     * @returns Events oldest → newest.
     */
    getAll() {
        const s = this.size;
        if (s === 0)
            return [];
        const result = new Array(s);
        if (this._totalAppended <= this._maxSize) {
            for (let i = 0; i < s; i++) {
                result[i] = this._ring[i];
            }
        }
        else {
            for (let i = 0; i < s; i++) {
                result[i] = this._ring[(this._head + i) % this._maxSize];
            }
        }
        return result;
    }
    /**
     * Return the event at a logical index (0 = oldest, size-1 = newest).
     *
     * @returns TimelineEvent or undefined when out of range.
     */
    getAt(index) {
        const s = this.size;
        if (index < 0 || index >= s)
            return undefined;
        if (this._totalAppended <= this._maxSize) {
            return this._ring[index];
        }
        return this._ring[(this._head + index) % this._maxSize];
    }
    // ── Time-travel controls ──────────────────────────────────────────────────
    /**
     * Return the event at the current cursor position, or undefined when live.
     */
    getCurrentEvent() {
        if (this.isLive)
            return undefined;
        return this.getAt(this._cursor);
    }
    /**
     * Step the cursor one event backward (toward oldest).
     * If already at index 0, the cursor stays at 0.
     *
     * @returns true if the cursor moved.
     */
    stepBack() {
        if (this._cursor <= 0)
            return false;
        this._cursor--;
        return true;
    }
    /**
     * Step the cursor one event forward (toward live).
     * When the cursor reaches the live position (past the newest event),
     * `isLive` becomes true.
     *
     * @returns true if the cursor moved.
     */
    stepForward() {
        if (this.isLive)
            return false;
        this._cursor++;
        return true;
    }
    /**
     * Seek the cursor to an absolute logical index.
     * Clamps to [0, size] where size === live.
     *
     * @param index — Target index (size = live).
     */
    seekTo(index) {
        const s = this.size;
        this._cursor = Math.max(0, Math.min(index, s));
    }
    /**
     * Seek to the nearest event at or before a given epoch ms timestamp.
     * If no events exist before the timestamp, seeks to index 0.
     * If all events are before or equal, seeks to the newest.
     *
     * @param epochMs — Target timestamp.
     */
    seekToTime(epochMs) {
        const all = this.getAll();
        if (all.length === 0)
            return;
        let best = 0;
        for (let i = 0; i < all.length; i++) {
            if ((all[i]?.capturedAt ?? Infinity) <= epochMs) {
                best = i;
            }
            else {
                break;
            }
        }
        this._cursor = best;
    }
    /**
     * Return the cursor to the live tail (exit time-travel mode).
     */
    exitTimeTravel() {
        this._cursor = this.size;
    }
    /**
     * Clear all retained events and reset to live position.
     */
    clear() {
        this._ring.fill(undefined);
        this._head = 0;
        this._totalAppended = 0;
        this._cursor = 0;
        this._nextSeq = 1;
    }
}
