utils.js

const rand = require('random-number-csprng')

/** @namespace utils */

/**
 * Convert a hex string to a byte array
 * @memberof utils
 * @param {string} hex 
 * @returns {number[]} bytes
 */
function hexToBytes(hex) {
    if(hex.substr(0,2)=='0x') hex = hex.substr(2)
    if(hex.length % 2 == 1) hex = '0'+ hex
    for (var bytes = [], c = 0; c < hex.length; c += 2)
    bytes.push(parseInt(hex.substr(c, 2), 16));
    return bytes
}

/**
 * Convert a byte array to a hex string
 * @memberof utils
 * @param {number[]} bytes 
 * @returns {string} hex
 */
function bytesToHex(bytes) {
    for (var hex = [], i = 0; i < bytes.length; i++) {
        hex.push((bytes[i] >>> 4).toString(16));
        hex.push((bytes[i] & 0xF).toString(16));
    }
    hex = hex.join("")
    if(hex.length % 2 == 1) hex = '0'+ hex
    return hex
}

// unint <~> hex
var uint8hex  = u => u.reduce((p,c)=>p+c.toString(16).padStart(2,'0'),'')
var uint16hex = u => u.reduce((p,c)=>p+c.toString(16).padStart(4,'0'),'')
var uint32hex = u => u.reduce((p,c)=>p+c.toString(16).padStart(8,'0'),'')
var uintN2hex = (n,u) => u.reduce((p,c)=>p+c.toString(16).padStart(n/8*2,'0'),'')

function hex2uintN(n,hex){
    if(n % 8 > 0 && n < 54) return -1 // muliple of 8 bits & less than Number.MAX_SAFE_INTEGER
    if(hex.length % 2 == 1) hex = '0' + hex
    blen = hex.length * 8 / 2
    if(blen % n > 0) hex = hex + 'xx'.repeat((n-blen%n)/8)
    regexp = new RegExp('[a-fA-Z0-9x]{'+2*n/8+'}','g')
    uintN = Array.from(hex.matchAll(regexp),m => parseInt(m[0].replace(/x/gi),16))
    return uintN
}

/**
 * Reverses hexadecimal string
 * @memberof utils
 * @param {string} hex - Hexadecimal string
 * @returns {string} Reversed hexadecimal string
 */
function reverseHex(hex) {
    if(hex.substr(0,2)=='0x') hex = hex.substr(2)
    if(hex.length % 2 == 1) hex = '0'+ hex 
    for (var reverse = '', i=0; i < hex.length; i += 2){
        // console.log(hex.length-2-i)
        reverse += hex.substr(hex.length-2-i,2)
    }
    return reverse
}

/**
 * Converts hex string to binary bits
 * @param {string} hex 
 * @returns {string} bits
 */
function hex2bits(hex) {
    if(hex.substr(0,2)=='0x') hex = hex.substr(2)
    if(hex.length % 2 == 1) hex = '0'+ hex
    for (var bits = [], c = 0; c < hex.length; c += 2){
        bits.push(parseInt(hex.substr(c, 2), 16).toString(2).padStart(8,'0'));
    }
    return bits.join('')
}
/**
 * Converts binary bits to hex string
 * @param {string} bits 
 * @returns {string} hex
 */
function bits2hex(bits) {
    buf = ''
    hex = ''
    while(bits.length > 0){
        buf = bits.substr(0,8)
        hex += parseInt(buf,2).toString(16).padStart(2,'0')
        bits = bits.substr(8)
        buf = ''
    }
    return hex
}

/**
 * Converts a bits string to an array of N-bit unsigned integers
 * @param   {number} n      Number of bits
 * @param   {string} bits   Bits string
 * @returns {number[]}   Array of N-bit unsigned integers
 */
function bits2uintN(n,bits) {
    uintN = []
    while(bits.length > 0){
        uintN.push(parseInt(bits.substr(0,n),2))
        bits = bits.substr(n)
    }
    return uintN
}

/**
 * Converts array of N-bit unsigned integers to bits string
 * @param {number} n Number of bits per number
 * @param {number[]} u Array of N-bit unsigned integers 
 * @returns {string} bits
 */
var uintN2bits = (n,u) => u.reduce((p,c)=>p+c.toString(2).padStart(n,'0'),'')

/**
 * Encodes Hex into Base32
 * @param {string} hex 
 * @returns {string} Base32 encoded string
 */
function hex2b32(hex){
    iambase32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
    bits = hex2bits(hex)
    b32 = []
    while(bits.length > 0){
        i = parseInt(bits.substr(0,5).padEnd(5,'0'),2)
        b32.push(iambase32.substr(i,1))
        bits = bits.substr(5)
    }
    pad = ''
    if(b32.length % 8 > 0) pad = '='.repeat(8 - b32.length % 8)
    return b32.join('') + pad
}

/**
 * Dencodes Base32 string
 * @param {string} b32 Base32 encoded string
 * @returns {string} Hex
 */
function b32hex(b32){
    iambase32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
    b32 = b32.replace(/=/gi,'')
    pad = (b32.length * 5) % 8
    bits = b32.split('').map(c => iambase32.search(c).toString(2).padStart(5,'0')).join('')
    if(pad > 0) bits = bits.substr(0, bits.length - pad)
    return bits2hex(bits)
}

/**
 * Converts array of bytes into array of 11-bit numbers
 * @param {number[]} bytes Array of bytes
 * @returns {number[]} Array of 11-bit numbers
 */
function bytes2b11(bytes){
    bits = ''
    b11 = []
    for (let i=bytes.length-1; i >= 0; i--)
        bits += bytes[i].toString(2).padStart(8,0)
    while (bits != ''){
        b11.push(parseInt(bits.substr(-11),2))
        bits = bits.substr(0,bits.length-11)
    }
    return b11
}

/**
 * Generates random bytes array in hexadecimal
 * @param {number} size Number of bytes
 * @returns {string} Hex
 */
var randomHex = size => randomArray(size).then(a => uint8hex(Uint8Array.from(a)))

/**
 * Generates random bytes array using a CSPRNG module
 * @param {number} size Number of bytes
 * @returns {Promise<number[]>} Array of bytes
 */
function randomArray(size){
    var a = []
    for (var i = 0; i < size; i++) {
        a.push(rand(0,255))
    }
    return Promise.all(a)
}

/**
 * Generates array with numbers from 0 [+ offset] to N [+ offset]
 * @param {number} n     Size of array
 * @param {number} [o=0] Offset
 * @returns {Array} Array with N numbers
 */
const range = (n,o=0) => Array.from(new Uint8Array(n).map((e,i)=>i+o))

/**
 * Split string every N position
 * @param {string} s String to split
 * @param {number} n 
 * @returns {}
 */
const splitter = (s,n) => Array.from(new Uint8Array(Math.ceil(s.length/n)).map( (e,i) => i*n ) ).map(i => s.substr(i,n))

/**
 * Chains functions together and passes return 
 * value to the next function as an argument
 * @param  {...any} fns List of functions
 * @returns {any}
 * @example
 * f1 = x => x**2
 * f2 = y => y-1
 * c1 = z => compose(f1,f2)(z)
 * c1(5) // returns 24
 * c1(4) // returns 15
 */
const compose = (...fns) => arg => fns.reduce((composed, f) => f(composed), arg)

module.exports = {
    hexToBytes, bytesToHex,
    uint8hex, uint16hex, uint32hex, uintN2hex,
    hex2uintN, reverseHex, hex2bits, bits2hex, 
    bits2uintN, uintN2bits, hex2b32, b32hex, 
    bytes2b11, randomHex, randomArray, range,
    splitter, compose
}