// dec2hex :: Integer -> String
// i.e. 0-255 -> '00'-'ff'
const dec2hex = (dec) => {
  return dec.toString(32).padStart(2, '0');
};

// generateId :: Integer -> String
const generateId = (len) => {
  let arr = new Uint8Array((len || 40) / 2);
  self.crypto.getRandomValues(arr);
  return Array.from(arr, dec2hex).join('');
};

const bg_client_uuid = generateId(20);
const bg_wss_ping = { action: 'ping', info: 'client ping', bg_client_uuid: bg_client_uuid };
const bg_wss_pong = { action: 'pong', info: 'client pong', bg_client_uuid: bg_client_uuid };
const bg_wss_subs = { action: 'subscribe', status: 'ok', bg_client_uuid: bg_client_uuid }
const bg_chan_ping = { action: 'ping', info: 'bg worker ping', bg_client_uuid: bg_client_uuid };
const bg_chan_pong = { action: 'pong', info: 'bg worker pong', bg_client_uuid: bg_client_uuid };


if (typeof CND == 'undefined') { var CND = {}; }
if (typeof CND.date == 'undefined') {
CND.date = {
    now : function() {
        let nd = new Date();
        let msNow = CND.date.getDatetime(nd) + ':' + CND.date.getSeconds(nd) + '.' + CND.date.getMilliseconds(nd);
        return msNow;
    }, // end function now
    getDatetime : function( dt='' ) {
        if ((typeof dt == 'string') || ((typeof dt == 'object') && (dt instanceof Date))) {
            return CND.date.getDate(dt) + ' ' + CND.date.getTime(dt);
        } else {
            return '';
        }
    }, // end getDatetime
    getDate : function( dt='' ) {
        if (typeof dt == 'string') {
            try {
                dt = new Date(dt);
            } catch(err) {
                console.error('Input string is not a datetime string', err);
                return '';
            } // end try Date
        } // end if dt
        if ((typeof dt == 'object') && (dt instanceof Date)) {
            let dd = dt.getDate();
            let mm = dt.getMonth() + 1; // Javascript months are zero indexed.
            let yyyy = dt.getFullYear();
            if (dd < 10) {
                dd = '0' + dd;
            }
            if (mm < 10) {
                mm = '0' + mm;
            }
            let d = yyyy + '-' + mm + '-' + dd;
            return d;
        } else {
            return '';
        } // end if dt
    }, // end getDate
    getTime : function( dt='' ) {
        if (typeof dt == 'string') {
            try {
                dt = new Date(dt);
            } catch(err) {
                console.error('Input string is not a datetime string', err);
                return '';
            } // end try Date
        } // end if dt
        if ((typeof dt == 'object') && (dt instanceof Date)) {
            let HH = dt.getHours();
            let MM = dt.getMinutes();
            //if (HH < 10) { HH = '0' + HH; }
            if (MM < 10) { MM = '0' + MM; }
            let t = HH + ':' + MM;
            return t;
        } else {
            return '';
        } // end if dt
    }, // end getTime
    getSeconds : function( dt='' ) {
        if (typeof dt == 'string') {
            try {
                dt = new Date(dt);
            } catch(err) {
                console.error('Input string is not a datetime string', err);
                return '';
            } // end try Date
        } // end if dt
        if ((typeof dt == 'object') && (dt instanceof Date)) {
            let SS = dt.getSeconds();
            if (SS < 10) { SS = '0' + SS; }
            return SS;
        } else {
            return '';
        } // end if dt
    }, // end getSeconds
    getMilliseconds : function( dt='' ) {
        if (typeof dt == 'string') {
            try {
                dt = new Date(dt);
            } catch(err) {
                console.error('Input string is not a datetime string', err);
                return '';
            } // end try Date
        } // end if dt
        if ((typeof dt == 'object') && (dt instanceof Date)) {
            let MS = dt.getMilliseconds();
            if (MS.toString().length == 1) { MS = MS.toString() + '00'; }
             else if (MS.toString().length == 2) { MS = MS.toString() + '0'; }
             else if (MS.toString().length > 3) { MS = MS.toString().substring(0, 3); }
            return MS;
        } else {
            return '';
        } // end if dt
    } // end getMilliseconds
};
} // end if CND.date

CND.tryImportScripts = function(scripts=[]) {
    scripts.forEach(CND.tryImportScript);
    //if (CND.importAllSuccess) {  console.log(CND.date.now(), 'All resources loaded!');  }
    return CND.importAllSuccess;
}; // end function tryImportScript

CND.tryImportScript = function(scriptURL='') {
    try {
        importScripts(scriptURL);
        return true;
    } catch (e) {
        console.log('Error Loading Script', scriptURL);
        console.log(e);
        CND.importAllSuccess = false;
        return false;
    }
}; // end function tryImportScript

CND.importAllSuccess = true;
CND.srcScripts = [ '/assets/moment.js', '/assets/moment-timezone.js', '/js/templates.js', '/js/voipawesome.js' ];
CND.srcsImported = CND.tryImportScripts(CND.srcScripts);
//if (CND.importAllSuccess) { console.log(CND.date.now(), 'OK!'); }



/////////////////////////


// Details related to the initial web request
CND.www = {
    protocol : 'http',
    domain : 'localhost',
    port : '8080',
    path : ''
};

// WSS Chat Test Javascript
CND.wss = {
    protocol : 'wss://',
    domain : '',
    port : '443',
    path : '',

    hc : {
        protocol : 'https://',
        domain : '',
        port : '443',
        path : ''
    },

    connection : null,
    isConnected : false,
    canReconnect : false,
    closeReconnect : true,
    pingable : true,
    pingTimeout : null,
    pingTimeoutDelay : 29000, // 29s
    lastPingDatetime : null,
    connectable : true,
    connectTimeout : null,
    connectTimeoutDelay : 2500, // 2.5s
    lastConnectDatetime : null,
    reconnectingTimeout : null,
    reconnectingTimeoutDelay : 250, // start at 1/4 sec
    setupAlarmsTimeout : null,

    onDocReady : function() {
        let debug = false;
        clearTimeout(CND.wss.setupAlarmsTimeout);
        CND.wss.setupAlarmsTimeout = setTimeout(function() { setupAlarms(); }, 10);
        if (typeof CND.jQueryLoader != 'undefined') {
            CND.jQueryLoader(function() {
                if (debug) { console.log(CND.date.now(), 'CND ready'); }
                if (typeof CND.getAccountInfo !== 'undefined') {
                    if (debug) { console.log(CND.date.now(), 'Body is VAI CND Enabled'); }
                    CND.getAccountInfo(function() {
                        CND.wss.init(true, true);
                    }, function() { if (debug) { console.log(CND.date.now(), 'Failed to load Account Info'); } }); // end CND.getAccountInfo
                } else if (debug) { console.log('Failed to load VAI CND'); } // end if CND.getAccountInfo
            }, function() { if (debug) { console.log('Failed to init CND'); } }); // end CND.jQueryLoader
        } else {
            if (debug) { console.log('Missing required dependency: /js/voipawesome.js'); }
        } // end if CND.jQueryLoader
    }, // end function onDocReady

    init : function(tryReconnect=true, initial=true) {
        let debug = false;
        if (typeof location.protocol != 'undefined') { CND.www.protocol = location.protocol; }
        if (typeof location.hostname != 'undefined') { CND.www.domain = location.hostname; }
        if (typeof location.port != 'undefined') { CND.www.port = location.port; }
        CND.wss.port = CND.www.port; // Default to the same port as the web request
        if (CND.www.protocol.includes('https') || CND.www.protocol.includes('chrome-extension')) {
            CND.wss.protocol = 'wss://';
            CND.wss.hc.protocol = 'https://';
        }
        if (debug) { console.log(CND.date.now(), 'CND.accountSettings', CND.accountSettings); }
        if (typeof CND.accountSettings.domain != 'undefined') {
            CND.wss.domain = CND.accountSettings.domain;
        } else if (typeof CND.www.domain != 'undefined') {
            CND.wss.domain = CND.www.domain;
        } else {
            CND.wss.domain = '';
        }
        if (typeof CND.getWSSDomain == 'function') {
            CND.wss.domain = CND.getWSSDomain(CND.wss.domain, false, 'wss');
        }
        CND.wss.port = CND.defaultWSSPort || '443';
        //CND.wss.protocol = 'ws://'; CND.wss.hc.protocol = 'http://'; CND.wss.domain = 'localhost'; CND.wss.port = '8080';
        if (CND.wss.domain == '') { console.log(CND.date.now(), 'Error determining the wss.domain, unable to initialize WSS connection'); return false; }

        //CND.wss.port = CND.wss.port; // This is only if the website and the wss are accessed on the same port
        CND.wss.hc.domain = CND.wss.domain; CND.wss.hc.port = CND.wss.port; // Set the same for now...
        if ((CND.wss.port !== '') && (CND.wss.port !== '443') && (CND.wss.port !== '80')) {
            CND.wss.path = CND.wss.protocol + CND.wss.domain + ':' + CND.wss.port + '/wss/chat';
            CND.wss.hc.path = CND.wss.hc.protocol + CND.wss.hc.domain + ':' + CND.wss.hc.port + '/wss/hc/';
        } else {
            CND.wss.path = CND.wss.protocol + CND.wss.domain + '/wss/chat';
            CND.wss.hc.path = CND.wss.hc.protocol + CND.wss.hc.domain + '/wss/hc/';
        } // end if CND.wss.port

        if (debug) { console.log(CND.date.now(), 'www.protocol', CND.www.protocol, 'wss.Path', CND.wss.path); }
        if ((typeof self == 'undefined') || !('WebSocket' in self)) { console.log(CND.date.now(), 'Error: No WebSocket in self'); return false; }

        setTimeout(function() { CND.wss.connect(initial); }, 1);
    }, // end function init

    connect : function(initial=false) {
        let debug = false;
        if (CND.wss.connectable && (CND.wss.checkConn() !== true)) {
            setTimeout(function() { CND.wss.connectable = true; }, 250); // rate limit the connect
            CND.wss.connectable = false;
            CND.ajax({
                'debug': debug,
                'url': CND.wss.hc.path,
                'success': function(data) {
                    if (debug) { console.log(CND.date.now(), 'OK: WS Server is ONLINE!', CND.wss.hc.path, data); }
                    CND.wss.newConn(true, initial);
                }, 'error': function(data, type, exception) {
                    if (debug) { console.log(CND.date.now(), 'ERROR: WS Server is OFFLINE!', CND.wss.hc.path, exception); }
                    CND.wss.reconnect(true, true);
                }, 'offline': function() {
                    if (debug) { console.log(CND.date.now(), 'ERROR: We are offline!'); }
                } // end ajax settings
            }); // end CND.ajax
        } // end if runWssConnect
    }, // end function connect

    newConn : function(tryReconnect=true, initial=true) {
        let debug = false;
        if (CND.wss.isConnected) {
            if (CND.wss.checkConn() !== true) {
                CND.wss.closed(false);
            }
        }
        if (initial) {
            if (debug) { console.log(CND.date.now(), '### Connecting to WS...', CND.wss.path); }
        } else {
            if (debug) { console.log(CND.date.now(), '### Reconnecting to WS...', CND.wss.path); }
        }
        try {
            CND.wss.connection = new WebSocket(CND.wss.path);
            CND.wss.connection.addEventListener('open', CND.wss.onOpen);
            CND.wss.connection.addEventListener('error', CND.wss.onErr);
            CND.wss.connection.addEventListener('close', CND.wss.onClose);
            CND.wss.connection.addEventListener('message', CND.wss.onMsg);
        } catch(err) {
            console.log(CND.date.now(), err);
            CND.wss.reconnect(tryReconnect, true);
            return false;
        } // end try new WebSocket

        return true;
    }, // end function newConn

    checkConn : function() {
        let debug = false;
        CND.wss.isConnected = false;
        CND.wss.canReconnect = false;
        if ((typeof CND.wss.connection !== 'undefined') && (CND.wss.connection !== null)) {
            if (CND.wss.connection instanceof WebSocket) {
                let readyState = CND.wss.connection.readyState || 3;
                if (readyState === 1) {
                    CND.wss.isConnected = true;
                } else if (readyState >= 2) {
                    if (debug) { console.log(CND.date.now(), 'Error: WS Connection is closed! readyState:', readyState); }
                    CND.wss.isConnected = false;
                    if (CND.wss.lastConnectDatetime !== null) {
                        CND.wss.canReconnect = true;
                    }
                } // end if this_ws
            } else {
                if (debug) { console.log(CND.date.now(), 'Error: Something went wrong with creating the WS Connection!'); }
                if (CND.wss.lastConnectDatetime !== null) {
                    CND.wss.canReconnect = true;
                }
            } // end if CND.wss.connection.send
        } else {
            if (debug) { console.log(CND.date.now(), 'Error: We dont have a WS Connection yet!'); }
            if (CND.wss.lastConnectDatetime !== null) {
                CND.wss.canReconnect = true;
            }
        } // end if CND.wss.connection
        return CND.wss.isConnected;
    }, // end function checkConn

    closeReason : function(code=0) {
        let reason;
        // See https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1
        if (code == 1000)
            reason = 'Normal closure, meaning that the purpose for which the connection was established has been fulfilled.';
        else if (code == 1001)
            reason = 'An endpoint is "going away", such as a server going down or a browser having navigated away from a page.';
        else if (code == 1002)
            reason = 'An endpoint is terminating the connection due to a protocol error';
        else if (code == 1003)
            reason = 'An endpoint is terminating the connection because it has received a type of data it cannot accept (e.g., an endpoint that understands only text data MAY send this if it receives a binary message).';
        else if (code == 1004)
            reason = 'Reserved. The specific meaning might be defined in the future.';
        else if (code == 1005)
            reason = 'No status code was actually present.';
        else if (code == 1006)
           reason = 'The connection was closed abnormally, e.g., without sending or receiving a Close control frame';
        else if (code == 1007)
            reason = 'An endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message (e.g., non-UTF-8 [https://www.rfc-editor.org/rfc/rfc3629] data within a text message).';
        else if (code == 1008)
            reason = 'An endpoint is terminating the connection because it has received a message that "violates its policy". This reason is given either if there is no other sutible reason, or if there is a need to hide specific details about the policy.';
        else if (code == 1009)
           reason = 'An endpoint is terminating the connection because it has received a message that is too big for it to process.';
        else if (code == 1010) // Note that this status code is not used by the server, because it can fail the WSS handshake instead.
            reason = 'An endpoint (client) is terminating the connection because it has expected the server to negotiate one or more extension, but the server didnt return them in the response message of the WebSocket handshake. <br /> Specifically, the extensions that are needed are: ' + event.reason;
        else if (code == 1011)
            reason = 'A server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.';
        else if (code == 1015)
            reason = 'The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate cant be verified).';
        else
            reason = 'Unknown reason';

        return reason;
    }, // end function closeReason

    closed : function(tryReconnect=true, code, reason) {
        let debug = false;
        if (CND.wss.connection instanceof WebSocket) {
            if (CND.wss.connection.readyState === 0 || CND.wss.connection.readyState === 1) {
                if (debug) { console.log(CND.date.now(), 'Closing the WS Connection', code, reason); }
                CND.wss.connection.close(code, reason);
            }
        }
        CND.chan.sendMsg({ action: 'wss_status', status: false });
        //CND.wss.connection = null;
        if (tryReconnect) {
            CND.wss.reconnect(tryReconnect, false);
        } // end if tryReconnect
    }, // end function closed

    onClose : function(event) {
        let debug = false;
        let reason = CND.wss.closeReason(event.code || null);
        if (debug) { console.log(CND.date.now(), 'WS Connection Closed! Reason:', reason); }
        CND.chan.sendMsg({ action: 'wss_status', status: false });
        CND.wss.isConnected = false;
        CND.wss.canReconnect = true;
        CND.wss.closed(CND.wss.closeReconnect);
    }, // end function onClose

    onErr : function(event) {
        console.log(CND.date.now(), 'WS Connection Error!', event);
        CND.wss.closed(true, 1000, 'closing due to unknown error');
    }, // end function onErr

    onMsg : function(event) {
        let debug = false;
        let msgType = event.type || null;
        let msgData = event.data || '{}';
        let msg = {};
        if (typeof msgData == 'object') {
            msg = msgData;
        } else if ((typeof msgData == 'string') && (msgData != '')) {
            msg = JSON.parse(msgData);
        }
        let action = msg.action || 'unknown';

        if (typeof action === 'undefined') {
            if (debug) { console.log(CND.date.now(), 'Error: No message type received', msg, event); }
        } else if (action === 'new_msg') {
            if (debug) { console.log(CND.date.now(), 'Message Received', msg); }
            CND.chan.sendMsg({ action: 'new_msg', data: msg });
        } else if (action === 'ping' || msgType == 'ping') {
            if (debug) { console.log(CND.date.now(), 'Ping reply received, lets reply so they know we are alive!', sender); }
            CND.wss.sendMsg(bg_wss_pong);
            clearTimeout(CND.wss.reconnectingTimeout); // cancel the reconnect - we got a reply
        } else if (action === 'pong' || msgType == 'pong') {
            CND.wss.isConnected = true;
            CND.wss.canReconnect = true;
            if (debug) { console.log(CND.date.now(), 'Pong reply received, the remote side is alive!', sender); }
            clearTimeout(CND.wss.reconnectingTimeout); // cancel the reconnect - we got a reply
            let reply = { action: 'wss_status', status: CND.wss.isConnected };
            CND.chan.sendMsg(reply);
        } else {
            if (debug) { console.log(CND.date.now(), 'Error: Unknown action', action, msg, event); }
        } // end if action
    }, // end function onMsg

    onOpen : function() {
        let debug = false;
        if (debug) { console.log(CND.date.now(), 'WS Connection is Open now!'); }
        CND.wss.connectable = true;
        CND.wss.canReconnect = true;
        clearTimeout(CND.wss.connectTimeout);
        clearTimeout(CND.wss.reconnectingTimeout);
        CND.wss.reconnectingTimeoutDelay = 1000;
        setTimeout(function() { CND.wss.pingable = true; CND.wss.pingServer(); }, 10); // send a ping message to confirm connected back to wss server
        clearTimeout(CND.wss.setupAlarmsTimeout);
        CND.wss.setupAlarmsTimeout = setTimeout(function() { setupAlarms(); }, 100);
    }, // end function onOpen

    reconnect : function(tryReconnect=true, setConnectionClosed=false) {
        if (tryReconnect && CND.wss.reconnectingTimeout !== null) { return; } // Let the last timeout finish
        if (setConnectionClosed) { CND.wss.closed(tryReconnect); return; } // Set the connection as closed and retry the connection

        clearTimeout(CND.wss.reconnectingTimeout); // Reset the timeout
        if (tryReconnect) {
            CND.wss.reconnectingTimeout = setTimeout(function() {
                CND.wss.reconnectingTimeout = null;
                if (CND.wss.reconnectingTimeoutDelay < 300) { CND.wss.reconnectingTimeoutDelay = 250; }
                if (CND.wss.reconnectingTimeoutDelay < 19000) {
                    CND.wss.reconnectingTimeoutDelay += 1000;
                }
                if (CND.wss.checkConn() !== true) {
                    CND.wss.init(tryReconnect, false);
                }
            }, CND.wss.reconnectingTimeoutDelay);
        }
    }, // end function reconnect

    sendMsg : function(msg) {
        let debug = false;
        let sent = false;
        let msgText = msg;
        if (typeof msgText == 'object') {
            msgText = JSON.stringify(msgText);
        }
        if (CND.wss.checkConn() === true) {
            try {
                CND.wss.connection.send(msgText);
                CND.wss.resetNextPing();
                sent = true;
                if (msgText != '') {
                    let msgBody = msg.Body || '';
                    if (debug) { if (msgBody != '') { console.log(CND.date.now(), 'OK! Sent WS message', msg); } }
                }
            } catch (e) {
                if (debug) { console.log(CND.date.now(), 'Failed to send WS message.', msg, 'Error', e); }
                CND.wss.isConnected = false;
                CND.wss.reconnectingTimeoutDelay = 1;
                CND.wss.reconnect(true, true);
            } // end try send
        } else {
            if (debug) { console.log(CND.date.now(), 'WS Backend is offline! Cant send WS message', msg); }
            CND.wss.reconnectingTimeoutDelay = 250;
            CND.wss.reconnect(true, true);
        } // end if CND.wss.isConnected
        return sent;
    }, // end function sendMsg

    resetNextPing : function() {
        CND.wss.pingable = false;
        clearTimeout(CND.wss.pingTimeout);
        CND.wss.pingTimeout = setTimeout(function() { CND.wss.pingable = true }, CND.wss.pingTimeoutDelay);
        CND.wss.pingTimeoutDelay = 29000; // reset to 29s - incase we override it somewhere it should only be for the next event
    }, // end function resetNextPing

    pingServer : function() {
        if (CND.wss.pingable) {
            if (CND.wss.checkConn() === true) {
                //console.log(CND.date.now(), '### sending-client-ping');
                CND.wss.resetNextPing();
                CND.wss.sendMsg(bg_wss_ping);
                CND.wss.lastPingDatetime = CND.date.now();
            } else {
                let initial = CND.wss.lastConnectionDatetime === null;
                CND.wss.init(true, initial);
            } // end if CND.wss.isConnected
        } // end if CND.wss.pingable
    }, // end function pingServer

}; // end object CND.wss


CND.chan = {
    list : {},
    members : {},
    disconnectingTimeout : {},
    disconnectingTimeoutDelay : 295e3, // 5 minutes minus 5s

    dcChan : function(ch) {
        let debug = false;
        if (typeof ch.name === 'undefined') { console.log(CND.date.now(), 'ch', ch); return true; }
        if (typeof CND.chan.list[ch.name] == 'undefined') { return true; }

        let chn = ch.name;
        if (typeof CND.chan.disconnectingTimeout[chn] != 'undefined') { clearTimeout(CND.chan.disconnectingTimeout[chn]); }
        if (typeof ch != 'undefined') {
            if (typeof ch.disconnect == 'function') {
                ch.disconnect();
            }
        }
        if (debug) { console.log(CND.date.now(), 'Channel disconnect', ch.name); }
        delete CND.chan.list[ch.name];
        ch = null;
    }, // end function dcChan

    onConn : function(chan) {
        if (typeof chan.name === 'undefined') { console.log(CND.date.now(), 'chan', chan); return true; }
        let debug = false;
        let chn = chan.name;
        if (typeof CND.chan.list[chn] == 'undefined') {
            CND.chan.list[chn] = chan;
        }

        chan.onMessage.addListener(CND.chan.onMsg);
        chan.onDisconnect.addListener(CND.chan.onDc);

        if (debug) { console.log(CND.date.now(), 'Channel connected', chn); }

        // Force a disconnect every 4:55 to keepalive the bg script
        if (typeof CND.chan.disconnectingTimeout[chn] != 'undefined') { clearTimeout(CND.chan.disconnectingTimeout[chn]); }
        CND.chan.disconnectingTimeout[chn] = setTimeout(CND.chan.dcChan, CND.chan.disconnectingTimeoutDelay, chan);
        clearTimeout(CND.wss.setupAlarmsTimeout);
        CND.wss.setupAlarmsTimeout = setTimeout(function() { setupAlarms(); }, 10);
        return true;
    }, // end function onConn

    onDc : function(ch) {
        if (typeof ch.name === 'undefined') { console.log(CND.date.now(), 'ch', ch); return true; }
        let debug = false;
        let chn = ch.name || 'not-set';
        if (typeof CND.chan.disconnectingTimeout[chn] != 'undefined') { clearTimeout(CND.chan.disconnectingTimeout[chn]); }
        if (typeof CND.chan.list[ch.name] != 'undefined') {
            if (typeof CND.chan.list[ch.name].disconnect == 'function') {
                CND.chan.list[ch.name].disconnect();
            }
            delete CND.chan.list[ch.name];
        }
        if (debug) { console.log(CND.date.now(), 'Channel disconnect', ch.name); }
        return true;
    }, // end function onDc

    onMsg : function(msg, ch, sendResponse) {
        let debug = false;
        if (debug) { console.log(CND.date.now(), 'msg', msg, ch); }
        if (typeof ch.name === 'undefined') { console.log(CND.date.now(), 'ch', ch); return true; }
        sender = ch.sender || null;
        if (typeof sender.id === 'undefined') { console.log(CND.date.now(), 'ch', ch, 'sender', sender); return true; }

        if (debug) { console.log(CND.date.now(), ch.name, sender.id, msg); }
        if (typeof CND.chan.list[ch.name] == 'undefined') {
            CND.chan.list[ch.name] = ch;
        }
        if (typeof CND.chan.members[ch.name] == 'undefined') {
            CND.chan.members[ch.name] = {};
        }
        if (typeof CND.chan.members[ch.name][sender.id] == 'undefined') {
            CND.chan.members[ch.name][sender.id] = sender;
        }

        if (typeof msg.action === 'undefined') {
            console.log(CND.date.now(), 'Error: No message type received', msg);
        } else if (msg.action === 'subscribe') {
            CND.chan.sendMsg(bg_wss_subs, ch);
        } else if (msg.action === 'send_msg') {
            let msgData = msg.data || msg;
            if (debug) { console.log(CND.date.now(), 'Send msg from user to C2D server. msgData', msgData); }
            if (msgData.action === 'new_msg') {
                console.warn(CND.date.now(), 'Blocked direct WSS new_msg send; persistent chat sends must use the C2D API send_msg action.');
                return true;
            }
            CND.wss.sendMsg(msgData);
        } else if (msg.action === 'ping') {
            if (debug) { console.log(CND.date.now(), 'Ping reply received, lets reply so they know we are alive!', sender); }
            CND.chan.sendMsg(bg_chan_pong);
        } else if (msg.action === 'pong') {
            CND.chan.isConnected = true;
            if (debug) { console.log(CND.date.now(), 'Pong reply received, the remote side is alive!', sender); }
        } else if (msg.action === 'check_wss_status') {
            if (debug) { console.log(CND.date.now(), 'Checking the WS Backend Connection', sender); }
            CND.wss.isConnected = CND.wss.checkConn();
            if (CND.wss.isConnected !== true) {
                if (CND.wss.connection instanceof WebSocket) {
                    let readyState = CND.wss.connection?.readyState || 3;
                    if ((readyState === 2) || (readyState === 3)) {
                        CND.wss.reconnectingTimeoutDelay = 100;
                        CND.wss.reconnect(true, true);
                    }
                } else {
                    CND.wss.reconnectingTimeoutDelay = 100;
                    CND.wss.reconnect(true, true);
                } // end if CND.wss.connection
            } // end if CND.wss.isConnected
            let reply = { action: 'wss_status', status: CND.wss.isConnected };
            CND.chan.sendMsg(reply, ch);
        } // end if msg.action
        return true;
    }, // end function onMsg

    sendMsg : function(msg={}, ch=null) {
        if (ch === null) {
            for (let name in CND.chan.list) {
                chan = CND.chan.list[name] || null;
                if (chan !== null) {
                    try {
                        chan.postMessage(msg);
                    } catch(e) {
                        console.error(CND.date.now(), 'Error: Failed to sendMsg to chan:', msg);
                        CND.chan.dcChan(chan);
                    } // end try chan.postMsg
                } // end if chan
            } // end foreach CND.chan.list
        } else {
            try {
                ch.postMessage(msg);
            } catch(e) {
                console.error(CND.date.now(), 'Error: Failed to sendMsg to ch:', msg);
                CND.chan.dcChan(ch);
            } // end try ch.postMsg
        } // end if ch
    } // end function chanSendMsg

}; // end object CND.chan






////
///////////////////// From Online Example /////////////////////
////
const WSS_DEF_URL = 'wss://' + (CND.defaultWSSDomain || 'wss.voipawesome.com') + ':' + (CND.defaultWSSPort || '443') + '/wss/chat';

const fetchData = () => {
  return fetch(
    'https://' + (CND.defaultWSSDomain || 'wss.voipawesome.com') + ':' + (CND.defaultWSSPort || '443') + '/wss/hc/'
  ).then((res) => res.json());
};

// Chrome Alarms
const setupAlarms = () => {
  chrome.alarms.get('c2d-ext-wss-connection-ping', (alarm) => {
    if (!alarm) {
      //console.log(CND.date.now(), '### ALARMS-SETUP');
      chrome.alarms.create('c2d-ext-wss-connection-ping', { periodInMinutes: 1.0 });
    }
  });
  chrome.alarms.onAlarm.addListener((e) => {
    if (e.name === 'c2d-ext-wss-connection-ping') {
        //console.log(CND.date.now(), '### ALARMS-WSS-PING');
        CND.wss.pingServer();
    } // end if e.name
  }); // end onAlarm
}; // end setupAlarms


// ON INSTALL
chrome.runtime.onInstalled.addListener(async function(e){
    chrome.alarms.clear('c2d-ext-wss-connection-ping');
    chrome.storage.local.set({ apiUrl: WSS_DEF_URL, count: 0 });
    //console.log(CND.date.now(), '### Extension installed', e);

    chrome.action.setBadgeText({ text: '0' });
    chrome.action.setBadgeBackgroundColor({ color: '#FF9900' });

    if (CND.wss.checkConn() !== true) {
        setTimeout(function() {
            CND.wss.connect(true);
        }, 300);
    } // end if runWssConnect

}); // end onInstalled


// ON SUSPEND
chrome.runtime.onSuspend.addListener(() => {
    //console.log(CND.date.now(), 'Unloading.');
    chrome.action.setBadgeText({ text: `off` });
});


self.addEventListener('install', async (event) => {
    //console.log(CND.date.now(), '====bg-install', event);
    chrome.action.setBadgeBackgroundColor({ color: '#a6e22e' });
});

self.addEventListener('activate', async (event) => {
    //console.log(CND.date.now(), '====bg=activate', event);
    chrome.action.setBadgeBackgroundColor({ color: '#FF9900' });
    setTimeout(function() { CND.wss.connect(true); }, 50);
    clearTimeout(CND.wss.setupAlarmsTimeout);
    CND.wss.setupAlarmsTimeout = setTimeout(function() { setupAlarms(); }, 100);
});

// Note: 'beforeunload' does not fire in an MV3 service worker. Connection
// teardown/reconnect is driven by the WSS close handlers and the keepalive
// alarm instead.


/*
self.addEventListener('push', function (event) {
  // Keep the service worker alive until the notification is created.
  event.waitUntil(
    self.registration.showNotification('Testing PUSH API', {
      body: 'coming from push event',
    })
  );
});
*/


// navigator.connection is not available in all service worker contexts;
// guard it so a missing API does not throw and terminate the worker.
if (typeof navigator !== 'undefined' && navigator.connection && typeof navigator.connection.addEventListener === 'function') {
    navigator.connection.addEventListener('change', function() {
        CND.wss.reconnect(true, true);
    });
}

chrome.runtime.onConnect.addListener(CND.chan.onConn);




//// END
