export class WebRTCPeer {
    offBandComm;
    conn;
    isPolite;
    makingOffer = false;
    ignoreOffer = false;
    constructor(offBandComm, conn, isPolite) {
        this.offBandComm = offBandComm;
        this.conn = conn;
        this.isPolite = isPolite;
        this.conn.onnegotiationneeded = this.handleNegotiationNeeded.bind(this);
        this.conn.onicecandidate = ({ candidate }) => {
            if (candidate) {
                console.log(`ICE candidate:`, candidate.toJSON());
                this.offBandComm.send({ candidate });
            }
            else {
                console.log(`Falsy ice candidate:`, candidate);
            }
        };
        this.conn.oniceconnectionstatechange = () => {
            if (this.conn.iceConnectionState === 'failed') {
                this.conn.restartIce();
            }
        };
        this.conn.addEventListener('connectionstatechange', () => {
            const connState = this.conn.connectionState;
            if (connState === 'disconnected') {
                setTimeout(() => this.close());
            }
        });
        this.offBandComm.observeIncoming({
            next: this.handleOffBandSignal.bind(this)
        });
        Object.seal(this);
    }
    static makeWith(rtcConfig, offBandComm, startsCall) {
        const peer = new WebRTCPeer(offBandComm, new RTCPeerConnection(rtcConfig), startsCall);
        return peer;
    }
    async handleNegotiationNeeded() {
        try {
            this.makingOffer = true;
            await this.conn.setLocalDescription();
            this.offBandComm.send({
                description: this.conn.localDescription
            });
        }
        catch (err) {
            console.error(err);
        }
        finally {
            this.makingOffer = false;
        }
    }
    async handleOffBandSignal({ description, candidate }) {
        try {
            if (description) {
                const offerCollision = (description.type === "offer") &&
                    (this.makingOffer || (this.conn.signalingState !== "stable"));
                this.ignoreOffer = !this.isPolite && offerCollision;
                if (this.ignoreOffer) {
                    return;
                }
                await this.conn.setRemoteDescription(description);
                if (description.type === "offer") {
                    await this.conn.setLocalDescription();
                    this.offBandComm.send({
                        description: this.conn.localDescription
                    });
                }
            }
            else if (candidate) {
                try {
                    await this.conn.addIceCandidate(candidate);
                }
                catch (err) {
                    if (!this.ignoreOffer) {
                        throw err;
                    }
                }
            }
        }
        catch (err) {
            console.error(err);
        }
    }
    sendMediaStream(stream) {
        for (const track of stream.getTracks()) {
            this.conn.addTrack(track, stream);
        }
    }
    set ontrack(cb) {
        this.conn.addEventListener('track', cb);
    }
    set ondisconnected(cb) {
        this.conn.addEventListener('connectionstatechange', () => {
            const connState = this.conn.connectionState;
            if (connState === 'disconnected') {
                cb();
            }
        });
    }
    async close() {
        try {
            this.conn.close();
            this.offBandComm.close();
        }
        catch (err) { }
    }
}
export function msgToJSON(msg) {
    const { candidate, description } = msg;
    if (candidate) {
        return { candidate: candidate.toJSON() };
    }
    else if (description) {
        return { description: description.toJSON() };
    }
    else {
        throw new Error(`Message has no relevant fields.`);
    }
}
export function msgFromJSON({ candidate, description }) {
    if (candidate) {
        return {
            candidate: new RTCIceCandidate(candidate)
        };
    }
    else if (description) {
        return {
            description: new RTCSessionDescription(description)
        };
    }
    else {
        throw new Error(`JSON has no relevant fields`);
    }
}
