"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.testFailChance = exports.getPrefixInt = exports.stringForKeys = exports.jsonHttpResWithSize = exports.logNode = exports.isValidShardusAddress = exports.formatErrorMessage = exports.selectIndexesWithOffeset = exports.getIndexesPicked = exports.pickIndexBasedOnHash = exports.fastIsPicked = exports.humanFileSize = exports.compareObjectShape = exports.generateArraySchema = exports.generateObjectSchema = exports.sumObject = exports.errorToStringFull = exports.validateTypes = exports.selectNeighbors = exports.debugExpand = exports.short = exports.makeShortHash = exports.getClosestHash = exports.XOR = exports.propComparator2 = exports.propComparator = exports.lerp = exports.mod = exports.deepCopy = exports.appdata_replacer = exports.isValidIPv4 = void 0;
const nestedCounters_1 = require("../nestedCounters");
const lib_types_1 = require("@shardeum-foundation/lib-types");
/**
 * Checks if the input string is a valid IPv4 address
 */
const isValidIPv4 = (ip) => {
    // eslint-disable-next-line security/detect-unsafe-regex
    const ipv4Regex = new RegExp('^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$');
    return ipv4Regex.test(ip);
};
exports.isValidIPv4 = isValidIPv4;
/**
 * this helper replacer is lossy and only for logging
 * @param _key
 * @param value
 * @returns
 */
const appdata_replacer = (_key, value) => {
    const originalObject = value;
    if (originalObject instanceof Map) {
        return {
            dataType: 'stringifyReduce_map_2_array',
            value: Array.from(originalObject.entries()),
        };
    }
    else if (typeof originalObject === 'bigint') {
        // Convert BigInt to string
        return originalObject.toString();
    }
    else if (originalObject instanceof Uint8Array) {
        //this is lossy but usefull for logs
        const buffer = Buffer.from(originalObject);
        return buffer.toString('hex');
    }
    else {
        return value;
    }
};
exports.appdata_replacer = appdata_replacer;
const deepCopy = (obj) => {
    if (typeof obj !== 'object') {
        throw Error('Given element is not of type object.');
    }
    return lib_types_1.Utils.safeJsonParse(lib_types_1.Utils.safeStringify(obj));
};
exports.deepCopy = deepCopy;
const mod = (n, m) => {
    return ((n % m) + m) % m;
};
exports.mod = mod;
/**
 * lerp from v0 to v1 by a
 * @param v0 the start value
 * @param v1 the end value
 * @param a the amount to lerp by (0-1) 0 being v0 and 1 being v1. 0.5 being halfway between v0 and v1
 * @returns
 */
const lerp = (v0, v1, a) => {
    return v0 * (1 - a) + v1 * a;
};
exports.lerp = lerp;
function propComparator(prop) {
    // eslint-disable-next-line security/detect-object-injection
    const comparator = (a, b) => (a[prop] > b[prop] ? 1 : a[prop] < b[prop] ? -1 : 0);
    return comparator;
}
exports.propComparator = propComparator;
function propComparator2(prop, prop2) {
    /* eslint-disable security/detect-object-injection */
    const comparator = (a, b) => a[prop] === b[prop] ? (a[prop2] === b[prop2] ? 0 : a[prop2] > b[prop2] ? 1 : -1) : a[prop] > b[prop] ? 1 : -1;
    /* eslint-enable security/detect-object-injection */
    return comparator;
}
exports.propComparator2 = propComparator2;
const XOR = (hexString1, hexString2) => {
    // tslint:disable-next-line: ban
    const num1 = parseInt(hexString1.substring(0, 8), 16);
    // tslint:disable-next-line: ban
    const num2 = parseInt(hexString2.substring(0, 8), 16);
    return (num1 ^ num2) >>> 0;
};
exports.XOR = XOR;
const getClosestHash = (targetHash, hashes) => {
    let closest = null;
    let closestDist = 0;
    for (const hash of hashes) {
        const dist = (0, exports.XOR)(targetHash, hash);
        if (dist === closestDist) {
            console.error(new Error(`Two hashes came out to the same distance from target hash!\n 1st hash: ${closest}\n 2nd hash: ${hash}\n Target hash: ${targetHash}`));
            return null;
        }
        if (dist > closestDist)
            closest = hash;
        closestDist = dist;
    }
    return closest;
};
exports.getClosestHash = getClosestHash;
const makeShortHash = (x, n = 4) => {
    if (!x) {
        return x;
    }
    if (x.length > 63) {
        if (x.length === 64) {
            return x.slice(0, n) + 'x' + x.slice(63 - n);
        }
        else if (x.length === 128) {
            return x.slice(0, n) + 'xx' + x.slice(127 - n);
        }
        else if (x.length === 192) {
            return x.slice(0, n) + 'xx' + x.slice(191 - n);
        }
    }
    return x;
};
exports.makeShortHash = makeShortHash;
/**
 * short
 * grab the first n (default=4) hex bytes of a string (4 bytes == 8 char hex string)
 * @param x
 * @param n
 */
const short = (x, n = 4) => {
    if (!x) {
        return x;
    }
    return x.slice(0, n * 2);
};
exports.short = short;
const debugExpand = (value) => {
    const res = value.slice(0, 4) + '0'.repeat(55) + value.slice(5, 5 + 5);
    return res;
};
exports.debugExpand = debugExpand;
const selectNeighbors = (array, ourIndex, neighborsOnEachSide) => {
    const length = array.length;
    const neighbors = [];
    if (length === 0)
        return neighbors;
    if (length <= neighborsOnEachSide * 2)
        return array.slice();
    try {
        for (let i = 1; i <= neighborsOnEachSide; i++) {
            const leftIndex = (ourIndex - i + length) % length;
            const rightIndex = (ourIndex + i) % length;
            if (leftIndex !== ourIndex) {
                neighbors.push(array[leftIndex]);
            }
            if (rightIndex !== ourIndex && rightIndex !== leftIndex) {
                neighbors.push(array[rightIndex]);
            }
        }
    }
    catch (e) {
        console.error(`Error selecting neighbors nodes: ${e.message}`);
    }
    return neighbors;
};
exports.selectNeighbors = selectNeighbors;
/*
inp is the input object to be checked
def is an object defining the expected input
{name1:type1, name1:type2, ...}
name is the name of the field
type is a string with the first letter of 'string', 'number', 'Bigint', 'boolean', 'array' or 'object'
type can end with '?' to indicate that the field is optional and not required
---
Example of def:
{fullname:'s', age:'s?',phone:'sn'}
---
Returns a string with the first error encountered or and empty string ''.
Errors are: "[name] is required" or "[name] must be, [type]"
 */
function validateTypes(inp, def) {
    if (inp === undefined)
        return 'input is undefined';
    if (inp === null)
        return 'input is null';
    if (typeof inp !== 'object')
        return 'input must be object, not ' + typeof inp;
    const map = {
        string: 's',
        number: 'n',
        boolean: 'b',
        bigint: 'B',
        array: 'a',
        object: 'o',
    };
    const imap = {
        s: 'string',
        n: 'number',
        b: 'boolean',
        B: 'bigint',
        a: 'array',
        o: 'object',
    };
    const fields = Object.keys(def);
    for (const name of fields) {
        /* eslint-disable security/detect-object-injection */
        const types = def[name];
        const opt = types.substr(-1, 1) === '?' ? 1 : 0;
        if (inp[name] === undefined && !opt)
            return name + ' is required';
        if (inp[name] !== undefined) {
            if (inp[name] === null && !opt)
                return name + ' cannot be null';
            let found = 0;
            let be = '';
            for (let t = 0; t < types.length - opt; t++) {
                let it = map[typeof inp[name]];
                it = Array.isArray(inp[name]) ? 'a' : it;
                const is = types.substr(t, 1);
                if (it === is) {
                    found = 1;
                    break;
                }
                else
                    be += ', ' + imap[is];
            }
            if (!found)
                return name + ' must be' + be;
        }
        /* eslint-enable security/detect-object-injection */
    }
    return '';
}
exports.validateTypes = validateTypes;
function errorToStringFull(error) {
    return `${error.name}: ${error.message} at ${error.stack}`;
}
exports.errorToStringFull = errorToStringFull;
function sumObject(sumObject, toAddObject) {
    for (const [key, val] of Object.entries(sumObject)) {
        // eslint-disable-next-line security/detect-object-injection
        const otherVal = toAddObject[key];
        if (otherVal == null) {
            continue;
        }
        switch (typeof val) {
            case 'number':
                // eslint-disable-next-line security/detect-object-injection
                sumObject[key] = val + otherVal;
                break;
            default:
                break;
        }
    }
}
exports.sumObject = sumObject;
// @param {LiteralObject} `obj` object to be genrate schema for
// @return {LiteralObject} will return schema object
// This function generate a schema (object) of the object it has been fed
function generateObjectSchema(obj, options = { arrTypeDiversity: false }) {
    const schema = {};
    if (Array.isArray(obj)) {
        throw new Error('Object schema generation function does not accept array as argument');
    }
    for (const [key, value] of Object.entries(obj)) {
        /* eslint-disable security/detect-object-injection */
        if (Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== null) {
            if ((key === 'devPublicKeys' || key === 'multisigKeys') && isDevPublicKeysValid(schema[key])) {
                schema[key] = '{ [publicKey: string]: DevSecurityLevel }';
            }
            else if (value.constructor === Object) {
                schema[key] = generateObjectSchema(value, { arrTypeDiversity: options.arrTypeDiversity });
            }
            else if (Array.isArray(value)) {
                schema[key] = generateArraySchema(value, { diversity: options.arrTypeDiversity });
            }
            else {
                schema[key] = typeof value;
            }
        }
        /* eslint-enable security/detect-object-injection */
    }
    return schema;
}
exports.generateObjectSchema = generateObjectSchema;
//Validate devPublicKeys object
function isDevPublicKeysValid(devPublicKeys) {
    for (const key in devPublicKeys) {
        // Check if the value associated with the key is of type DevSecurityLevel (number)
        // eslint-disable-next-line security/detect-object-injection
        if (typeof devPublicKeys[key] !== 'number') {
            return false; // If any value is not of the expected type, return false
        }
    }
    return true; // If all values are of the expected type, return true
}
// @param {Array} arr, the array to generate schema
// @return {String} will return schema array, Example: `string[]`
// This function generate a schema (array) of the array it has been fed
// SYMBOLS
// [1, 2, 4]                    -> 'number[]'
// ['john','doe']               -> 'string[]'
// [(str)=>str, console.log()]  -> 'function[]'
// [{id: 1}, {id: 2}]           -> '{}[]'
// [new Date(), new Date()]     -> 'object[]'
// [[1,3,2],[1,3,4]]            -> 'array[]'
// [new Date(), 'false', 1]     -> 'any[]' if options.diversity set true
function generateArraySchema(arr, options = { diversity: false }) {
    let schema;
    for (let i = 0; i < arr.length; i++) {
        // let's return 'any' when array is holding multiple types
        /* eslint-disable security/detect-object-injection */
        if (i > 0 && arr[i].constructor !== arr[i - 1].constructor) {
            if (options.diversity) {
                return 'any[]';
            }
            else {
                throw new Error('Array schema generation does not allowed type diversities in an array unless specified');
            }
        }
        // declare conditions for readability
        const IS_MULTI_DIMENSIONAL = Array.isArray(arr[i]);
        if (arr[i].constructor === Object) {
            schema = '{}[]';
        }
        else if (IS_MULTI_DIMENSIONAL) {
            schema = 'array[]';
        }
        else {
            schema = `${typeof arr[i]}[]`;
        }
        /* eslint-enable security/detect-object-injection */
    }
    return schema;
}
exports.generateArraySchema = generateArraySchema;
// @param {LiteralObject} idol, This is the object the function will hold standard to
// @param {LiteralObject} admirer, This is the object the function compare against standard object idol
// Note: these positional parameter matter at which position the object is passed to
// This first parameter idol object will be idolized
// and the function will determine if the second parameter (admirer) object fit the idolized object schema
// Note idol object does not accept type diversive array like this { arr: ['doe', 1, false] } will throw errors.
function compareObjectShape(idol, admirer) {
    let isValid;
    let error = undefined;
    const defectoChain = [];
    let idol_schema;
    try {
        idol_schema = generateObjectSchema(idol, { arrTypeDiversity: false });
    }
    catch (e) {
        throw new Error('Type varies array detected inside idol object');
    }
    const admirer_schema = generateObjectSchema(admirer, { arrTypeDiversity: true });
    if (lib_types_1.Utils.safeStringify(idol_schema) === lib_types_1.Utils.safeStringify(admirer_schema)) {
        isValid = true;
        return { isValid, error };
    }
    // this function compare prop types
    // this function is not meant to be call outside of this block
    const smartComparator = (idol_type, admirer_type) => {
        if (typeof idol_type === 'object' && idol_type.constructor === Object) {
            return lib_types_1.Utils.safeStringify(idol_type) === lib_types_1.Utils.safeStringify(admirer_type);
        }
        else {
            return idol_type === admirer_type;
        }
    };
    // this function is not meant to be call outside of this block
    // worshipped represent idolized schema
    // worshipper represent admirer's schema
    const defectoHunter = (worshipped, worshipper) => {
        const l1 = Object.keys(worshipped).length;
        const l2 = Object.keys(worshipper).length;
        //this variable represent whichever object that has the most properpties(key)
        const bigger_obj = l1 >= l2 ? worshipped : worshipper;
        for (const key in bigger_obj) {
            /* eslint-disable security/detect-object-injection */
            const DEFECTOR_FOUND = smartComparator(worshipped[key], worshipper[key]) === false;
            if (DEFECTOR_FOUND) {
                // save the path to the prop , Example: ['server', 'log']
                defectoChain.push(key);
                if (Object.prototype.hasOwnProperty.call(worshipped, key) && worshipped[key].constructor === Object) {
                    return defectoHunter(worshipped[key], worshipper[key]);
                }
                else {
                    return { [key]: worshipper[key] };
                }
            }
            /* eslint-enable security/detect-object-injection */
        }
    };
    error = {
        defectiveProp: defectoHunter(idol_schema, admirer_schema),
        defectiveChain: defectoChain,
    };
    isValid = false;
    return { isValid, error };
}
exports.compareObjectShape = compareObjectShape;
// adapted from stack overflow post
function humanFileSize(size) {
    const i = Math.max(size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)), 4);
    const value = Number(size / Math.pow(1024, i)).toFixed(2);
    // eslint-disable-next-line security/detect-object-injection
    return value + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
}
exports.humanFileSize = humanFileSize;
function fastIsPicked(ourIndex, groupSize, numToPick, offset = 0) {
    let isPicked = false;
    const fstride = groupSize / numToPick;
    const finalOffset = ourIndex + offset;
    let steps = finalOffset / fstride;
    steps = Math.round(steps);
    const fendPoint = steps * fstride;
    const endpoint = Math.round(fendPoint);
    if (endpoint === finalOffset) {
        isPicked = true;
    }
    return isPicked;
}
exports.fastIsPicked = fastIsPicked;
/**
 * Returns a random index based on a hash and group size.
 * This is useful for picking a random index from a group of nodes based on txId or similar,
 * to ensure all nodes in the group pick the same index.
 * @param groupSize
 * @param hash
 */
function pickIndexBasedOnHash(groupSize, hash) {
    if (!hash.startsWith('0x')) {
        hash = '0x' + hash;
    }
    const hashAsBigInt = BigInt(hash);
    const offset = Number(hashAsBigInt % BigInt(groupSize));
    return (groupSize - offset) % groupSize;
}
exports.pickIndexBasedOnHash = pickIndexBasedOnHash;
//Write a function that uses fastIsPicked to return an arrray of all the indexes that are picked
function getIndexesPicked(groupSize, numToPick, offset = 0) {
    const indexesPicked = [];
    for (let i = 0; i < groupSize; i++) {
        if (fastIsPicked(i, groupSize, numToPick, offset)) {
            indexesPicked.push(i);
        }
    }
    return indexesPicked;
}
exports.getIndexesPicked = getIndexesPicked;
//Selects a specific number of unique indexes from an array, starting at an offset and using a dynamic stride for spacing
function selectIndexesWithOffeset(arraySize, numberToPick, offset) {
    let currentIndex = (0, exports.mod)(offset, arraySize);
    const strideAmount = Math.max(1, (offset + 1337) % Math.ceil(arraySize / numberToPick));
    const selectedIndexes = new Set();
    while (selectedIndexes.size < numberToPick) {
        currentIndex += strideAmount;
        if (currentIndex >= arraySize) {
            currentIndex -= arraySize;
        }
        //linear probing if this index is already in the list.  This prevents being stuck forever
        //..assuming the input parameters are valid
        while (selectedIndexes.has(currentIndex)) {
            currentIndex++;
            if (currentIndex >= arraySize) {
                currentIndex = 0;
            }
        }
        selectedIndexes.add(currentIndex);
    }
    return Array.from(selectedIndexes);
}
exports.selectIndexesWithOffeset = selectIndexesWithOffeset;
/**
 * Try to print a variety of possible erros for debug purposes
 * @param err
 * @param printStack
 * @returns
 */
function formatErrorMessage(err, printStack = true) {
    let errMsg = 'An error occurred';
    if (typeof err === 'string') {
        errMsg = err;
    }
    else if (err instanceof Error) {
        errMsg = err.message;
        if (printStack && err.stack) {
            errMsg += ` \nStack trace:\n${err.stack}`;
        }
    }
    else if (typeof err === 'object' && err !== null) {
        //chat gpt reccomended this fancy part but the linter doesn't like it
        // const keys = Object.keys(err)
        // if (keys.length > 0) {
        //   errMsg = 'Error properties:\n'
        //   const errObj = err as object
        //   for (const key of keys) {
        //     errMsg += `${key}: ${errObj[key]}\n`
        //   }
        // } else {
        errMsg = `Unknown error: ${lib_types_1.Utils.safeStringify(err)}`;
        // }
    }
    else {
        errMsg = `Unknown error: ${err}`;
    }
    return errMsg;
}
exports.formatErrorMessage = formatErrorMessage;
/**
 * checks if a hex-string is a valid shardus address by
 * checking if it's 64 chars long & 32-bytes in size
 */
function isValidShardusAddress(hexStrings) {
    for (let i = 0; i < hexStrings.length; i++) {
        // eslint-disable-next-line security/detect-object-injection
        if (!(hexStrings[i].length === 64) || !(Buffer.from(hexStrings[i], 'hex').length === 32))
            return false;
    }
    return true;
}
exports.isValidShardusAddress = isValidShardusAddress;
function logNode(node) {
    return `Node ID : ${node.id} externalPort : ${node.externalPort} externalIP : ${node.externalIp}`;
}
exports.logNode = logNode;
/**
 * Equivalent to .json() but gets the size for us
 * @param res
 * @param obj
 * @returns
 */
function jsonHttpResWithSize(res, obj) {
    const str = lib_types_1.Utils.safeStringify(obj);
    res.setHeader('Content-Length', str.length);
    res.setHeader('Content-Type', 'application/json');
    res.write(str);
    res.end();
    return str.length;
}
exports.jsonHttpResWithSize = jsonHttpResWithSize;
/**
 * Returns a string for the given object, using the given keys, usually for logging and debugging purposes.
 * Keys can be null or undefined, in which case all keys are used.
 * Keys can be a single string of comma- or space-separated keys.
 * Keys can be an array of keys.
 * If the object is an array, the keys are applied to each element of the array.
 * Do not pass in data with circular references.
 * Does not throw exceptions which would be bad for logging, but instead returns a string with the exception info.
 * Examples:
 * stringForKeys(node, 'publicKey ip host')
 * stringForKeys(nodelist, 'publicKey ip host')
 */
function stringForKeys(obj, keys = null) {
    if (obj === undefined)
        return 'undefined';
    if (obj === null)
        return 'null';
    try {
        if (Array.isArray(obj))
            return `[${obj.map((item) => stringForKeys(item, keys)).join(', ')}]`;
        // at this point, obj is really an object
        if (keys == null)
            keys = Object.keys(obj);
        else if (typeof keys == 'string')
            keys = keys.split(/[ ,]+/);
        // justification for the suppression below: the keys are not originated by user input, but developer source code
        const items = Array.from(keys)
            .map((key) => (obj[key] === undefined ? 'undefined' : lib_types_1.Utils.safeStringify(obj[key]))) // eslint-disable-line security/detect-object-injection
            .join(', ');
        return `{${items}}`;
    }
    catch (e) {
        // throwing an exception would be bad for logging/debugging, so we just return a string
        let objStr;
        try {
            objStr = lib_types_1.Utils.safeStringify(obj);
        }
        catch (e) {
            objStr = '(stringForKeys(): exception for Utils.safeStringify())';
        }
        return `(stringForKeys(): exception: ${e}, obj: ${objStr})`;
    }
}
exports.stringForKeys = stringForKeys;
function getPrefixInt(hexAddress, length = 8) {
    // if (hexAddress.length !== 64) {
    //   throw new Error("Input hex address should be exactly 64 characters long.");
    // }
    if (length < 1 || length > 8) {
        throw new Error('Length parameter should be between 1 and 8.');
    }
    const prefixHex = hexAddress.slice(0, length);
    const prefixInt = parseInt(prefixHex, 16);
    if (isNaN(prefixInt)) {
        throw new Error('Invalid hex characters in the input.');
    }
    return prefixInt;
}
exports.getPrefixInt = getPrefixInt;
/**
 * test once at the given probability to fail.  If it fails, log the message and return true.  If it doesnt fail, return false.
 * @param failChance
 * @param debugName
 * @param key
 * @param message
 * @param verboseRequired
 * @returns
 */
function testFailChance(failChance, debugName, key, message, verboseRequired) {
    if (failChance == null) {
        return false;
    }
    const rand = Math.random();
    if (failChance > rand) {
        if (debugName != null) {
            // if (verboseRequired === false || logFlags.verbose) {
            //   this.logger.playbackLogNote(`dbg_fail_${debugName}`, key, message)
            // }
            nestedCounters_1.nestedCountersInstance.countEvent('dbg_fail_', debugName ?? 'unknown');
        }
        return true;
    }
    return false;
}
exports.testFailChance = testFailChance;
//# sourceMappingURL=general.js.map