export class InputRecorder {
    events = [];
    maxEvents;
    recording = false;
    startTimeMs = 0;
    lastTimestampMs = 0;
    frame = 0;
    constructor(options = {}) {
        this.maxEvents = options.maxEvents ?? 4096;
    }
    start(startTimeMs = 0) {
        this.events.length = 0;
        this.recording = true;
        this.startTimeMs = finite(startTimeMs, 0);
        this.lastTimestampMs = this.startTimeMs;
        this.frame = 0;
    }
    stop(endTimeMs = this.lastTimestampMs) {
        this.recording = false;
        this.lastTimestampMs = Math.max(this.startTimeMs, finite(endTimeMs, this.lastTimestampMs));
        return this.recordingSnapshot();
    }
    recordFrame(deltaTimeMs, timestampMs = this.lastTimestampMs + deltaTimeMs) {
        this.push({
            type: "frame",
            timestampMs: finite(timestampMs, this.lastTimestampMs),
            frame: this.frame,
            deltaTimeMs: Math.max(0, finite(deltaTimeMs, 0))
        });
        this.frame += 1;
    }
    recordKey(code, down, timestampMs = this.lastTimestampMs) {
        this.push({ type: "key", timestampMs: finite(timestampMs, this.lastTimestampMs), frame: this.frame, code, down });
    }
    recordPointerButton(button, down, x, y, timestampMs = this.lastTimestampMs) {
        this.push({ type: "pointer-button", timestampMs: finite(timestampMs, this.lastTimestampMs), frame: this.frame, button, down, x, y });
    }
    recordPointerMove(x, y, deltaX, deltaY, timestampMs = this.lastTimestampMs) {
        this.push({ type: "pointer-move", timestampMs: finite(timestampMs, this.lastTimestampMs), frame: this.frame, x, y, deltaX, deltaY });
    }
    recordPointerWheel(deltaX, deltaY, timestampMs = this.lastTimestampMs) {
        this.push({ type: "pointer-wheel", timestampMs: finite(timestampMs, this.lastTimestampMs), frame: this.frame, deltaX, deltaY });
    }
    recordGamepadAxis(gamepadIndex, controlIndex, value, timestampMs = this.lastTimestampMs) {
        this.push({ type: "gamepad-axis", timestampMs: finite(timestampMs, this.lastTimestampMs), frame: this.frame, gamepadIndex, controlIndex, value });
    }
    recordGamepadButton(gamepadIndex, controlIndex, down, value, timestampMs = this.lastTimestampMs) {
        this.push({ type: "gamepad-button", timestampMs: finite(timestampMs, this.lastTimestampMs), frame: this.frame, gamepadIndex, controlIndex, down, value });
    }
    recordAction(context, action, value, vector, timestampMs = this.lastTimestampMs) {
        this.push({ type: "action", timestampMs: finite(timestampMs, this.lastTimestampMs), frame: this.frame, context, action, value, vector });
    }
    recordingSnapshot() {
        const endTimeMs = Math.max(this.lastTimestampMs, this.events.at(-1)?.timestampMs ?? this.startTimeMs);
        return Object.freeze({
            metadata: Object.freeze({
                version: "1.0.0",
                source: "origin-master-input-recorder-playback-adapted",
                startTimeMs: this.startTimeMs,
                endTimeMs,
                durationMs: Math.max(0, endTimeMs - this.startTimeMs),
                frameCount: this.frame,
                eventCount: this.events.length,
                evidence: replayEvidence()
            }),
            events: Object.freeze([...this.events])
        });
    }
    toJSON() {
        return JSON.stringify(this.recordingSnapshot());
    }
    push(event) {
        if (!this.recording)
            throw new Error("InputRecorder must be started before recording events.");
        if (this.events.length >= this.maxEvents)
            throw new Error(`InputRecorder exceeded maxEvents=${this.maxEvents}.`);
        this.lastTimestampMs = Math.max(this.lastTimestampMs, event.timestampMs);
        this.events.push(Object.freeze({ ...event, timestampMs: this.lastTimestampMs }));
    }
}
export class InputPlayback {
    recording;
    state = "stopped";
    currentTimeMs = 0;
    eventIndex = 0;
    emittedEvents = 0;
    loopCount = 0;
    speed;
    loop;
    constructor(options = {}) {
        this.speed = Math.max(0.01, finite(options.speed, 1));
        this.loop = options.loop ?? false;
    }
    load(recording) {
        this.recording = normalizeRecording(recording);
        this.stop();
    }
    play() {
        if (!this.recording)
            throw new Error("InputPlayback requires a recording before play().");
        this.state = "playing";
    }
    pause() {
        if (this.state === "playing")
            this.state = "paused";
    }
    stop() {
        this.state = "stopped";
        this.currentTimeMs = 0;
        this.eventIndex = 0;
    }
    seek(timeMs) {
        const recording = this.requireRecording();
        this.currentTimeMs = clamp(finite(timeMs, 0), 0, recording.metadata.durationMs);
        this.eventIndex = recording.events.findIndex((event) => event.timestampMs >= recording.metadata.startTimeMs + this.currentTimeMs);
        if (this.eventIndex < 0)
            this.eventIndex = recording.events.length;
    }
    update(deltaTimeMs) {
        if (this.state !== "playing")
            return [];
        const recording = this.requireRecording();
        this.currentTimeMs += Math.max(0, finite(deltaTimeMs, 0)) * this.speed;
        const absoluteTime = recording.metadata.startTimeMs + this.currentTimeMs;
        const emitted = [];
        while (this.eventIndex < recording.events.length && (recording.events[this.eventIndex]?.timestampMs ?? Infinity) <= absoluteTime) {
            emitted.push(recording.events[this.eventIndex]);
            this.eventIndex += 1;
        }
        if (this.currentTimeMs >= recording.metadata.durationMs) {
            if (this.loop && recording.metadata.durationMs > 0) {
                this.loopCount += 1;
                this.currentTimeMs %= recording.metadata.durationMs;
                this.eventIndex = 0;
            }
            else {
                this.state = "stopped";
                this.currentTimeMs = recording.metadata.durationMs;
            }
        }
        this.emittedEvents += emitted.length;
        return Object.freeze(emitted);
    }
    snapshot() {
        return Object.freeze({
            state: this.state,
            currentTimeMs: Number(this.currentTimeMs.toFixed(3)),
            emittedEvents: this.emittedEvents,
            loopCount: this.loopCount,
            recordingEventCount: this.recording?.metadata.eventCount ?? 0,
            evidence: replayEvidence()
        });
    }
    requireRecording() {
        if (!this.recording)
            throw new Error("InputPlayback has no loaded recording.");
        return this.recording;
    }
}
export function parseInputRecording(json) {
    return normalizeRecording(JSON.parse(json));
}
function normalizeRecording(recording) {
    const events = [...recording.events].sort((left, right) => left.timestampMs - right.timestampMs || left.frame - right.frame);
    return Object.freeze({
        metadata: Object.freeze({
            ...recording.metadata,
            evidence: replayEvidence()
        }),
        events: Object.freeze(events.map((event) => Object.freeze({ ...event })))
    });
}
function replayEvidence() {
    return {
        oldCodebasePort: true,
        recording: true,
        playback: true,
        seek: true,
        looping: true
    };
}
function finite(value, fallback) {
    const number = Number(value);
    return Number.isFinite(number) ? number : fallback;
}
function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
}
//# sourceMappingURL=InputReplay.js.map