
// VOIP Awesome Click-and-Dial Extension for Google Chrome
// Shared Functions & Variables
//
//  Copyright:  VOIP Awesome Inc, 2021
//  Author:     Jambu Atchison
//  @ Support:  cs@voipawesome.com
//


///////////// Global Variables /////////////
if (typeof CND == 'undefined') { var CND = {}; }

CND.manifest = chrome.runtime.getManifest();
CND.currentDate = new Date();
CND.currentYear = CND.currentDate.getFullYear();
CND.wwwUrl = 'https://www.voipawesome.com';
CND.defaultC2DApiUrl = 'https://c2d.voipawesome.com';
CND.defaultWSSDomain = 'wss.voipawesome.com';
CND.defaultWSSPort = '443';
CND.apiUrl = CND.defaultC2DApiUrl;
CND.pbxApiUrl = CND.apiUrl;
CND.extensionName = 'VOIP Awesome';
CND.signup_message = 'You need to set up an account before you can use the '+
                            CND.extensionName+' plugin. No Account? <a href="'+
                            CND.wwwUrl+'/sign-up" target="VAI Sign Up">Sign Up Now</a>';
CND.extensionId = chrome.i18n.getMessage('@@extension_id');
CND.altName = 'C&D';
CND.isInstalled = true;
CND.newwindow = null;
CND.online = true; // default true
if (typeof window != "undefined") {
    CND.online = window.navigator.onLine; // if we have a window, check if we're online
}
CND.loopCount = 0;
CND.newwindow = null;
CND.windowPosition = {};
CND.defaultSettings = {
    domain  : '',
};
CND.accountSettings = {
    domain  : '',
    apikey : '',
    extension : '',
    valid : false,
}; // end object accountSettings
CND.defaultDomainRestrictions = CND.domainRestrictions = {
    domain_restriction_type: 'all_allowed',
    allowed_on_domains_list: '',
    disabled_on_domains_list: '',
    loaded_from_storage: false
}; // end object domainRestrictions

///////////// Vanilla DOM helpers (jQuery replacement) /////////////
// CND.dom is a small chainable query API covering only the methods this
// extension actually used from jQuery. Prefer CND.dom(sel) over legacy $(sel).
CND._domListenerMap = typeof WeakMap !== 'undefined' ? new WeakMap() : null;

CND._domNormalize = function(sel) {
    if (sel == null || sel === '') { return []; }
    if (sel instanceof CND.DomSet) { return sel.elements.slice(); }
    if (typeof sel === 'string') {
        let s = sel.trim();
        if (s.charAt(0) === '<' && s.charAt(s.length - 1) === '>') {
            let tag = s.replace(/^<\/?([a-zA-Z0-9]+).*$/, '$1');
            return [document.createElement(tag || 'div')];
        }
        try {
            return Array.prototype.slice.call(document.querySelectorAll(s));
        } catch (err) {
            console.log(CND.date.now(), 'CND.dom selector error', s, err);
            return [];
        }
    }
    if (sel === document || sel === window) { return [sel]; }
    if (sel.nodeType) { return [sel]; }
    if (typeof sel.length === 'number' && typeof sel !== 'function') {
        return Array.prototype.slice.call(sel);
    }
    return [];
};

CND.DomSet = function(elements) {
    this.elements = elements || [];
    this.length = this.elements.length;
    for (let i = 0; i < this.elements.length; i++) {
        this[i] = this.elements[i];
    }
};

CND.DomSet.prototype = {
    each: function(fn) {
        for (let i = 0; i < this.elements.length; i++) {
            fn.call(this.elements[i], i, this.elements[i]);
        }
        return this;
    },
    find: function(sel) {
        let out = [];
        this.each(function() {
            if (!this || !this.querySelectorAll) { return; }
            let found = this.querySelectorAll(sel);
            for (let i = 0; i < found.length; i++) { out.push(found[i]); }
        });
        return new CND.DomSet(out);
    },
    last: function(sel) {
        if (typeof sel === 'string' && sel !== '') {
            let found = this.find(sel);
            if (found.length === 0) { return new CND.DomSet([]); }
            return new CND.DomSet([found.elements[found.length - 1]]);
        }
        if (this.length === 0) { return new CND.DomSet([]); }
        return new CND.DomSet([this.elements[this.length - 1]]);
    },
    parent: function() {
        let out = [];
        this.each(function() {
            if (this && this.parentNode) { out.push(this.parentNode); }
        });
        return new CND.DomSet(out);
    },
    closest: function(sel) {
        let out = [];
        this.each(function() {
            if (this && this.closest) {
                let m = this.closest(sel);
                if (m) { out.push(m); }
            }
        });
        return new CND.DomSet(out);
    },
    children: function(sel) {
        let out = [];
        this.each(function() {
            if (!this || !this.children) { return; }
            let kids = this.children;
            for (let i = 0; i < kids.length; i++) {
                if (!sel || (kids[i].matches && kids[i].matches(sel))) {
                    out.push(kids[i]);
                }
            }
        });
        return new CND.DomSet(out);
    },
    html: function(val) {
        if (typeof val === 'undefined') {
            return this.length ? (this.elements[0].innerHTML || '') : undefined;
        }
        this.each(function(i) {
            let next = val;
            if (typeof val === 'function') {
                next = val.call(this, i, this.innerHTML || '');
            }
            if (typeof next === 'undefined' || next === null) { return; }
            // jQuery.html() accepts strings, DOM nodes, or collections
            if (typeof next === 'string' || typeof next === 'number') {
                this.innerHTML = next;
                return;
            }
            this.innerHTML = '';
            if (next instanceof CND.DomSet) {
                for (let j = 0; j < next.elements.length; j++) {
                    this.appendChild(next.elements[j]);
                }
            } else if (next.nodeType) {
                this.appendChild(next);
            } else if (typeof next.length === 'number') {
                for (let j = 0; j < next.length; j++) {
                    if (next[j] && next[j].nodeType) { this.appendChild(next[j]); }
                }
            } else {
                this.innerHTML = String(next);
            }
        });
        return this;
    },
    text: function(val) {
        if (typeof val === 'undefined') {
            return this.length ? (this.elements[0].textContent || '') : undefined;
        }
        this.each(function(i) {
            let next = val;
            if (typeof val === 'function') {
                next = val.call(this, i, this.textContent || '');
            }
            if (typeof next === 'undefined' || next === null) { return; }
            this.textContent = next;
        });
        return this;
    },
    val: function(val) {
        if (typeof val === 'undefined') {
            if (!this.length) { return undefined; }
            let el = this.elements[0];
            return (typeof el.value !== 'undefined') ? el.value : '';
        }
        this.each(function(i) {
            if (typeof this.value === 'undefined') { return; }
            let next = val;
            if (typeof val === 'function') {
                next = val.call(this, i, this.value);
            }
            if (typeof next === 'undefined' || next === null) { return; }
            this.value = next;
        });
        return this;
    },
    attr: function(name, val) {
        if (typeof val === 'undefined') {
            if (!this.length || !this.elements[0].getAttribute) { return undefined; }
            return this.elements[0].getAttribute(name);
        }
        this.each(function() {
            if (this.setAttribute) { this.setAttribute(name, val); }
        });
        return this;
    },
    prop: function(name, val) {
        if (typeof val === 'undefined') {
            return this.length ? this.elements[0][name] : undefined;
        }
        this.each(function() { this[name] = val; });
        return this;
    },
    css: function(name, val) {
        if (typeof name === 'object') {
            let styles = name;
            this.each(function() {
                for (let k in styles) {
                    if (styles.hasOwnProperty(k)) { this.style[k] = styles[k]; }
                }
            });
            return this;
        }
        if (typeof val === 'undefined') {
            if (!this.length) { return undefined; }
            return this.elements[0].style[name] || (window.getComputedStyle ? window.getComputedStyle(this.elements[0])[name] : '');
        }
        this.each(function() { this.style[name] = val; });
        return this;
    },
    addClass: function(cls) {
        let parts = (cls || '').split(/\s+/);
        this.each(function() {
            for (let i = 0; i < parts.length; i++) {
                if (parts[i] && this.classList) { this.classList.add(parts[i]); }
            }
        });
        return this;
    },
    removeClass: function(cls) {
        let parts = (cls || '').split(/\s+/);
        this.each(function() {
            for (let i = 0; i < parts.length; i++) {
                if (parts[i] && this.classList) { this.classList.remove(parts[i]); }
            }
        });
        return this;
    },
    hasClass: function(cls) {
        if (!this.length || !this.elements[0].classList) { return false; }
        return this.elements[0].classList.contains(cls);
    },
    toggleClass: function(cls) {
        this.each(function() {
            if (this.classList) { this.classList.toggle(cls); }
        });
        return this;
    },
    on: function(events, handler) {
        let names = (events || '').split(/\s+/);
        this.each(function() {
            let el = this;
            if (!CND._domListenerMap) { return; }
            let store = CND._domListenerMap.get(el);
            if (!store) { store = {}; CND._domListenerMap.set(el, store); }
            for (let i = 0; i < names.length; i++) {
                let ev = names[i];
                if (!ev) { continue; }
                if (!store[ev]) { store[ev] = []; }
                store[ev].push(handler);
                el.addEventListener(ev, handler);
            }
        });
        return this;
    },
    off: function(events, handler) {
        let names = (events || '').split(/\s+/);
        this.each(function() {
            let el = this;
            if (!CND._domListenerMap) { return; }
            let store = CND._domListenerMap.get(el);
            if (!store) { return; }
            for (let i = 0; i < names.length; i++) {
                let ev = names[i];
                if (!ev || !store[ev]) { continue; }
                let list = store[ev];
                if (typeof handler === 'function') {
                    el.removeEventListener(ev, handler);
                    store[ev] = list.filter(function(h) { return h !== handler; });
                } else {
                    for (let j = 0; j < list.length; j++) {
                        el.removeEventListener(ev, list[j]);
                    }
                    store[ev] = [];
                }
            }
        });
        return this;
    },
    bind: function(events, handler) {
        return this.on(events, handler);
    },
    unbind: function(events, handler) {
        return this.off(events, handler);
    },
    prepend: function(content) {
        this.each(function() {
            if (typeof content === 'string') {
                this.insertAdjacentHTML('afterbegin', content);
            } else if (content instanceof CND.DomSet) {
                for (let i = content.elements.length - 1; i >= 0; i--) {
                    this.insertBefore(content.elements[i], this.firstChild);
                }
            } else if (content && content.nodeType) {
                this.insertBefore(content, this.firstChild);
            }
        });
        return this;
    },
    append: function(content) {
        this.each(function() {
            if (typeof content === 'string') {
                this.insertAdjacentHTML('beforeend', content);
            } else if (content instanceof CND.DomSet) {
                for (let i = 0; i < content.elements.length; i++) {
                    this.appendChild(content.elements[i]);
                }
            } else if (content && content.nodeType) {
                this.appendChild(content);
            }
        });
        return this;
    },
    appendTo: function(target) {
        CND.dom(target).append(this);
        return this;
    },
    remove: function() {
        this.each(function() {
            if (this && this.parentNode) { this.parentNode.removeChild(this); }
        });
        return this;
    },
    clone: function() {
        let out = [];
        this.each(function() {
            if (this && this.cloneNode) { out.push(this.cloneNode(true)); }
        });
        return new CND.DomSet(out);
    },
    sort: function(cmp) {
        let arr = this.elements.slice().sort(cmp);
        return new CND.DomSet(arr);
    },
    offset: function() {
        if (!this.length || !this.elements[0].getBoundingClientRect) {
            return { top: 0, left: 0 };
        }
        let rect = this.elements[0].getBoundingClientRect();
        return {
            top: rect.top + (window.pageYOffset || document.documentElement.scrollTop || 0),
            left: rect.left + (window.pageXOffset || document.documentElement.scrollLeft || 0)
        };
    },
    height: function() {
        if (!this.length) { return 0; }
        let el = this.elements[0];
        return el.offsetHeight || el.clientHeight || 0;
    },
    innerHeight: function() {
        if (!this.length) { return 0; }
        return this.elements[0].clientHeight || 0;
    },
    scrollTop: function(val) {
        if (typeof val === 'undefined') {
            if (!this.length) { return 0; }
            let el = this.elements[0];
            if (el === window || el === document) {
                return window.pageYOffset || document.documentElement.scrollTop || 0;
            }
            return el.scrollTop || 0;
        }
        this.each(function() {
            if (this === window || this === document) {
                window.scrollTo(0, val);
            } else {
                this.scrollTop = val;
            }
        });
        return this;
    },
    animate: function(props, duration) {
        duration = typeof duration === 'number' ? duration : 250;
        this.each(function() {
            let el = this;
            if (props && typeof props.scrollTop !== 'undefined') {
                let target = parseInt(props.scrollTop, 10) || 0;
                let start = el.scrollTop || 0;
                let diff = target - start;
                if (duration <= 0 || Math.abs(diff) < 1) {
                    el.scrollTop = target;
                    return;
                }
                let t0 = null;
                let step = function(ts) {
                    if (t0 === null) { t0 = ts; }
                    let p = Math.min(1, (ts - t0) / duration);
                    el.scrollTop = start + diff * p;
                    if (p < 1) { requestAnimationFrame(step); }
                };
                requestAnimationFrame(step);
            }
        });
        return this;
    },
    confirmation: function(options) {
        options = options || {};
        this.each(function() {
            CND.attachConfirmation(this, options);
        });
        return this;
    }
};

CND.dom = function(sel) {
    return new CND.DomSet(CND._domNormalize(sel));
};

CND.ready = function(callback, failureCallback) {
    let run = function() {
        if (typeof callback === 'function') {
            setTimeout(function() { callback(); }, 1);
        }
    };
    try {
        if (typeof document === 'undefined') {
            run();
            return;
        }
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            run();
        } else {
            document.addEventListener('DOMContentLoaded', run, { once: true });
        }
    } catch (err) {
        console.log(CND.date.now(), 'CND.ready error', err);
        if (typeof failureCallback === 'function') {
            setTimeout(function() { failureCallback(); }, 1);
        }
    }
};

// Compatibility shim: historical callers wait on jQueryLoader; now DOM ready.
CND.jQueryLoader = function(successCallback, failureCallback) {
    CND.ready(successCallback, failureCallback);
};

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
    isMomentObj : function(testObj=null) {
        if ((typeof Moment === 'function') || (typeof Moment === 'object') || ('Moment' in self)) {
            if (testObj instanceof Moment) {
                return true;
            }
        }
        return false;
    }, // end function isMomentObj
    momentCalendar : {
        lastDay : "[Yesterday at] h:mm a",
        sameDay : "[Today at] h:mm a",
        nextDay : "[Tomorrow at] h:mm a",
        lastWeek : "[Last] ddd [at] h:mm a",
        nextWeek : "ddd [at] h:mm a",
        sameElse : "ddd, MMM Do YYYY [at] h:mm a"
    },
    getMonthName : function( number=0 ) {
        let month = [];
        month[0] = "January";
        month[1] = "February";
        month[2] = "March";
        month[3] = "April";
        month[4] = "May";
        month[5] = "Jun";
        month[6] = "July";
        month[7] = "August";
        month[8] = "September";
        month[9] = "October";
        month[10] = "November";
        month[11] = "December";
        return month[number];
    },
    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 (CND.date.isMomentObj(dt)) {
            return dt.format('YYYY-MM-DD');
        }
        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 (CND.date.isMomentObj(dt)) {
            return dt.format('HH:mm');
        }
        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 (CND.date.isMomentObj(dt)) {
            dt = dt.format('YYYY-MM-DD HH:mm:ss.SSSSS Z');
        }
        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 (CND.date.isMomentObj(dt)) {
            dt = dt.format('YYYY-MM-DD HH:mm:ss.SSSSS Z');
        }
        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
    getTimezoneName : function() {
        let localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
        if ((typeof localTimezone != 'undefined') && (localTimezone !== null)) { return localTimezone; }

        const today = new Date();
        const short = today.toLocaleDateString(undefined);
        const full = today.toLocaleDateString(undefined, { timeZoneName: 'long' });

        // Trying to remove date from the string in a locale-agnostic way
        const shortIndex = full.indexOf(short);
        if (shortIndex >= 0) {
            const trimmed = full.substring(0, shortIndex) + full.substring(shortIndex + short.length);

            // by this time `trimmed` should be the timezone's name with some punctuation -
            // trim it from both sides
            return trimmed.replace(/^[\s,.\-:;]+|[\s,.\-:;]+$/g, '');

        } else {
            // in some magic case when short representation of date is not present in the long one, just return the long one as a fallback, since it should contain the timezone's name
            return full;
        } // end if shortIndex
    },
    myLocalTZ : {
        name : function() { return CND.date.getTimezoneName(); }
    },
    convertTZ : function(dt=null, srcTZ='', dstTZ='', strRet=false) {
        if (CND.date.isMomentObj(dt)) {
            dt = dt.format('YYYY-MM-DD HH:mm:ss.SSSSS Z');
        }
        if (!dt) { return null; }
        if (!srcTZ || srcTZ === null || srcTZ == '') { srcTZ = "Etc/UTC"; }
        if (!dstTZ || dstTZ === null || dstTZ == '') { dstTZ = CND.date.myLocalTZ.name(); }
        let srcDate = moment.tz(dt, srcTZ);
        let returnDate = srcDate.clone().tz(dstTZ);
        if (strRet) {
            returnDate = returnDate.format('YYYY-MM-DD HH:mm:ss.SSSSS Z');
        }
        return returnDate;
    },
    local_date_format : function(dt, srcTZ, dstTZ) {
        let thisDateLocal = CND.date.convertTZ(dt, srcTZ, dstTZ);
        return thisDateLocal.calendar(null, momentCalendar);
    },
    secToHHMMSS : function(tsec) {
        if (!tsec) { return '00:00'; }
        let h=0, m=0, s=0, rsec = tsec, hhmmss='00:00';
        if (typeof tsec === 'number') {
            if (rsec > 3599) {
                h = Math.floor(rsec / 3600);
                rsec = rsec - (h * 3600);
            }
            if (rsec > 59) {
                m = Math.floor(rsec / 60);
                rsec = rsec - (m * 60);
            }
            if (rsec > 0) {
                s = rsec;
            }
            hhmmss = (h>0?h+":":"") + (m<10?"0":"") + m + ":" + (s<10?"0":"") + s;
        } // end if tsec
        return hhmmss;
    }, // end function secToHHMMSS
    moment_format : function(dt, srcTZ, dstTZ) {
        let thisDateLocal = CND.date.convertTZ(dt, srcTZ, dstTZ);
        let thisDateLocalStr = thisDateLocal.fromNow();
        if ((CND.str.occurrences(thisDateLocalStr, "second") == 0) && (CND.str.occurrences(thisDateLocalStr, "minute") == 0)) {
            thisDateLocalStr = thisDateLocal.calendar(null, momentCalendar);
        } else if (CND.str.occurrences(thisDateLocalStr, "Invalid date") > 0) {
            thisDateLocalStr = "ERROR";
        }
        return thisDateLocalStr;
    }
}; // end object date
CND.str = {
    capitalizeFirstLetter : function(str='') {
        return str.charAt(0).toUpperCase() + str.slice(1);
    }, // end method capitalizeFirstLetter
    phoneNumber : function(phoneNum='', format='human') {
        phoneNum = phoneNum.toString(); // cast to string
        phoneNum = phoneNum.replace(/[^0-9\+]/g, '');
        let resultPhoneNum = phoneNum;
        if ((phoneNum != '') && CND.str.testPhoneNumber(phoneNum)) {
            //console.log(CND.date.now(), phoneNum.length, phoneNum.substring(0, 1), phoneNum.substring(0, 0));
            //console.log(CND.date.now(), phoneNum);
            if (format == 'human') {
                if ((phoneNum.length == 12) && (phoneNum.substring(0, 2) == '+1')) {
                    resultPhoneNum = '(' + phoneNum.substring(2, 5) + ') ' + phoneNum.substring(5, 8) + '-' + phoneNum.substring(8, 12);
                } else if ((phoneNum.length == 11) && (phoneNum.substring(0, 1) == '1')) {
                    resultPhoneNum = '(' + phoneNum.substring(1, 4) + ') ' + phoneNum.substring(4, 7) + '-' + phoneNum.substring(7, 11);
                } else if ((phoneNum.length == 10) && (phoneNum.substring(0, 1) != '+')) {
                    resultPhoneNum = '(' + phoneNum.substring(0, 3) + ') ' + phoneNum.substring(3, 6) + '-' + phoneNum.substring(6, 10);
                } // end if phoneNum.length
            } else {
                if ((phoneNum.length == 12) && (phoneNum.substring(0, 2) == '+1')) {
                    resultPhoneNum = '1' + phoneNum.substring(2, 5) + phoneNum.substring(5, 8) + phoneNum.substring(8, 12);
                } else if ((phoneNum.length == 10) && (phoneNum.substring(0, 1) != '+')) {
                    resultPhoneNum = '1' + phoneNum.substring(0, 3) + phoneNum.substring(3, 6) + phoneNum.substring(6, 10);
                } // end if phoneNum.length
            } // end if format
        } // end if phoneNum
        return resultPhoneNum;
    }, // end method phoneNumber
    testPhoneNumber : function( text='' ) {
        let usnanpa = /^(\+1|1)?[-.]?[ ]?\(?([2-9]{1})([0-9]{2})\)?[-.]?[ ]?([2-9]{1})([0-9]{2})[-.]?[ ]?([0-9]{4})$/;
        return usnanpa.test( text );
    }, // end method testPhoneNumber
    checkPhoneNumber : function( element_id='', format='human' ) {
        if (element_id == '') { return false; }
        let inputtxt = CND.dom(element_id).val();
        let result = false;
        let resulttxt = inputtxt;

        if (CND.str.testPhoneNumber( inputtxt )) {
            result = true;
            if (format == 'machine') {
                resulttxt = CND.str.phoneNumber( inputtxt, 'machine' );
            } else {
                resulttxt = CND.str.phoneNumber( inputtxt, 'human' );
            } // end if format
            //console.log(CND.date.now(), "Phone Number is a valid US Number.", inputtxt, resulttxt);
            if (inputtxt !== resulttxt) {
                CND.dom(element_id).val( resulttxt );
            }
        } else {
            console.log(CND.date.now(), "Phone Number is NOT a valid US Number", inputtxt);
        }
        return result;
    }, // end method checkPhoneNumber
    timeoutCheckPhoneNumber : function( element_id ) {
        if (checkPhoneNumberTimeout) { clearTimeout( checkPhoneNumberTimeout ); }
        if (CND.dom(element_id).val() != "") {
            checkPhoneNumberTimeout = setTimeout(function() { CND.str.checkPhoneNumber( element_id ); }, checkPhoneNumberDelay );
        } // end if checkPhoneNumber
    }, // end method timeoutCheckPhoneNumber
    number_format : function(number=0, decimals=2, dec_point='.', thousands_sep=',') {
        // Strip all characters but numerical ones.
        number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
        let n = !isFinite(+number) ? 0 : +number,
        prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
        sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
        dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
        s = '',
        toFixedFix = function (n, prec) {
            let k = Math.pow(10, prec);
            return '' + Math.round(n * k) / k;
        };
        // Fix for IE parseFloat(0.55).toFixed(0) = 0;
        s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
        if (s[0].length > 3) {
            s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
        }
        if ((s[1] || '').length < prec) {
            s[1] = s[1] || '';
            s[1] += new Array(prec - s[1].length + 1).join('0');
        }
        return s.join(dec);
    }, // end method number_format
    occurrences : function(str='', sub='', allowOverlapping=false) {
        str = str.toString(); sub = sub.toString();
        if (sub.length <= 0) return (str.length + 1);
        let n = 0, pos = 0, step = allowOverlapping ? 1 : sub.length;
        while (true) {
            pos = str.indexOf(sub, pos);
            if (pos >= 0) { ++n; pos += step; } else break;
        }
        return n;
    } // end method occurrences
}; // end object str
CND.ui = {
    html : function(el, html) {
        try {
            if (CND.dom(el).length > 0) {
                CND.dom(el).html(html);
            } // end if DOM element
        } catch (err) { console.log(CND.date.now(), err); }
    }, // end method html
    show : function(el) {
        try {
            if (CND.dom(el).length > 0) {
                CND.dom(el).removeClass('none');
            } // end if DOM element
        } catch (err) { console.log(CND.date.now(), err); }
    }, // end method show
    hide : function(el) {
        try {
            if (CND.dom(el).length > 0) {
                if (!CND.dom(el).hasClass('none')) {
                    CND.dom(el).addClass('none');
                } // end if hasClass
            } // end if DOM element
        } catch (err) { console.log(CND.date.now(), err); }
    }, // end method hide
    successColor : '#00bc7c',
    errorColor : '#ff5b57',
    successBadgeColor : '#f13628',
    errorBadgeColor : '#f13628',
    showError : function(error) {
        try {
            CND.dom('#error_msg').find('p').html(error);
            CND.ui.show('#error_msg_row');
        } catch (err) {
            console.log(CND.date.now(), err, error);
        } // end try jquery
    },
    showSuccess : function(msg) {
        try {
            CND.dom('#success_msg').find('p').html(msg);
            CND.ui.show('#success_msg_row');
        } catch (err) {
            console.log(CND.date.now(), err, msg);
        } // end try jquery
    },
    hideSuccess : function() {
        try {
            CND.ui.hide('#success_msg_row');
        } catch (err) {
            console.log(CND.date.now(), err);
        }
    },
    hideError : function() {
        try {
            CND.ui.hide('#error_msg_row');
        } catch (err) {
            console.log(CND.date.now(), err);
        }
    }, // end method hideError
    scrollBarWidth : function() {
        let scrollBarWidth = 0;
        // Add temporary box to wrapper
        let scrollbox = document.createElement('div');

        // Make box scrollable
        scrollbox.style.overflow = 'scroll';

        // Append box to document
        document.body.appendChild(scrollbox);

        // Measure inner width of box
        scrollBarWidth = scrollbox.offsetWidth - scrollbox.clientWidth;

        // Remove box
        document.body.removeChild(scrollbox);
        return scrollBarWidth;
    }, // end function scrollBarWidth
    scrollTo : function(scrollable=null, find=null, padding=0, duration=250, start=0, debug=false) {
        // 'start' is the default starting position, examples; top, bottom, end, 0, 150
        try {
            let list_top = 0, list_height = 0, list_half = 0;
            list_top = parseInt(CND.dom(scrollable).offset().top); // Top offset of container
            list_height = parseInt(CND.dom(scrollable).innerHeight()); // Height of container
            list_half = Math.round(list_height / 2); // Half Height of container
            if (debug) { console.log(CND.date.now(), scrollable, 'list_top', list_top, 'list_height', list_height, 'list_half', list_half); }

            let content_height = 0, current_scroll_top = 0, current_scroll_end = 0;
            content_height = parseInt(CND.dom(scrollable)[0].scrollHeight);  // Height of content
            current_scroll_top = parseInt(CND.dom(scrollable).scrollTop());
            current_scroll_end = current_scroll_top + list_height;
            if (debug) { console.log(CND.date.now(), scrollable, 'content_height', content_height, 'current_scroll_top', current_scroll_top, 'current_scroll_end', current_scroll_end); }

            let direction = 'asc';
            if ((start == 'top') || (start == '0')) { start = 0; }
             else if ((start == 'end') || (start == 'bottom')) { start = content_height; direction = 'desc'; }

            let selected_top = start, selected_height = 0, selected_half = 0;
            if ((typeof find != 'undefined') && (CND.dom(scrollable).find(find).length > 0)) {
                if (direction == 'desc') {
                    selected_top = parseInt(CND.dom(scrollable).last(find)[0].offsetTop);
                    selected_height = parseInt(CND.dom(scrollable).last(find).height());
                } else {
                    selected_top = parseInt(CND.dom(scrollable).find(find)[0].offsetTop);
                    selected_height = parseInt(CND.dom(scrollable).find(find).height());
                }
                selected_half = Math.round(selected_height / 2);
            } // end if find
            if (debug) { console.log(CND.date.now(), scrollable, 'selected_top', selected_top, 'selected_height', selected_height, 'selected_half', selected_half); }

            let current_buff_top = 0, current_buff_end = 0;
            current_buff_top = current_scroll_top + selected_height + padding;
            current_buff_end = current_scroll_end - selected_height *2 - padding;
            if (current_buff_end < 0) { current_buff_end = current_buff_top; }
            if (debug) { console.log(CND.date.now(), scrollable, 'current_buff_top', current_buff_top, 'current_buff_end', current_buff_end); }

            let scroll_to_middle = 0, scroll_to = start;
            scroll_to = scroll_to_middle = selected_top - list_half + selected_half + padding;
            if (scroll_to_middle <= (padding + selected_height)) { scroll_to = 0; }
             else if (scroll_to_middle >= (content_height - list_height)) { scroll_to = content_height - list_height; }
            if (debug) { console.log(CND.date.now(), scrollable, 'scroll_to_middle', scroll_to_middle, 'scroll_to', scroll_to); }

            if (scroll_to != current_scroll_top) { if (debug) { console.log(CND.date.now(), scrollable, 'scroll_to != current_scroll_top', scroll_to,  current_scroll_top); } }
            if (selected_top < current_buff_top) { if (debug) { console.log(CND.date.now(), scrollable, 'selected_top < current_buff_top', selected_top, current_buff_top); } }
            if (current_buff_end < selected_top) { if (debug) { console.log(CND.date.now(), scrollable, 'selected_top > current_buff_end', selected_top, current_buff_end); } }
            if ((scroll_to != current_scroll_top) && ((selected_top < current_buff_top) || (current_buff_end < selected_top))) {
                if (debug) { console.log(CND.date.now(), scrollable, 'Scrolling Now!'); }
                setTimeout(function() { CND.dom(scrollable).animate({ scrollTop : scroll_to.toString() + 'px' }, duration); }, 1);
                if (debug) { setTimeout(function() { let new_scroll_top = CND.dom(scrollable).scrollTop(); console.log(CND.date.now(), scrollable, 'new_scroll_top', new_scroll_top); }, (duration + 101)); }
            } // end if scroll_to
        } catch (err) { console.log(CND.date.now(), err); }
    }, // end function scrollTo
    sortElements : function(parent='', sort=[]) {
        let element_sort = {};
        if (sort.length > 0) {
            for (let i = 0; i < sort.length; ++i) {
                element_sort = sort[i];
                CND.dom(parent).children('div').sort(CND_dec_sort).appendTo(this[0]);
            } // end foreach sort
        } // end if sort
        function CND_dec_sort(a, b) {
            let sort_mode = 0; // 0 | 1 | -1
            for (let attr in element_sort) {
                if(element_sort.hasOwnProperty(attr)) {
                    let dir = element_sort[attr] || 'asc';
                        dir = dir.toString().toLowerCase();
                    let val_a = CND.dom(a).attr(attr);
                    let val_b = CND.dom(b).attr(attr);
                    if (((dir == 'asc') && (val_a > val_b)) || ((dir == 'desc') && (val_a < val_b))) {
                        sort_mode = 1; // A first
                    } else if (((dir == 'asc') && (val_a < val_b)) || ((dir == 'desc') && (val_a > val_b))) {
                        sort_mode = -1; // B first
                    } // else no change
                } // end if hasOwnProperty
            } // end foreach attr
            return sort_mode;
        } // end function CND_dec_sort
    }, // end method sortElements
    handleShiftOn : false,
    handleFormEnter : function(form_id, callback) {
        if (typeof callback == 'function') {
            try {
                if (CND.dom(form_id).length > 0) {
                    CND.dom(form_id).find('input[type="text"], input[type="number"], input[type="password"], textarea').off('keydown keyup').on('keydown keyup', function(evt) {
                        //console.log(CND.date.now(), 'keyCode', evt.keyCode);
                        if (evt.keyCode === 16 && evt.type === 'keydown') {
                            CND.ui.handleShiftOn = true;
                        } else if (evt.keyCode === 16 && evt.type === 'keyup') {
                            CND.ui.handleShiftOn = false;
                        } // end if shift
                        if (CND.ui.handleShiftOn === false && evt.keyCode === 13) {
                            evt.preventDefault();
                            if (evt.type === 'keyup') {
                                setTimeout(function() { callback(evt); }, 1);
                            } // end if type
                        } // end if enter
                    }); // end on keyup || keydown
                } // end if form_id
            } catch (err) {
                console.log(CND.date.now(), err);
            } // end try form_id
        } // end if callback
    }, // end method handleFormEnter
    unhandleFormEnter : function(form_id) {
        try {
            if (CND.dom(form_id).length > 0) {
                CND.dom(form_id).find('input[type="text"], input[type="number"], input[type="password"], textarea').off('keydown keyup');
            } // end if form_id
        } catch (err) {
            console.log(CND.date.now(), err);
        } // end try form_id
    } // end method unhandleFormEnter
}; // end object Ui


/////// Required Dependencies
if (typeof CND.tryImportScripts == 'undefined') {
    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 (err) {
            console.error(CND.date.now(), 'Error Loading Script', scriptURL);
            console.error(CND.date.now(), err);
            CND.importAllSuccess = false;
            return false;
        }
    }; // end function tryImportScript
} // end if CND.tryImportScripts

if (typeof CND.importAllSuccess == 'undefined') { CND.importAllSuccess = true; }
if (typeof CND.srcScripts == 'undefined') {
    CND.srcScripts = [ '/assets/moment.js', '/assets/moment-timezone.js', '/js/templates.js', '/js/voipawesome.js' ];
    CND.srcsImported = false;
}




///////////// Global Functions /////////////
CND.handleIsLoadedWait = 1;
CND.handleIsLoadedTimeout = null;
CND.handleIsLoaded = function(fn='', successCallback=null, failureCallback=null, maxWait=10000, type='function') {
    if (!((typeof fn == 'string') && (fn !== ''))) { return; }
    if (CND.handleIsLoadedTimeout !== null) { clearTimeout(CND.handleIsLoadedTimeout); }
    CND.handleIsLoadedTimeout = setTimeout(function() {
        if (typeof fn == type) {
            if (typeof successCallback == 'function') { setTimeout(function() { successCallback(); }, 1); }
        } else if ((typeof fn == 'string') && (typeof CND != 'undefined') && (typeof CND[fn] == type)) {
            if (typeof successCallback == 'function') { setTimeout(function() { successCallback(); }, 1); }
        } else if (CND.popup.handleIsLoadedWait < maxWait) {
            CND.handleIsLoadedWait = CND.handleIsLoadedWait + CND.handleIsLoadedWait * 3;
            CND.handleIsLoaded(fn, successCallback, failureCallback, maxWait);
        } else {
            if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(); }, 1); }
        } // end if fn
    }, CND.handleIsLoadedWait);
}; // end function handleIsLoaded

CND.serializeParams = function(data={}) {
    let parts = [];

    let appendParam = function(key, value) {
        if (typeof value == 'undefined') { return; }
        if (value === null) { value = ''; }
        if (typeof value == 'boolean') { value = value ? 'true' : 'false'; }
        parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
    };

    let buildParams = function(prefix, value) {
        if (typeof value == 'undefined') { return; }
        if (value === null) { appendParam(prefix, ''); return; }
        if (Array.isArray(value)) {
            value.forEach(function(item) {
                buildParams(prefix, item);
            });
            return;
        }
        if (typeof value == 'object') {
            Object.keys(value).forEach(function(key) {
                buildParams(prefix + '[' + key + ']', value[key]);
            });
            return;
        }
        appendParam(prefix, value);
    };

    Object.keys(data || {}).forEach(function(key) {
        buildParams(key, data[key]);
    });

    return parts.join('&');
}; // end function CND.serializeParams

CND.ajax = function(o={}, successCallback=null, failureCallback=null, debug=false) {
    let d = o.data || null;
    let p = null;
    if (d !== null) {
        if ((typeof d == 'string') && (d != '')) {
            p = d;
        } else if (typeof d == 'object') {
            if (typeof CND.serializeParams == 'function') {
                p = CND.serializeParams(d);
            } else {
                p = [];
                for (let k in d) {
                    if (d.hasOwnProperty(k)) {
                        p.push(encodeURIComponent(k) + '=' + encodeURIComponent(d[k]));
                    }
                }
                p = p.join('&');
            }
        } // end if typeof
    } // end if d
    let dt = o.dataType || 'json';
    let url = o.url || '';
    let a = {
        url : o.url || '',
        type: o.type || o.method || 'POST',
        method: o.method || o.type || 'POST',
        dataType: o.dataType || 'json',
        data: p,
        headers: o.headers || {},
        async: o.async || true,
        beforeSend: function(xhr, settings) {
            if (typeof o.beforeSend == 'function') {
                if (debug) {
                    console.log(CND.date.now(), 'Ajax beforeSend');
                    console.log(CND.date.now(), 'url', url);
                    console.log(CND.date.now(), 'data', d);
                    console.log(CND.date.now(), 'xhr', xhr);
                    console.log(CND.date.now(), 'settings', settings);
                } // end if debug
            } // end if o.beforeSend
            if (typeof o.beforeSend == 'function') { setTimeout(function() { o.beforeSend(xhr, settings); }, 1); }
        },
        success: function(data, status, xhr) {
            if ((typeof o.success == 'function') || (typeof successCallBack == 'function')) {
                if (debug) {
                    console.log(CND.date.now(), 'Ajax success');
                    console.log(CND.date.now(), 'url', url);
                    console.log(CND.date.now(), 'data', data);
                    console.log(CND.date.now(), 'xhr', xhr);
                    console.log(CND.date.now(), 'status', status);
                } // end if debug
            } // end if functions
            if (typeof o.success == 'function') { setTimeout(function() { o.success(data, status, xhr); }, 1); }
            if (typeof successCallBack == 'function') { setTimeout(function() { successCallback(data, status, xhr); }, 1); }
        },
        complete: function(xhr, status) {
            if (typeof o.complete == 'function') {
                if (debug) {
                    console.log(CND.date.now(), 'Ajax complete');
                    console.log(CND.date.now(), 'url', url);
                    console.log(CND.date.now(), 'data', d);
                    console.log(CND.date.now(), 'xhr', xhr);
                    console.log(CND.date.now(), 'status', status);
                } // end if debug
            } // end if o.complete
            if (typeof o.complete == 'function') { setTimeout(function() { o.complete(xhr, status); }, 1); }
        },
        timeout: o.timeout || 15000,
        username: o.username || null,
        password: o.password || null,
        statusCode: o.statusCode || {},
        crossDomain: o.crossDomain || true,
        accepts: o.accepts || 'application/' + dt,
        contentType: o.contentType || 'application/x-www-form-urlencoded; charset=UTF-8',
        mimeType: o.mimeType || null,
        cache: o.cache || false
    }; // end a
    //console.log(CND.date.now(), 'a.timeout', a.timeout || null);
    if (CND.online && (typeof a.url == 'string') && (a.url != '')) {
            // Use vanilla javascript request as a fetch

            let url = a.url || '';
            let method = a.method || 'POST';
            let data = a.data || [];
            let dt = a.dataType || 'json';
            let async = a.async || true;
            let headers = a.headers || {};
            if (typeof headers['Content-Type'] == 'undefined' && a.contentType) {
                headers['Content-Type'] = a.contentType;
            }

            if (debug) {
                console.log(CND.date.now(), async, method, dt, url, data, headers);
            }

            if (dt == 'json') {
                let contentType = (headers['Content-Type'] || '').toString().toLowerCase();
                if (typeof data == 'object' && contentType.includes('application/json')) {
                    try {
                        data = JSON.stringify(data);
                    } catch (err) {
                        console.log(CND.date.now(), 'data is not JSONifiable', data, err);
                    }
                }
                if (typeof headers['Content-Type'] == 'undefined') {
                    headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
                }

            } else {
                if (method === 'POST') {
                    if (typeof headers['Content-Type'] == 'undefined') {
                        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
                    }
                }
            }

            fetch(url, {
                'method': method,
                'headers': headers,
                'body': data,
            }).then(response => {
                if (debug) {
                    console.log(CND.date.now(), 'Request response', response);
                }
                let status = response.status || 408;
                let xhr = response;

                if (response.ok) {
                    if (dt == 'json') {
                        response.json().then(response => {
                            if (debug) { console.log(CND.date.now(), 'JSON Request success', response); }
                            let data = response;
                            if ((typeof o.success == 'function') || (typeof successCallBack == 'function')) {
                                if (debug) {
                                    console.log(CND.date.now(), 'Ajax success');
                                    console.log(CND.date.now(), 'url', url);
                                    console.log(CND.date.now(), 'data', data);
                                    console.log(CND.date.now(), 'xhr', xhr);
                                    console.log(CND.date.now(), 'status', status);
                                } // end if debug
                            } // end if functions
                            if (typeof o.success == 'function') { setTimeout(function() { o.success(data, status, xhr); }, 1); }
                            if (typeof successCallBack == 'function') { setTimeout(function() { successCallback(data, status, xhr); }, 1); }

                        }).catch(exception => {
                            if (debug) { console.log(CND.date.now(), 'Request exception', exception); }
                            if ((typeof o.error == 'function') || (typeof failureCallBack == 'function')) {
                                if (debug) {
                                    console.log(CND.date.now(), 'Caught something');
                                    console.log(CND.date.now(), err);
                                    console.log(CND.date.now(), 'url', url);
                                    console.log(CND.date.now(), 'data', d);
                                } // end if debug
                            } // end if functions
                            if (typeof o.error == 'function') { setTimeout(function() { o.error(null, 'error', err); }, 1); }
                            if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(null, 'error', err); }, 1); }
                        });
                    } else {
                        response.text().then(response => {
                            if (debug) { console.log(CND.date.now(), 'Plain Request success', response); }
                            let data = response;
                            if ((typeof o.success == 'function') || (typeof successCallBack == 'function')) {
                                if (debug) {
                                    console.log(CND.date.now(), 'Ajax success');
                                    console.log(CND.date.now(), 'url', url);
                                    console.log(CND.date.now(), 'data', data);
                                    console.log(CND.date.now(), 'xhr', xhr);
                                    console.log(CND.date.now(), 'status', status);
                                } // end if debug
                            } // end if functions
                            if (typeof o.success == 'function') { setTimeout(function() { o.success(data, status, xhr); }, 1); }
                            if (typeof successCallBack == 'function') { setTimeout(function() { successCallback(data, status, xhr); }, 1); }

                        });
                    } // end if dataType
                } else {
                    if (dt == 'json') {
                        response.json().then(response => {
                            if (debug) { console.log(CND.date.now(), 'JSON Request failure', response); }
                            let data = response;
                            if ((typeof o.error == 'function') || (typeof failureCallBack == 'function')) {
                                if (debug) {
                                    console.log(CND.date.now(), 'Request failed');
                                    console.log(CND.date.now(), data);
                                    console.log(CND.date.now(), 'url', url);
                                    console.log(CND.date.now(), 'data', d);
                                } // end if debug
                            } // end if functions
                            if (typeof o.error == 'function') { setTimeout(function() { o.error(null, 'error', data); }, 1); }
                            if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(null, 'error', data); }, 1); }

                        }).catch(exception => {
                            if (debug) { console.log(CND.date.now(), 'Request exception', exception); }
                            if ((typeof o.error == 'function') || (typeof failureCallBack == 'function')) {
                                if (debug) {
                                    console.log(CND.date.now(), 'Caught something');
                                    console.log(CND.date.now(), exception);
                                    console.log(CND.date.now(), 'url', url);
                                    console.log(CND.date.now(), 'data', d);
                                } // end if debug
                            } // end if functions
                            if (typeof o.error == 'function') { setTimeout(function() { o.error(null, 'error', exception); }, 1); }
                            if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(null, 'error', exception); }, 1); }

                        });
                    } else {
                        response.text().then(response => {
                            if (debug) { console.log(CND.date.now(), 'Plain Request failure', response); }
                            if ((typeof o.error == 'function') || (typeof failureCallBack == 'function')) {
                                if (debug) {
                                    console.log(CND.date.now(), 'Request failed');
                                    console.log(CND.date.now(), response);
                                    console.log(CND.date.now(), 'url', url);
                                    console.log(CND.date.now(), 'data', d);
                                } // end if debug
                            } // end if functions
                            if (typeof o.error == 'function') { setTimeout(function() { o.error(null, 'error', response); }, 1); }
                            if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(null, 'error', response); }, 1); }

                        });
                    } // end if dataType
                } // end if response.ok
            }).catch(exception => {
                if (debug) { console.log(CND.date.now(), 'Request exception', exception); }
                if ((typeof o.error == 'function') || (typeof failureCallBack == 'function')) {
                    if (debug) {
                        console.log(CND.date.now(), 'Caught something');
                        console.log(CND.date.now(), exception);
                        console.log(CND.date.now(), 'url', url);
                         console.log(CND.date.now(), 'data', d);
                    } // end if debug
                } // end if functions
                if (typeof o.error == 'function') { setTimeout(function() { o.error(null, 'error', exception); }, 1); }
                if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(null, 'error', exception); }, 1); }

            }); // end fetch
    } else if (!CND.online) {
        if ((typeof o.offline == 'function') || (typeof offlineCallBack == 'function')) {
            if (debug) {
                console.log(CND.date.now(), 'Offline! Try Later');
                console.log(CND.date.now(), 'url', url);
                console.log(CND.date.now(), 'data', d);
            } // end if debug
        } // end if functions
        if (typeof o.offline == 'function') { setTimeout(function() { o.offline(null, 'offline', null); }, 1); }
        if (typeof offlineCallback == 'function') { setTimeout(function() { offlineCallback(); }, 1); }
    } else {
        if ((typeof o.error == 'function') || (typeof failureCallBack == 'function')) {
            if (debug) {
                console.log(CND.date.now(), 'No URL provided');
                console.log(CND.date.now(), 'url', url);
                console.log(CND.date.now(), 'data', d);
            } // end if debug
        } // end if functions
        if (typeof o.error == 'function') { setTimeout(function() { o.error(null, 'error', 'no url provided'); }, 1); }
        if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(null, 'error', 'no url provided'); }, 1); }
    } // end if url
}; // end function CND.ajax

CND.hasFocus = function() {
    let visible = !!(document.visibilityState == 'visible');
    let focused = false;
    let iframes = document.querySelector('iframe');
    if ((typeof iframes != 'undefined') && (iframes !== null)) {
        //console.log(CND.date.now(), 'iframes', iframes);
        focused = !!(document.hasFocus() || document.getElementById('iframe').contentWindow.document.hasFocus());
    } else {
        focused = !!(document.hasFocus());
    }
    //console.log(CND.date.now(), 'visible', visible, 'focused', focused);
    return visible && focused;
}; // end function CND.hasFocus

CND.reload = function() {
    location.reload();
}; // end function CND.reload

CND.loadExtensionPage = function(href='', hash='') {
    setTimeout(function() {
        if (typeof window != "undefined") {
            window.location.hash = hash;
            window.location.href = href;
        }
    }, 10);
}; // end function CND.loadExtensionPage

CND.getSearchResults = function(x=[], s='', all=true) {
    if ((typeof s != 'string') || (s == '')) { return x; }
    let i = 0, ii = 0, m = all ? x : [];
    //console.log(CND.date.now(), 'Rows', typeof x, x.length);
    if ((typeof x == 'object') && (x.length > 0)) {
        //console.log(CND.date.now(), 'Rows', x.length);
        //console.log(CND.date.now(), 'search', s);
        for (i = 0; i < x.length; ++i) {
            //console.log(CND.date.now(), 'i', i);
            //console.log(CND.date.now(), 'r', x[i]);
            if (CND.contains(x[i], s)) {
                if (all) {
                    m[i].matched_result = true;
                } else {
                    m[ii] = x[i]; ii++;
                } // end if all
            } // end if contains
        } // end foreach x
    } // end if x
    //console.log(CND.date.now(), 'Results', m);
    return m;
}; // end function CND.getSearchResults

CND.contains = function(a=null, s='', f=false) {
    if ((typeof s != 'string') || (s == '')) { return f; }
    let j = 0;
    s = s.toLowerCase();
    //console.log(CND.date.now(), 'a', typeof a, a.length, a);
    if ((typeof a == 'object') && (a.length > 0)) {
        // Indexed Array
        for (j = 0; j < a.length; ++j) {
            if (CND.contains(a[j], s, f)) {
                return true;
            } // end if contains
        } // end foreach a
    } else if ((typeof a == 'object') && (Object.keys(a).length > 0)) {
        // Associative Array
        for (let j in a) {
            if (CND.contains(a[j], s, f)) {
                return true;
            } // end if contains
        } // end foreach j
    } else if ((typeof a == 'string') && (a != '')) {
        a = a.toLowerCase();
        if (a.includes(s)) {
            f = true;
        }
    } else if ((typeof a == typeof s) && (a === s)) {
        f = true;
    }
    return f;
}; // end function CND.contains


CND.getScript = function(source, successCallback, failureCallback, type) {
    let exists = document.querySelectorAll('script[src="' + source + '"]');
    if (exists.length > 0) {
        if (typeof successCallback == 'function') { setTimeout(function() { successCallback(); }, 1); }
    } else {
        let script = document.createElement('script');
        let prior = document.getElementsByTagName('script')[0];
        if (type) { script.type = type; }
         else { script.type = 'text/javascript'; }
        script.async = 1;

        script.onload = script.onreadystatechange = function( _, isAbort ) {
            if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState) ) {
                script.onload = script.onreadystatechange = null;
                script = undefined;

                if (!isAbort && (typeof successCallback == 'function')) { setTimeout(function() { successCallback(); }, 1); }
                 else if (isAbort && (typeof failureCallback == 'function')) { setTimeout(function() { failureCallback(); }, 1); }
            } // end if readyState
        }; // end onload

        script.src = source;
        prior.parentNode.insertBefore(script, prior);
    } // end if exists
}; // end function getScript


CND.handleOnCNDReady = function(successCallback, failureCallback) {
    if ((typeof CND != 'undefined') && (typeof CND.jQueryLoader == 'function')) {
        if (typeof successCallback == 'function') { setTimeout(function() { successCallback(); }, 1); }
    } else {
        setTimeout(function() {
            if ((typeof CND != 'undefined') && (typeof CND.jQueryLoader == 'function')) {
                if (typeof successCallback == 'function') { setTimeout(function() { successCallback(); }, 1); }
            } else {
                setTimeout(function() {
                    if ((typeof CND != 'undefined') && (typeof CND.jQueryLoader == 'function')) {
                        if (typeof successCallback == 'function') { setTimeout(function() { successCallback(); }, 1); }
                    } else {
                        console.log(CND.date.now(), 'CND never loaded...');
                        if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(); }, 1); }
                    } // end if CND.jQueryLoader
                }, 300);
            } // end if CND.jQueryLoader
        }, 100);
    } // end if CND.jQueryLoader
}; // end function handleOnCNDReady

CND.getValue = function(type='string', nez=false, a=null, b=null, c=null, d=null, e=null) {
    if ((typeof a == type) && (!nez || ((a != '') && (a != 0) && (a !== null)))) {
        return a;
    } else if ((typeof b == type) && (!nez || ((b != '') && (b != 0) && (b !== null)))) {
        return b;
    } else if ((typeof c == type) && (!nez || ((c != '') && (c != 0) && (c !== null)))) {
        return c;
    } else if ((typeof d == type) && (!nez || ((d != '') && (d != 0) && (d !== null)))) {
        return d;
    } else if ((typeof e == type) && (!nez || ((e != '') && (e != 0) && (e !== null)))) {
        return e;
    } // end if type && nez
    if (type == 'string') {
        return '';
    } else if (type == 'number') {
        return 0;
    } else if (type == 'object') {
        return [];
    } else if (type == 'boolean') {
        return false;
    } else {
        return null;
    } // end if type
}; // end function CND.getValue


CND.rand = function(min=0, max=100, coin=0) { // min and max included
    if (Math.random() >= coin) {
        return Math.floor(Math.random() * (max - min + 1) + min);
    } else {
        return 0;
    }
}; // end function rand


CND.resetAllSettings = function() {
    CND.setAccountInfo('', '', '', '', true, 'login.html');
}; // end function resetAllSettings

CND.setDefaultInfo = function(domain='', extension='') {
    // Get information from the Cookies. Nom Nom
    chrome.storage.local.set({ 'def_domain': domain, 'def_extension': extension }, function() {
        CND.defaultSettings.domain = domain;
        CND.defaultSettings.extension = extension;
        //console.log(CND.date.now(), 'Saved default extension information');
    });
}; // end function setDefaultInfo


CND.getDefaultInfo = function() {
    chrome.storage.local.get([ 'def_domain', 'def_extension' ], function(items) {
        // items = [{ 'phasersTo': 'awesome' } ]
        //console.log(CND.date.now(), items);
        CND.defaultSettings.domain = items.domain || '';
        CND.defaultSettings.extension = items.extension || '';
        //console.log(CND.date.now(), items);
    }); // end chrome.storage.local.get
}; // end function getDefaultInfo


CND.setStorageValues = function(keyValues={}, callbackFunction=null) {
    // Get information from the Cookies. Nom Nom
    chrome.storage.local.set(keyValues, function() {
        if (typeof callbackFunction === 'function') {
            callbackFunction(keyValues);
        }
        //console.log(CND.date.now(), 'Saved default extension information');
    });
}; // end function setDefaultInfo


CND.getStorageValues = function(keys=[], callbackFunction=null) {
    chrome.storage.local.get(keys, function(items) {
        // items = [{ 'phasersTo': 'awesome' } ]
        //console.log(CND.date.now(), items);
        if (typeof callbackFunction === 'function') {
            callbackFunction(items);
        }
        //console.log(CND.date.now(), items);
    }); // end chrome.storage.local.get
}; // end function getDefaultInfo


CND.saveDomainsRestrictions = function(restrictions={}, callbackFunction=null) {
    CND.domainRestrictions.domain_restriction_type = restrictions.domain_restriction_type || CND.domainRestrictions.domain_restriction_type;
    CND.domainRestrictions.allowed_on_domains_list = restrictions.allowed_on_domains_list || CND.domainRestrictions.allowed_on_domains_list;
    CND.domainRestrictions.disabled_on_domains_list = restrictions.disabled_on_domains_list || CND.domainRestrictions.disabled_on_domains_list;
    CND.setStorageValues(restrictions, function() {
        if (typeof callbackFunction === 'function') {
            callbackFunction(restrictions);
        }
    });
}; // end function CND.saveDomainsRestrictions


CND.loadDomainsRestrictions = function(callbackFunction=null) {
    CND.getStorageValues([ 'domain_restriction_type', 'allowed_on_domains_list', 'disabled_on_domains_list' ], function(items) {
        CND.domainRestrictions.domain_restriction_type = items.domain_restriction_type || CND.domainRestrictions.domain_restriction_type;
        CND.domainRestrictions.allowed_on_domains_list = items.allowed_on_domains_list || CND.domainRestrictions.allowed_on_domains_list;
        CND.domainRestrictions.disabled_on_domains_list = items.disabled_on_domains_list || CND.domainRestrictions.disabled_on_domains_list;
        CND.domainRestrictions.loaded_from_storage = true;

        if (typeof callbackFunction === 'function') {
            callbackFunction(items);
        }
    });
}; // end function CND.loadDomainsRestrictions


CND.checkDomainRestrictionsAreLoaded = false;
CND.checkDomainRestrictions = function(domain='', successCallback=null, failureCallback=null) {
    if (typeof domain !== 'string') { return; }
    if (domain == '') { return; }
    if (!CND.domainRestrictions.loaded_from_storage) {
        CND.loadDomainsRestrictions(function() {
            if (CND.checkDomainRestrictionsAreLoaded === false) {
                CND.checkDomainRestrictionsAreLoaded = true;
                CND.checkDomainRestrictions(domain, successCallback, failureCallback); // run again one time
            } // end if CND.checkDomainRestrictionsAreLoaded
        }); // end CND.loadDomainsRestrictions
        return;
    } // end if CND.domainRestrictions.loaded_from_storage

    domain = domain.toLowerCase().trim();
    let domainIsAllowed = true; // default is true
    let restriction_type = CND.domainRestrictions.domain_restriction_type || CND.defaultDomainRestrictions.domain_restriction_type;
    let allowed_on_domains_list = CND.domainRestrictions.allowed_on_domains_list || CND.defaultDomainRestrictions.allowed_on_domains_list;
    let disabled_on_domains_list = CND.domainRestrictions.disabled_on_domains_list || CND.defaultDomainRestrictions.disabled_on_domains_list;

    if (restriction_type == 'all_allowed') {
        domainIsAllowed = true;
    } else if (restriction_type == 'all_disabled') {
        domainIsAllowed = false;
    } else if (restriction_type == 'allowed_on_domains') {
        domainIsAllowed = false;
        if (allowed_on_domains_list !== '') {
            let domains_list = allowed_on_domains_list.split(",");
            for (let i=0; i < domains_list.length; ++i) {
                let d = domains_list[i] || '';
                if (typeof d == 'string') {
                    d = d.toLowerCase().trim();
                    if (d != '') {
                        if (CND.checkDomainMatches(d, domain)) {
                            domainIsAllowed = true;
                        } // end if matches
                    } // end if d
                } // end if string
            } // end foreach domain
        } // end if domains_list
    } else if (restriction_type == 'disabled_on_domains') {
        domainIsAllowed = true;
        if (disabled_on_domains_list !== '') {
            let domains_list = disabled_on_domains_list.split(",");
            for (let i=0; i < domains_list.length; ++i) {
                let d = domains_list[i] || '';
                if (typeof d == 'string') {
                    d = d.toLowerCase().trim();
                    if (d != '') {
                        if (CND.checkDomainMatches(d, domain)) {
                            domainIsAllowed = false;
                        } // end if matches
                    } // end if d
                } // end if string
            } // end foreach domain
        } // end if domains_list
    } // end if restriction_type

    //console.log(CND.date.now(), CND.domainRestrictions, domain, domainIsAllowed);

    if (domainIsAllowed === true) {
        if (typeof successCallback === 'function') {
            successCallback();
        }
    } else {
        if (typeof failureCallback === 'function') {
            failureCallback();
        }
    }
}; // end function CND.checkDomainRestrictions


CND.checkDomainMatches = function(regexStr='', domain='') {
    if ((regexStr == '') || (domain == '')) { return false; }
    if (regexStr === domain) { return true; }
    //console.log(CND.date.now(), 'regexStr', regexStr, 'domain', domain);
    regexStr = regexStr.replace('\\.', '.').replace('\.', '.').replace('.', '\\\\.').replace('.*', '*').replace('*', '.*');
    //console.log(CND.date.now(), 'regexStr', regexStr, 'domain', domain);
    let rex = new RegExp(`\\b${regexStr}\\b||\\/${regexStr}\\/`, 'gi');
    if (rex.test(domain)) { return true; }
    return false;
}; // end function CND.checkDomainMatches


CND.setAccountInfo = function(domain='', apikey='', extension='', redirect=true, redirect_uri='') {
    // Get information from the Cookies. Nom Nom
    let valid = false;
    if (domain != '' && apikey != '' && extension != '') { valid = true; }
    //console.log(CND.date.now(), 'valid', valid);
    chrome.storage.local.set({ 'domain': domain, 'apikey': apikey, 'extension': extension, 'valid': valid }, function() {
        CND.accountSettings.domain = domain;
        CND.accountSettings.apikey = apikey;
        CND.accountSettings.extension = extension;
        CND.accountSettings.valid = valid;
        CND.pbxApiUrl = CND.getApiUrl(domain, false);
        //console.log(CND.date.now(), 'valid', valid);
        CND.ui.hideSuccess();
        //CND.getAccountInfo(null, null, redirect);
        if (valid) {
            CND.setDefaultInfo(domain, extension);
        }
        if (redirect && (typeof redirect_uri == 'string') && (redirect_uri != '')) {
            CND.loadExtensionPage(redirect_uri);
        } else if (redirect && !CND.locationIncludes('settings')) {
            chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
                if (tabs && tabs[0] && typeof tabs[0].id !== 'undefined') {
                    chrome.tabs.reload(tabs[0].id);
                }
            });
        } // end if settings
        //console.log(CND.date.now(), 'Saved extension information');
    });
}; // end function setAccountInfo

CND.getApiUrlDomain = function(url='', proto=false, prefix='https', type='c2d', fallback='') {
    // Old static format:   pbx-{tag}.voipawesome.com and {type}-pbx-{tag}.voipawesome.com
    // New wildcard format: {tag}.pbx.voipawesome.com and {tag}.{type}.voipawesome.com
    // Private Domain format: pbx.somedomain.com and {type}.somedomain.com
    let domain = (typeof url == 'string') ? url.trim() : '';
    let protoPrefix = prefix+':/'+'/';
    if (domain == '') {
        domain = fallback;
    }
    if (domain == '') { return domain; }
    if ((typeof domain == 'string') && (domain != '')) {
        let pbxpf = 'pbx.'; // {tag}.pbx.voipawesome.com AND pbx.somedomain.com
        let typepf = type + '.'; // {tag}.{type}.voipawesome.com AND {type}.somedomain.com
        domain = domain.replace(/^https?:\/\//i, '').replace(/\/.*$/, ''); // remove protocol and path (keeps the domain and port if any)
        if (!domain.includes(typepf)) {
            if (domain.includes(pbxpf)) {
                domain = domain.replace(pbxpf, typepf); // new format
            } else if ((domain.substring(0, 4) != type+'-') && (domain.substring(0, 4) != type+'.')) {
                domain = type + '-' + domain; // old format, prepend {type}-{accountPbxDomain}
            } // end if c2d
        } // end if typepf
        if (proto) {
            // Include protocol prefix
            if (!domain.includes(protoPrefix)) {
                domain = protoPrefix + domain;
            }
        } else {
            // Strip protocol prefix
            if (domain.includes(protoPrefix)) {
                domain = domain.replace(protoPrefix, '');
            }
        } // end if proto
    } // end if domain
    return domain;
}; // end function getApiUrlDomain

CND.getApiUrl = function(domain='', proto=false, prefix='https') {
    // Old static format:   pbx-{tag}.voipawesome.com and c2d-pbx-{tag}.voipawesome.com
    // New wildcard format: {tag}.pbx.voipawesome.com and {tag}.c2d.voipawesome.com
    let fallback = CND.defaultC2DApiUrl || CND.apiUrl || 'c2d.voipawesome.com';
    let url = (typeof domain == 'string') ? domain.trim() : '';
    if (typeof url != 'string' || url == '') { return fallback; }
    url = CND.getApiUrlDomain(url, proto, prefix, 'c2d', fallback);
    return url;
}; // end function getApiUrl

CND.getWSSDomain = function(domain='', proto=false, prefix='wss') {
    // Old static format:   pbx-{tag}.voipawesome.com and wss-pbx-{tag}.voipawesome.com
    // New wildcard format: {tag}.pbx.voipawesome.com and {tag}.wss.voipawesome.com
    let fallback = CND.defaultWSSDomain || 'wss.voipawesome.com';
    let url = (typeof domain == 'string') ? domain.trim() : '';
    if (typeof url != 'string' || url == '') { return fallback; }
    url = CND.getApiUrlDomain(url, proto, prefix, 'wss', fallback);
    return url;
}; // end function getWSSDomain

CND.goToWebsite = function() {
    //console.log(CND.date.now(), 'Going to Website');
    chrome.tabs.create({ url: CND.wwwUrl });
}; // end function goToWebsite


CND.getAccountInfo = function(successCallback=null, failureCallback=null, redirect=true) {
    chrome.storage.local.get([ 'domain', 'apikey', 'extension', 'valid' ], function(items) {
        // items = [{ 'phasersTo': 'awesome' } ]
        //console.log(CND.date.now(), items);
        CND.accountSettings.domain = items.domain || '';
        CND.accountSettings.apikey = items.apikey || '';
        CND.accountSettings.extension = items.extension || '';
        CND.accountSettings.valid = items.valid || false;
        CND.pbxApiUrl = CND.getApiUrl(items.domain, false);

        if (CND.accountSettings.valid) {
            if (typeof successCallback == 'function') {
                setTimeout(function() { successCallback(); }, 1);
            }
        } else {
            CND.setIconBadge('1', CND.ui.errorBadgeColor);
            if (typeof failureCallback == 'function') {
                setTimeout(function() { failureCallback(); }, 1);
            }
            if (redirect && CND.locationIncludes('chat') && !CND.locationIncludes('login')) {
                console.log(CND.date.now(), 'redirecting to login... ');
                CND.loadExtensionPage('login.html');
            } // end if settings || menu || popup || background
        } // end if valid
        //console.log(CND.date.now(), items);
    }); // end chrome.storage.local.get
}; // end function getAccountInfo


CND.locationIncludes = function(find='', inv=false) {
    let found = false;
    if (inv) { found = true; } // Inverse... default to true if not found
    if (typeof window != 'undefined') {
        if (typeof window.location != 'undefined') {
            if (inv) {
                if ((typeof window.location.hash != 'undefined') && (typeof window.location.href != 'undefined')) {
                    if (!window.location.hash.includes(find) && !window.location.href.includes(find)) {
                        found = false;
                    } // end if found
                } else if (typeof window.location.hash != 'undefined') {
                    if (!window.location.hash.includes(find)) {
                        found = false;
                    } // end if found
                } else if (typeof window.location.href != 'undefined') {
                    if (!window.location.href.includes(find)) {
                        found = false;
                    } // end if found
                } // end if href
            } else {
                if ((typeof window.location.hash != 'undefined') && (typeof window.location.href != 'undefined')) {
                    if (window.location.hash.includes(find) || window.location.href.includes(find)) {
                        found = true;
                    } // end if found
                } else if (typeof window.location.hash != 'undefined') {
                    if (window.location.hash.includes(find)) {
                        found = true;
                    } // end if found
                } else if (typeof window.location.href != 'undefined') {
                    if (window.location.href.includes(find)) {
                        found = true;
                    } // end if found
                } // end if href
            } // end if inv
        } // end if location
    } // end if window
    return found;
}; // end function locationIncludes


CND.loadPopupWindow = function(close_src=false, popup_src='') {
    if (popup_src == '') {
        if (CND.locationIncludes('popup') && CND.locationIncludes('chat')) {
            popup_src = 'chat.html';
        } else if (CND.locationIncludes('popup') && CND.locationIncludes('settings')) {
            popup_src = 'settings.html';
        } else if (CND.locationIncludes('popup') && CND.locationIncludes('login')) {
            popup_src = 'login.html';
        } // end if location_hash
    } // end if popup_src
    if (popup_src != '') {
        //console.log(CND.date.now(), 'Time to Poptastic it', popup_src);
        CND.poptastic(popup_src, 'VOIP Awesome CND', close_src);
    } else {
        console.log(CND.date.now(), 'No poptastic source');
    } // end if popup_src
}; // end function loadPopupWindow


CND.poptastic = function(url='', name='', close_src=false) {
    //console.log(CND.date.now(), 'screen.resolution', screen.height, screen.width); return;
    CND.getLastWindowPosition(function() {
        let x = CND.windowPosition.left;
        let y = CND.windowPosition.top;
        let h = CND.windowPosition.height;
        let w = CND.windowPosition.width;

        //console.log(CND.date.now(), CND.windowPosition);
        let specs = 'height='+h+',width='+w+',top='+y+',left='+x+',menubar=0,toolbar=0,scrollbars=0,location=0';
        if (typeof window != "undefined") {
            //console.log(CND.date.now(), 'window.open', url, name, specs);
            //console.log(CND.date.now(), 'typeof CND.newwindow', typeof CND.newwindow, CND.newwindow);
            CND.newwindow = window.open(url, name, specs, false);
            if (CND.newwindow !== null) {
                if (window.focus || close_src) { setTimeout(function() { window.blur(); CND.newwindow.focus(); }, 100); }
                if (close_src) { setTimeout(window.close, 1); }
            } // end if newwindow
        } else {
            console.log(CND.date.now(), "No window element available to open");
        }
    }); // end getLastWindowPosition
}; // end function poptastic


CND.getLastWindowPosition = function(successCallback) {
    let sh = screen.height *1;
    let sw = screen.width *1;
    let h = 600;
    let w = 740;
    let t = (sh > 800 ? sh - 600 - 200 : 100 );
    let l = (sw > 800 ? sw - 400 - 300 : 0 );
    CND.windowPosition.top = t;
    CND.windowPosition.left = l;
    CND.windowPosition.height = h;
    CND.windowPosition.width = w;
    // Get information from the Cookies. Nom Nom
    chrome.storage.local.get(['window_top','window_left','window_height','window_width'], function(items) {
        // items = [{ 'phasersTo': 'awesome' } ]
        //console.log(CND.date.now(), items);
        CND.windowPosition.top = items.window_top || CND.windowPosition.top;
        CND.windowPosition.left = items.window_left || CND.windowPosition.left;
        CND.windowPosition.height = items.window_height || CND.windowPosition.height;
        CND.windowPosition.width = items.window_width || CND.windowPosition.width;
        if (typeof successCallback == 'function') {
            setTimeout(function() { successCallback(); }, 1);
        } // end if successCallback
        //console.log(CND.date.now(), CND.windowPosition);
    }); // end chrome.storage.local.get
}; // end function getLastWindowPosition


CND.setLastWindowPosition = function(x, y, h, w) {
    // Set information into the Cookies. Nom Nom
    chrome.storage.local.set({ 'window_top': y, 'window_left': x, 'window_height': h , 'window_width': w }, function() {
        CND.windowPosition.top = y;
        CND.windowPosition.left = x;
        CND.windowPosition.height = h;
        CND.windowPosition.width = w;
        //console.log(CND.date.now(), 'Saved window position information', CND.windowPosition);
    });
}; // end function setLastWindowPosition


CND.saveCurrentWindowPositionTimeout = null;
CND.saveCurrentWindowPosition = function() {
    let sh = screen.height *1;
    let sw = screen.width *1;
    let x = (sw > 800 ? sw - 400 - 300 : 0 );
    let y = (sh > 800 ? sh - 600 - 200 : 100 );
    let h = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
    let w = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;

    //console.log(CND.date.now(), 'x,y', x, y);
    x = window.screenX || window.screenLeft || x;
    y = window.screenY || window.screenTop || y;
    if (x < 0) { x = sw + x; }
    //console.log(CND.date.now(), 'x,y', x, y);

    if ((typeof CND.windowPosition.top == 'undefined') ||
        (h != CND.windowPosition.height) ||
        (w != CND.windowPosition.width) ||
        (x != CND.windowPosition.left) ||
        (y != CND.windowPosition.top)) {
        //console.log(CND.date.now(), 'save', x, y, h, w);
        CND.setLastWindowPosition(x, y, h, w);
    } // end if CND.windowPosition
}; // end function saveCurrentWindowPosition


CND.setIconBadge = function(btext='', bcolor='#ff5b57') {
    if ((btext === 0) || (btext === '0')) { btext = ''; }
    chrome.action.setBadgeText({text: btext});
    chrome.action.setBadgeBackgroundColor({color: bcolor});
};


CND.setFooterVersion = function() {
    try {
        CND.dom('#version').html('Version ' + manifest.version);
        CND.dom('#copyright_year').html('2010-' + CND.currentYear);
        CND.dom('#go_to_website').prop('href', CND.wwwUrl);
    } catch (err) { console.log(CND.date.now(), err); }
}; // end function setFooterVersion


CND.checkIsInstalled = function() {
    // Make sure the chrome.runtime.getManifest() function exists
    if (typeof chrome == 'object' && typeof chrome.runtime == 'object' && typeof chrome.runtime.getManifest == 'function') {
        manifest = chrome.runtime.getManifest();
        if (!manifest) {
            return true;
        }
        if (typeof manifest.key == 'undefined' && typeof manifest.update_url == 'undefined') {
            return false;
        } else {
            return true;
        } // end if manifest
    } // end if chrome

    // We are unsure if the extension is installed or in developer mode. Assume installed.
    return true;
}; // end function isInstalled
CND.isInstalled = CND.checkIsInstalled();


CND.createNotification = function(user_options={}, callBack=null) {
    let default_options = {
        buttons : [],
        title: 'VOIP Awesome',
        message: 'Notification',
        contextMessage: '',
        expandedMessage: '',
        iconUrl: '../assets/voip-awesome-128.png',
        isClickable: true,
        requireInteraction: false,
        priority: 2,
        type: 'basic'
    };
    let options = Object.assign({}, default_options, user_options);
    chrome.notifications.getPermissionLevel(function(granted='denied') {
        if (granted == 'granted') {
            setTimeout(function() {
                chrome.notifications.create('', options, function(id) {
                    //console.log(CND.date.now(), 'notification', id);
                    if (typeof callBack == 'function') {
                        setTimeout(function() { callBack(id); }, 1);
                    }
                });
            }, 1); // end setTimeout
        } else {
            console.log(CND.date.now(), 'notifications are not granted', granted);
        } // end if granted
    }); // end chrome.notifications.getPermissionLevel
    return true;
}; // end function CND.createNotification


CND.destroySlimScroll = function(element) {
    if (typeof element == 'undefined') { element = document; }
    try {
        CND.dom(element).find('[data-scrollbar=true]').each(function() {
            let el = this;
            el.classList.remove('cnd-native-scroll');
            el.style.overflowY = '';
            el.style.height = '';
            el.removeAttribute('data-scroll-init');
        });
    } catch (err) { console.log(CND.date.now(), err); }
}; // end function CND.destroySlimScroll


CND.handleSlimScroll = function(element, scrollTop, scrolltoElement) {
    'use strict';
    if (typeof element == 'undefined') { element = document; }
    try {
        CND.dom(element).find('[data-scrollbar=true]').each(function() {
            CND.generateSlimScroll(CND.dom(this), scrollTop, scrolltoElement);
        });
    } catch (err) { console.log(CND.date.now(), err); }
}; // end function handleSlimScroll


CND.generateSlimScroll = function(element, scrollTop, scrolltoElement) {
    'use strict';
    try {
        if (CND.dom(element).attr('data-scroll-init')) {
            return false;
        }
    } catch (err) { console.log(CND.date.now(), err); return false; }
    let overrideScrollTop = false;
    if ((scrollTop !== null) && (typeof scrollTop == 'number') && (scrollTop >= 0)) {
        overrideScrollTop = true;
    }
    let dataScrollLoading = CND.dom(element).attr('data-scroll-loading') || false;
    let dataElementHeight = CND.dom(element).prop('scrollHeight') || 0;
    let dataPosition = CND.dom(element).attr('data-scroll-position') || 'top';
    let dataScrollTo = 0;
    if (overrideScrollTop) {
        dataScrollTo = scrollTop;
    }
    let dataHeight = CND.dom(element).attr('data-height');
    if (typeof dataHeight == 'undefined' || dataHeight === null) {
        dataHeight = 'auto';
    }
    if (dataPosition == 'bottom' && !overrideScrollTop) {
        dataScrollTo = dataElementHeight;
    }

    let el = CND.dom(element);
    el.addClass('cnd-native-scroll');
    if (dataHeight && dataHeight !== 'auto') {
        el.css('height', dataHeight);
    }
    el.css('overflow-y', 'auto');
    el.css('overflow-x', 'hidden');
    el.attr('data-scroll-init', true);

    if (dataPosition == 'bottom' || overrideScrollTop) {
        setTimeout(function() { el.scrollTop(dataScrollTo); }, 1);
    }

    if (dataScrollLoading) {
        let dataScrollLoadingID = CND.dom(element).attr('data-scroll-loading-id') || '';
        if (dataScrollLoadingID != '') {
            CND.dom('#' + dataScrollLoadingID).css('opacity', 0);
            setTimeout(function() { CND.dom('#' + dataScrollLoadingID).remove(); }, 300);
        }
    }
    return true;
}; // end function generateSlimScroll


CND.attachConfirmation = function(el, options) {
    options = options || {};
    if (!el || el.getAttribute('data-cnd-confirm-bound')) { return; }
    el.setAttribute('data-cnd-confirm-bound', '1');
    let title = options.title || el.getAttribute('data-title') || el.getAttribute('data-original-title') || 'Are you sure?';
    let placement = options.placement || 'bottom';
    let onConfirm = typeof options.onConfirm === 'function' ? options.onConfirm : function() {};
    let onCancel = typeof options.onCancel === 'function' ? options.onCancel : function() {};

    el.addEventListener('click', function(evt) {
        evt.preventDefault();
        evt.stopPropagation();
        let existing = document.querySelector('.cnd-confirm-popover');
        if (existing && existing.parentNode) { existing.parentNode.removeChild(existing); }

        let pop = document.createElement('div');
        pop.className = 'cnd-confirm-popover cnd-confirm-' + placement;
        pop.innerHTML = '<div class="cnd-confirm-title"></div>' +
            '<div class="cnd-confirm-buttons">' +
            '<button type="button" class="btn btn-xs btn-primary cnd-confirm-ok">Yes</button> ' +
            '<button type="button" class="btn btn-xs btn-default cnd-confirm-cancel">No</button>' +
            '</div>';
        pop.querySelector('.cnd-confirm-title').textContent = title;
        document.body.appendChild(pop);

        let rect = el.getBoundingClientRect();
        let top = rect.bottom + (window.pageYOffset || 0) + 6;
        let left = rect.left + (window.pageXOffset || 0);
        if (placement === 'top') {
            top = rect.top + (window.pageYOffset || 0) - pop.offsetHeight - 6;
        }
        pop.style.top = top + 'px';
        pop.style.left = left + 'px';

        let close = function() {
            if (pop.parentNode) { pop.parentNode.removeChild(pop); }
            document.removeEventListener('click', outside, true);
        };
        let outside = function(e) {
            if (!pop.contains(e.target) && e.target !== el) { close(); }
        };
        pop.querySelector('.cnd-confirm-ok').addEventListener('click', function(e) {
            e.preventDefault();
            close();
            onConfirm.call(el);
        });
        pop.querySelector('.cnd-confirm-cancel').addEventListener('click', function(e) {
            e.preventDefault();
            close();
            onCancel.call(el);
        });
        setTimeout(function() { document.addEventListener('click', outside, true); }, 0);
    });
}; // end function CND.attachConfirmation


CND.handleScrollToBottom = function(main_id='chats-main-section', msgs_id='messages-list-scroll-div') {
    let main_section = document.getElementById(main_id);
    let main_height = 0;
    if (typeof main_section != 'undefined') {
        main_height = main_section.height || 0;
    }

    let msgs_list = document.getElementById(msgs_id);
    let msgs_list_scrollheight = 0;
    if (typeof msgs_list != 'undefined') {
        msgs_list_scrollheight = msgs_list.scrollHeight || 0;
        if (msgs_list_scrollheight > main_height) {
            msgs_list.scrollTo(0, msgs_list_scrollheight);
        }
    }
    //console.log(CND.date.now(), { 'main_height' : main_height, 'msgs_list_scrollheight' : msgs_list_scrollheight });
}; // end function handleScrollToBottom


CND.checkBodyIsCND = function(successCallback, failureCallback) {
    let vai_click_and_dial = false;
    if (document.body.hasAttribute('data-vai-click-and-dial')) {
        vai_click_and_dial = document.body.getAttribute('data-vai-click-and-dial');
    } // end if body
    if ((vai_click_and_dial !== false) && (vai_click_and_dial != 'false')) {
        vai_click_and_dial = true;
    } // end if vai_click_and_dial
    if (vai_click_and_dial === true) {
        if (typeof successCallback == 'function') {
            setTimeout(function() { successCallback(); }, 1);
        } // end if successCallback
    } else {
        if (typeof failureCallback == 'function') {
            setTimeout(function() { failureCallback(); }, 1);
        } // end if failureCallback
    } // end if vai_click_and_dial
}; // end function checkBodyIsCND


CND.checkApiKey = function(successCallback=null, failureCallback=null, offlineCallback=null, redirect=true) {
    let domain = CND.accountSettings.domain || CND.defaultSettings.domain || '';
    let extension = CND.accountSettings.extension || CND.defaultSettings.extension || '';
    let apikey = CND.accountSettings.apikey || '';
    if (CND.online && (apikey != '')) {
        let requestApiUrl = CND.getApiUrl(domain, true);
        let data = {
            'action': 'check_key',
            'domain': domain,
            'key': apikey,
            'ext': extension
        };
        CND.ajax({
            'url': requestApiUrl,
            'data': data,
            'success': function(data, status, xhr) {
                if (data.status) {
                    if (typeof successCallback == 'function') { setTimeout(function() { successCallback(); }, 1); }
                } else {
                    CND.setAccountInfo(CND.accountSettings.domain, '', CND.accountSettings.extension, redirect);
                    if (data.error == 'invalid credentials') {
                        CND.ui.showError('The user credentials provided are not valid, please try again.');
                    } else {
                        CND.ui.showError(data.error);
                    }
                    if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(xhr, data.error || 0, data.message || ''); }, 1); }
                } // end if data.success
            }, // end success
            'error': function(xhr, error, message) {
                console.log(CND.date.now(), 'Error checking API Key');
                if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(xhr, error, message); }, 1); }
            }, // end error
            'offline' : function() {
                console.log(CND.date.now(), 'Offline');
                if (typeof offlineCallback == 'function') { setTimeout(function() { offlineCallback(); }, 1); }
            } // end offline
        }, null, null, null, true); // end ajax
    } else if (!CND.online) {
        console.log(CND.date.now(), 'Offline');
        if (typeof offlineCallback == 'function') { setTimeout(function() { offlineCallback(); }, 1); }
    } else if (apikey == '') {
        console.log(CND.date.now(), 'No API Key');
        CND.setAccountInfo('', '', '', redirect);
        if (typeof failureCallback == 'function') { setTimeout(function() { failureCallback(); }, 1); }
    } // end if apikey
}; // end function checkApiKey
