x-utils.es.js

/// <reference path="./x-utils.es.d.ts" />

/**
 * @namespace xutils
 * @module x-utils
 * @license MIT
 * {@link https://eaglex.net Eaglex}
 * @description Simple javascript, lodash alternative library, for support contact me at eaglex.net
 * @author Developed by Anon
 * @version ^2.x.x
 */
/* eslint-disable no-proto */
// eslint-disable-next-line semi

/* istanbul ignore next */ 
const isWindow = () => {
    try {
        if ((process.env || {}).NODE_ENV === 'test') return false

        /* istanbul ignore next */ 
        if (window) return true
    } catch (err) {
        return false
    }   
}

/**
 * @typedef {'log' | 'warn' | 'onerror' | 'error' | 'alert'| 'attention' | 'debug' | 'stack' | 'errorTrace'} logType 
 */

/** 
 * 
 * If you used logging in your application from the moment this method was called all logging will be disabled
 * - it affects: log, warn,error, onerror, errorTrace, stack, attention, alert, debug
 * @returns {true|false} 
*/
const disableLogging = () => {
    try {
        /* istanbul ignore next */ 
        if (isWindow()) {
            //  on browser
            // @ts-ignore
            if (window.xUtilsConfig) {
                // @ts-ignore
                window.xUtilsConfig.logging = 'off'
            } else {
                // @ts-ignore
                window.xUtilsConfig = {
                    logging: 'off'
                }
            }

            return true
        }
    } catch (err) {
        //
    }

    try {
        // in node
        // @ts-ignore
        if (global.xUtilsConfig) {
            // @ts-ignore
            global.xUtilsConfig.logging = 'off'
        } else {
           
            // @ts-ignore
            global.xUtilsConfig = {
                logging: 'off'
            }

        }

        return true
    } catch (err) {
        /* istanbul ignore next */ 
        return false
    }
   
}

/** 
 * Change state of xutils loggers when calling at top of hoist level. 
 * - affects: log, warn,error, onerror, errorTrace, stack, attention, alert, debug
 * @returns {true|false} 
 * 
 * @example
 * resetLogging() // will reset any previously set loggerSetting(...) options
*/
const resetLogging = () => {
    try {
        /* istanbul ignore next */ 
        if (isWindow()) {
            // @ts-ignore
            if (window.xUtilsConfig) {
                // @ts-ignore
                window.xUtilsConfig.logging = 'on'
            } else {
                // @ts-ignore
                window.xUtilsConfig = {
                    logging: 'on'
                }
            }

            return true
        }

    } catch (err) {
        //
    }

    try {
        // @ts-ignore
        if (global.xUtilsConfig) {
            // @ts-ignore
            global.xUtilsConfig.logging = 'on'
        } else {
            // @ts-ignore
            global.xUtilsConfig = {
                logging: 'on'
            }
        }

        return true
    } catch (err) {
        /* istanbul ignore next */ 
        return false
    }
}

/** 
 * Allow enabling and disabling of loggers: `log/warn/error/onerror/attention/debug/alert`
 * @param {logType} logType logger name
 * @param {string} logMode off/on
 * @returns {true|false} 
 * 
 * @example
 * loggerSetting('log','off') // future calls to log() will be disabled
 * // this applies to all logger methods: 
*/
const loggerSetting = (logType = 'log', logMode = 'off') => {
    let availTypes = ['log', 'warn', 'onerror', 'error', 'alert', 'attention', 'debug', 'stack', 'errorTrace']
    let availModes = ['on', 'off']

    if (!availTypes.includes(logType) || !logType) return false
    if (!availModes.includes(logMode) || !logMode) return false
    if (logType === 'onerror') logType = 'error'

    try {
        /* istanbul ignore next */ 
        if (isWindow()) {
            //  on browser
            // @ts-ignore
            if (window.xUtilsConfig) {
                // @ts-ignore
                window.xUtilsConfig[logType] = logMode
            } else {
                // @ts-ignore
                window.xUtilsConfig = {
                    [logType]: logMode
                }
            }

            return true
        }
    } catch (err) {
        //
    }

    try {
        // in node
        // @ts-ignore
        if (global.xUtilsConfig) {
            // @ts-ignore
            global.xUtilsConfig[logType] = logMode
        } else {
            // @ts-ignore
            global.xUtilsConfig = {
                [logType]: logMode
            }
        }
        return true
    } catch (err) {
        /* istanbul ignore next */ 
        return false
    }
   
}

/** 
 * 
 * Internal method
 * check if any log,warn,error,onerror are currently disabled
 * @param {logType} logType
 * @returns {'on'|'off'}
*/
const checkLoggerSetting = (logType = undefined) => {
    try {
        /* istanbul ignore next */ 
        if (isWindow()) {
            // @ts-ignore
            if (!window.xUtilsConfig) window.xUtilsConfig = {}
            //  on browser
            // @ts-ignore
            if (window.xUtilsConfig) {
                // @ts-ignore
                if (window.xUtilsConfig.logging === 'off') return 'off'
                // @ts-ignore
                else return (window.xUtilsConfig[logType] ? window.xUtilsConfig[logType] : 'on').toString()
            } else {
                return 'on'
            }
          
        }
    } catch (err) {
        //
    }

    try {
        // @ts-ignore
        if (!global.xUtilsConfig) global.xUtilsConfig = {}
        // in node
        // @ts-ignore
        if (global.xUtilsConfig) {
            // @ts-ignore
            if (global.xUtilsConfig.logging === 'off') return 'off'
            // @ts-ignore
            else return (global.xUtilsConfig[logType] ? global.xUtilsConfig[logType] : 'on').toString()
        } else {
            return 'on'
        }

    } catch (err) {
        //
    }
    return 'on'
}

/** 
 * When xUtilsConfig wasn't set, then we are on, else if ..xUtilsConfig==='off', do not print logs
 * @effects `log, warn,error, onerror, errorTrace, stack, debug, alert, attention`
 * @returns {true|false}
 */
const loggingON = () => {
    try {
        /* istanbul ignore next */ 
        // @ts-ignore
        if (isWindow()) return (window.xUtilsConfig || {}).logging === 'on' || (window.xUtilsConfig || {}).logging === undefined
    } catch (err) {
        //
    }
    try {
        // @ts-ignore
        return (global.xUtilsConfig || {}).logging === 'on' || (global.xUtilsConfig || {}).logging === undefined
    } catch (err) {
        //
    }
    return true
}

/** 
 * @ignore
*/
const callFN = (cb = undefined) => {
    if (typeof cb !== 'function') return false
    try {
        let d = cb()
        return d === true || d > 0
    } catch (err) {
        return false
    }
}

/** 
 * @ignore
*/
/* istanbul ignore next */ 
const logConstract = function (type = '', args) {

    if (!args.length) args[0] = ''
    let allData = args.filter(n => typeof n === 'string' || n === undefined).length === 0
    let format = allData ? '\%o' : ''

    if (type === 'log') args = [].concat(`\x1b[90m[log]\x1b[0m\x1b[2m${format} `, args, '\x1b[0m')
    if (type === 'debug') args = [].concat(`\x1b[90m[debug]\x1b[0m\x1b[32m${format} `, args, '\x1b[0m')
    if (type === 'warn') args = [].concat(`\x1b[90m[warning]\x1b[0m\x1b[1m${format} `, args, '\x1b[0m')
    if (type === 'alert') args = [].concat(`\x1b[90m[alert]\x1b[0m\x1b[33m${format} `, args, '\x1b[0m')
    if (type === 'attention') args = [].concat(`\x1b[90m[attention]\x1b[0m\x1b[36m${format} `, args, '\x1b[0m')

    console.log.apply(null, args)
}

/**
 * Extends console.log with [log] prefix
 * @borrows console.log
 * @param  {...any} args 
 */
const log = function (...args) {
    if (!loggingON()) return
    if (checkLoggerSetting('log') === 'off') return

    return logConstract('log', args)
}

/** 
 * 
 * Extends console.log with [debug] prefix
 * - produces green color output
 * @borrows console.log
 * @param  {...any} args 
*/
const debug = function (...args) {
    if (!loggingON()) return
    if (checkLoggerSetting('debug') === 'off') return

    return logConstract('debug', args)
}

/** 
 * Extends console.log with [warn] prefix
 * - produces bright color output
 * @param  {...any} args 
*/
const warn = function (...args) {
    if (!loggingON()) return
    if (checkLoggerSetting('warn') === 'off') return

    return logConstract('warn', args)
}

/** 
 * Extends console.log with [alert] prefix
 * - produces yellow color output
 * - this method does not work on window object _( for obvious reasons! )_
 * @param  {...any} args 
*/
const alert = function (...args) {
    if (isWindow()) return
    if (!loggingON()) return
    if (checkLoggerSetting('alert') === 'off') return

    return logConstract('alert', args)
}

/** 
 * Extends console.log with [attention] prefix
 * - produces blue color output
 * @param  {...any} args 
*/
const attention = function (...args) {
    if (!loggingON()) return
    if (checkLoggerSetting('attention') === 'off') return

    return logConstract('attention', args)
}

/** 
 * Extends console.error with [error] prefix
 * - produces red color output
 * @param  {...any} args 
*/
const onerror = function (...args) {
    if (!loggingON()) return
    if (checkLoggerSetting('error') === 'off' || checkLoggerSetting('onerror') === 'off') return

    if (!args.length) args[0] = ''
    let allData = args.filter(n => typeof n === 'string' || n === undefined).length === 0
    let format = allData ? '\%o' : ''

    try {
        /* istanbul ignore next */ 
        if (isWindow()) {
            args = [].concat(`\x1b[31m[error]\x1b[0m\x1b[31m${format} `, args, '\x1b[0m')
            console.error.apply(null, args)
            return
        }
    } catch (err) {
        // using node
    }
    
    args = [].concat(`\x1b[41m[error]\x1b[0m\x1b[31m${format} `, args, '\x1b[0m')
    console.error.apply(null, args)
}

/** 
 * For stack tracing 
 * - produces/prefixed [STACK TRACE]: ...
 * @param {any} data 
 * @param {boolean} asArray if set, will output stack trace as array, otherwise a string
*/
const stack = (data, asArray = false) => {
    if (!loggingON()) return
    if (checkLoggerSetting('stack') === 'off') return
    let stackList = new Error(JSON.stringify(data)).stack.split('(')
    stackList.splice(1, 1)
    let stackHead = stackList[0].split(/\n/)[0].replace('Error', '[STACK TRACE]')
    stackList.splice(0, 1)
    stackList.unshift(stackHead)
    if (asArray) console.log(stackList)
    else console.log.apply(null, stackList)
    return undefined
}

/**
 *  Extended console.error,  stack trace
 * - produces/prefixed [ERROR]: ...
 * @param {any} data optional
 * @param {boolean} asArray if set, will output stack trace as array, otherwise a string
 * 
 * @example
 * errorTrace('error data', true) // returns [[ERROR],... including full stack trace
 */
const errorTrace = (data, asArray = false) => {
    if (!loggingON()) return
    if (checkLoggerSetting('errorTrace') === 'off') return
    let stackList = new Error(JSON.stringify(data)).stack.split('(')
    stackList.splice(1, 1)
    let errHead = stackList[0].split(/\n/)[0].replace('Error', '[ERROR]')
    stackList.splice(0, 1)
    stackList.unshift(errHead)
    if (asArray) console.error(stackList)
    else console.error.apply(null, stackList)
    return undefined
}

/**
 * @alias onerror
 * 
 */
const error = onerror

/**
 * Check if item is a function
 * @param {any} el 
 * @returns {true|false}
 * 
 * @example
 * isFunction(()=>{}) // true 
 * isFunction(Function) // true 
 */
const isFunction = (el = undefined) => typeof el === 'function'

/**
 * Test provided item is BigInt 
 * @param {any} n 
 * @returns {true|false}
 * 
 * @example 
 * isBigInt( BigInt(Number.MAX_SAFE_INTEGER) ) // true
 * isBigInt( 1n ) // true
 * isBigInt( (2n ** 54n) ) // true
 * 
 */
const isBigInt = (n) => typeof (n) === 'bigint'

/**
 * loopCB 
 * @ignore
 * @callback loopCB
 * @param {string} param string
 */

/**
 * @description Looping each item inside of callback
 * - Returned cb is pushed to array
 * - break loop when returning `{break:true}` inside callback
 * @param {number} size
 * @param {loopCB} cb callback issued at end of each loop que
 * @returns {array} whatever was returned inside the loop
 *
 * @example
 * loop(5,inx=>10+inx) // [10, 11, 12, 13, 14]
 * loop(3,inx=>{
 *   if(inx===3) return {break:true}
 *   return {[inx]:inx+1}
 * }) //  [ { '0': 1 }, { '1': 2 }, { '2': 3 } ]
*/
// @ts-ignore
const loop = function (size = 0, cb = (index = 0) => {}) {
    let isFN = typeof cb === 'function'
    let isNum = typeof size === 'number'
    if (!isFN || !isNum) return []
    let d = []
    for (let inx = 0; inx < Array(size).length; inx++) {

        let r = cb.apply(this, [inx])

        // add support for break from the loop inside callback function
        try {
            if (r && Object.entries(r).length) {
                if (r.break) break
            }
        } catch (err) {
            // 
        }

        d.push(r) // always grub any data       
    }

    return d
}

/** 
 * Evaluate if data is an actual `Date`
 * @param {Date} dt 
 * @param {function|undefined} cbEval (optional) callback operator, continue checking when callback returns !!true
 * @returns {true|false}
 * 
 * @example
 * validDate(new Date('')) // false
 * validDate(new Date()) // true
 * validDate( new Date(), ()=>false ) // false callback !!false
*/
const validDate = (dt, cbEval = undefined) => {
    if (isFunction(cbEval) && !callFN(cbEval)) return false
    try {
        // @ts-ignore
        if (dt.__proto__ === Date.prototype && (dt).toString() !== 'Invalid Date') return true
        else return false
    } catch (err) {
        return false
    }
}

/**
 * Check item is an array
 * @param {any} arr
 * @param {function | undefined} cbEval (optional) callback operator, continue checking when callback returns !!true
 * @returns {true|false}
 * 
 * @example
 * isArray([]) // true
 * isArray({}) // false
 * isArray(new Array()) // true
 * isArray(new Array(), ()=>[1,2].length===1) // false, because callback return !!false
 * isArray({}, ()=>true) // false // not array
 */
const isArray = (arr = undefined, cbEval = undefined) => {
    if (isFunction(cbEval) && !callFN(cbEval)) return false
    if (isBigInt(arr)) return false
    else return !arr ? false : Array.prototype === (arr).__proto__
}

/**
 * Test item is an array, and check the size
 * @param {array} arr 
 * @returns {number}
 * 
 * @example
 * arraySize([1,2,3]) // 3
 * arraySize({a:1}) // 0
 */
const arraySize = (arr = undefined) => {
    let inx = 0
    if (!isArray(arr)) return inx
    inx = arr.length
    return inx
}

/**
 * Examines element for its `type`, provided `value`, and `primitive value`
 * @param {any} el
 * @param {boolean} standard `standard==true` > return javascript standard types, `standard==false` > return user friendly definition types:`[date,NaN,promise,array,...typeof]`
 * @returns {{type:any,value:number,primitiveValue:object}} `{ "type":date,NaN,promise,instance,prototype,array,...typeof, value: number, primitiveValue }`
 * 
 * @example
 * typeCheck({}) // {type:'object', value:0, primitiveValue: Object() }
 * typeCheck({a:1,b:2}) // {type:'object', value:2, primitiveValue: Object() }
 * typeCheck([2,3],false) // {type:'array', value:2, primitiveValue: Array() }
 * typeCheck(Date,false) // {type:'date', value:1, primitiveValue: Date() }
 * typeCheck(2) // {type:'number', value:2, primitiveValue: Number() }
 * typeCheck(false) // {type:'boolean', value:0, primitiveValue: Boolean() }
 * typeCheck(true) // {type:'boolean', value:1, primitiveValue: Boolean() }
 * typeCheck(null,false) // {type:'null', value:0, primitiveValue: Object() }
 * typeCheck(null) // {type:'object', value:0, primitiveValue: Object() }
 * typeCheck(undefined) // {type:'undefined', value:0, primitiveValue: undefined }
 * typeCheck(function () { }) // {type:'function', value:1, primitiveValue: Function }
 * typeCheck(Promise.resolve(),false) // {type:'promise', value:1, primitiveValue: Function }
 * typeCheck(Promise.resolve()) // {type:'object', value:1, primitiveValue: Function }
 * typeCheck(BigInt(1)) // { type: 'bigint', value: 1, primitiveValue: 0n }
 * typeCheck( new Error()) // { type: 'object', value: 0, primitiveValue: Error() }
*/
const typeCheck = (el, standard = true) => {

    const ofType = (type) => {
        if (standard) return typeof el
        else return type || typeof el
    }

    const asPrototype = (Type) => {
        return Type.prototype === el.prototype
    }

    try {
        /* istanbul ignore next */ 
        if (typeof el === 'symbol') return { "type": ofType(), value: 0, primitiveValue: Symbol('') }

        if (el === undefined) return { "type": ofType(), value: 0, primitiveValue: undefined }

        if (typeof el === 'boolean') return { "type": ofType(), value: +(el), primitiveValue: Boolean() }
        
        /* istanbul ignore next */ 
        if (typeof el === 'bigint' && typeof Object(el) === 'object') return { "type": ofType(), value: 1, primitiveValue: BigInt('') } // eslint-disable-line no-undef

        if (el === null) return { "type": ofType('null'), value: 0, primitiveValue: Object() }
        /* istanbul ignore next */ 
        if (el.__proto__ === Date.prototype || asPrototype(Date)) return { "type": ofType('date'), value: 1, primitiveValue: new Date() }

        if (String.prototype === (el).__proto__) return { 'type': ofType(), value: el.length, primitiveValue: String() }

        if (Array.prototype === (el).__proto__ || asPrototype(Array)) return { "type": ofType('array'), value: (el || []).length, primitiveValue: Array() } // eslint-disable-line no-array-constructor

        if (Promise.prototype === (el || '').__proto__ || asPrototype(Promise)) return { type: ofType('promise'), value: 1, primitiveValue: Function }

        if (Function.prototype === (el).__proto__ || asPrototype(Function)) return { type: ofType(), value: 1, primitiveValue: Function }

        if ((Object.prototype === (el).__proto__) || asPrototype(Object)) return { "type": ofType(), value: Object.keys(el).length, primitiveValue: Object() }

        if ((Error.prototype === (el).__proto__) || asPrototype(Error)) return { "type": ofType('error'), value: Object.keys(el).length, primitiveValue: Error() }

        /* istanbul ignore next */ 
        if ((el).__proto__ === Number.prototype || asPrototype(Number)) {
            if (isNaN(el)) return { "type": ofType('NaN'), value: 0, primitiveValue: Number() }
            else return { "type": ofType(), value: el, primitiveValue: Number() }

            // Unary plus operator
            /* istanbul ignore next */ 
        } else if ((+(el) >= 0) === false) return { 'type': typeof el, value: +(el), primitiveValue: undefined }
        else return { 'type': typeof el, value: 0, primitiveValue: undefined }
    } catch (err) {
        /* istanbul ignore next */ 
        error(err)
        // @ts-ignore
        return {}
    }
}

/** 
 * Check if item is `lt < 1`, `false`, `null` or `undefined`
 * @param {any} el number/boolean
 * @returns {true|false}
 * 
 * @example
 * isFalse(undefined) // false
 * isFalse(5) // false
 * isFalse(0) // true
 * isFalse(-1) // true
 * isFalse(true) // false
 * isFalse(false) // true
 * isFalse({}) // false
 * isFalse( new Boolean(false) ) // true
 * 
*/
const isFalse = (el) => {
    if (el === null) return true
    if (typeof el === 'undefined') return true
    if (typeof el === 'number' && el < 1) return true
    if (typeof el === 'boolean' && el === false) return true
    try {
        if (el instanceof Boolean) {
            return el.valueOf() === false
        }
    } catch (err) {
        //
    }

    return false
}

/** 
 * Check if item is `gth > 0`, `true`, basically opposite of `isFalse()`
 * @param {any} el number/boolean
 * @returns {true|false}
 * 
 * @example
 * isTrue(undefined) // false
 * isTrue(5) // true
 * isTrue(0) // false
 * isTrue(-1) // false
 * isTrue(true) // true
 * isTrue(false) // false
 * isTrue([]) // false
 * isTrue( new Boolean(true) ) // true
 * 
*/
const isTrue = (el) => {
    if (el === null) return false
    if (typeof el === 'undefined') return false
    if (typeof el === 'number' && el > 0) return true
    if (typeof el === 'boolean' && el === true) return true
    
    try {
        if (el instanceof Boolean) {
            return el.valueOf() === true
        }
    } catch (err) {
        //
    }
    return false
}

/** 
 * Check if item is a boolean
 * @param {any} el
 * @returns {true|false}
 * 
 * @example
 * isBoolean(null) // false
 * isBoolean(undefined) // false
 * isBoolean(false) // true
 * isBoolean(new Boolean(false)) // true 
 * 
*/
const isBoolean = (el) => {
    if (el === undefined) return false
    if (el === null) return false
    if (el === true || el === false) return true
    try {
        if (el instanceof Boolean) {
            return true
        }
    } catch (err) {
        //
    }
    return false
}

/** 
 * Check if item is `===null`
 * @param {any} el
 * @returns {true|false}
 * 
 * @example
 * isNull(null) // true
 * isNull(undefined) // false
*/
const isNull = (el) => {
    if (el === null) return true
    else return false
}

/** 
 * Check if item is `===undefined`
 * @param {any} el
 * @returns {true|false}
 * 
 * @example
 * isUndefined(undefined) // true
 * isUndefined(null) // false
 * 
*/
const isUndefined = (el) => {
    if (typeof el === 'undefined') return true
    else return false
}

/** 
 * Check item has some value, set of props, or length
 * @param {any} value any
 * @borrows typeCheck
 * @returns {boolean}
 * 
 * @example 
 * isEmpty({}) // true
 * isEmpty({a:1}) // false
 * isEmpty([]) // true
 * isEmpty([0]) // false
 * isEmpty(1) // false
 * isEmpty(false) // true
*/
const isEmpty = (value) => {
    if (isError(value)) return false
    return !typeCheck(value).value
}

/** 
 * Get first item from array 
 * - Allow 1 level [[1,2]]
 * @param {array} arr
 * @returns {any} first array[] item[0] 
 * 
 * @example
 * head([[{ value: 1 }, { value: 2 }]]) // { value: 1 }
 * head([[ [1], {value:1} ]]) // [1]
 * head([1,2]) // 1
 * 
*/
const head = (arr = []) => {
    // @ts-ignore
    if (Array.prototype !== (arr || null).__proto__) return undefined
    // @ts-ignore
    return arr.flat().shift()
}

/**
 * Gets last item from array 
 * @param {array} arr 
 * @returns {any}
 * 
 * @example 
 * last([{},{},[1], { value: 1 }]) // { value: 1 }
 */
const last = (arr = []) => {
    // @ts-ignore
    return (arr && Array.prototype === (arr).__proto__) ? arr[arr.length - 1] : undefined
}

/**
 * @ignore
 * @callback timerCB
 */

/**
 * Timer callback executes on timeout
 * @param {timerCB} cb 
 * @param {number} time 
 * 
 * @example
 * timer(() => log('timer called'), 2000) // executed after time expired
 * 
 */
const timer = (cb = () => {}, time = 0) => {
    const isFN = typeof cb === 'function'
    if (!isFN) return undefined
    time = (typeof time === 'number' && time >= 0) ? time : 0 // must provide number
    const s = setTimeout(() => {
        cb()
        clearTimeout(s)
    }, time)
}

/**
 * @ignore
 * @callback intervalCB
 */

/**
  * Execute callback every interval, then exit on endTime
  * @param {intervalCB} cb 
  * @param {number} every 
  * @param {number} endTime 
  * 
  * @example
  * interval(() => log('interval called'), 100, 300) 
  * 
  **/
const interval = (cb = () => {}, every = 0, endTime = 0) => {
    const isFN = typeof cb === 'function'
    if (!isFN) return null
    every = (typeof every === 'number' && every >= 0) ? every : 0 // must provide number
    endTime = (typeof endTime === 'number' && endTime >= 0) ? endTime : 0 // must provide number  

    let counter = 0
    const c = setInterval(() => {
        cb()
        if (endTime <= counter) return clearInterval(c)      
        counter = counter + every
    }, every)

}

/** 
 * SimpleQ / instanceof Promise & SimpleQ
 * - Deferred simplified promise 
 * - Available methods: `resolve() / reject() / (get) promise / progress( (value:string,time:number)=>self,every?:number,timeout?:number ):self`,  _progress() calls with values: `resolved | rejected | in_progress | timeout`, its discarted when fulfilled or timedout_
 *
 * @borrows Promise
 * @returns `{SimpleQ}`
 * 
 * @example 
 * let defer = sq()
 *
 * let every = 100 // how often to check
 * let timeout = 5000 // exists if not already resolved or rejected
 *
 * defer.progress((val,time)=>{
 *     // val //> "resolved" | "rejected" | "in_progress" | "timeout"
 *    log('[progress]',val,time)
 * }, every, timeout)
 *
 *  .then(n=>{
 *         log('[sq][resolve]',n)
 *  }).catch(err=>{
 *        onerror('[sq][reject]',err)
 *  })
 *
 * defer.resolve('hello world')
 * // defer.reject('kill it')
 * // or
 * defer.resolve('hello world')
 * .then(log)
 *
 * // or
 * defer.then(log)
 * .resolve('hello world')
 *
 * // or 
 * defer.reject('ups')
 * .catch(onerror)
 *
 * // or either
 * await defer // resolves // rejects? 
 * await defer.promise // resolves // rejects? 
 * 
 *
**/
//  @ts-ignore
const sq = () => {

    /**
     * @interface
     * @ignore
     */
    class SimpleQ extends Promise {
    /**
     * deferrerCallback
     * @ignore
     * @callback SimpleQdeferrerCallback
     * @param {function} resolve
     * @param {function} reject
     * 
     */
        /* istanbul ignore next */
        /**
     * 
     * @param {SimpleQdeferrerCallback} deferrerCallback 
     */
        /** @typedef {"resolved"|"rejected"|"in_progress"|"timeout"} val */
        /** @typedef {number} time */

        /**
            * @typedef {function(val, time):void} callback
            */

        // @ts-ignore
        constructor(deferrerCallback = (resolve = (data) => { }, reject = (data) => { }) => { }) {    
            super(deferrerCallback)
            
            /**
            * @ignore 
            * @typedef { {every:number,timeout:number,cb:callback,done:boolean,index:number} } progress_cb 
            */
            
            /**
             * @ignore  
             * @type {progress_cb}
             */
            this._progress_cb = {
                every: 100,
                timeout: 1000,
                cb: undefined,
                done: undefined,
                index: 0
            }

        }

        /**
         * @name progress
         * @method progress 
         * Returns callback when promise if resolved or rejected
         * - On resolved cb() value is `resolved`
         * - On rejected cb() value is `rejected`
         * - If neither, cb initiated on {every} until {timeout}, or before {done}, with value `in_progress`
         * - If promise is never resolved and {timeout} is reached, final callback value is `timeout`
         * @memberof SimpleQ
         * @param {callback} cb 
         * @param {number} every 
         * @param {number} timeout when to stop checking progress (will exit setInterval)
         */
        progress(cb, every = 100, timeout = 1000) {
            // make sure every is never bigger then timeout
            if (every > timeout) {
                every = 0
            }
            let index = 0
            this._progress_cb.cb = cb
            this._progress_cb.every = every
            this._progress_cb.timeout = timeout
            this._progress_cb.index = index
           
            // execute either { timeout | in_progress } based on selection
            let t = setInterval(() => {
                this._progress_cb.index = index
                if (this._progress_cb.done) return clearInterval(t)
                if (index >= timeout) {
                    this._progress_cb.cb('timeout', index)
                    this._progress_cb.done = true
                    return clearInterval(t)
                } else this._progress_cb.cb('in_progress', index)

                index = index + every
            }, every)
            
            return this
        }

        /**
        *
        * Resolve your promise outside of callback
        * @param {*} data
        * @memberof SimpleQ
        */
        resolve(data) {
        // @ts-ignore
            let res = SimpleQ._resolve
            if (res instanceof Function) {
                res(data)
                if (isFunction(this._progress_cb.cb) && !this._progress_cb.done) {
                    this._progress_cb.cb('resolved', this._progress_cb.index)
                    this._progress_cb.done = true
                }
            // eslint-disable-next-line brace-style
            }
            /* istanbul ignore next */
            else onerror('[SimpleQ][resolve]', 'not callable')

            return this
        }

        /**
        * Reject your promise outside of callback
        * @memberof SimpleQ
        * @param {*} data 
        */
        reject(data) {
        // @ts-ignore
            let rej = SimpleQ._reject
            if (rej instanceof Function) {
                rej(data)
                if (isFunction(this._progress_cb.cb) && !this._progress_cb.done) {
                    this._progress_cb.cb('rejected', this._progress_cb.index)
                    this._progress_cb.done = true
                }
            } else {
            /* istanbul ignore next */
                onerror('[SimpleQ][reject]', 'not callable')
            }

            return this
        }

        /**
        * - Returns promise, and instanceof Promise
        * @memberof Promise
        * @readonly
        * @memberof SimpleQ
        */
        get promise() {
        // @ts-ignore
            let promise = SimpleQ._promise
            return promise instanceof Promise ? promise : undefined
        }
    }

    const deferred = SimpleQ._promise = new SimpleQ((resolve, reject) => {
        // @ts-ignore
        SimpleQ._resolve = resolve
        // @ts-ignore
        SimpleQ._reject = reject
        
    })

    // for recongnition
    // @ts-ignore
    deferred.__proto__.entity = 'SimpleQ'

    if (deferred instanceof Promise &&
        deferred instanceof SimpleQ) {
        return deferred

    } else {
        /* istanbul ignore next */
        throw ('sq() not a valid Promise ?')
    }
}

/**
 * @ignore
 * @param {Object} o
 * @param {string} o.error
 * @param {Promise<any>} o.defer
 * @param {string} o.id
 */
// @ts-ignore
// eslint-disable-next-line no-unused-vars
const cancelPromiseCB = ({ error, defer, id }) => {}

/** 
 * Cancelable synchronous process, determines how long to wait before we exit
 * - If the promise never resolves or takes too long, we can cancel when maxWait expires
 * 
 * @param {object} config `{defer,checkEvery,maxWait,cbErr,message,logging,id}`
 * @param {Promise<any>} config.defer resolved when process complete or called from callback on timeout
 * @param {number} config.checkEvery how frequently to check if promise is resolved
 * @param {number} config.maxWait how long to wait before exciting with cbErr
 * @param {cancelPromiseCB} config.cbErr called on timeout cbErr(({error,defer,id})) here you can either resolve or reject the pending promise
 * @param {string} config.message (optional) defaults: **taken too long to respond**, or provide your own
 * @param {boolean} config.logging (optional) will prompt waiting process
 * @param {string|number} config.id (optional) added to error callback, and to logging
 * @returns {Promise} the promise provided in config.defer, dont need to use it
 * 
 * @example 
 * let dfr = sq()
 * cancelPromise({ defer:dfr, // can use standard Promise, sq(), or node.js q.defer
 *   checkEvery: 200, // << log process on every 
 *   maxWait: 3000, // expire promise 
 *   message: 'waited too long', // << use this error message
 *   logging: true, // display process
 *   id: new Date().getTime(), // custom id to display or on error
 *   cbErr: function({ error, defer, id }) {
 *       // update our reject message
 *       defer.reject(error)
 *   }
 * }) // returns promise
 * 
 * // will catch the timeout rejection
 * df2.promise.catch(onerror)
 *   
 * // or would exist waiting process and resolve
 * // dfr.resolve('success')
 * // dfr.promise.then(log)
 * 
**/
// @ts-ignore
const cancelPromise = ({ defer = undefined, checkEvery = 500, maxWait = 9500, cbErr = ({ error, defer, id }) => {}, message = 'taken too long to respond', logging = false, id = undefined }) => {
   
    let isFN = (el) => typeof el === 'function'
    let validPromise = isPromise(defer) || isQPromise(defer)
    
    if (!validPromise || !isFN(cbErr) || !maxWait) {
        onerror('[cancelPromise]', '{defer,maxWait,cbErr} must be provided')
        return Promise.reject('{defer,maxWait,cbErr} must be provided')
    }

    let exit_interval
    let every = checkEvery || 500
    maxWait = maxWait || 1

    let inx = 0
    const t = setInterval(() => {
        if (exit_interval) {
            //  if (logging) log('[cancelPromise]', 'cleared')
            /* istanbul ignore next */ 
            return clearInterval(t)
        }

        if (inx > maxWait) {
            let args = { error: `${message}, time: ${inx}`, defer, id }

            try {             
                cbErr.apply(args, [args])   
                // @ts-ignore
                defer.reject(`${message}, time: ${inx}`) 
            } catch (err) {
                // ups
                /* istanbul ignore next */ 
                onerror('[cancelPromise]', err)
            }

            return clearInterval(t)
        } else {
            if (logging) {
                if (id) log('-- processing: ', id)
                /* istanbul ignore next */ 
                else alert('-- processing ')
            }
        }
        inx = every + inx
    }, every)

    const deffer = (def) => {
        return def.then(n => {
            // will exit the interval
            exit_interval = true
            return n
        }, err => {
            exit_interval = true
            return Promise.reject(err)
        })      
    }
 
    if (isSQ(defer) || (isPromise(defer) && !isQPromise(defer))) return deffer(defer)
    // @ts-ignore
    if (isQPromise(defer)) return deffer(defer.promise)
    /* istanbul ignore next */ 
    else return Promise.reject('[cancelPromise], Supplied {defer} is not a promise')
   
}

/**
 * Convert to string, remove spaces, toLowerCase
 * @param {string|number} id 
 * @returns {string} 
 * 
 * @example 
 * validID('sdfkj 45 AMKD') // sdfkj45amkd
 **/
const validID = (id = '') => !(id || '') ? '' : (id || '').toString().toLowerCase().replace(/\s/g, '')

/**
 * Check item is a number
 * @param {any} n 
 * @returns {true|false}
 * 
 * @example
 * isNumber(-1) // true
 * isNumber( new Number(-1) ) // true
 * isNumber(NaN) // true
 * isNumber(true) // false
 * isNumber([]) // false
 **/
const isNumber = (n) => {
    if (isBigInt(n)) return false
    try {
        if (n instanceof Number) return true
    } catch (err) {
        //
    }
    return n !== undefined && n !== null && n !== '' ? (n).__proto__ === Number.prototype : false
}

/**
 * @deprecated in favour of validDate()
 * Check item is a date, example: new Date()
 * @param {any} d 
 * @returns {true|false}
 **/
/* istanbul ignore next */ 
const isDate = (d) => {
    try {
        return (d) instanceof Date
    } catch (err) {
        return false
    }
}

/**
 * Test the length of string
 * @param {string} str 
 * @returns {number} length of string
 * 
 * @example
 * stringSize('abc') // 3 
 * stringSize(-1) // 0
 * stringSize('-1') // 2
 * stringSize(undefined) // 0
 * stringSize([123]) // 0
 */
// @ts-ignore
const stringSize = (str = '') => str !== undefined && str !== null ? (str).__proto__ === String.prototype ? str.length : 0 : 0

/** 
 * There are 2 types of promises available javascript standard Promise and the node.js `q.defer()` promise
 * - this method tests for the q.defer node.js promise version
 * @param {any} defer q.defer() promise to check against
 * @returns {true|false}
 * 
 * @example
 * isQPromise(Promise.resolve()) }) // false 
 * isQPromise( sq() ) // false
 * isQPromise( q.defer() ) // true (referring to node.js q )
 * 
**/
const isQPromise = (defer = undefined) => {

    try {
        if (
            (defer.promise !== undefined &&
                typeof defer.resolve === 'function' &&
                typeof defer.reject === 'function' &&
                typeof defer.fulfill === 'function' &&
                typeof defer.notify === 'function'
            ) === true
        ) {
            return true
        }
    } catch (err) {
        // 
    }
    return false
}

/**
 * Test if item is our SimpleQ promise
 * @param {any} defer 
 * @returns {true|false}
 * 
 * @example
 * isSQ( sq() ) // true
 * isSQ( Promise.resolve() ) // false 
 * isSQ( q.defer() ) // false
 */
const isSQ = (defer) => {
    try {
        return defer.entity === 'SimpleQ'
    } catch (err) {
        return false
    }
}

/** 
 * Check for Promise / q.defer / and xutils promise ( sq() ),
 * - test if its a resolvable promise
 * @param {any} defer
 * @returns {true|false}
 * 
 * @example
 * isPromise( function () { } ) // false
 * isPromise( Promise.resolve()) ) // true
 * isPromise( sq() ) // true
 * isPromise( q.defer() ) // true
 * 
*/
const isPromise = (defer) => {
    if (isQPromise(defer)) return true
    else {
        try {
            if (defer instanceof Promise) return true
            // if (isSQ(defer)) return true
        } catch (err) {
            // onerror('err', err)
        }
        return false
    }
}

/**
 * Test item is a true object, and not array
 * - Should not be a function/primitive, or class (except for instance)
 * @param {any} obj
 * @param {function|undefined} cbEval (optional) callback operator, continue checking when callback returns !!true
 * @returns {true|false}
 * 
 * @example 
 * isObject({}) // true
 * isObject([]) // false
 * isObject( (new function(){}) ) // true
 * isObject((function () { })) }) // false
 * isObject((new class { })) // true
 * isObject( (class{}) ) // false
 * isObject(new Error()) // true
 * isObject(null) // false 
 * isObject( {}, ()=>false ) // false, due to callback !!false
 * isObject( [], ()=>Object.keys({1:1}).length ) // false, not an object
 *
 */
const isObject = (obj = undefined, cbEval = undefined) => {
    if (isFunction(cbEval) && !callFN(cbEval)) return false
    if (isBigInt(obj)) return false
    if (isNaN(obj) && typeof obj === 'number') return false
    if (typeof obj === 'function') return false
    if (!isNaN((+obj)) || obj === undefined) return false
    // @ts-ignore
    if ((obj).__proto__ === ([]).__proto__) return false // is array 
    // testing standard Object and Error
    const a = (Object.prototype === (obj).__proto__ || Error.prototype === (obj).__proto__)
    const ab = a && (obj instanceof Object)
    if (ab) return true

    if (obj.__proto__ !== undefined) {
        try {
            return obj instanceof Object
        } catch (err) {
            /* istanbul ignore next */ 
            return false
        }
    }
    /* istanbul ignore next */ 
    if (obj.prototype) return true
    return false
}

/**
 * Returns new array of unique values
 * @param {array} arr 
 * @returns {array}
 *
 * @example 
 * uniq([1, 1, 3, 'a', 'b', 'a', null, null, true, true]) 
 * // [1,3,'a','b',null,true]
 */
const uniq = (arr = []) => {
    let o = []
    o = arr.filter((el, i, all) => all.indexOf(el) === i)
    return o instanceof Array ? o : []
}

/** 
 * Randomise items in array
 * @param {array} arr
 * @returns {array} 
 *
*/
const shuffle = (arr = []) => {
    if (!isArray(arr)) return []
    for (let i = arr.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * i)
        const k = arr[i]
        arr[i] = arr[j]
        arr[j] = k
    }
    return arr
}

/** 
 * Select data from array of objects by reference, and go down recursively in order of selectBy `['a.b']` ref
 * @param {Array<string>} selectBy list of uniq references, example ['a.b.c.d.e','e.f.g'], each selectBy/item targets nested object props
 * @param {Array<any>} data list of objects to target by select ref
 * @returns {array} by selected order in same pair index
 *
 * @example 
 * // select b from both arrays, return same index order
 * selectiveArray(['a.b'], [ { a: { b:'hello' }, b:{c:'hello'} },{ a: { b:'world' },b:{c:'world'} } ]) 
 * //=>  [ [ 'hello'], [ 'world'] ] 
 *
 * //select b, and select c from both arrays, return same index order
 * selectiveArray(['a.b','b.c'], [ { a: { b:'hello' }, b:{c:'hello'} },{ a: { b:'world' },b:{c:'world'} } ]) 
 * //=> [ ['hello','hello'], ['world','world'] ]
 * 
 * // destructuring example : 
 * let [b,c]=Array.from( flatten(selectiveArray(['a.b','b.c'], [ { a: { b:'hello' }, b:{c:'world'} }]) ) ).values()
 *  // b==="hello", c ==="world"
*/
const selectiveArray = (selectBy = [], data = []) => {

    if (!isArray(data)) return []
    if (!data.length) return []
    // NOTE if selectBy is empty or invalid will return same data
    if (!isArray(selectBy)) return data
    if (!selectBy.length) return data
    selectBy = uniq(selectBy)

    let nData = []

    // go down recursively
    let findNest = (s, item, inx = 0) => {
        let lastItem = null
        let found
        if (!s) return undefined
        if (!isArray(s)) return undefined
        if (!s.length) return undefined

        try {
            if (item[s[inx]] !== undefined) {
                lastItem = item[s[inx]]
                found = lastItem
                inx = inx + 1

                if (s[inx]) return findNest(s, found, inx)
                else return found
            }

        } catch (err) {
            /* istanbul ignore next */ 
            console.log(err.toString())
        }

        return found
    }

    for (let i = 0; i < data.length; i++) {
        let item = data[i]

        if (!isObject(item)) {
            // each item in an array must be an object to be able to selectBy nested prop 
            nData.push([item])
            continue
        }

        let found
        let collective = [] // insert collective 
        for (let o = 0; o < selectBy.length; o++) {
            let sArr = (selectBy[o] || "").split('.')
            try {
                found = findNest(sArr, item, 0)
                collective.push(found)
            } catch (err) {
                //
            }
        }

        // if all items are undef and selectBy/size matches collective/size
        // if all collective are undef filter them out
        // this helps with positioning of uneven results, and 1 side has match and the other does not, 
        // valid example:[ [ 'abc', undefined ], 'efg', undefined  ] << pairs should be consistent, when selectBy has more then 1
        if (selectBy.length === collective.length) {
            let allUndef = collective.filter(n => n === undefined)
            if (allUndef.length === selectBy.length) collective = collective.filter(n => !!n)
        }

        if (collective.length) {
            nData.push([].concat(collective))
        } else if (found !== undefined) nData.push(found)
    }

    return nData
}

/**
 * Test item is a class{} constractor, that can be initiated
 * @param {any} obj
 * @param {*} cbEval (optional) callback operator, continue checking when callback returns !!true
 * @returns {true|false}
 * 
 * @example
 * isClass(Array) // true
 * isClass(Object) //true
 * isClass((class {}) ) //true
 * // instance of a class
 * isClass( (new function() {}()) ) // false
 * isClass( new Object() ) // false
 */
const isClass = (obj, cbEval = undefined) => {
    if (isFunction(cbEval) && !callFN(cbEval)) return false
    if (!obj) return false
    if ((obj).prototype !== undefined) return true
    return false
}

/** 
 * Same as isClass() method
 * @method hasPrototype(obj)
 * @alias isClass 
 *
*/
const hasPrototype = isClass

/**
 * Check if item has access to __proto__
 * @param {any} el 
 * @param {function|undefined} cbEval  optional callback, continue checking when callback returns !!true
 * @returns {boolean}
 *
 * @example
 * hasProto({}) // true
 * hasProto('') // true
 * hasProto(-1) // true
 * hasProto(false) // true
 * hasProto(undefined) // false
 * hasProto(null) // false
 * hasProto(NaN) // true
 * hasProto({}, ()=> Object.keys({}).length ) // false because object has no keys
 */
const hasProto = (el, cbEval = undefined) => {
    if (isFunction(cbEval) && !callFN(cbEval)) return false
    try {
        return el.__proto__ !== undefined
    } catch (err) {
        return false
    }
}

/**
 * Check pattern is an expression of RegExp
 * @param {RegExp} expression
 * @returns {boolean}
 *
 * @example
 * isRegExp('abc') // false
 * isRegExp(/abc/) // true
 */
const isRegExp = (expression = (/\\/)) => {
    try {
        return expression instanceof RegExp
    } catch (err) {
        /* istanbul ignore next */ 
        return false
    }
}

/**
 * Testing if item{} is a `new Item{}`, instance of a class
 * @param {any} obj
 * @param {function|undefined} cbEval (optional) continue checking when callback returns !!true
 * @returns {boolean}
 *
 * @example 
 * isInstance({}) // false
 * isInstance(new function(){}) // true 
 * isInstance(new class(){} ) // true 
 * isInstance(function () { }) // false
 * isInstance([]) // false
 */
const isInstance = (obj = {}, cbEval = undefined) => {
    if (isFunction(cbEval) && !callFN(cbEval)) return false
    if (!obj) return false
    if (isArray(obj)) return false
    if (obj.__proto__ && !isClass(obj)) {
        try {
            return obj.__proto__ instanceof Object
        } catch (err) {
            /* istanbul ignore next */ 
            return false
        }
    }
    return false
}

/** 
 * Check size object, we want to know how many keys are set
 * @param {object} obj
 * @returns {number} number of keys on the object
 *  
 * @example 
 * objectSize({ a: 1, b: 2 }) }) // 2
 * objectSize([1,2]) // 0
 * objectSize( (new function(){this.a=1}()) ) // 1
 * objectSize( (new function(){}()) ) // 0
 */
const objectSize = (obj = {}) => {
    if (!obj || !isNaN(+(obj))) return 0
    if (isInstance(obj)) return Object.keys(obj).length
    return ((Object.prototype === (obj).__proto__) || Error.prototype === (obj).__proto__) ? Object.keys(obj).length : 0
}

/**
 * Check if any item type is falsy, object, array, class/instance, having no props set
 * @param {any} el 
 * @returns {boolean}
 *
 * @example
 * isFalsy({}) // true
 * isFalsy({a:1}) // false
 * isFalsy([]) // true
 * isFalsy([1]) // false
 * isFalsy(true) // false
 * isFalsy(false) // true
 * isFalsy(0) // true
 * isFalsy( (new function(){}()) ) // true
 * isFalsy( (new function(){this.a=false}()) ) // false
 */
const isFalsy = (el = undefined) => {
    if (el === undefined || 
        el === null) return true     
    if (el === false && typeof el === 'boolean') return true
    if (el === true && typeof el === 'boolean') return false
    if (typeof el === 'number' && el > 0) return false

    if (String.prototype === (el).__proto__) return el.length < 1
    if (Array.prototype === (el).__proto__) return (el || []).length === 0
    if (Promise.prototype === (el || {}).__proto__) return false
    if (typeof el === 'function') return false
    if ((Object.prototype === (el).__proto__) || isInstance(el)) return Object.keys(el).length === 0
    if ((Error.prototype === (el).__proto__)) return false
    if (el !== undefined && (el).__proto__ === Number.prototype) {
        if (isNaN(el)) return true
        else return el <= 0
    }
    /* istanbul ignore next */ 
    if ((+(el) > 0) === false) return true
    if (el) return false
    else return false
}

/**
 * Test item is a string type
 * @param {any} str 
 * @param {function|undefined} cbEval (optional) operator, continue checking when callback returns !!true
 * @returns {boolean}
 *
 * @example 
 * isString('') // true
 * isString(new String()) // true
 * isString(NaN) // false
 * isString(new Date()) // false
 * isString('123', ()=>'123'.length>5) // false, callback return !!false
 * isString('123', ()=>'123'.length>2) // true
 */
const isString = (str = undefined, cbEval = undefined) => {
    if (isFunction(cbEval) && !callFN(cbEval)) return false
    if (str === undefined) return false
    if (str === null) return false
    if (typeof str === 'boolean') return false
    return str === '' ? true : String.prototype === (str).__proto__
}

/**
 * Copy object by property name
 * @param {object} obj 
 * @param {array} refs supply list of keys  
 * @returns {object} new object of copied values
 * 
 * @example
 * copyBy({ a: 1, b: 2, c: 3 }, ['a', 'c'])  // {a: 1, c: 3}
 * copyBy({ a: 1, b: 2, c: 3 })  // {}
 * copyBy({}) } // {}
 */
const copyBy = (obj = {}, refs = []) => {
    if (!isObject(obj)) return {}
    // @ts-ignore
    const d = [].concat(refs).reduce((n, el, i) => {
        if (obj[el] !== undefined) n[el] = obj[el]
        return n
    }, {})

    try {
        return JSON.parse(JSON.stringify(d))
    } catch (err) {
        /* istanbul ignore next */ 
        return {}
    }
}

/**
 * Makes item copy
 * @param {any} data
 * @returns {any} copy of the same input type, or primitiveValue type if class or method supplied
 *
 * @example
 * copy({ a: 1, b:function(){} }) //=>  {a:1}
 * copy([1,2,3]) //=> / [1,2,3]
 * copy( function(){}) //=>  Function: anonymous
 * copy(null) //=>  null 
 * copy(true) //=>  true
 */
const copy = (data) => {
    try {
        return JSON.parse(JSON.stringify(data))
    } catch (err) {
        return typeCheck(data).primitiveValue
    }
}

/**
 * 
 * Provided data is returned in pretty json
 * @param {any} data array/object
 * @returns {string} JSON.stringify(o , null, 2)
 *
 * @example  
 * asJson( { a:{ b: { c:'hello world' } } } )
 * // returns:
 * {
 *  "a": {
 *    "b": {
 *      "c": "hello world"
 *    }
 *  }
 * }
 */
const asJson = (data) => {
    try {
        return JSON.stringify(data, null, 2)
    } catch (err) {
        /* istanbul ignore next */ 
        return `[asJson], ` + err.toString()
    }
}

/** 
 * For complex arrays of objects: `[{...},{...}]`
 * - will copy each array item separately and check for Object>object then make copy
 * @param {any} data object or array
 * @return {any} copy of the same input type, or primitiveValue type where method supplied
 * 
 * @example
 * copyDeep({ a: {b:{c:{}}} }) //=>  { a: {b:{c:{}}} })
 * copyDeep([{ a: (new function(){this.b=1}()) }]) //=>  [ { a: {b:1} } ]
 * copyDeep({ a: (new function(){this.b=1}()) }) //=>  { a: { b:1 } }
*/
const copyDeep = (data) => {

    if (isArray(data)) {
        return data.map(n => copy(n))
    }

    if (isObject(data)) {
        return Object.entries(data).reduce((n, [k, val]) => {
            if (isObject(val)) n[k] = { ...copy(val) }
            else n[k] = val
            return n
        }, {})
    } else {
        try {
            return JSON.parse(JSON.stringify(data))
        } catch (err) {
            return typeCheck(data).primitiveValue
        }
    }
}

/**
 * Delay a sync/async process, to be executed after `delay` is resolved
 * @param {number} time in ms
 * @returns {Promise} always resolves
 * 
 * @example 
 *  // async 
 *  log('delay start')
 *  await delay(2000)
 *  // continue with process
 *  // sync
 *  delay(2000).then(()=>{...})
 */
const delay = (time = 0) => {
    const isNum = typeof time === 'number' && time >= 0 // must provide number
    if (!isNum) return Promise.resolve(true) // or resolve 
    // @ts-ignore
    return new Promise((resolve, reject) => {
        const t = setTimeout(() => {
            clearTimeout(t)
            resolve(true)
        }, time)
    })
}

/**
 *  
 * Test if ANY keys match between object{} and source{} 
 * @param {object} object 
 * @param {object} source 
 * @param {function|undefined} cbEval (optional) operator, continue checking when callback returns !!true
 * @returns {boolean} when at least 1 key is found between 2 objects, return true
 *
 * @example 
 * someKeyMatch({ a: 2, b: 1, c: 2 }, { d: 1, e: 1, a: 1 }) 
 * //=>  true , {a} was found
 * someKeyMatch({ a: 2, b: 1, c: 2 }, { d: 1, e: 1, a: 1 }, ()=>1-1===1) 
 * //=>  false, because callback return !!false
*/
const someKeyMatch = (object = {}, source = {}, cbEval = undefined) => {
    if (isFunction(cbEval) && !callFN(cbEval)) return false
    // test if its an object
    if (!(!object ? false : Object.prototype === (object).__proto__)) return false
    if (!(!source ? false : Object.prototype === (source).__proto__)) return false

    const a = Object.keys(object)
    const b = Object.keys(source)
    if (a.length >= b.length) return a.filter(z => b.filter(zz => zz === z).length).length > 0
    else return b.filter(z => a.filter(zz => zz === z).length).length > 0
}

/** 
 * Test if ALL keys match between object{} and source{} 
 * @param {object} object 
 * @param {object} source 
 * @param {function|undefined} cbEval (optional) operator, continue checking when callback returns !!true
 * @returns {boolean} when ALL keys found between 2 objects, return true
 * 
 * @example
 * exactKeyMatch({ a: 2, b: 1, c: 2 }, { c: 1, a: 1, b: 1 }) //=>  true
 * exactKeyMatch({ a: 2, b: 1 }, { c: 1, a: 1, b: 1 }) //=> false
 * exactKeyMatch({}, { c: 1, d: 1}) //=>  false
 * exactKeyMatch({ a: 2, b: 1, c: 2 }, { c: 1, a: 1, b: 1 }, ()=> 1+1===3) 
 * //=> false, because callback return !!false
*/
const exactKeyMatch = (object = {}, source = {}, cbEval = undefined) => {
    if (isFunction(cbEval) && !callFN(cbEval)) return false
    // test if its an object
    if (!(!object ? false : Object.prototype === (object).__proto__)) return false
    if (!(!source ? false : Object.prototype === (source).__proto__)) return false

    const a = Object.keys(object)
    const b = Object.keys(source)
    if (a.length >= b.length) return a.filter(z => b.filter(zz => zz === z).length).length === a.length
    else return b.filter(z => a.filter(zz => zz === z).length).length === b.length
}

/**
 * Exclude any falsy values from array, such as: `[0,null,false,{},undefined, -1,'',[]]`
 * @param {Array<any>} arr mixed
 * @returns {Array<any>} only non falsy items are returned 
 * 
 * @example 
 * trueVal([-1, 0,1, {}, "hello", [], { name: 'jack' }, false, null, NaN, undefined,true]) 
 * //=> [1,'hello',{ name: 'jack' },true]
 **/
const trueVal = (arr = []) => {
    // provided must be array
    // @ts-ignore
    if (!(!arr ? false : Array.prototype === (arr).__proto__)) return []
    // @ts-ignore
    return [].concat(arr).filter((itm, inx) => isFalsy(itm) !== true)
}

/**
 * Exclude any falsy values from array: `[0,null,false,{},undefined, -1,'',[]]`, but testing 1 level deeper, compared to `trueVal()`
 * @param {Array<any>} arr mixed
 * @returns {Array<any>} only non falsy items are returned 
 * 
 * @example
 * trueValDeep([1, 0, [], {}, "hello", [0, undefined, -1, false, NaN, 1], { name: 'jack' }, false, null, undefined])
 * //=> [ 1, 'hello', [ 1 ], { name: 'jack' } ] 
 */
const trueValDeep = (arr = []) => {
    // provided must be array
    // @ts-ignore
    if (!(!arr ? false : Array.prototype === (arr).__proto__)) return []
    if (!arr.length) return []
    // @ts-ignore
    return [].concat(arr).map((itm, inx) => {
        const typeIs = typeCheck(itm, false)
        // this item has child, check for false entities
        if (typeIs.type === 'array' && typeIs.value > 0) {
            return itm.map(child => {
                // return only true entities, from 1 depth
                if (typeCheck(child, false).value > 0) return child
                else return null
            }).filter(n => !!n)
        }
        if (typeIs.type === 'object' && typeIs.value) {
            // @ts-ignore
            return Object.entries(itm).reduce((n, [k, v], i) => {
                if (typeCheck(k, false).value > 0) n[k] = v
                return n
            }, {})
        } else if (typeIs.value > 0) return itm
        else return null
    }).filter(n => !!n)
}

/**
 * Object with true entities will be returned 
 * @param {object} obj required
 * @returns {object} 
 * 
 * @example  
 * trueProp({ a: NaN, b: 0, c: false, d: -1, e: NaN, f: [], g: 'hello', h: {}, i: undefined, j:'' })
 * //=> {g: 'hello'}
 */
const trueProp = (obj = {}) => {
    if (!(!obj ? false : Object.prototype === (obj).__proto__)) return {}

    // @ts-ignore
    return Object.entries(obj).reduce((n, [key, val], inx) => {
        if (!isFalsy(val)) n[key] = val
        return n
    }, {})
}

/**
 * @ignore
 * @callback resolverCB
 * @returns {any}
 */

/** 
 * Run some method that returns value in future, checking updates until timeout, or exit when data becomes available
 * @param {resolverCB} fn callable method that returns some value
 * @param {number} timeout (ms) specify max time to wait for data before timeout
 * @param {number} testEvery how ofter to test data availability
 * @returns {Promise<any>} always resolves, when return is empty it will be wrapped in an {error}
 * 
 * @example 
 * resolver(()=>Promise.resolve({data:'hello world'}),5000,50).then(n=>{
 *   log({resolver:n})
 * })
 * 
 * resolver(()=>Promise.reject('some error'),5000,50).then(n=>{
 *   log({resolver:n}) // {error: 'some error'}
 * })
*/
const resolver = (fn = () => {}, timeout = 5000, testEvery = 50) => {
    let isFunction = typeof fn === 'function'
    if (!isFunction) {
        return Promise.reject('fn() must be callable')
    }
    // @ts-ignore
    return new Promise((resolve, reject) => {
        let every = testEvery || 50
        let max = timeout
        let inx = 0

        let test = async () => {
            try {
                return await fn()
            } catch (error) {
                if (isError(error)) return { error }
                if (isObject(error)) {
                    if (error.error) return error
                    else return { error: error }
                } else return { error }
            }
        }

        let t = setInterval(async () => {
            let anon = test() 

            // lets catch any errors      
            anon.catch(err => {
                return err
            })

            if (inx >= max) {
                // @ts-ignore
                if ((anon || {}).resolve) anon.resolve(undefined)   
                resolve(undefined)
                return clearInterval(t)
            }

            inx = inx + every
            try {
                // NOTE for promise with asyn we need the counter above,
                inx = inx + every

                let d = await anon
                if (d) {
                    resolve(d)
                    return clearInterval(t)
                }
                /* istanbul ignore next */ 
            } catch (error) {

                if (isError(error)) resolve(error)
                if (isObject(error)) {
                    if (error.error) resolve(error)
                    else resolve({ error })
                } else resolve({ error })
                return clearInterval(t)
            }
           
        }, every)
    })
}

/**
 * Flatten 2 level array to 1 level: `[[]] > [], [[[]]] > [[]]`
 * @param {array} arr 
 * @returns {array}
 * 
 * @example
 * flatten([['hello world']]) // ['hello world']
 */
const flatten = (arr = []) => {
    if (!isArray(arr)) return []
    return [].concat(...arr)
}

/** 
 * Flatten all array levels to 1, example: `[[['hello']]] > ['hello']`
 * @param {array} arr
 * @returns {array}
 * 
 * @example 
 * flattenDeep([[[['hello world']]]) // ['hello world']
*/
const flattenDeep = (arr = []) => {
    let o = []
    if (!isArray(arr)) return o
    function test(arr, d = 1) {
        return d > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? test(val, d - 1) : val), [])
            : arr.slice()
    }
    try {
        o = test(arr, Infinity) || []
        if (o instanceof Array) return o
        /* istanbul ignore next */ 
        else return []
    } catch (err) {
        /* istanbul ignore next */ 
        return []
    }
    
}

/** 
  * Split array to chunks by providing size number
  * @param {array} arr required
  * @param {number} size how many chunks per batch
  * @returns {array} chunked array by size
  * 
  * @example 
  * chunks( [1,2,3,4,5,6] , 2) // [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ]
 */
const chunks = (arr = [], size = 0) =>
    // @ts-ignore
    Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>
        arr.slice(i * size, i * size + size)
    )

/** 
 * Duplicate item x:number of times
 * @param {any} item
 * @param {number} index times to duplicate the item
 * @returns {array} same item duplicated
 * 
 * @example
 * dupes('any', 2) 
 * // ['any','any']
 * 
 * dupes([{a:1},{b:1}], 2) 
 * // [ [{a:1},{b:1}], [{a:1},{b:1}] ]
*/
const dupes = (item, index = 0) => {
    const dups = []
    // @ts-ignore
    let n = parseInt(index)
    while (n > 0) {
        n--
        dups.push(item)
    }
    return dups
}

/**
 * New array with uniq property values
 * - Selects first match ignoring others of those which prop values are repeated
 * - non matching objects, preserved as usual
 * @param {array} arr mixed array of items and objects
 * @param {string} propName select key and test values, are repeated
 * @returns {array} [{},...] items with object items whos values are uniq
 * 
 * @example
 * uniqBy([{ a: 1, b: 2 }, 1, { b: 1 }, 5, { a: 1 }, null, { a: 1, b: 2 }], 'a')
 * //=> [ { a: 1, b: 2 }, 1, { b: 1 }, 5, null ]
 * 
 * uniqBy([{ c: 1, b: 2 }, { c: 1 }, { c: 1 }, { c: 1, b: 2 }], 'c')
 * //=> [ { c: 1, b: 2 } ]
 * 
 * uniqBy([{ a: 1, b: 2 }, null, undefined, true, 1, { a: 1 }, { a: 3 }, false], 'a')
 * //=> [ { a: 1, b: 2 }, null, undefined, true, 1, { a: 3 }, false ] prop[values] are not uniq
 */
const uniqBy = (arr = [], propName = '') => {
    const stored = {}
    const n = []
    if (!propName) return []
    if (!(arr || []).length) return []

    for (let inx = 0; inx < arr.length; inx++) {
        let item = arr[inx]

        if (isUndefined(item) || isNull(item)) {
            n.push(item)
            continue
        }
        
        if (!isObject(item)) {
            n.push(item)
            continue
        }

        if (item[propName] === undefined) {
            n.push(item)
            continue
        }

        // @ts-ignore
        let exists = Object.entries(stored).filter(([k], i) => item[propName] === stored[k]).length
        if (exists) continue

        if (item[propName] !== stored[propName + `:${inx}`]) {
            stored[propName + `:${inx}`] = item[propName]
            n.push(item)
        }

    }

    return n
}

/**
 * Mixed array of objects and values, grab items[] that include specific prop 
 * @param {array} arr mixed [{a:true},...]
 * @param {string} prop
 * @returns {array} items that include specific prop 
 * 
 * @example
 * arrayWith([ [], { a: undefined }, { b: 3 }, { a: 1 } ], 'a') 
 * //  [ { a: undefined }, { a: 1 }] 
 * 
 * arrayWith([ { a: 1 } , 1,[], undefined, { b: 3 } ], 'b') 
 * //  [ { b: 3 }] 
**/
const arrayWith = (arr = [], prop = '') => {
    if (!isArray(arr)) return []
    let objWith = (o) => {
        // if()
        if (isObject(o)) {
            if (Object.keys(o).indexOf(prop) !== -1) return o
            else return undefined
        } else return undefined
    }

    try {
        let o = arr.map(n => objWith(n)).filter(n => !!n)
        return o instanceof Array ? o : []
    } catch (err) {
        /* istanbul ignore next */ 
        return []
    }

}

/**
 * Array including any objects and values
 * - Exclude items from array matchd by `excludes[]`, and replace with undefined keeping index position
 * @param {array} arr mixed with objects to exclude by propName
 * @param {Array<string>} excludes propNames to match each object in arr[x]
 * @returns {array} mixed with any other types as per input, in same index position
 * 
 * @example
 * exFromArray([{ a: 1, c: 5 }, { a: 10 }, { b: 2 }, { c: 1, a: 2 }], ['a', 'b']) 
 * // [ { c: 5 }, undefined, undefined, { c: 1 }] 
 *
 * exFromArray([ null,1,{ a: 1, c: 5 }, { a: 10 }, { b: 2 }, { c: 1, a: 2 },'2'], ['a', 'c']) 
 * // [null,1, undefined,undefined,{ b: 2 },undefined,'2']
 **/
const exFromArray = (arr = [], excludes = [/** propName,propName */]) => {
    let o = []
    try {
        if (!(arr instanceof Array)) return o
    } catch (err) {
        /* istanbul ignore next */ 
        return []
    }
    excludes = [].concat(excludes)

    if (!excludes.length) return arr

    const excludeFrom = (obj = {}, excludes = []) => {
        if (!isObject(obj)) return obj

        const d = Object.entries(obj).reduce((n, [k, val]) => {
            if (excludes.indexOf(k) === -1) n[k] = val
            return n
        }, {})
        if (isFalsy(d)) return undefined
        else return d
    }

    try {
        let o = arr.map(n => excludeFrom(n, excludes)) // .filter(n => n !== undefined)
        return o instanceof Array ? o : []
    } catch (err) {
        /* istanbul ignore next */ 
        return []
    }

}

/**
 * Array selection tool
 * - Filter items in `array[item,item]` by `picks[Types|primitives,values]` conditions 
 * - Does not support deep selections from picks[], only 1 level deep, but you can use object types,
 * @param {array} arr array of any 
 * @param {Array<any>} picks item in picks tests for all passing conditions, like object types, primitive type, and matching values
 * - Empty types and strings, are excluded [{},[],'',NaN]
 * @returns {array} items that passed each pick condition, keeping the same index order
 * 
 * @example
 * let picks = [Boolean, 'hello', Object, { a: 1 }, BigInt] // only these types/values will be tested 
 * pickFromArray([false, undefined, { a: 1 }, 'hello', ['hello'], {}, 1234567890123456789012345678901234567890n, 'not selected'], picks )
 * //>  [ false,{ a: 1 },'hello',{},1234567890123456789012345678901234567890n ] 
 * 
 * let picks = [Number, Boolean] // select only numbers and booleans from array
 * pickFromArray([undefined, 1, {}, 2, null, [], 'hello world', 3, true, 4, null, 5], picks) 
 * //> [ 1, 2, 3, true, 4, 5 ]
 * 
 * let picks = [undefined, [undefined] ] // select all undefined from array
 * pickFromArray([undefined, false, 1, true, {}, [1], [undefined], null], picks)
 * // [undefined, [undefined]]
 * 
 * // we only want to pick items that are {data} objects containing inner objects
 * let picks = [{ data: Object }] 
 * pickFromArray([{ data: { a: 1 } }, { data: 1 },false, ['hello'], { data: { d: 2 } }, { data: { b: 2 } }, 1, 2, [], {}], picks)
  * //=> [{ data: { a: 1 } },{ data: { d: 2 } },{ data: { b: 2 } } ]
  * 
  * let picks = [{ a: Object, b: 1 }]  // narrowing down the results, should select all array objects that at least contain all the above
  * pickFromArray([{ a: { a: 1 }, b: 1, c:1  }, { data: 1 }, { a: { a: 1 }, b: 1 }, { data: null }, false, 1, 2, [], {}], picks)
  * //=>  [ { a: { a: 1 }, b: 1, c: 1 }, { a: { a: 1 }, b: 1 }]
 */
const pickFromArray = (arr = [], picks = []) => {
    let o = []
    try {
        if (!(arr instanceof Array)) return o
    } catch (err) {
        /* istanbul ignore next */ 
        return []
    }

    if (!isArray(picks)) picks = [].concat(picks)
    if (!picks.length) return arr
    let allowedPicks = [undefined, null, false]
    // @ts-ignore
    picks = picks.filter(n => !isFalsy(n) || allowedPicks.filter(nn => nn === n || (isNumber(n) && !isNaN(n)).length))

    if (!picks.length) return arr

    /**
     * when checking primitives we test by name and to lowercase
     * @param {*} item 
     * @param {*} pick 
     */
    let isInstanceByName = (item, pick) => {

        // do exect array and Object checks

        if (isArray(item)) {
            if (isFunction(pick)) {
                if (pick.name.toLowerCase() === 'object') return false
                if (pick.name.toLowerCase() === 'array') return true
            }
        }

        if (isObject(item)) {

            if (isFunction(pick)) {
                if (pick.name.toLowerCase() === 'array') return false
                if (pick.name.toLowerCase() === 'object') return true
            }
        }
        
        try {
            // eslint-disable-next-line valid-typeof
            return (pick.name || '').toLowerCase() === typeof item

        } catch (err) {
            return undefined
        }
    }

    /**
     * evaluate each item on all picks 
     * 
     * @param {*} item 
     */
    let evalItem = (item) => {

        let selected

        for (let inx = 0; inx < picks.length; inx++) {
            let pick = picks[inx]

            if (isObject(pick) && isObject(item)) {
                // all entries on pick must match that on each item
                let pEntries = Object.entries(pick)
                let pass = pEntries.filter(([k, val]) => {

                    let ok = item[k] === val
                    if (ok) return true
                    else if (item[k] !== undefined) {
                        return isInstanceByName(item[k], val)
                    }
                })
                // all picks must match the requirement and object can have more props then pic has
                // @ts-ignore
                pass = pass.length === pEntries.length && Object.entries(item).length >= pass.length

                if (pass && objectSize(item) >= objectSize(pick)) {
                    selected = true
                    break
                }
            }

            // array === array (also matching contents of each pick)
            // each pick contents that can be matched =[number, boolean,string, primitiveValue]
            if (isArray(pick) && isArray(item)) {
               
                let pass = pick.filter(n => item.filter(nn => nn === n || isInstanceByName(nn, n)).length)

                // all picks must match the requirement and array can have more items then pick has
                pass = pass.length === pick.length && item.length >= pass.length

                if (pass) {
                    selected = true
                    break
                }

            } else if (pick === item) {
                selected = true
                break
            } else if (
                isNumber(item) ||
                isBoolean(item) ||
                isString(item) ||
                isArray(item) ||
                isObject(item) ||
                isFunction(item)
            ) {

                if (isInstanceByName(item, pick)) {
                    selected = true
                    break
                }

            } else if (isInstanceByName(item, pick)) {
                selected = true
                break
            }
        }

        return selected
    }
    try {
        o = arr.reduce((n, el) => {
            if (evalItem(el)) n.push(el)
            return n
        }, [])
        if (o instanceof Array) return o
        /* istanbul ignore next */ 
        else return []
    } catch (err) {
        /* istanbul ignore next */ 
        return []
    }
  
}

function Dispatcher(uid, debug) {

    const plugin = `[dispatcher]`
    /* istanbul ignore next */ 
    this.uid = ((uid || '').toString() || new Date().getTime()).toString() // id generated if not provided
    
    this.debug = debug
    this.cbQueue = {}
    this.dispatchInstance = {}
    this._isActive = null
    this._onComplete_cb = null
    this.index = 0 // count callbacks
    this.data = null // dynamic next data becomes available when subscribe event is received
    // shorthand aliases

    /** 
     * @ignore
     * @onComplete
     * when subscribe event is deleted complete even callback can be called
    */
    this.onComplete = (cb) => {
        this._onComplete_cb = cb
        return this
    }

    /** 
     * @ignore
     * @Dispatch
     * initialize the dispatcher
    */
    this.initListener = () => {
        this.Dispatch()
        this._isActive = true
        return this
    }

    /**
     * @ignore
     * @next
     * send data to `subscribe` callback
     * @param {*} data any
     */
    this.next = (data = null) => {
        if (this._isActive !== false) this.initListener() // in case next is called above subscribe, we need to make sure it is initiated
        if (this.dispatchInstance[this.uid]) this.dispatchInstance[this.uid].next(data)
        else {
            /* istanbul ignore next */ 
            if (this.debug) log({ message: `${plugin} for uid not available`, uid: this.uid })
        }
        return this
    }

    /**
     * @ignore
     * @Dispatch
     * master listener, sends all event callbacks to `subscribe`
     */
    this.Dispatch = () => {
        if (this.dispatchInstance[this.uid]) return this
        const self = this
        const D = function () {
            this.uid = self.uid
            this.data = null

            this.next = (data) => {
                if ((data || {}).type !== 'cb') this.data = data
                /**
                     * @next
                     * acts as a reverse callback, it sends back the `cb` from `subscribe`
                     */
                if ((data || {}).type === 'cb') {
                    if (typeof data.cb === 'function') {
                        // when calling next before subscribe is initiated
                        // collect cb from .next
                        if (!self.cbQueue[self.uid]) self.cbQueue[self.uid] = data.cb
                        if (this.data) {
                            self.index++
                            self.data = this.data
                            data.cb.call(self, this.data, self.uid, self.index)

                        }
                    }

                    return
                }

                if (this.data) {
                    if (typeof self.cbQueue[self.uid] === 'function') {
                        self.index++
                        self.data = this.data
                        self.cbQueue[self.uid].call(self, this.data, self.uid, self.index)

                    }
                } else {
                    /* istanbul ignore next */ 
                    if (self.debug) warn(`${plugin} no callback data`)
                }
            }
        }

        if (!this.dispatchInstance[this.uid]) this.dispatchInstance[this.uid] = new D()
        return this
    }

    /** 
     * @isActive
     * check if current Dispatcher is still valid and active
    */
    this.isActive = () => {
        return this._isActive
    }

    /** 
     * @ignore
     * @del
     * delete dispatcher
    */
    this.del = () => {
        delete this.cbQueue[this.uid]
        delete this.dispatchInstance[this.uid]
        this._isActive = false
        if (typeof this._onComplete_cb === 'function') this._onComplete_cb(this.uid)
        return this
    }

    /**
     * @ignore
     * @subscribe
     * wait for callbacks forwarded from Dispatch and returned in callback of this method
     * - Dispatch must be set initially before you call `subscribe`
     * @param {*} cb required
     */
    this.subscribe = (cb) => {
        const isFN = typeof cb === 'function'
        if (!isFN) {
            /* istanbul ignore next */ 
            if (this.debug) warn(`${plugin}[subscribe] cb must be set`)
            return this
        }
        if (!this.dispatchInstance[this.uid]) {
            // this means subscribe was executed prior to `Dispatch`, because it has forward with next
            // it will get executed anyway
            this.Dispatch()
        }
        if (this.dispatchInstance[this.uid]) this.dispatchInstance[this.uid].next({ type: 'cb', cb })
        return this
    }

    /** 
     * @ignore
    * @alias initListener
    */
    this.init = this.initListener

    /** 
     * @ignore
     * @alias subscribe
    */
    this.sub = this.subscribe

    /** 
     * @ignore
    * @alias next
    */
    this.emit = this.next

    /** 
     * @ignore
    * @alias del
    */
    this.delete = this.del

    /** 
     * @ignore
     * @alias del
    */
    this.unsubscribe = this.del

    // end
}

/**
 * Lightweight Event Dispatcher, allowing dispatch anywhere in code, very handy in callback/hell situations, deep promises, or other computations. Integrated with callback memory so you dont have to subscribe first to get your data.
 * - Call next before subscribe
 * - Avoid ugly callback > callback > callback hell!
 * - Avoid messy Promises
 * - Prefer clean, readable code hierarchy
 * @param {string|number} uid (optional) or generated
 * @param {boolean} debug for extra debug messages
 * @returns {Dispatcher}
 * 
 * @example
 * const ds = dispatcher()
 * ds.next({ data: 'hello world' })
 * ds.subscribe(function (data, uid, index) {
 *    log('on subscribe', data, uid, index)
 *    // this.delete() // delete dispatcher
 * }).onComplete(uid => {
 * // last call on deletion
 * })
 * ds.next({ data: 'hello again' })
 * ds.delete() // delete dispatcher
 * ds.next({ data: 'another' }) // never called
 */
const dispatcher = (uid = undefined, debug = false) => {
    // @ts-ignore
    return new Dispatcher(uid, debug)
}

/**
 * @ignore
 * @callback withHocCB
 * @param {...any} args
 */

/**
 * High order caller, concept taken from React HOC.
 * - Promise support, we can provide deferred callback
 * - if rejectable error is not callable, message is: `DEFERRED_NOT_CALLABLE`
 * @param {withHocCB} item callable function
 * @param {*} args (optional) any number of arguments (,,,,) that callable function has available
 * @returns {function} callable function withHoc(...args) OR deferred if a promise
 * 
 * @example
 * function fn(a = 1, b = 2, c = 3) {
 *     return a + b + c
 * }
 * 
 * // example 1
 * let fnHocked = withHoc(fn)
 * fnHocked() // > 6
 * 
 * // example 2
 * fnHocked = withHoc(fn, 4, 5, 6) // provided fn() arguments from upper caller
 * fnHocked() // > 15
 *
 * // example 3
 * fnHocked = withHoc(fn, 4, 5, 6) 
 * // above arguments  replaced with in final call 
 * fnHocked(7, 8, 9) // > 24  
 * 
 * // example 4
 * fnHocked = withHoc(Promise.resolve(fn), 4, 5, 6) 
 * // above arguments  replaced with in final call 
 * fnHocked(7, 8, 9).then(log) // > 24  
 *
 * // example 5
 * fnHocked = withHoc(Promise.reject(fn), 4, 5, 6) 
 * // above arguments  replaced with in final call 
 * fnHocked(7, 8, 9).catch(onerror) // > 24  
 *
 * // example 6 not a deferred caller: 
 * fnHocked = withHoc(Promise.reject('fn'), 4, 5, 6) 
 * // above arguments  replaced with in final call 
 * fnHocked(7, 8, 9).catch(onerror) // > DEFERRED_NOT_CALLABLE  
 */
const withHoc = (item = () => { }, ...args) => {
    let extraArgs = args
    const hoc = (...args) => {

        let argsFN = () => {
            let _args
            if (extraArgs) _args = [].concat(args, extraArgs)
            else _args = args
            return _args
        }

        if (item instanceof Function) {

            try {

                // @ts-ignore
                return item(...argsFN())
            } catch (err) {
                /* istanbul ignore next */ 
                onerror('[HOC]', err)
            }

        } else if (isPromise(item)) {

            // if provided a defered callable item we can wait and then call if
            // example expecting Promise.resolve(()=>{}) OR  Promise.reject(()=>{})
            let fn = () => {

                let asPromise = () => {
                    // @ts-ignore
                    if (item.promise) return item.promise
                    else return item
                }

                return asPromise().then(defItem => {
                    if (isFunction(defItem)) return defItem(...argsFN())
                    else return Promise.reject('DEFERRED_NOT_CALLABLE')
                }, err => {
                    // should always return a function of constant message
                    if (isFunction(err)) return Promise.reject(err(...argsFN()))
                    else return Promise.reject('DEFERRED_NOT_CALLABLE')
                })

            }
            // rejectable call
            return fn()

        } else {
            /* istanbul ignore next */ 
            onerror('[HOC]', 'item() must be callable function')
        }

    }
    return hoc
}

/**
 * Return new object excluding all undefined values in top level
 * @param {object} obj
 * @returns {object}
 * 
 * @example
 * truthFul({ a: undefined, b: 1, c: {} }) // { b: 1, c: {} }
 */
const truthFul = (obj = {}) => {
    if (!isObject(obj)) return {}
    return Object.entries(obj).reduce((n, [k, v]) => {
        if (v !== undefined) n[k] = v
        return n
    }, {})
}

/**
 * Test accuracy of a `match[x]` in a string
 * @param {string} str to match against
 * @param {Array<RegExp>} patterns RegExp patterns to test against
 * @returns {number} size of index patterns that matched in the string
 * 
 * @example
 * inIndex('ab cd eFG', [/fg/i, /\sCD\s/i, /ab/]) // 3 < found in three pattern arrays
 * inIndex('abcdeFG', [/%fg/i, /1CD/i, /ab/]) // 1 (last)
 * 
 */
const inIndex = (str = '', patterns) => {

    let o = 0
    if (!isArray(patterns)) return o
    if (!patterns.length) return o
    if (typeof str !== 'string') return o
    if (!str) return o

    let regx = (patt, s, inx) => {
        try {
            return new RegExp(patt).test(s)
        } catch (err) {
            /* istanbul ignore next */ 
            onerror('[inIndex]', `wrong pattern/expression at index:${inx}`)
            return false
        }
    }
    o = patterns.filter((n, inx) => regx(n, str, inx)).length 
    return o
}

/**
 * Match string value by expression
 * @param {string} str to match against expression
 * @param {RegExp} expression valid expression /xyz/
 * @returns {boolean}
 * 
 * @example 
 * matched('aabc', /^abc/)) // false
 * matched('aaBC', /abc/i) // true
 */
const matched = (str = '', expression = /\\/) => {

    let o = false
    if (!isString(str)) return o

    if (!isRegExp(expression)) return o
    if (typeof str !== 'string') return o
    if (!str) return o

    let regx = (patt, s) => {
        try {
            return new RegExp(patt).test(s)
        } catch (err) {
            /* istanbul ignore next */ 
            onerror('[matched]', err.toString())
            return false
        }
    }
    o = regx(expression, str)
    return o
}

/**
 * Compare match array items with the id, if any were found return true
 * @param {any} id single item represented in mach array
 * @param {Array<number>} matchArr array of item type that we can match by id
 * @returns {boolean} when id was found in matchArr
 * 
 * @example 
 * includes(1, [2,'0',false,1]) // true
 * includes('5', [2,'5',false,1]) // true
 */
const includes = (id, matchArr) => {
    if (!isArray(matchArr)) return false
    return matchArr.filter((n) => id === n).length > 0
}

/** 
 * Unsubscribe from an RX/subscription, by providing an array of active subs
 * - invalids are silently dismissed and disposed
 * - source input is finally spliced
 * @param {Array<any>?} subscriptions Array of RX subscriptions
 * @param {string?} message optional message displayed when item is unsubscribed
 * @returns {number} index count of items unsubscribed
 * 
 * @example 
 * unsubscribe([sub1,sub2,sub3],'on component destroyed') // 3
 * unsubscribe([sub1,'',sub3],'unsubscribed') // 2, but the empty item is also disposed from array
 */
const unsubscribe = (subscriptions, message) => {
    if (!(subscriptions || []).length) return 0
    let inx = 0

    subscriptions.forEach((sub, i) => {

        try {
            /* istanbul ignore next */
            if (sub.unsubscribe !== undefined) {
                sub.unsubscribe()
                inx++
            }
          
        } catch (err) {
            // ups
        }
        
    })

    // splice all array items
    subscriptions.splice(0, subscriptions.length)
   
    /* istanbul ignore next */
    if (inx) log(`unsub ${message ? 'from:' + message : ''} index: ${inx}`)
    return inx
}

/**
 * @ignore
 */
class XReferenceError extends ReferenceError {
    constructor(namee, msg, fName, lNumber, colNumber) {
        // @ts-ignore
        super(msg, fName, lNumber)
        // due to native support, if no available add it!     
        // override existing reference
        if (stringSize(namee) > 0) this.name = namee
        if (typeof fName === 'string' && !this.fileName) this.fileName = fName
        if (typeof lNumber === 'number' && this.lineNumber === undefined) this.lineNumber = lNumber
        if (typeof colNumber === 'number' && this.columnNumber === undefined) this.columnNumber = colNumber
    }
}

/**
 * @description Extended ReferenceError with extra props, native behaviour describe on `(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError`
 * - method extends {ReferenceError}
 * 
 * @param {Object} opts
 * @param {string?} opts.name (optional) defaults to ReferenceError, provide your own
 * @param {string?} opts.message (optional)
 * @param {string?} opts.fileName (optional)
 * @param {number?} opts.lineNumber (optional)
 * @param {number?} opts.columnNumber (optional)
 * @returns {XReferenceError} extended new ReferenceError(...)
 *
 * @example 
 * try {
 *   throw referenceError({name:'MyReferenceError',message:'my message',fileName:'example.js' lineNumber:1})
 * } catch(err){
 *   log(e instanceof ReferenceError)  // true
 *   log(e.name)                       // "MyReferenceError"
 *   log(e.message)                    // "my message"
 *   log(e.fileName)                   // "example.js"
 *   log(e.lineNumber)                 // 1
 *   log(e.stack)                      // "@Scratchpad/2:2:9\n"
 * }
 * 
 */
const referenceError = (opts = { name: 'ReferenceError', message: undefined, fileName: undefined, lineNumber: undefined, columnNumber: undefined }) => new XReferenceError(opts.name, opts.message, opts.fileName, opts.lineNumber, opts.columnNumber)

/**
 * @ignore
 */
class XError extends Error {
    constructor(name, id, message, fileName, lineNumber) {
        // @ts-ignore
        super(message, fileName, lineNumber)
        this.id = undefined
        this.name = undefined
        if (typeof fileName === 'string' && this.fileName === undefined) this.fileName = fileName
        if (typeof lineNumber === 'number' && this.lineNumber === undefined) this.lineNumber = lineNumber
        if (stringSize(name)) this.name = name
        else this.name = 'XError'
        
        if (isString(id) || isNumber(id)) this.id = id.toString()
    }
}

/**
 * 
 * @description Extended Error(...) with extra `{id,name}` used to throw exception. Access to available props as describe on `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error`
 * - method extends {Error}
 *
 * @param {Object} opts
 * @param {string?} opts.name (optional) defaults to XError, provide your own 
 * @param {string?} opts.id (optional) assign error id 
 * @param {string?} opts.message (optional) reason for the error 
 * @param {string?} opts.fileName (optional)
 * @param {string?} opts.lineNumber (optional)
 * @returns {XError} extended new Error(...)
 *
 * @example
 *    try {
 *       throw xError({ id:123,name: 'MyError', message: 'my message', fileName: 'example.js', lineNumber: 20 })
 *   } catch (e) {
 *       console.log(e instanceof Error)   // true
 *       console.log(e.id)                         // "123"
 *       console.log(e.name)                       // "MyError"
 *       console.log(e.message)                    // "my message"      
 *       console.log(e.fileName)                   // "example.js"
 *       console.log(e.lineNumber)                 // 20
 *       console.log(e.stack)                      // "@Scratchpad/2:2:9\n"
 *   }
 *
 */
const xError = (opts = { name: 'XError', id: undefined, message: undefined, fileName: undefined, lineNumber: undefined }) => new XError(opts.name, opts.id, opts.message, opts.fileName, opts.lineNumber)

/**
 * Check item is of Error object family 
 * @param {any} el 
 * @returns {true|false}
 * 
 * @example
 * isError(Error()) // true
 * isError(new Error()) // true
 * isError(true) // false
 * isError(xError()) // true
 * isError(referenceError()) // true
 */
const isError = (el) => {
    let generic = () => Error.prototype === (el || '').__proto__
    try {
        if (generic()) return true
        return (el instanceof Error) || (el instanceof XReferenceError)
    } catch (err) {
        return false
    }
}

/**
 * Extended require version, does not modify global require()  
 * THIS METHOD ONLY WORK FOR COMMON.JS modules, and not for browser
 * - Does not throw when second argument `ref=ERR_NO_THROW` provided
 * - _( Does not provide Intellisense unfortunately )_
 * @param {string} path require(>path<)
 * @param {string?} dir must provide `__dirname` when executing NONE npm packages, so it can correctly map file paths
 * @param {string?} ref // ERR_NO_THROW and it wont throw an error
 * @returns {any} module.require output or undefined
 * 
 * @example 
 * xrequire('your_npm_package') // your npm package
 * xrequire('./path/to/module', __dirname) // your module script
 * xrequire('./blah/not/found', __dirname, 'ERR_NO_THROW') // returns undefined
 * xrequire('./blah/not/found', '', 'ERR_NO_THROW') // returns undefined
 */
function xrequire(path = '', dir = '', ref) {

    /* istanbul ignore next */

    if (isWindow()) return undefined

    const Mod = function () { }

    Mod.prototype = Object.create(module.constructor.prototype)
    Mod.prototype.constructor = module.constructor

    Mod.prototype.require = function (_path, ref) {
        const self = this
        try {
            // check if loading module
            let loadingNPMmod = _path.indexOf('./') !== 0 && _path.indexOf('.') !== 0
            let amendedPath = _path
            /* istanbul ignore next */
            if (!dir && ref === 'ERR_NO_THROW' && !loadingNPMmod) return undefined
            /* istanbul ignore next */
            if (!dir && ref !== 'ERR_NO_THROW' && !loadingNPMmod) {
                // @ts-ignore
                throw referenceError({ name: 'xrequire', message: 'xrequire needs your __dirname to correctly map the path to require script' })
               
            }

            if (!loadingNPMmod) {

                // gets location of script execution
                let nicePath = dir.replace(/\\/g, '/')
                let combine = () => {
                    if (_path.indexOf('..') === -1 &&
                        _path.indexOf('./') !== -1) {
                        _path = _path.replace('./', '')
                    }

                    amendedPath = nicePath + '/' + _path
                }

                combine()

            }

            // @ts-ignore
            return self.constructor._load(amendedPath, self)
        } catch (err) {
            
            /* istanbul ignore next */
            if (err.name === 'xrequire') throw err.message

            // NOTE magic if the ref has match instead of throw we return undefined
            /* istanbul ignore next */
            if (ref === 'ERR_NO_THROW') return undefined
            // if module not found, we have nothing to do, simply throw it back.
            /* istanbul ignore next */
            if (err.code === 'MODULE_NOT_FOUND') {
                throw err.stack
            }
        }
    }

    /* istanbul ignore next */
    if (!(Mod.prototype instanceof module.constructor)) return undefined

    // @ts-ignore
    else return Mod.prototype.require(path, ref)
}

/**
 * No operation function
 * @returns {void}
 */
const noop = () => {}

/**
 * Trim boths sides of string, including new lines, and multiple spaces to single space
 * @param {string} str
 * @returns {string}
 * 
 * @example 
 * trim(`  \n hello  
 * \n
 * \r 
 *
 * \n
 * \r
 * world  
 * 
 * `) //> hello world
 */
const trim = (str) => {
    if (typeof str !== 'string') return ''
    return str.replace(/(\r\n?|\n|\t)/g, '').trim().replace(/\s\s +/g, ' ')
}

/**
 * @description Spread data of an object as you would ...data, but with selected prop names that match the object 
 * 
 * @param {object} data must be an object
 * @param {Array<string>} props prop list matching first level props on an object
 * @returns {object} 
 * 
 * @example 
 * spread({a:1,b:2,c:{}},['a','c']) // {a:1,c:{}}
 * spread({a:1,b:2,c:3},[]) // {}
 */
const spread = (data, props) => {
    if (!isObject(data)) return {}
    if (!props || !(props || []).length) return {}

    return Object.entries(data).reduce((n, [key, el], inx) => {
        if (props.filter(x => x === key).length) n[key] = el
        return n
    }, {})
}

/**
 * @description Spread only selected array items matching index number
 * @param {Array<any>} arr 
 * @param {Array<number>} indexArr index number matching array
 * @returns {Array<any>}
 * 
 * @example 
 * spreadWith(['a','b','c'],[1,2]) // ['b','c']
 * spreadWith(['a','b','c'],[2,4]) // ['c']
 */
const spreadWith = (arr, indexArr) => {
    if (!(arr || []).length) return []
    if (!(indexArr || []).length) return []

    let list = []
    for (let inx = 0; inx < indexArr.length; inx++) {
        let i = indexArr[inx]
        if (typeof i === 'number') {
            if (arr[i] !== undefined) list.push(arr[i])
        }
    }
    return list
}

/**
 * @deprecated Use spread() method instead
 * @alias spread
 * @param {*} data 
 * @param {*} props 
 * @returns {object} 
 */
const objectIterateWith = (data, props) => spread(data, props)

export { disableLogging }
export { resetLogging }
export { loggerSetting }
export { checkLoggerSetting }
export { stack }
export { errorTrace }
export { loop }
export { isFalse }
export { isTrue }
export { isBoolean }
export { isNull }
export { isUndefined }
export { isEmpty }
export { head }
export { last }
export { timer }
export { interval }
export { sq }
export { validID }
export { isBigInt }
export { isNumber }
export { stringSize }
export { cancelPromise }
export { shuffle }
export { selectiveArray }
export { hasPrototype }
export { hasProto }
export { objectSize }
export { isString }
export { isFunction }
export { copyBy }
export { copyDeep }
export { delay }
export { someKeyMatch }
export { exactKeyMatch }
export { trueVal }
export { trueValDeep }
export { trueProp }
export { resolver }
export { flatten }
export { flattenDeep }
export { chunks }
export { dupes }
export { uniqBy }
export { arrayWith }
export { exFromArray }
export { copy }
export { uniq }
export { isPromise }
export { isQPromise }
export { debug }
export { log }
export { warn }
export { onerror }
export { error }
export { alert }
export { attention }
export { isObject }
export { isFalsy }
export { isError }
export { typeCheck }
export { validDate }
export { isInstance }
export { isClass }
export { isArray }
export { arraySize }
export { pickFromArray }
export { dispatcher }
export { isSQ }
export { withHoc }
export { isDate }
export { xrequire }
export { asJson }
export { truthFul }
export { inIndex }
export { isRegExp }
export { matched }
export { referenceError }
export { xError }
export { noop }
export { trim }
export { includes }
export { unsubscribe }
export { objectIterateWith }
export { spread }
export { spreadWith }