import { assert } from "../../../core/assert.js";

/**
 * Estimates the clock offset between this peer and a remote peer using the
 * NTP four-timestamp protocol.
 *
 * Each round trip provides four timestamps:
 *   t0: local time when the request was sent
 *   t1: remote time when the request was received
 *   t2: remote time when the response was sent
 *   t3: local time when the response was received
 *
 * From these:
 *   `RTT     = (t3 - t0) - (t2 - t1)`              — excludes remote processing time
 *   `offset  = ((t1 - t0) + (t2 - t3)) / 2`        — remote_time = local_time + offset
 *
 * Time units are application-defined; the module just does arithmetic. Callers
 * typically pass milliseconds.
 *
 * Reports the offset from the sample with lowest RTT (the standard SNTP estimator — error on offset is bounded by ±RTT/2, so the lowest-RTT sample is the tightest estimate)
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class TimeSync {
    #rtt_samples;
    #offset_samples;
    #sample_cursor;
    #best_rtt;
    #best_offset;


    /**
     * @param {{ window_size?: number }} [options]
     */
    constructor({ window_size = 8 } = {}) {
        assert.isPositiveInteger(window_size, 'window_size');

        /**
         * @readonly
         * @type {number}
         */
        this.window_size = window_size;

        // initializing samples to Infinity prevents them from being picked for "best" and saves us having to keep an extra "length/size" count.

        /**
         * @type {Float64Array}
         * @private
         */
        this.#rtt_samples = new Float64Array(window_size).fill(Infinity);

        /**
         * @type {Float64Array}
         * @private
         */
        this.#offset_samples = new Float64Array(window_size).fill(Infinity);

        /**
         *
         * @type {number}
         * @private
         */
        this.#sample_cursor = 0;

        /**
         * @type {number}
         * @private
         */
        this.#best_rtt = 0;

        /**
         * @type {number}
         * @private
         */
        this.#best_offset = 0;
    }

    /**
     * Record a complete four-timestamp sample.
     *
     * @param {number} t0 local time at send
     * @param {number} t1 remote time at receive
     * @param {number} t2 remote time at send
     * @param {number} t3 local time at receive
     */
    record(t0, t1, t2, t3) {
        assert.isNumber(t0, 't0');
        assert.isNumber(t1, 't1');
        assert.isNumber(t2, 't2');
        assert.isNumber(t3, 't3');

        const rtt = (t3 - t0) - (t2 - t1);
        const offset = ((t1 - t0) + (t2 - t3)) / 2;

        const sample_index = this.#sample_cursor;

        // advance the cursor to the next sample
        this.#sample_cursor = (this.#sample_cursor + 1) % this.window_size;

        this.#rtt_samples[sample_index] = rtt;
        this.#offset_samples[sample_index] = offset;

        // as per SNTP, pick lowest RTT sample
        // see https://www.rfc-editor.org/rfc/rfc5905.html#section-10
        let best_rtt = this.#rtt_samples[0];
        let best_offset = this.#offset_samples[0];

        const sample_count = this.window_size;

        for (let i = 1; i < sample_count; i++) {
            if (this.#rtt_samples[i] < best_rtt) {
                best_rtt = this.#rtt_samples[i];
                best_offset = this.#offset_samples[i];
            }
        }

        this.#best_rtt = best_rtt;
        this.#best_offset = best_offset;
    }

    /**
     * RTT of the sample whose offset is currently being reported — i.e. the
     * lowest RTT in the current window. This is the *best-case* round-trip,
     * not a typical one. Don't use it to size timeouts, jitter buffers, or
     * render delay; for that, measure observed jitter directly
     * (see `AdaptiveRenderDelay`).
     *
     * @returns {number}
     */
    best_rtt() {
        return this.#best_rtt;
    }

    /**
     * Estimated `remote_time - local_time`. Add to a local timestamp to get the
     * estimated remote timestamp.
     * @returns {number}
     */
    offset() {
        return this.#best_offset;
    }

    /**
     * Convert a local timestamp to the corresponding estimated remote timestamp.
     * @param {number} local_time
     * @returns {number}
     */
    estimate_remote(local_time) {
        return local_time + this.#best_offset;
    }

}