text: /**
  * @file Defines a single WebRTC client (peer-to-peer connection) interface.
  *
  * @version 0.4.1
  */
  
  /**
  * @class A WebRTC client interface used to connect to and communicate
  * with a single peer.
  * @extends EventDispatcher
  */
  class WebRTCClient extends EventDispatcher {
  
     //Event definitions:
  
     /**
     * An ICE candidate has been received from one of the STUN servers,
     *
     * @event WebRTCClient#icecandidate
     * @type {Event}
     * @property {RTCIceCandidate} candidate The received candidate.
     * @property {RTCSessionDescription} description The local session description including
     * the <code>candidate</code> data.
     */
     /**
     * A received offer generated by an initiating peer has been received, processed,
     * and an answer has been generated.
     *
     * @event WebRTCClient#offer
     * @type {Event}
     * @property {Object} offer The received offer as sent by the initiating peer.
     * @property {Object} answer The generated answer.
     */
     /**
     * A received answer generated by an responding peer has been received and processed.
     *
     * @event WebRTCClient#answer
     * @type {Event}
     * @property {Object} answer The received answer generated by the responding peer.
     */
     /**
     * The data channel ({@link dataChannel}) created by the initiating peer has been created
     * locally. Note that this even will not be dispatched if this instance is the initiating peer.
     *
     * @event WebRTCClient#remotechannel
     * @type {Event}
     */
     /**
     * The data channel ({@link dataChannel}) has been successfully connected and
     * bi-directional communication can now take place (e.g. using the {@link sendMessage}
     * function).
     *
     * @event WebRTCClient#peerconnect
     * @type {Event}
     */
     /**
     * The peer connection ({@link peerConnection}) or data channel ({@link dataChannel})
     * has disconnected.
     *
     * @event WebRTCClient#peerdisconnect
     * @type {Event}
     */
     /**
     * The data channel ({@link dataChannel}) has received a message from a
     * connected peer.
     *
     * @event WebRTCClient#message
     * @type {Event}
     *
     * @property {*} message The received message.
     */
     /**
     * The private ID of this connection has changed and a notification
     * has been sent to the connected peer.
     *
     * @event WebRTCClient#privateid
     * @type {Event}
     *
     * @property {String} privateID The new private ID for this connection.
     */
  
     /**
     * Creates a new instance of WebRTCClient.
     *
     * @param {Object} [signallingServer=null] A reference to the signalling
     * server or mechanism for this instance to use during connection
     * attempts and negotiations. If null, the [router]{@link WebRTCClient@router}
     * property must be set manually prior to attempting a connection.
     */
     constructor(p2pRouter=null) {
        super();
        if (p2pRouter != null) {
           this.router = p2pRouter;
        }
     }
  
     /**
     * @property {Array|RTCIceServer} iceServers An indexed list of ICE/STUN servers
     * to use when creating a new connection.<br/>
     * Additional servers:<br/>
     * https://gist.github.com/zziuni/3741933
     * http://olegh.ftp.sh/public-stun.txt
     * https://gist.github.com/mondain/b0ec1cf5f60ae726202e
     * @readonly
     */
     get iceServers() {
        var serverList = [
           {urls:["stun.l.google.com:19302",
                  "stun1.l.google.com:19302",
                  "stun2.l.google.com:19302",
                  "stun3.l.google.com:19302",
                  "stun4.l.google.com:19302"]
           },
           {urls:"stunserver.org"},
           {urls:"stun.stunprotocol.org:3478"}
        ]
     }
  
     /**
     * @property {Object} connectionConfig A <code>RTCPeerConnection</code> configuration
     * object used to configure the instance.
     * @readonly
     */
     get connectionConfig() {
        var config = new Object();
        config.iceServers = this.iceServers;
     }
  
     /**
     * @property {RTCPeerConnection} peerConnection=null The peer connection object being
     * used by this instance. This property is set either when {@link createPeerConnection}
     * is invoked or when {@link setRemoteDescription} is invoked. Returns <code>null</code>
     * if no connection object currently exists.
     * @readonly
     */
     get peerConnection() {
        if (this["_peerConnection"] == undefined) {
           this._peerConnection = null;
        }
        return (this._peerConnection);
     }
  
     /**
     * @property {String} channelName="p2pchannel" The default data channel name used by this
     * instance.
     * @readonly
     */
     get channelName() {
        return ("p2pchannel");
     }
  
     /**
     * @property {Object} router=null The P2P router instance to use as a signalling mechanism
     * for exchanging messages during connection and negotiation attempts.
     */
     get router() {
        if (this._router == undefined) {
           this._router = null;
        }
        return (this._router);
     }
  
     set router(ssSet) {
        this._router = ssSet;
     }
  
     /**
     * @property {String} peerID The private ID of the remote peer to which this
     * instance is connecting or connected.
     */
     get peerPID () {
        if (this._peerPID == undefined) {
           this._peerPID = null;
        }
        return (this._peerPID);
     }
  
     /**
     * @property {RTCDataChannel} dataChannel The main data channel used to communicate
     * with a connected peer. Returns <code>null</code> if no data channel has been established.
     * @readonly
     */
     get dataChannel() {
        if (this["_dataChannel"] == undefined) {
           this._dataChannel = null;
        }
        return (this._dataChannel);
     }
  
     /**
     * Adds the standard RTCDataChannel handlers to the {@link dataChannel} instance.
     *
     * @private
     */
     addChannelHandlers() {
        if (this.dataChannel == null) {
           throw (new Error("No data channel established to which to attach listeners."));
        }
        this.dataChannel._client = this;
        this.dataChannel.addEventListener("open", this.onChannelOpen);
        this.dataChannel.addEventListener("close", this.onChannelClose);
        this.dataChannel.addEventListener("message", this.onChannelMessage);
     }
  
     /**
     * Begins checking the state of the {@link WebRTCClient#peerConnection} by calling
     * {@link checkConnectionState} at regular intervals.
     *
     * @param {Number} checkInterval The millisecond interval at which to check the connection
     * state.
     * @private
     * @todo Investigate further to see if there's a better way to detect disconnections
     */
     startConnectionStateCheck(checkInterval=1000) {
        this._stateCheckInterval = setInterval (this.checkConnectionState, checkInterval, this);
     }
  
     /**
     * Function invoked at regular interval ticks to check the state of the
     * {@link WebRTCClient#peerConnection} connection state and fire
     * a "disconnect" event when a disconnection is detected.
     *
     * @param {WebRTCClient} context The execution context in which to invoke the
     * check.
     * @fires WebRTCClient#disconnect
     * @private
     */
     checkConnectionState(context) {
        if (context.peerConnection.iceConnectionState == "disconnected") {
           clearInterval(context._stateCheckInterval);
           var newEvent = new Event("peerdisconnect");
           context.dispatchEvent(newEvent);
        }
     }
  
     /**
     * Attempts to establish a direct peer connection over WebRTC.
     *
     * @param {String} privateID The private ID of the peer to connect to,
     * as assigned by the {@link P2PRouter} or other external ID manager.
     * @param {Object} [config=null] A RTCPeerConnection configuration object. If
     * omitted or null, the [connectionConfig]{@link WebRTCClient#connectionConfig} object is used.
     *
     * @async
     */
     async connect(privateID, config=null) {
        if (config == null) {
           config = this.connectionConfig;
        }
        this._peerPID = privateID;
        this._peerConnection = new RTCPeerConnection(config); //create initiating connection
        this.peerConnection._client = this; //store reference for event handlers
        this.peerConnection.addEventListener("negotiationneeded", this.onConnectionNegotiate);
        this.peerConnection.addEventListener("icecandidate", this.onICECandidate);
        this.router.addEventListener("message", this.onSignalMessage, this);
        this._dataChannel = this.peerConnection.createDataChannel(this.channelName);
        this.addChannelHandlers();
        return (true);
     }
  
     /**
     * Changes the private ID associated with this connection. Any attached
     * peer is notified of this changed via a <code>session</code> message.
     *
     * @param {String} newPrivateID The new private ID to set for this connection.
     *
     * @return {Promise} The promise resolves with <code>true</code> if the private
     * ID was successfully changed, otherwise it rejects with <code>false</code>.
     *
     * @async
     */
     async changePrivateID(newPrivateID) {
        var JSONObj = buildJSONRPC("notification");
        JSONObj.result.type = "session";
        JSONObj.result.change = new Object();
        JSONObj.result.change.oldPrivateID = this.privateID;
        JSONObj.result.change.newPrivateID = newPrivateID;
        this.dataChannel.send(JSON.stringify(JSONObj)); //don't wait for response
        this._privateID = newPrivateID;
        var event = new Event("privateid");
        event.privateID = newPrivateID;
        this.dispatchEvent(event);
        return (true);
     }
  
     /**
     * Sets a remote offer SDP received from an initiating peer during a connection
     * negotiation.
     *
     * @param {String} privateID The private ID of the peer sending the offer SDP.
     * @param {Object|String} descData The SDP data sent by the <code>privateID</code>
     * If this is a string, it's assumed to be stringified JSON data which will be
     * parsed before being processed.
     *
     * @fires WebRTCClient#offer
     * @async
     * @private
     */
     async setRemoteOffer(privateID, descData) {
        if (typeof(descData) == "string") {
           var description = new RTCSessionDescription(JSON.parse(descData));
        } else {
           description = new RTCSessionDescription(descData);
        }
        this._peerPID = privateID;
        this._peerConnection = new RTCPeerConnection(this.connectionConfig); //create responding connection
        this.peerConnection.addEventListener("icecandidate", this.onICECandidate);
        this.peerConnection._client = this; //store reference for event handlers
        this.peerConnection.ondatachannel = this.onDataChannelEstablished; //use initiating data channel
        this.router.addEventListener("message", this.onSignalMessage, this);
        var result = await this.peerConnection.setRemoteDescription(description);
        var answerDesc = await this.peerConnection.createAnswer();
        result = await this.peerConnection.setLocalDescription(answerDesc);
        var answer = this.peerConnection.localDescription;
        this.sendSignalMessage("webrtcanswer", {
           answer: answer
        });
        var event = new Event("offer");
        event.offer = result;
        event.answer = answer;
        this.dispatchEvent(event);
        return (answer);
     }
  
     /**
     * Sends a peer-to-peer "direct" message to the connected peer.
     *
     * @param {*} data The data / message to send.
     * @param {Object|Array} recipients An object or an array of recipient private
     * IDs. If this parameter is an object, one or more additional properties are
     * expected:
     * @param {Array} [recipients.rcp] An indexed array of recipient private IDs.
     * If this list is provided as the <code>recipients</code> parameter this
     * structure is dynamicaly generated before sending.
     *
     * @return {Promise} An asynchronous Promise that will contain the result of
     * the send or reject with an <code>Error</code> object on failure.
     * @async
     * @TODO Update functionality to support message signing and forwarding for peer-to-peer meshes.
     */
     async send(data, recipients=null) {
        if (typeof(recipients) != "object") {
           throw (new Error(`"recipients" parameter must be an array!`));
        }
        if (typeof(recipients["length"]) == "number") {
           //recipients is an array so add it to the "rcp" property
           var WSSRecpObj = new Object();
           WSSRecpObj.rcp = recipients;
        } else {
           //recipients is al_wssReady an object so assume everything is in place
           WSSRecpObj = recipients;
        }
        var JSONObj = buildJSONRPC("notification"); //don't include id
        JSONObj.result.type = "direct";
        JSONObj.result.data = data;
        this.dataChannel.send(JSON.stringify(JSONObj));
        return (this.createMessageSentEvent());
     }
  
     /**
     * Sends a peer-to-peer "broadcast" message to the connected peer.
     *
     * @param {*} data The data / message to broadcast.
     * @param {Object|Array} [excludeRecipients=null] NOT CURRENTLY IMPLEMENTED.
     *
     * @return {Promise} An asynchronous Promise that will contain the result of
     * the send or reject with an <code>Error</code> object on failure.
     * @async
     * @TODO Update functionality to support message signing and forwarding for peer-to-peer meshes
     */
     async broadcast (data, excludeRecipients=null) {
        var JSONObj = buildJSONRPC("notification"); //don't include id
        JSONObj.result.type = "broadcast";
        JSONObj.result.data = data;
        this.dataChannel.send(JSON.stringify(JSONObj));
        return (this.createMessageSentEvent());
     }
  
     /**
     * Sends a peer-originating "update" message to the connected peer,
     * mimicking an API server "update" message.
     *
     * @param {String|Object} data The message to send. If the message is
     * not a string it is stringified into JSON string data.
     *
     * @todo Test this functionality
     */
     sendUpdate (data) {
        var JSONObj = buildJSONRPC("notification"); //don't include id
        JSONObj.result.type = "update";
        JSONObj.result.data = data;
        this.dataChannel.send(JSON.stringify(JSONObj));
     }
  
     /**
     * Creates a message-sent response event that mimics one received
     * when messages are sent to peers over WebSocket Sessions.
     *
     * @return {Event} A message-sent event mimicking that received from
     * a WebSocket Sessions server after a peer message was sent.
     */
     createMessageSentEvent() {
        var responseEvent = new Event("message");
        Object.defineProperty(responseEvent, "target", {writable: true});
        Object.defineProperty(responseEvent, "currentTarget", {writable: true});
        responseEvent.target = this;
        responseEvent.currentTarget = this;
        var responseObj = buildJSONRPC("result", {"id":uniqueRPCID()});
        responseObj = new Object();
        responseObj.message = "ok";
        responseEvent.data = responseObj;
     }
  
     /**
     * Sends a signalling message to the potential peer using the {@link P2PRouter}.
     *
     * @param {String} messageType The type of signalling message to send. See the
     * [onSignalMessage]{@link WebRTCClient#onSignalMessage} function for supported
     * message types.
     * @param {*} [data=null] Optional data to include with the signalling message.
     * @param {Boolean} internal=true If true, the message is handled by this WebRTCClient
     * instance. If false, it's handled by the {@link P2PRouter} instance (typically this
     * is just the negotiation-start message).
     */
     sendSignalMessage(messageType, data=null, internal=true) {
        if (internal == false) {
           //router-based (external) signalling message
           var message = this.router.buildRouterMessage(messageType);
        } else {
           //instance-based (internal) signalling message
           message = this.buildSignalMessage(messageType);
        }
        if (data != null) {
           for (var item in data) {
              if (item != "type") {
                 message[item] = data[item];
              }
           }
        }
        this.router.send(message, [this.peerPID]);
     }
  
     /**
     * Sends an API request using the [dataChannel]{@link WebRTCClient#dataChannel}.
     * <b>NOT YET IMPLEMENTED</b>
     *
     * @param {Object} requestObj The JSON-RPC 2.0 request to send.
     *
     * @return {Promise} An asynchronous promise that will resolve with the JSON-RPC 2.0
     * API response.
     */
     request(requestObj) {
        var promise = new Promise((resolve, reject) => {
           reject (new Error("Function not implemented yet."));
        })
        return (promise);
     }
  
     /**
     * Builds a WebRTCClient signalling message.
     *
     * @param {String} messageType The signalling message type to create.
     *
     * @return {Object} A signalling message object containing the <code>webRTCSignalMsg</code>
     * property which contains the message type.
     */
     buildSignalMessage(messageType) {
        var messageObj = new Object();
        messageObj.webRTCSignalMsg = messageType;
        return (messageObj);
     }
  
     /**
     * Verifies if a supplied message event object contains a valid WebRTC signal message.
     *
     * @param {Event} event The "message" event, as usually dispatched by the
     * peer-to-peer interface, to examine.
     *
     * @return {Boolean} True if the event contains a valid WebRTC signal message
     * (though its type may not be supported).
     * @private
     */
     isSignalMsgEvent(event) {
        try {
           if (typeof(event["data"]) != "object") {
              //not sure what this is
              return (false);
           }
           if (typeof(event.data["result"]) != "object") {
              //may not be a JSON-RPC message
              return (false);
           }
           if (typeof(event.data.result["data"]) != "object") {
              //not a router-formatted message
              return (false);
           }
           return (this.isSignalMessage(event.data.result.data));
        } catch (err) {
           return (false);
        }
     }
  
     /**
     * Verifies if a supplied object is a valid WebRTC signal message.
     *
     * @param {Object} message The object to examine.
     *
     * @return {Boolean} True if the object seems to be a valid WebRTC signal message
     * (though it may not be supported).
     * @private
     */
     isSignalMessage(message) {
        if ((message["webRTCSignalMsg"] == undefined) || (message["webRTCSignalMsg"] == null) || (message["webRTCSignalMsg"] == "")) {
           //not a P2PRouter message or it's blank (mo message type)
           return (false);
        }
        return (true);
     }
  
     /**
     * Handles any "message" events received on the signalling transport. Any non-signalling messages
     * are ignored.
     *
     * @param {Event} event A message event received from the signalling transport.
     *
     * @private
     */
     async onSignalMessage(event) {
        if (this.isSignalMsgEvent(event)) {
           var fromPID = event.data.result.from;
           var msgData = event.data.result.data;
           var messageType = msgData.webRTCSignalMsg;
           switch (messageType) {
              case "webrtcnegotiate":
                 event.target._client = this;
                 this.onConnectionNegotiate(event);
                 break;
              case "webrtcanswer":
                 var result = await this.peerConnection.setRemoteDescription(msgData.answer);
                 var newEvent = new Event("answer");
                 newEvent.answer = msgData.answer;
                 this.dispatchEvent(newEvent);
                 break;
              case "webrtcicecandidate":
                 var candidate = new RTCIceCandidate(msgData.ice);
                 this.peerConnection.addIceCandidate(candidate);
                 break;
              case "webrtcicecomplete":
                 this.peerConnection.addIceCandidate(null);
                 break;
              default:
                 //not a recognized signalling message type
                 break;
           }
        } else {
           //not a signalling message
           return (false);
        }
        return (true);
     }
  
     /**
     * Handles "negotiationneeded" events for the [peerConnection]{@link WebRTCClient#peerConnection}.
     *
     * @param {Event} event A "negotiationneeded" RTCPeerConnection event.
     *
     * @private
     */
     async onConnectionNegotiate(event) {
         var wrtcClient = event.target._client;
         wrtcClient.peerConnection.createOffer().then(offer => {
            wrtcClient.peerConnection.setLocalDescription(offer).then(result => {
               wrtcClient.sendSignalMessage("peerconnectreq", {
                  transportType: "webrtc",
                  offer: offer,
                  options: wrtcClient.router.constructor.supportedTransports.options
              }, false);
           });
         }).catch(err => {
            console.error (err);
         });
     }
  
     /**
     * Handles "icecandidate" events for the [peerConnection]{@link WebRTCClient#peerConnection}.
     *
     * @param {Event} event A "icecandidate" RTCPeerConnection event.
     *
     * @fires WebRTCClient#icecandidate
     * @private
     */
     onICECandidate(event) {
        var wrtcClient = event.target._client;
        if ((event.target.iceGatheringState == "complete") || (event.candidate == null)) {
            wrtcClient.sendSignalMessage("webrtcicecomplete");
        } else if (event["candidate"] != undefined) {
            var ice = event.candidate;
            wrtcClient.sendSignalMessage("webrtcicecandidate", {"ice":ice});
            var newEvent = new Event("icecandidate");
            newEvent.candidate = ice;
            newEvent.description = ice.description;
            wrtcClient.dispatchEvent(newEvent);
        }
     }
  
     /**
     * Handles "iceconnectionstatechange" events for the [peerConnection]{@link WebRTCClient#peerConnection}.
     *
     * @param {Event} event A "iceconnectionstatechange" RTCPeerConnection event.
     *
     * @fires WebRTCClient#peerdisconnect
     * @private
     */
     onIceConnectionStateChange(event) {
        if (this.peerConnection.iceConnectionState == "disconnected") {
           var newEvent = new Event("peerdisconnect");
           this.dispatchEvent(newEvent);
        }
     }
  
     /**
     * Event handler invoked when the initiating peer's channel has been locally established.
     *
     * @param {Event} event A <code>RTCPeerConnection</code> event.
     *
     * @fires WebRTCClient#remotechannel
     * @private
     */
     onDataChannelEstablished(event) {
        var wrtcClient = event.target._client;
        wrtcClient._dataChannel = event.channel;
        wrtcClient.addChannelHandlers();
        var newEvent = new Event("remotechannel");
        wrtcClient.dispatchEvent(newEvent);
     }
  
     /**
     * Event handler invoked when the {@link dataChannel} connects.
     *
     * @param {Event} event A <code>RTCPeerConnection</code> event.
     *
     * @fires WebRTCClient#peerconnect
     * @private
     */
     onChannelOpen(event) {
        var wrtcClient = event.target._client;
        if (event.target.readyState == "open") {
           wrtcClient.startConnectionStateCheck();
           wrtcClient.addEventListener("iceconnectionstatechange", wrtcClient.onIceConnectionStateChange, wrtcClient);
           var newEvent = new Event("peerconnect");
           wrtcClient.dispatchEvent(newEvent);
        }
     }
  
     /**
     * Event handler invoked when the {@link dataChannel} disconnects.
     *
     * @param {Event} event A <code>RTCDataChannel</code> event.
     *
     * @fires WebRTCClient#peerdisconnect
     * @private
     */
     onChannelClose(event) {
        var wrtcClient = event.target._client;
        var newEvent = new Event("peerdisconnect");
        wrtcClient.dispatchEvent(newEvent);
     }
  
     /**
     * Event handler invoked when the {@link dataChannel} receives a message
     * from a connected peer.
     *
     * @param {Event} event A <code>RTCDataChannel</code> event.
     *
     * @fires WebRTCClient#message
     * @private
     */
     onChannelMessage(event) {
        var wrtcClient = event.target._client;
        try {
           var dataObj = JSON.parse(event.data);
           switch (dataObj.result.type) {
              case "broadcast":
                 var newEvent = new Event("message");
                 newEvent.data = dataObj;
                 wrtcClient.dispatchEvent(newEvent);
                 break;
              case "direct":
                 newEvent = new Event("message");
                 newEvent.data = dataObj;
                 wrtcClient.dispatchEvent(newEvent);
                 break;
              case "update":
                 newEvent = new Event("update");
                 newEvent.data = dataObj;
                 wrtcClient.dispatchEvent(newEvent);
                 break;
              case "session":
                 if (typeof(dataObj.result.change) == "object") {
                    //in the future we may support different types of session updates
                    event = new Event("peerpid");
                    event.data = dataObj;
                    wrtcClient.peerID = dataObj.result.newPrivateID;
                    wrtcClient.dispatchEvent(event);
                 } else {
                    //unhandled session message
                 }
                 break;
              default:
                 console.error ("Unrecognized messaged type \""+dataObj.result.type+"\"");
                 //unhandled message type
                 break;
           }
        } catch (err) {
           console.error(err);
        }
     }
  
     /**
     * Disconnects the WebRTC client connection and removes any event listeners.
     *
     * @async
     */
     async disconnect() {
        try {
           this.router.removeEventListener("message", this.onSignalMessage);
        } catch (err) {
        }
        try {
           this.dataChannel.removeEventListener("open", this.onChannelOpen);
           this.dataChannel.removeEventListener("close", this.onChannelClose);
           this.dataChannel.removeEventListener("message", this.onChannelMessage);
           delete this.dataChannel._client;
        } catch (err) {
        }
        try {
           this.peerConnection.removeEventListener("message", this.handleSocketMessage);
           this.peerConnection.removeEventListener("close", this.handleSocketClose);
           this.peerConnection.removeEventListener("negotiationneeded", this.onConnectionNegotiate);
           this.peerConnection.removeEventListener("icecandidate", this.onICECandidate);
           this.peerConnection.removeEventListener("iceconnectionstatechange", this.onIceConnectionStateChange);
           this.peerConnection.close();
           delete this.peerConnection._client;
           this._peerConnection = null;
        } catch (err) {
        }
        return (true);
     }
  
     toString() {
        return ("WebRTCClient");
     }
  
  }
  