/*
 * Software License Agreement (MIT License)
 *
 * Author: Duke Fong <[email protected]>
 */

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function read_file(file) {
    return await new Promise((resolve, reject) => {
        let reader = new FileReader();

        reader.onload = () => {
            resolve(new Uint8Array(reader.result));
        };
        reader.onerror = reject;
        reader.readAsArrayBuffer(file);
    })
}

async function load_img(img, url) {
    let ret = -1;
    await new Promise(resolve => {
        img.src = url;
        img.onload = () => { ret = 0; resolve(); };
        img.onerror = () => { console.error(`load_img: ${url}`); resolve(); };
    });
    return ret;
}

function date2num() {
    let d = (new Date()).toLocaleString('en-GB');
    let s = d.split(/[^0-9]/);
    return `${s[2]}${s[1]}${s[0]}${s[4]}${s[5]}${s[6]}`;
}

function timestamp() {
    let date = new Date();
    let time = date.toLocaleString('en-GB');
    return time.split(' ')[1] + '.' + String(date.getMilliseconds()).padStart(3, '0');
}

async function sha256(dat) {
    const hashBuffer = await crypto.subtle.digest('SHA-256', dat);
    return new Uint8Array(hashBuffer);
}

async function aes256(dat, key, type='encrypt') {
    let iv = new Uint8Array(16); // zeros
    let _key = await crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['encrypt', 'decrypt']);

    if (type == 'encrypt')
        return new Uint8Array(await crypto.subtle.encrypt({name: 'AES-CBC', iv: iv}, _key, dat));
    else
        return new Uint8Array(await crypto.subtle.decrypt({name: 'AES-CBC', iv: iv}, _key, dat));
}

function dat2hex(dat, join='', le=false) {
    let dat_array = Array.from(dat);
    if (le)
        dat_array = dat_array.reverse();
    return dat_array.map(b => b.toString(16).padStart(2, '0')).join(join);
}

function hex2dat(hex, le=false) {
    hex = hex.replace('0x', '').replace(/\s/g,'')
    let ret = new Uint8Array(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
    if (le)
        return ret.reverse();
    return ret;
}

function dat2str(dat) {
    return new TextDecoder().decode(dat);
}

function str2dat(str) {
    let encoder = new TextEncoder();
    return encoder.encode(str);
}

function val2hex(val, fixed=4, prefix=false, upper=false, float=false) {
    let sign = Math.sign(val);
    val = Math.abs(val);
    let str = upper ? val.toString(16).toUpperCase() : val.toString(16);
    let arr = str.split('.');
    if (arr[0].length < fixed)
        arr[0] = '0'.repeat(fixed - arr[0].length) + arr[0];
    if (prefix)
        arr[0] = '0x' + arr[0];
    if (sign == -1)
        arr[0] = '-' + arr[0];
    if (float && arr.length == 1)
        arr.push('0');
    return arr.join('.');
}

// list: ['x', 'y']
// map: {'rotation': 'r'}
function cpy(dst, src, list, map = {}) {
    for (let i of list) {
        if (i in src)
            dst[i] = src[i];
    }
    for (let i in map) {
        if (i in src)
            dst[map[i]] = src[i];
    }
}

class Queue {
    constructor() {
        this.fifo = [];
        this.wakeup = null;
    }
    
    put(t) {
        this.fifo.push(t);
        if (this.wakeup)
            this.wakeup();
    }
    
    async get(timeout=null) {
        if (this.fifo.length)
            return this.fifo.shift();
        if (timeout == 0)
            return null;
        
        let p = new Promise(resolve => { this.wakeup = resolve; });
        let t;
        if (timeout)
            t = setTimeout(() => { this.wakeup(); }, timeout, null); // unit: ms
        
        await p;
        
        this.wakeup = null;
        if (timeout)
            clearTimeout(t);
        if (this.fifo.length)
            return this.fifo.shift();
        return null;
    }
    
    // now some utilities:
    size() {
        return this.fifo.length;
    }
    flush() {
        this.fifo = [];
        if (this.wakeup)
            this.wakeup();
        this.wakeup = null;
    }
}

function download_url(data, fileName) {
    var a;
    a = document.createElement('a');
    a.href = data;
    a.download = fileName;
    document.body.appendChild(a);
    a.style = 'display: none';
    a.click();
    a.remove();
};

function download(data, fileName='dat.bin', mimeType='application/octet-stream') {
    var blob, url;
    blob = new Blob([data], {type: mimeType});
    url = window.URL.createObjectURL(blob);
    download_url(url, fileName);
    setTimeout(function() { return window.URL.revokeObjectURL(url); }, 1000);
};

function escape_html(unsafe) {
    return unsafe
         .replace(/&/g, "&amp;")
         .replace(/</g, "&lt;")
         .replace(/>/g, "&gt;")
         .replace(/"/g, "&quot;")
         .replace(/'/g, "&#039;");
}

function readable_size(bytes, fixed=3, si=true) {
    var thresh = si ? 1000 : 1024;
    if(Math.abs(bytes) < thresh) {
        return bytes + ' B';
    }
    var units = si
        ? ['kB','MB','GB','TB','PB','EB','ZB','YB']
        : ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
    var u = -1;
    do {
        bytes /= thresh;
        ++u;
    } while(Math.abs(bytes) >= thresh && u < units.length - 1);
    return bytes.toFixed(fixed)+' '+units[u];
}

function readable_float(num, double=false) {
    if (!isFinite(num))
        return num.toString();
    let fixed = 12;
    if (!double)
        num = parseFloat(num.toPrecision(7)); // for 32-bit float
    let n = num.toFixed(fixed);
    if (n.indexOf('e') != -1)
        return n;
    for (let i = 0; i < fixed / 3; i++) {
        if (n.endsWith('000'))
            n = n.slice(0, n.length - 3);
        else
            break;
    }
    if (n.endsWith('.'))
        n += '0';
    return n;
}

async function blob2dat(blob) {
    let ret;
    await new Promise(resolve => {
        new Response(blob).arrayBuffer().then(buf => {
            ret = new Uint8Array(buf);
            resolve();
        });
    });
    return ret;
}

export {
    sleep, read_file, load_img, date2num, timestamp,
    sha256, aes256,
    dat2hex, hex2dat, dat2str, str2dat, val2hex,
    cpy, Queue,
    download,
    escape_html, readable_size, readable_float,
    blob2dat
};