import { EventTarget, Event, defineEventAttribute } from 'event-target-shim/index';
import { NativeModules } from 'react-native';

import { addListener, removeListener } from './EventEmitter';
import Logger from './Logger';
import MediaStream from './MediaStream';
import MediaStreamTrack from './MediaStreamTrack';
import MediaStreamTrackEvent from './MediaStreamTrackEvent';
import RTCDataChannel from './RTCDataChannel';
import RTCDataChannelEvent from './RTCDataChannelEvent';
import RTCIceCandidate from './RTCIceCandidate';
import RTCIceCandidateEvent from './RTCIceCandidateEvent';
import RTCRtpReceiveParameters from './RTCRtpReceiveParameters';
import RTCRtpReceiver from './RTCRtpReceiver';
import RTCRtpSendParameters from './RTCRtpSendParameters';
import RTCRtpSender from './RTCRtpSender';
import RTCRtpTransceiver from './RTCRtpTransceiver';
import RTCSessionDescription, { RTCSessionDescriptionInit } from './RTCSessionDescription';
import RTCTrackEvent from './RTCTrackEvent';
import * as RTCUtil from './RTCUtil';

const log = new Logger('pc');
const { WebRTCModule } = NativeModules;

type RTCSignalingState =
    | 'stable'
    | 'have-local-offer'
    | 'have-remote-offer'
    | 'have-local-pranswer'
    | 'have-remote-pranswer'
    | 'closed';

type RTCIceGatheringState = 'new' | 'gathering' | 'complete';

type RTCPeerConnectionState = 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';

type RTCIceConnectionState = 'new' | 'checking' | 'connected' | 'completed' | 'failed' | 'disconnected' | 'closed';

type RTCDataChannelInit = {
    ordered?: boolean,
    maxPacketLifeTime?: number,
    maxRetransmits?: number,
    protocol?: string,
    negotiated?: boolean,
    id?: number
};

type RTCIceServer = {
    credential?: string,
    url?: string, // Deprecated.
    urls?: string | string[],
    username?: string
};

type RTCConfiguration = {
    bundlePolicy?: 'balanced' | 'max-compat' | 'max-bundle',
    iceCandidatePoolSize?: number,
    iceServers?: RTCIceServer[],
    iceTransportPolicy?: 'all' | 'relay',
    rtcpMuxPolicy?: 'negotiate' | 'require'
};

type RTCPeerConnectionEventMap = {
    connectionstatechange: Event<'connectionstatechange'>
    icecandidate: RTCIceCandidateEvent<'icecandidate'>
    icecandidateerror: RTCIceCandidateEvent<'icecandidateerror'>
    iceconnectionstatechange: Event<'iceconnectionstatechange'>
    icegatheringstatechange: Event<'icegatheringstatechange'>
    negotiationneeded: Event<'negotiationneeded'>
    signalingstatechange: Event<'signalingstatechange'>
    datachannel: RTCDataChannelEvent<'datachannel'>
    track: RTCTrackEvent<'track'>
    error: Event<'error'>
}

let nextPeerConnectionId = 0;

export default class RTCPeerConnection extends EventTarget<RTCPeerConnectionEventMap> {
    localDescription: RTCSessionDescription | null = null;
    remoteDescription: RTCSessionDescription | null = null;
    signalingState: RTCSignalingState = 'stable';
    iceGatheringState: RTCIceGatheringState = 'new';
    connectionState: RTCPeerConnectionState = 'new';
    iceConnectionState: RTCIceConnectionState = 'new';

    _pcId: number;
    _transceivers: { order: number, transceiver: RTCRtpTransceiver }[];
    _remoteStreams: Map<string, MediaStream>;
    _pendingTrackEvents: any[];

    constructor(configuration?: RTCConfiguration) {
        super();

        this._pcId = nextPeerConnectionId++;

        // Sanitize ICE servers.
        if (configuration) {
            const servers = configuration?.iceServers ?? [];

            for (const server of servers) {
                let urls = server.url || server.urls;

                delete server.url;
                delete server.urls;

                if (!urls) {
                    continue;
                }

                if (!Array.isArray(urls)) {
                    urls = [ urls ];
                }

                // Native WebRTC does case sensitive parsing.
                server.urls = urls.map(url => url.toLowerCase());
            }

            // Filter out bogus servers.
            configuration.iceServers = servers.filter(s => s.urls);
        }

        if (!WebRTCModule.peerConnectionInit(configuration, this._pcId)) {
            throw new Error('Failed to initialize PeerConnection, check the native logs!');
        }

        this._transceivers = [];
        this._remoteStreams = new Map();
        this._pendingTrackEvents = [];

        this._registerEvents();

        log.debug(`${this._pcId} ctor`);
    }

    async createOffer(options) {
        log.debug(`${this._pcId} createOffer`);

        const {
            sdpInfo,
            newTransceivers,
            transceiversInfo
        } = await WebRTCModule.peerConnectionCreateOffer(this._pcId, RTCUtil.normalizeOfferOptions(options));

        log.debug(`${this._pcId} createOffer OK`);

        newTransceivers?.forEach(t => {
            const { transceiverOrder, transceiver } = t;
            const newSender = new RTCRtpSender({ ...transceiver.sender, track: null });
            const remoteTrack
                = transceiver.receiver.track ? new MediaStreamTrack(transceiver.receiver.track) : null;
            const newReceiver = new RTCRtpReceiver({ ...transceiver.receiver, track: remoteTrack });
            const newTransceiver = new RTCRtpTransceiver({
                ...transceiver,
                sender: newSender,
                receiver: newReceiver,
            });

            this._insertTransceiverSorted(transceiverOrder, newTransceiver);
        });

        this._updateTransceivers(transceiversInfo);

        return sdpInfo;
    }

    async createAnswer() {
        log.debug(`${this._pcId} createAnswer`);

        const {
            sdpInfo,
            transceiversInfo
        } = await WebRTCModule.peerConnectionCreateAnswer(this._pcId, {});

        this._updateTransceivers(transceiversInfo);

        return sdpInfo;
    }

    setConfiguration(configuration): void {
        WebRTCModule.peerConnectionSetConfiguration(configuration, this._pcId);
    }

    async setLocalDescription(sessionDescription?: RTCSessionDescription | RTCSessionDescriptionInit): Promise<void> {
        log.debug(`${this._pcId} setLocalDescription`);

        let desc;

        if (sessionDescription) {
            desc = {
                type: sessionDescription.type,
                sdp: sessionDescription.sdp ?? ''
            };

            if (!RTCUtil.isSdpTypeValid(desc.type)) {
                throw new Error(`Invalid session description: invalid type: ${desc.type}`);
            }
        } else {
            desc = null;
        }

        const {
            sdpInfo,
            transceiversInfo
        } = await WebRTCModule.peerConnectionSetLocalDescription(this._pcId, desc);

        if (sdpInfo.type && sdpInfo.sdp) {
            this.localDescription = new RTCSessionDescription(sdpInfo);
        } else {
            this.localDescription = null;
        }

        this._updateTransceivers(transceiversInfo, /* removeStopped */ desc?.type === 'answer');

        log.debug(`${this._pcId} setLocalDescription OK`);
    }

    async setRemoteDescription(sessionDescription: RTCSessionDescription | RTCSessionDescriptionInit): Promise<void> {
        log.debug(`${this._pcId} setRemoteDescription`);

        if (!sessionDescription) {
            return Promise.reject(new Error('No session description provided'));
        }

        const desc = {
            type: sessionDescription.type,
            sdp: sessionDescription.sdp ?? ''
        };

        if (!RTCUtil.isSdpTypeValid(desc.type ?? '')) {
            throw new Error(`Invalid session description: invalid type: ${desc.type}`);
        }

        const {
            sdpInfo,
            newTransceivers,
            transceiversInfo
        } = await WebRTCModule.peerConnectionSetRemoteDescription(this._pcId, desc);

        if (sdpInfo.type && sdpInfo.sdp) {
            this.remoteDescription = new RTCSessionDescription(sdpInfo);
        } else {
            this.remoteDescription = null;
        }

        newTransceivers?.forEach(t => {
            const { transceiverOrder, transceiver } = t;
            const newSender = new RTCRtpSender({ ...transceiver.sender, track: null });
            const remoteTrack
                = transceiver.receiver.track ? new MediaStreamTrack(transceiver.receiver.track) : null;
            const newReceiver = new RTCRtpReceiver({ ...transceiver.receiver, track: remoteTrack });
            const newTransceiver = new RTCRtpTransceiver({
                ...transceiver,
                sender: newSender,
                receiver: newReceiver,
            });

            this._insertTransceiverSorted(transceiverOrder, newTransceiver);
        });

        this._updateTransceivers(transceiversInfo, /* removeStopped */ desc.type === 'answer');

        // Fire track events. They must fire before sRD resolves.
        const pendingTrackEvents = this._pendingTrackEvents;

        this._pendingTrackEvents = [];

        for (const ev of pendingTrackEvents) {
            const [ transceiver ] = this
                .getTransceivers()
                .filter(t => t.receiver.id ===  ev.receiver.id);

            // We need to fire this event for an existing track sometimes, like
            // when the transceiver direction (on the sending side) switches from
            // sendrecv to recvonly and then back.

            // @ts-ignore
            const track: MediaStreamTrack = transceiver.receiver.track;

            transceiver._mid = ev.transceiver.mid;
            transceiver._currentDirection = ev.transceiver.currentDirection;
            transceiver._direction = ev.transceiver.direction;

            // Get the stream object from the event. Create if necessary.
            const streams: MediaStream[] = ev.streams.map(streamInfo => {
                // Here we are making sure that we don't create stream objects that already exist
                // So that event listeners do get the same object if it has been created before.
                if (!this._remoteStreams.has(streamInfo.streamId)) {
                    const stream = new MediaStream({
                        streamId: streamInfo.streamId,
                        streamReactTag: streamInfo.streamReactTag,
                        tracks: []
                    });

                    this._remoteStreams.set(streamInfo.streamId, stream);
                }

                const stream = this._remoteStreams.get(streamInfo.streamId);

                if (!stream?._tracks.includes(track)) {
                    stream?._tracks.push(track);
                }

                return stream;
            });

            const eventData = {
                streams,
                transceiver,
                track,
                receiver: transceiver.receiver
            };


            this.dispatchEvent(new RTCTrackEvent('track', eventData));

            streams.forEach(stream => {
                stream.dispatchEvent(new MediaStreamTrackEvent('addtrack', { track }));
            });

            // Dispatch an unmute event for the track.
            track._setMutedInternal(false);
        }

        log.debug(`${this._pcId} setRemoteDescription OK`);
    }

    async addIceCandidate(candidate): Promise<void> {
        log.debug(`${this._pcId} addIceCandidate`);

        if (!candidate || !candidate.candidate) {
            // XXX end-of candidates is not implemented: https://bugs.chromium.org/p/webrtc/issues/detail?id=9218
            return;
        }

        if ((candidate.sdpMLineIndex === null ||
             candidate.sdpMLineIndex === undefined) &&
            (candidate.sdpMid === null ||
             candidate.sdpMid === undefined)
        ) {
            throw new TypeError('`sdpMLineIndex` and `sdpMid` must not be both null or undefined');
        }

        const newSdp = await WebRTCModule.peerConnectionAddICECandidate(
            this._pcId,
            RTCUtil.deepClone(candidate)
        );

        this.remoteDescription = new RTCSessionDescription(newSdp);
    }

    /**
     * @brief Adds a new track to the {@link RTCPeerConnection},
     * and indicates that it is contained in the specified {@link MediaStream}s.
     * This method has to be synchronous as the W3C API expects a track to be returned
     * @param {MediaStreamTrack} track The track to be added
     * @param {...MediaStream} streams One or more {@link MediaStream}s the track needs to be added to
     * https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-addtrack
     */
    addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender {
        log.debug(`${this._pcId} addTrack`);

        if (this.connectionState === 'closed') {
            throw new Error('Peer Connection is closed');
        }

        if (this._trackExists(track)) {
            throw new Error('Track already exists in a sender');
        }

        const streamIds = streams.map(s => s.id);
        const result = WebRTCModule.peerConnectionAddTrack(this._pcId, track.id, { streamIds });

        if (result === null) {
            throw new Error('Could not add sender');
        }

        const { transceiverOrder, transceiver, sender } = result;

        // According to the W3C docs, the sender could have been reused, and
        // so we check if that is the case, and update accordingly.
        const [ existingSender ] = this
            .getSenders()
            .filter(s => s.id === sender.id);

        if (existingSender) {
            // Update sender
            existingSender._track = track;

            // Update the corresponding transceiver as well
            const [ existingTransceiver ] = this
                .getTransceivers()
                .filter(t => t.sender.id === existingSender.id);

            existingTransceiver._direction = transceiver.direction;
            existingTransceiver._currentDirection = transceiver.currentDirection;

            return existingSender;
        }

        // This is a new transceiver, should create a transceiver for it and add it
        const newSender = new RTCRtpSender({ ...transceiver.sender, track });
        const remoteTrack = transceiver.receiver.track ? new MediaStreamTrack(transceiver.receiver.track) : null;
        const newReceiver = new RTCRtpReceiver({ ...transceiver.receiver, track: remoteTrack });
        const newTransceiver = new RTCRtpTransceiver({
            ...transceiver,
            sender: newSender,
            receiver: newReceiver,
        });

        this._insertTransceiverSorted(transceiverOrder, newTransceiver);

        return newSender;
    }

    addTransceiver(source: 'audio' | 'video' | MediaStreamTrack, init): RTCRtpTransceiver {
        log.debug(`${this._pcId} addTransceiver`);

        let src = {};

        if (source === 'audio') {
            src = { type: 'audio' };
        } else if (source === 'video') {
            src = { type: 'video' };
        } else {
            src = { trackId: source.id };
        }

        // Extract the stream ids
        if (init && init.streams) {
            init.streamIds = init.streams.map(stream => stream.id);
        }

        const result = WebRTCModule.peerConnectionAddTransceiver(this._pcId, { ...src, init: { ...init } });

        if (result === null) {
            throw new Error('Transceiver could not be added');
        }

        const t = result.transceiver;
        let track: MediaStreamTrack | null = null;

        if (typeof source === 'string') {
            if (t.sender.track) {
                track = new MediaStreamTrack(t.sender.track);
            }
        } else {
            // 'source' is a MediaStreamTrack
            track = source;
        }

        const sender = new RTCRtpSender({ ...t.sender, track });
        const remoteTrack = t.receiver.track ? new MediaStreamTrack(t.receiver.track) : null;
        const receiver = new RTCRtpReceiver({ ...t.receiver, track: remoteTrack });
        const transceiver = new RTCRtpTransceiver({
            ...result.transceiver,
            sender,
            receiver
        });

        this._insertTransceiverSorted(result.transceiverOrder, transceiver);

        return transceiver;
    }

    removeTrack(sender: RTCRtpSender) {
        log.debug(`${this._pcId} removeTrack`);

        if (this._pcId !== sender._peerConnectionId) {
            throw new Error('Sender does not belong to this peer connection');
        }

        if (this.connectionState === 'closed') {
            throw new Error('Peer Connection is closed');
        }

        const existingSender = this
            .getSenders()
            .find(s => s === sender);

        if (!existingSender) {
            throw new Error('Sender does not exist');
        }

        if (existingSender.track === null) {
            return;
        }

        // Blocking!
        WebRTCModule.peerConnectionRemoveTrack(this._pcId, sender.id);

        existingSender._track = null;

        const [ existingTransceiver ] = this
            .getTransceivers()
            .filter(t => t.sender.id === existingSender.id);

        existingTransceiver._direction = existingTransceiver.direction === 'sendrecv' ? 'recvonly' : 'inactive';
    }

    async getStats(selector?: MediaStreamTrack) {
        log.debug(`${this._pcId} getStats`);

        if (!selector) {
            const data = await WebRTCModule.peerConnectionGetStats(this._pcId);

            /**
             * On both Android and iOS it is faster to construct a single
             * JSON string representing the Map of StatsReports and have it
             * pass through the React Native bridge rather than the Map of
             * StatsReports. While the implementations do try to be faster in
             * general, the stress is on being faster to pass through the React
             * Native bridge which is a bottleneck that tends to be visible in
             * the UI when there is congestion involving UI-related passing.
             */
            return new Map(JSON.parse(data));
        } else {
            const senders = this.getSenders().filter(s => s.track === selector);
            const receivers = this.getReceivers().filter(r => r.track === selector);
            const matches = senders.length + receivers.length;

            if (matches === 0) {
                throw new Error('Invalid selector: could not find matching sender / receiver');
            } else if (matches > 1) {
                throw new Error('Invalid selector: multiple matching senders / receivers');
            } else {
                const sr = senders[0] || receivers[0];

                return sr.getStats();
            }
        }
    }

    getTransceivers(): RTCRtpTransceiver[] {
        return this._transceivers.map(e => e.transceiver);
    }

    getSenders(): RTCRtpSender[] {
        // @ts-ignore
        return this._transceivers.map(e => !e.transceiver.stopped && e.transceiver.sender).filter(Boolean);
    }

    getReceivers(): RTCRtpReceiver[] {
        // @ts-ignore
        return this._transceivers.map(e => !e.transceiver.stopped && e.transceiver.receiver).filter(Boolean);
    }

    close(): void {
        log.debug(`${this._pcId} close`);

        if (this.connectionState === 'closed') {
            return;
        }

        WebRTCModule.peerConnectionClose(this._pcId);

        // Mark transceivers as stopped.
        this._transceivers.forEach(({ transceiver })=> {
            transceiver._setStopped();
        });
    }

    restartIce(): void {
        WebRTCModule.peerConnectionRestartIce(this._pcId);
    }

    _registerEvents(): void {
        addListener(this, 'peerConnectionOnRenegotiationNeeded', (ev: any) => {
            if (ev.pcId !== this._pcId) {
                return;
            }

            this.dispatchEvent(new Event('negotiationneeded'));
        });

        addListener(this, 'peerConnectionIceConnectionChanged', (ev: any) => {
            if (ev.pcId !== this._pcId) {
                return;
            }

            this.iceConnectionState = ev.iceConnectionState;

            this.dispatchEvent(new Event('iceconnectionstatechange'));
        });

        addListener(this, 'peerConnectionStateChanged', (ev: any) => {
            if (ev.pcId !== this._pcId) {
                return;
            }

            this.connectionState = ev.connectionState;

            this.dispatchEvent(new Event('connectionstatechange'));

            if (ev.connectionState === 'closed') {
                // This PeerConnection is done, clean up.
                removeListener(this);

                WebRTCModule.peerConnectionDispose(this._pcId);
            }
        });

        addListener(this, 'peerConnectionSignalingStateChanged', (ev: any) => {
            if (ev.pcId !== this._pcId) {
                return;
            }

            this.signalingState = ev.signalingState;

            this.dispatchEvent(new Event('signalingstatechange'));
        });

        // Consider moving away from this event: https://github.com/WebKit/WebKit/pull/3953
        addListener(this, 'peerConnectionOnTrack', (ev: any) => {
            if (ev.pcId !== this._pcId) {
                return;
            }

            log.debug(`${this._pcId} ontrack`);

            // NOTE: We need to make sure the track event fires right before sRD completes,
            // so we queue them up here and dispatch the events when sRD fires, but before completing it.
            // In the future we should probably implement out own logic and drop this event altogether.
            this._pendingTrackEvents.push(ev);
        });

        addListener(this, 'peerConnectionOnRemoveTrack', (ev: any) => {
            if (ev.pcId !== this._pcId) {
                return;
            }

            log.debug(`${this._pcId} onremovetrack ${ev.receiverId}`);

            const receiver = this.getReceivers().find(r => r.id === ev.receiverId);
            const track = receiver?.track;

            if (receiver && track) {
                // As per the spec:
                // - Remove the track from any media streams that were previously passed to the `track` event.
                // https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-removetrack,
                // - Mark the track as muted:
                // https://w3c.github.io/webrtc-pc/#process-remote-track-removal
                for (const stream of this._remoteStreams.values()) {
                    if (stream._tracks.includes(track)) {
                        const trackIdx = stream._tracks.indexOf(track);

                        log.debug(`${this._pcId} removetrack ${track.id}`);

                        stream._tracks.splice(trackIdx, 1);

                        stream.dispatchEvent(new MediaStreamTrackEvent('removetrack', { track }));

                        // Dispatch a mute event for the track.
                        track._setMutedInternal(true);
                    }
                }
            }
        });

        addListener(this, 'peerConnectionGotICECandidate', (ev: any) => {
            if (ev.pcId !== this._pcId) {
                return;
            }

            const sdpInfo = ev.sdp;

            // Can happen when doing a rollback.
            if (sdpInfo.type && sdpInfo.sdp) {
                this.localDescription = new RTCSessionDescription(sdpInfo);
            } else {
                this.localDescription = null;
            }

            const candidate = new RTCIceCandidate(ev.candidate);

            this.dispatchEvent(new RTCIceCandidateEvent('icecandidate', { candidate }));
        });

        addListener(this, 'peerConnectionIceGatheringChanged', (ev: any) => {
            if (ev.pcId !== this._pcId) {
                return;
            }

            this.iceGatheringState = ev.iceGatheringState;

            if (this.iceGatheringState === 'complete') {
                const sdpInfo = ev.sdp;

                // Can happen when doing a rollback.
                if (sdpInfo.type && sdpInfo.sdp) {
                    this.localDescription = new RTCSessionDescription(sdpInfo);
                } else {
                    this.localDescription = null;
                }

                this.dispatchEvent(new RTCIceCandidateEvent('icecandidate', { candidate: null }));
            }

            this.dispatchEvent(new Event('icegatheringstatechange'));
        });

        addListener(this, 'peerConnectionDidOpenDataChannel', (ev: any) => {
            if (ev.pcId !== this._pcId) {
                return;
            }

            const channel = new RTCDataChannel(ev.dataChannel);

            this.dispatchEvent(new RTCDataChannelEvent('datachannel', { channel }));

            // Send 'open' event. Native doesn't update the state since it's already
            // set at this point.
            channel.dispatchEvent(new RTCDataChannelEvent('open', { channel }));
        });

        addListener(this, 'mediaStreamTrackMuteChanged', (ev: any) => {
            if (ev.pcId !== this._pcId) {
                return;
            }

            const [
                track
            ] = this.getReceivers().map(r => r.track).filter(t => t?.id === ev.trackId);

            if (track) {
                track._setMutedInternal(ev.muted);
            }
        });
    }

    /**
     * Creates a new RTCDataChannel object with the given label. The
     * RTCDataChannelInit dictionary can be used to configure properties of the
     * underlying channel such as data reliability.
     *
     * @param {string} label - the value with which the label attribute of the new
     * instance is to be initialized
     * @param {RTCDataChannelInit} dataChannelDict - an optional dictionary of
     * values with which to initialize corresponding attributes of the new
     * instance such as id
     */
    createDataChannel(label: string, dataChannelDict?: RTCDataChannelInit): RTCDataChannel {
        if (arguments.length === 0) {
            throw new TypeError('1 argument required, but 0 present');
        }

        if (dataChannelDict && 'id' in dataChannelDict) {
            const id = dataChannelDict.id;

            if (typeof id !== 'number') {
                throw new TypeError('DataChannel id must be a number: ' + id);
            }
        }

        const channelInfo = WebRTCModule.createDataChannel(this._pcId, String(label), dataChannelDict);

        if (channelInfo === null) {
            throw new TypeError('Failed to create new DataChannel');
        }

        return new RTCDataChannel(channelInfo);
    }

    /**
     * Check whether a media stream track exists already in a sender.
     * See https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-addtrack for more information
     */
    _trackExists(track: MediaStreamTrack): boolean {
        const [ sender ] = this
            .getSenders()
            .filter(
                sender => sender.track?.id === track.id
            );

        return sender? true : false;
    }

    /**
     * Updates transceivers after offer/answer updates if necessary.
     */
    _updateTransceivers(transceiverUpdates, removeStopped = false) {
        for (const update of transceiverUpdates) {
            const [ transceiver ] = this
                .getTransceivers()
                .filter(t => t.sender.id === update.transceiverId);

            if (!transceiver) {
                continue;
            }

            if (update.currentDirection) {
                transceiver._currentDirection = update.currentDirection;
            }

            transceiver._mid = update.mid;
            transceiver._stopped = Boolean(update.isStopped);
            transceiver._sender._rtpParameters = new RTCRtpSendParameters(update.senderRtpParameters);
            transceiver._receiver._rtpParameters = new RTCRtpReceiveParameters(update.receiverRtpParameters);
        }

        if (removeStopped) {
            const stopped = this.getTransceivers().filter(t => t.stopped);
            const newTransceivers = this._transceivers.filter(t => !stopped.includes(t.transceiver));

            this._transceivers = newTransceivers;
        }
    }

    /**
     * Inserts transceiver into the transceiver array in the order they are created (timestamp).
     * @param order an index that refers to when it it was created relatively.
     * @param transceiver the transceiver object to be inserted.
     */
    _insertTransceiverSorted(order: number, transceiver: RTCRtpTransceiver) {
        this._transceivers.push({ order, transceiver });
        this._transceivers.sort((a, b) => a.order - b.order);
    }
}

/**
 * Define the `onxxx` event handlers.
 */
const proto = RTCPeerConnection.prototype;

defineEventAttribute(proto, 'connectionstatechange');
defineEventAttribute(proto, 'icecandidate');
defineEventAttribute(proto, 'icecandidateerror');
defineEventAttribute(proto, 'iceconnectionstatechange');
defineEventAttribute(proto, 'icegatheringstatechange');
defineEventAttribute(proto, 'negotiationneeded');
defineEventAttribute(proto, 'signalingstatechange');
defineEventAttribute(proto, 'datachannel');
defineEventAttribute(proto, 'track');
defineEventAttribute(proto, 'error');
