text: const Logger = require('../Logger');
  const EnhancedEventEmitter = require('../EnhancedEventEmitter');
  const { UnsupportedError } = require('../errors');
  const utils = require('../utils');
  const ortc = require('../ortc');
  const edgeUtils = require('./ortc/edgeUtils');
  
  const logger = new Logger('Edge11');
  
  class Edge11 extends EnhancedEventEmitter
  {
  	static async getNativeRtpCapabilities()
  	{
  		logger.debug('getNativeRtpCapabilities()');
  
  		return edgeUtils.getCapabilities();
  	}
  
  	constructor(
  		{
  			direction,
  			iceParameters,
  			iceCandidates,
  			dtlsParameters,
  			iceServers,
  			iceTransportPolicy,
  			proprietaryConstraints, // eslint-disable-line no-unused-vars
  			extendedRtpCapabilities
  		}
  	)
  	{
  		super(logger);
  
  		logger.debug('constructor() [direction:%s]', direction);
  
  		// Generic sending RTP parameters for audio and video.
  		// @type {Object}
  		this._sendingRtpParametersByKind =
  		{
  			audio : ortc.getSendingRtpParameters('audio', extendedRtpCapabilities),
  			video : ortc.getSendingRtpParameters('video', extendedRtpCapabilities)
  		};
  
  		// Transport remote ICE parameters.
  		// @type {RTCIceParameters}
  		this._remoteIceParameters = iceParameters;
  
  		// Transport remote ICE candidates.
  		// @type {Array<RTCIceCandidate>}
  		this._remoteIceCandidates = iceCandidates;
  
  		// Transport remote DTLS parameters.
  		// @type {RTCDtlsParameters}
  		this._remoteDtlsParameters = dtlsParameters;
  
  		// Got transport local and remote parameters.
  		// @type {Boolean}
  		this._transportReady = false;
  
  		// ICE gatherer.
  		this._iceGatherer = null;
  
  		// ICE transport.
  		this._iceTransport = null;
  
  		// DTLS transport.
  		// @type {RTCDtlsTransport}
  		this._dtlsTransport = null;
  
  		// Map of RTCRtpSenders indexed by id.
  		// @type {Map<String, RTCRtpSender}
  		this._rtpSenders = new Map();
  
  		// Map of RTCRtpReceivers indexed by id.
  		// @type {Map<String, RTCRtpReceiver}
  		this._rtpReceivers = new Map();
  
  		// Local RTCP CNAME.
  		// @type {String}
  		this._cname = `CNAME-${utils.generateRandomNumber()}`;
  
  		this._setIceGatherer({ iceServers, iceTransportPolicy });
  		this._setIceTransport();
  		this._setDtlsTransport();
  	}
  
  	close()
  	{
  		logger.debug('close()');
  
  		// Close the ICE gatherer.
  		// NOTE: Not yet implemented by Edge.
  		try { this._iceGatherer.close(); }
  		catch (error) {}
  
  		// Close the ICE transport.
  		try { this._iceTransport.stop(); }
  		catch (error) {}
  
  		// Close the DTLS transport.
  		try { this._dtlsTransport.stop(); }
  		catch (error) {}
  
  		// Close RTCRtpSenders.
  		for (const rtpSender of this._rtpSenders.values())
  		{
  			try { rtpSender.stop(); }
  			catch (error) {}
  		}
  
  		// Close RTCRtpReceivers.
  		for (const rtpReceiver of this._rtpReceivers.values())
  		{
  			try { rtpReceiver.stop(); }
  			catch (error) {}
  		}
  	}
  
  	async getTransportStats()
  	{
  		return this._iceTransport.getStats();
  	}
  
  	async send({ track, encodings })
  	{
  		logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id);
  
  		if (!this._transportReady)
  			await this._setupTransport({ localDtlsRole: 'server' });
  
  		logger.debug('send() | calling new RTCRtpSender()');
  
  		const rtpSender = new RTCRtpSender(track, this._dtlsTransport);
  		const rtpParameters =
  			utils.clone(this._sendingRtpParametersByKind[track.kind]);
  		const useRtx = rtpParameters.codecs
  			.some((codec) => /.+\/rtx$/i.test(codec.mimeType));
  
  		if (!encodings)
  			encodings = [ {} ];
  
  		for (const encoding of encodings)
  		{
  			encoding.ssrc = utils.generateRandomNumber();
  
  			if (useRtx)
  				encoding.rtx = { ssrc: utils.generateRandomNumber() };
  		}
  
  		rtpParameters.encodings = encodings;
  
  		// Fill RTCRtpParameters.rtcp.
  		rtpParameters.rtcp =
  		{
  			cname       : this._cname,
  			reducedSize : true,
  			mux         : true
  		};
  
  		// NOTE: Convert our standard RTCRtpParameters into those that Edge
  		// expects.
  		const edgeRtpParameters = edgeUtils.mangleRtpParameters(rtpParameters);
  
  		logger.debug(
  			'send() | calling rtpSender.send() [params:%o]',
  			edgeRtpParameters);
  
  		await rtpSender.send(edgeRtpParameters);
  
  		// Store it.
  		this._rtpSenders.set(track.id, rtpSender);
  
  		return rtpParameters;
  	}
  
  	async stopSending({ localId })
  	{
  		logger.debug('stopSending() [localId:%s]', localId);
  
  		const rtpSender = this._rtpSenders.get(localId);
  
  		if (!rtpSender)
  			throw new Error('RTCRtpSender not found');
  
  		this._rtpSenders.delete(localId);
  
  		try
  		{
  			logger.debug('stopSending() | calling rtpSender.stop()');
  
  			rtpSender.stop();
  		}
  		catch (error)
  		{
  			logger.warn('stopSending() | rtpSender.stop() failed:%o', error);
  
  			throw error;
  		}
  	}
  
  	async replaceTrack({ localId, track })
  	{
  		logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id);
  
  		const rtpSender = this._rtpSenders.get(localId);
  
  		if (!rtpSender)
  			throw new Error('RTCRtpSender not found');
  
  		const oldTrack = rtpSender.track;
  
  		rtpSender.setTrack(track);
  
  		// Replace key.
  		this._rtpSenders.delete(oldTrack.id);
  		this._rtpSenders.set(track.id, rtpSender);
  	}
  
  	async setMaxSpatialLayer({ localId, spatialLayer })
  	{
  		logger.debug(
  			'setMaxSpatialLayer() [localId:%s, spatialLayer:%s]',
  			localId, spatialLayer);
  
  		const rtpSender = this._rtpSenders.get(localId);
  
  		if (!rtpSender)
  			throw new Error('RTCRtpSender not found');
  
  		const parameters = rtpSender.getParameters();
  
  		parameters.encodings
  			.forEach((encoding, idx) =>
  			{
  				if (idx <= spatialLayer)
  					encoding.active = true;
  				else
  					encoding.active = false;
  			});
  
  		await rtpSender.setParameters(parameters);
  	}
  
  	async getSenderStats({ localId })
  	{
  		const rtpSender = this._rtpSenders.get(localId);
  
  		if (!rtpSender)
  			throw new Error('RTCRtpSender not found');
  
  		return rtpSender.getStats();
  	}
  
  	async receive({ id, kind, rtpParameters })
  	{
  		logger.debug('receive() [id:%s, kind:%s]', id, kind);
  
  		if (!this._transportReady)
  			await this._setupTransport({ localDtlsRole: 'server' });
  
  		logger.debug('receive() | calling new RTCRtpReceiver()');
  
  		const rtpReceiver = new RTCRtpReceiver(this._dtlsTransport, kind);
  
  		rtpReceiver.addEventListener('error', (event) =>
  		{
  			logger.error('iceGatherer "error" event [event:%o]', event);
  		});
  
  		// NOTE: Convert our standard RTCRtpParameters into those that Edge
  		// expects.
  		const edgeRtpParameters =
  			edgeUtils.mangleRtpParameters(rtpParameters);
  
  		logger.debug(
  			'receive() | calling rtpReceiver.receive() [params:%o]',
  			edgeRtpParameters);
  
  		await rtpReceiver.receive(edgeRtpParameters);
  
  		const localId = id;
  
  		// Store it.
  		this._rtpReceivers.set(localId, rtpReceiver);
  
  		return { localId, track: rtpReceiver.track };
  	}
  
  	async stopReceiving({ localId })
  	{
  		logger.debug('stopReceiving() [localId:%s]', localId);
  
  		const rtpReceiver = this._rtpReceivers.get(localId);
  
  		if (!rtpReceiver)
  			throw new Error('RTCRtpReceiver not found');
  
  		this._rtpReceivers.delete(localId);
  
  		try
  		{
  			logger.debug('stopReceiving() | calling rtpReceiver.stop()');
  
  			rtpReceiver.stop();
  		}
  		catch (error)
  		{
  			logger.warn('stopReceiving() | rtpReceiver.stop() failed:%o', error);
  		}
  	}
  
  	async getReceiverStats({ localId })
  	{
  		const rtpReceiver = this._rtpReceivers.get(localId);
  
  		if (!rtpReceiver)
  			throw new Error('RTCRtpReceiver not found');
  
  		return rtpReceiver.getStats();
  	}
  
  	async restartIce({ iceParameters })
  	{
  		logger.debug('restartIce()');
  
  		this._remoteIceParameters = iceParameters;
  
  		if (!this._transportReady)
  			return;
  
  		logger.debug('restartIce() | calling iceTransport.start()');
  
  		this._iceTransport.start(
  			this._iceGatherer, iceParameters, 'controlling');
  
  		for (const candidate of this._remoteIceCandidates)
  		{
  			this._iceTransport.addRemoteCandidate(candidate);
  		}
  
  		this._iceTransport.addRemoteCandidate({});
  	}
  
  	// eslint-disable-next-line no-unused-vars
  	async updateIceServers({ iceServers })
  	{
  		logger.debug('updateIceServers()');
  
  		// NOTE: Edge 11 does not implement iceGatherer.gater().
  		throw new UnsupportedError('not supported');
  	}
  
  	_setIceGatherer({ iceServers, iceTransportPolicy })
  	{
  		const iceGatherer = new RTCIceGatherer(
  			{
  				iceServers   : iceServers || [],
  				gatherPolicy : iceTransportPolicy || 'all'
  			});
  
  		iceGatherer.addEventListener('error', (event) =>
  		{
  			logger.error('iceGatherer "error" event [event:%o]', event);
  		});
  
  		// NOTE: Not yet implemented by Edge, which starts gathering automatically.
  		try
  		{
  			iceGatherer.gather();
  		}
  		catch (error)
  		{
  			logger.debug(
  				'_setIceGatherer() | iceGatherer.gather() failed: %s', error.toString());
  		}
  
  		this._iceGatherer = iceGatherer;
  	}
  
  	_setIceTransport()
  	{
  		const iceTransport = new RTCIceTransport(this._iceGatherer);
  
  		// NOTE: Not yet implemented by Edge.
  		iceTransport.addEventListener('statechange', () =>
  		{
  			switch (iceTransport.state)
  			{
  				case 'checking':
  					this.emit('@connectionstatechange', 'connecting');
  					break;
  				case 'connected':
  				case 'completed':
  					this.emit('@connectionstatechange', 'connected');
  					break;
  				case 'failed':
  					this.emit('@connectionstatechange', 'failed');
  					break;
  				case 'disconnected':
  					this.emit('@connectionstatechange', 'disconnected');
  					break;
  				case 'closed':
  					this.emit('@connectionstatechange', 'closed');
  					break;
  			}
  		});
  
  		// NOTE: Not standard, but implemented by Edge.
  		iceTransport.addEventListener('icestatechange', () =>
  		{
  			switch (iceTransport.state)
  			{
  				case 'checking':
  					this.emit('@connectionstatechange', 'connecting');
  					break;
  				case 'connected':
  				case 'completed':
  					this.emit('@connectionstatechange', 'connected');
  					break;
  				case 'failed':
  					this.emit('@connectionstatechange', 'failed');
  					break;
  				case 'disconnected':
  					this.emit('@connectionstatechange', 'disconnected');
  					break;
  				case 'closed':
  					this.emit('@connectionstatechange', 'closed');
  					break;
  			}
  		});
  
  		iceTransport.addEventListener('candidatepairchange', (event) =>
  		{
  			logger.debug(
  				'iceTransport "candidatepairchange" event [pair:%o]', event.pair);
  		});
  
  		this._iceTransport = iceTransport;
  	}
  
  	_setDtlsTransport()
  	{
  		const dtlsTransport = new RTCDtlsTransport(this._iceTransport);
  
  		// NOTE: Not yet implemented by Edge.
  		dtlsTransport.addEventListener('statechange', () =>
  		{
  			logger.debug(
  				'dtlsTransport "statechange" event [state:%s]', dtlsTransport.state);
  		});
  
  		// NOTE: Not standard, but implemented by Edge.
  		dtlsTransport.addEventListener('dtlsstatechange', () =>
  		{
  			logger.debug(
  				'dtlsTransport "dtlsstatechange" event [state:%s]', dtlsTransport.state);
  
  			if (dtlsTransport.state === 'closed')
  				this.emit('@connectionstatechange', 'closed');
  		});
  
  		dtlsTransport.addEventListener('error', (event) =>
  		{
  			logger.error('dtlsTransport "error" event [event:%o]', event);
  		});
  
  		this._dtlsTransport = dtlsTransport;
  	}
  
  	async _setupTransport({ localDtlsRole })
  	{
  		logger.debug('_setupTransport()');
  
  		// Get our local DTLS parameters.
  		const dtlsParameters = this._dtlsTransport.getLocalParameters();
  
  		dtlsParameters.role = localDtlsRole;
  
  		// Need to tell the remote transport about our parameters.
  		await this.safeEmitAsPromise('@connect', { dtlsParameters });
  
  		// Start the RTCIceTransport.
  		this._iceTransport.start(
  			this._iceGatherer, this._remoteIceParameters, 'controlling');
  
  		// Add remote ICE candidates.
  		for (const candidate of this._remoteIceCandidates)
  		{
  			this._iceTransport.addRemoteCandidate(candidate);
  		}
  
  		// Also signal a 'complete' candidate as per spec.
  		// NOTE: It should be {complete: true} but Edge prefers {}.
  		// NOTE: If we don't signal end of candidates, the Edge RTCIceTransport
  		// won't enter the 'completed' state.
  		this._iceTransport.addRemoteCandidate({});
  
  		// NOTE: Edge does not like SHA less than 256.
  		this._remoteDtlsParameters.fingerprints = this._remoteDtlsParameters.fingerprints
  			.filter((fingerprint) =>
  			{
  				return (
  					fingerprint.algorithm === 'sha-256' ||
  					fingerprint.algorithm === 'sha-384' ||
  					fingerprint.algorithm === 'sha-512'
  				);
  			});
  
  		// Start the RTCDtlsTransport.
  		this._dtlsTransport.start(this._remoteDtlsParameters);
  
  		this._transportReady = true;
  	}
  }
  
  module.exports = Edge11;
  