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

/**
 * Transport adapter that wraps a browser `RTCDataChannel` configured for
 * unreliable, unordered delivery — the only sane low-latency channel on the web.
 *
 * The data channel must be created with `{ ordered: false, maxRetransmits: 0 }`
 * and reach `readyState === "open"` before this adapter is used. Channel setup
 * (negotiating the `RTCPeerConnection`, ICE, signalling) is the application's
 * responsibility; this adapter only handles byte transport.
 *
 * Duck-typed against `RTCDataChannel` so the same adapter works with mocks in
 * tests. The expected interface is:
 *   - `binaryType` (writable; we set it to `'arraybuffer'`)
 *   - `addEventListener(type, handler)` for `'message'`, `'close'`, `'error'`
 *   - `send(ArrayBuffer)`
 *   - `close()`
 *   - `readyState` (read in some debug paths)
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class WebRTCDataChannelTransport extends Transport {
    #stats;
    #on_message;
    #on_close;
    #on_error;
    #disconnect_fired;


    /**
     * @param {{ data_channel: RTCDataChannel }} options
     */
    constructor({ data_channel }) {
        super();
        assert.ok(data_channel && typeof data_channel.send === 'function', 'data_channel must implement send()');

        /** @type {RTCDataChannel} */
        this.data_channel = data_channel;
        this.data_channel.binaryType = 'arraybuffer';

        /** @private */
        this.#stats = { bytes_in: 0, bytes_out: 0, packets_in: 0, packets_out: 0 };

        // RTCDataChannel may fire 'close' and 'error' separately for the same
        // failure (browsers vary). Latch on the first to avoid double-dispatch.
        /** @private */
        this.#disconnect_fired = false;

        /** @private */
        this.#on_message = (event) => {
            // event.data is an ArrayBuffer because we set binaryType above.
            const ab = event.data;
            const bytes = new Uint8Array(ab);
            this.#stats.bytes_in += bytes.length;
            this.#stats.packets_in += 1;
            this.onReceive.send2(bytes, bytes.length);
        };

        /** @private */
        this.#on_close = () => {
            if (this.#disconnect_fired) return;
            this.#disconnect_fired = true;
            this.onDisconnect.send1('channel_closed');
        };

        /** @private */
        this.#on_error = (event) => {
            if (this.#disconnect_fired) return;
            this.#disconnect_fired = true;
            const reason = event && event.error && event.error.message
                ? event.error.message
                : 'channel_error';
            this.onDisconnect.send1(reason);
        };

        this.data_channel.addEventListener('message', this.#on_message);
        this.data_channel.addEventListener('close', this.#on_close);
        this.data_channel.addEventListener('error', this.#on_error);
    }

    /**
     * @param {Uint8Array} bytes
     * @param {number} length
     */
    send(bytes, length) {
        assert.isNonNegativeInteger(length, 'length');
        // RTCDataChannel.send wants the exact bytes; if the source view extends past `length`,
        // we need to slice. The slice copies, which is unavoidable for over-MTU correctness.
        if (length === 0) return;
        const payload = (length === bytes.byteLength)
            ? bytes
            : bytes.subarray(0, length);
        // .send accepts ArrayBufferView; pass the typed array directly.
        this.data_channel.send(payload);
        this.#stats.bytes_out += length;
        this.#stats.packets_out += 1;
    }

    /**
     * Close the underlying channel. After this, sends are no-ops at the channel level.
     *
     * Locally-initiated disconnect: we suppress the channel's own `close`
     * event from firing `onDisconnect` again — callers who explicitly
     * disconnect don't expect a notification echo.
     */
    disconnect() {
        this.#disconnect_fired = true;
        this.data_channel.removeEventListener('message', this.#on_message);
        this.data_channel.removeEventListener('close', this.#on_close);
        this.data_channel.removeEventListener('error', this.#on_error);
        try {
            this.data_channel.close();
        } catch (e) {
            // Channel may already be closed; ignore.
        }
    }

    /**
     * @returns {{bytes_in: number, bytes_out: number, packets_in: number, packets_out: number}}
     */
    getStats() {
        return {
            bytes_in: this.#stats.bytes_in,
            bytes_out: this.#stats.bytes_out,
            packets_in: this.#stats.packets_in,
            packets_out: this.#stats.packets_out,
        };
    }
}
