import { areAddressesEqual, sendMsg, toCanonicalAddress, watchIncomingMsgs } from "./asmail-utils.js";
export const webrtcCAP = {
    addCallSignalingChannel: (peerAddr, callId, callStartMsg) => signalingRouter.addCallSignalingChannel(peerAddr, callId, callStartMsg),
    getSignalingChannel: async (peerAddr, callId) => {
        const { callStartedAt, channel } = await signalingRouter.getSignalingChannel(peerAddr, callId);
        return {
            callStartedAt,
            rtcConfig: {
                iceServers: [{
                        urls: ['stun:stunserver2024.stunprotocol.org:3478']
                    }],
                iceTransportPolicy: 'all'
            },
            channel
        };
    }
};
class WebRTCSignalingRouter {
    buffer = new MsgBuffer();
    channels = new Channels();
    async startWatchingMsgs() {
        const msgSrc = watchIncomingMsgs('webrtc-signaling').getReader();
        while (true) {
            const { done, value: msg } = await msgSrc.read();
            if (done) {
                return;
            }
            const webrtcJSON = validWebRTCMsgFrom(msg.jsonBody);
            if (msg.establishedSenderKeyChain && webrtcJSON) {
                const peerAddr = msg.sender;
                const { callId, signal } = webrtcJSON;
                const channel = this.channels.get(peerAddr, callId);
                if (channel) {
                    channel.handleMsgFromPeer(signal);
                    this.dropMsg(msg.msgId);
                }
                else {
                    this.buffer.add(peerAddr, callId, signal);
                }
            }
            this.dropMsg(msg.msgId);
        }
    }
    async dropMsg(msgId) {
        try {
            // XXX immediate drop will race with core's reading ASMail messages on
            //     other clients, some time delay mechanism can be introduced
            await w3n.mail.inbox.removeMsg(msgId);
        }
        catch (err) {
        }
    }
    async addCallSignalingChannel(peerAddr, callId, startMsg) {
        this.channels.reserveChannel(peerAddr, callId, {
            origin: (startMsg ? 'local' : 'remote'),
            startMsg
        });
    }
    async getSignalingChannel(peerAddr, callId) {
        const callStartedAt = this.channels.getCallOrigin(peerAddr, callId);
        const channel = new Channel(peerAddr, callId, async () => this.channels.remove(channel), this.buffer.drain(peerAddr, callId), ((callStartedAt === 'local') ? 200 : 500));
        this.channels.add(channel);
        return { callStartedAt, channel: channel.wrap() };
    }
}
function validWebRTCMsgFrom(json) {
    if (!json && (typeof json !== 'object')
        || !Number.isInteger(json.callId)) {
        return;
    }
    if (Array.isArray(json.signal)) {
        if (json.signal.length > 0) {
            for (const s of json.signal) {
                if (!isOffBandMessage(s)) {
                    return;
                }
            }
            return json;
        }
        else {
            return;
        }
    }
    else if (json.signal && (typeof json.signal === 'object')) {
        return (isOffBandMessage(json.signal) ? json : undefined);
    }
}
function isOffBandMessage(o) {
    if (!o || (typeof o !== 'object')) {
        return false;
    }
    if (o.candidate) {
        return true;
    }
    else if (o.description) {
        return true;
    }
    return false;
}
class MsgBuffer {
    msgs = [];
    add(peerAddr, callId, signal) {
        if (Array.isArray(signal)) {
            for (const s of signal) {
                this.msgs.push({ peerAddr, callId, signal: s });
            }
        }
        else {
            this.msgs.push({ peerAddr, callId, signal });
        }
    }
    drain(peerAddr, callId) {
        const buffer = [];
        for (let i = this.msgs.length - 1; i >= 0; i -= 1) {
            const m = this.msgs[i];
            if ((m.callId === callId) && areAddressesEqual(m.peerAddr, peerAddr)) {
                buffer.push(m.signal);
                this.msgs.splice(i, 1);
            }
        }
        return buffer.reverse();
    }
}
class Channels {
    channels = [];
    newChannels = new Map();
    get(peerAddr, callId) {
        return this.channels.find(c => ((c.callId === callId) && areAddressesEqual(c.peerAddr, peerAddr)));
    }
    identityStrFor(peerAddr, callId) {
        return `${toCanonicalAddress(peerAddr)}#${callId}`;
    }
    reserveChannel(peerAddr, callId, reservation) {
        if (this.get(peerAddr, callId)) {
            throw new Error('Call channel is already opened');
        }
        const cid = this.identityStrFor(peerAddr, callId);
        this.newChannels.set(cid, reservation);
    }
    getCallOrigin(peerAddr, callId) {
        const cid = this.identityStrFor(peerAddr, callId);
        const reservation = this.newChannels.get(cid);
        if (reservation) {
            return reservation.origin;
        }
        else {
            throw new Error(`Channel wasn't reserved`);
        }
    }
    add(channel) {
        const cid = this.identityStrFor(channel.peerAddr, channel.callId);
        if (!this.newChannels.delete(cid)) {
            throw new Error(`Channel wasn't reserved`);
        }
        if (this.get(channel.peerAddr, channel.callId)) {
            throw new Error(`Channel aleady exists for pair of call id + addr`);
        }
        this.channels.push(channel);
    }
    remove(channel) {
        const ind = this.channels.indexOf(channel);
        if (ind > -1) {
            this.channels.splice(ind, 1);
        }
    }
}
class Channel {
    peerAddr;
    callId;
    close;
    bufferedMsgs;
    obs = undefined;
    sendingBuffer = undefined;
    sendingProc = undefined;
    constructor(peerAddr, callId, close, bufferedMsgs, delaySendingFirstMsgs) {
        this.peerAddr = peerAddr;
        this.callId = callId;
        this.close = close;
        this.bufferedMsgs = bufferedMsgs;
        if (delaySendingFirstMsgs) {
            this.sendingBuffer = [];
            setTimeout(() => {
                const msgs = this.sendingBuffer;
                this.sendingBuffer = undefined;
                this.sendMsgToPeer(msgs);
            }, delaySendingFirstMsgs);
        }
    }
    handleMsgFromPeer(msg) {
        if (this.bufferedMsgs) {
            if (Array.isArray(msg)) {
                this.bufferedMsgs.push(...msg);
            }
            else {
                this.bufferedMsgs.push(msg);
            }
        }
        else if (this.obs?.next) {
            if (Array.isArray(msg)) {
                for (const m of msg) {
                    this.obs.next(m);
                }
            }
            else {
                this.obs.next(msg);
            }
        }
    }
    async handleBufferedMsg() {
        if (!this.bufferedMsgs) {
            return;
        }
        const msg = this.bufferedMsgs.shift();
        if (this.bufferedMsgs.length === 0) {
            this.bufferedMsgs = undefined;
        }
        if (!msg) {
            return;
        }
        if (this.obs?.next) {
            this.obs.next(msg);
        }
        setTimeout(() => this.handleBufferedMsg());
    }
    wrap() {
        const close = () => {
            this.bufferedMsgs = undefined;
            this.obs = undefined;
            this.close();
        };
        return {
            observeIncoming: obs => {
                if (this.obs) {
                    throw new Error(`Channel already has observer set`);
                }
                else {
                    this.obs = obs;
                    setTimeout(() => this.handleBufferedMsg());
                }
                return close;
            },
            send: async (msg) => {
                if (this.sendingBuffer) {
                    this.sendingBuffer.push(msg);
                }
                else {
                    await this.sendMsgToPeer(msg);
                }
            },
            close: async () => close()
        };
    }
    async sendMsgToPeer(msg) {
        while (this.sendingProc) {
            await this.sendingProc;
        }
        const proc = sendMsg({
            msgType: 'webrtc-signaling',
            recipients: [this.peerAddr],
            jsonBody: {
                callId: this.callId,
                signal: msg
            }
        });
        const clearSendingProc = () => { this.sendingProc = undefined; };
        this.sendingProc = proc.then(clearSendingProc, clearSendingProc);
        await proc;
    }
}
const signalingRouter = new WebRTCSignalingRouter();
signalingRouter.startWatchingMsgs().catch(err => console.error(err));
