import { assert } from "../../../core/assert.js";
import Signal from "../../../core/events/signal/Signal.js";
import { ack_bitfield_build, ack_bitfield_for_each, } from "../core/sequence/ack_bitfield.js";
import { seq16_advance, seq16_distance, seq16_greater_than, SEQ16_HALF_RANGE } from "../core/sequence/seq16.js";

/**
 * Reliability-notification layer over a {@link Transport}.
 *
 * Adds an 8-byte header to every packet:
 * ```
 *   uint16:  outgoing_seq
 *   uint16:  ack_latest    (most recent seq received from peer, or 0 if none)
 *   uint32:  ack_bitfield  (the previous 32 seqs: bit i set => latest-1-i was received)
 * ```
 *
 * The header gives positive acknowledgement of up to 33 packets per inbound
 * packet — robust enough that a single dropped ack-bearing packet typically
 * doesn't matter (a later one carries the same info).
 *
 * Channel does NOT retransmit. It surfaces:
 *   - `onPacketAcked(seq)` when a peer's ack indicates an outgoing packet arrived
 *   - `onPacketLost(seq)` when an outgoing packet falls outside the 33-seq ack
 *     window without ever being acked (peer has acked 32 newer packets, so this
 *     one is presumed lost)
 *   - `onPayload(bytes, length)` for the payload portion of received packets,
 *     with the header stripped
 *
 * Reliability-by-retransmission lives in a higher-level pipeline (separate file,
 * yet to build). Channel just provides the notification primitive.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */

const HEADER_SIZE = 8;

export class Channel {
    #next_outgoing_seq;
    #unacked_outgoing;
    #latest_received_seq;
    #received_seqs;
    #send_buffer;
    #send_view;
    #lost_scratch;


    /**
     * @param {{
     *   transport: { send(bytes: Uint8Array, length: number): void, onReceive: Signal },
     * }} options
     */
    constructor({ transport }) {
        assert.ok(transport && typeof transport.send === 'function', 'transport must implement send()');
        assert.ok(transport.onReceive, 'transport must expose onReceive signal');

        /**
         * @type {{ send(bytes: Uint8Array, length: number): void, onReceive: Signal }}
         */
        this.transport = transport;
        this.transport.onReceive.add(this.#handle_inbound, this);

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

        /**
         * Outgoing seqs waiting on ack from the peer.
         * @type {Set<number>}
         * @private
         */
        this.#unacked_outgoing = new Set();

        /**
         * Most recent seq we've received from the peer; -1 if none yet.
         * @type {number}
         * @private
         */
        this.#latest_received_seq = -1;

        /**
         * Seqs we have received. Used to build the ack bitfield piggyback.
         * Pruned when entries fall outside the 64-seq window.
         * @type {Set<number>}
         * @private
         */
        this.#received_seqs = new Set();

        /**
         * Pre-allocated send buffer. Resized only if a payload exceeds capacity.
         * @type {Uint8Array}
         * @private
         */
        this.#send_buffer = new Uint8Array(1500);
        /** @type {DataView} @private */
        this.#send_view = new DataView(this.#send_buffer.buffer);

        /**
         * Scratch list for collecting "to be marked lost" outgoing seqs during
         * an inbound handler (avoids modifying the Set while iterating).
         * @type {number[]}
         * @private
         */
        this.#lost_scratch = [];

        /**
         * Fired when an outgoing seq has been positively acknowledged by the peer.
         * @type {Signal}
         */
        this.onPacketAcked = new Signal();

        /**
         * Fired when an outgoing seq has aged out of the ack window without being acked.
         * @type {Signal}
         */
        this.onPacketLost = new Signal();

        /**
         * Fired when an inbound packet's payload is ready (header already stripped).
         * Handler args: (payload: Uint8Array starting at offset 0, length: number).
         * The Uint8Array is owned by the Channel and may be reused after the handler returns.
         * @type {Signal}
         */
        this.onPayload = new Signal();
    }

    /**
     * Wrap and send a payload. Returns the seq number assigned to it.
     *
     * @param {Uint8Array} payload
     * @param {number} length payload length in bytes
     * @returns {number} seq
     */
    send(payload, length) {
        assert.isNonNegativeInteger(length, 'length');

        const total_size = HEADER_SIZE + length;
        if (total_size > this.#send_buffer.length) {
            this.#send_buffer = new Uint8Array(Math.max(total_size, this.#send_buffer.length * 2));
            this.#send_view = new DataView(this.#send_buffer.buffer);
        }

        const seq = this.#next_outgoing_seq;
        this.#next_outgoing_seq = seq16_advance(seq);

        const ack_latest = this.#latest_received_seq < 0 ? 0 : this.#latest_received_seq;
        const ack_bitfield = this.#latest_received_seq < 0
            ? 0
            : ack_bitfield_build(this.#latest_received_seq, s => this.#received_seqs.has(s));

        this.#send_view.setUint16(0, seq, true);
        this.#send_view.setUint16(2, ack_latest, true);
        this.#send_view.setUint32(4, ack_bitfield, true);
        if (length > 0) {
            this.#send_buffer.set(payload.subarray(0, length), HEADER_SIZE);
        }

        this.transport.send(this.#send_buffer, total_size);
        this.#unacked_outgoing.add(seq);

        return seq;
    }

    /**
     * Number of outgoing packets sent but not yet acked or marked lost.
     * @returns {number}
     */
    unacked_count() {
        return this.#unacked_outgoing.size;
    }

    /**
     * Most recent seq received from the peer, or -1 if none yet.
     * @returns {number}
     */
    latest_received_seq() {
        return this.#latest_received_seq;
    }

    /**
     * Test-only: seed the next outgoing seq number. Used to fast-forward
     * the seq counter so the seq16 wraparound can be exercised in
     * unit tests without sending 65k packets. Do not call from production.
     * @param {number} seq
     */
    seed_outgoing_seq_for_testing(seq) {
        this.#next_outgoing_seq = seq;
    }

    /**
     * @param {Uint8Array} bytes
     * @param {number} length
     * @private
     */
    #handle_inbound(bytes, length) {
        if (length < HEADER_SIZE) {
            // Malformed: too short to contain a header. Drop silently.
            return;
        }

        const view = new DataView(bytes.buffer, bytes.byteOffset, length);
        const seq = view.getUint16(0, true);
        const ack_latest = view.getUint16(2, true);
        const ack_bitfield = view.getUint32(4, true);

        // Record receipt of this seq.
        if (this.#latest_received_seq < 0 || seq16_greater_than(seq, this.#latest_received_seq)) {
            this.#latest_received_seq = seq;
        }
        this.#received_seqs.add(seq);
        this.#prune_received_seqs();

        // Mark each acknowledged outgoing seq.
        ack_bitfield_for_each(ack_latest, ack_bitfield, acked_seq => {
            if (this.#unacked_outgoing.delete(acked_seq)) {
                this.onPacketAcked.send1(acked_seq);
            }
        });

        // Anything older than (ack_latest - 32) that is still unacked is presumed lost.
        // Sanity: if `d` ever approaches half-range we're flying so many
        // unacked packets that distance is about to wrap to negative and the
        // loss-detection would silently break. Non-goal to support; assert.
        this.#lost_scratch.length = 0;
        for (const out_seq of this.#unacked_outgoing) {
            const d = seq16_distance(out_seq, ack_latest);
            assert.lessThan(d, SEQ16_HALF_RANGE / 2, `Channel: outgoing seq ${out_seq} is ${d} packets behind ack_latest ${ack_latest} — approaching seq16 half-range wraparound`);
            if (d > 32) {
                this.#lost_scratch.push(out_seq);
            }
        }
        for (let i = 0; i < this.#lost_scratch.length; i++) {
            const lost = this.#lost_scratch[i];
            this.#unacked_outgoing.delete(lost);
            this.onPacketLost.send1(lost);
        }

        // Strip header and surface the payload.
        const payload_length = length - HEADER_SIZE;
        const payload = bytes.subarray(HEADER_SIZE, HEADER_SIZE + payload_length);
        this.onPayload.send2(payload, payload_length);
    }

    /**
     * Detach this Channel from its transport. The transport.onReceive subscription
     * is removed so the Channel stops processing inbound bytes; the Channel's own
     * Signals are left in place (callers usually hold no references to them after
     * dispose, so they GC with the Channel). Required before discarding a Channel
     * whose transport will be reused — otherwise the old Channel keeps consuming
     * inbound packets in parallel with whatever replaces it.
     */
    dispose() {
        this.transport.onReceive.remove(this.#handle_inbound, this);
    }

    /**
     * Drop received-seq entries that are too old to matter for the next ack bitfield.
     * Keeps a window slightly larger than the 33-seq bitfield to absorb out-of-order arrival.
     * @private
     */
    #prune_received_seqs() {
        const latest = this.#latest_received_seq;
        if (latest < 0) return;
        // Window: 64 seqs back. Anything farther can be dropped.
        for (const s of this.#received_seqs) {
            if (seq16_distance(s, latest) > 64) {
                this.#received_seqs.delete(s);
            }
        }
    }
}
