Source: Utils.js

const fs        = require('fs-extra');
const path      = require('path');
const jsonfile  = require('jsonfile');

/**
 * Set of utility functions.
 * */
class Utils {

    /**
    * isDirectory - description
    *
    * @param  {type} pathToCheck description
    * @return {type}             description
    */
    static isDirectory(pathToCheck) {
        if (fs.existsSync(pathToCheck)) {
            return fs.lstatSync(pathToCheck).isDirectory();
        } else {
            return false;
        }
    }

    static count(elements, condition) {
        var count = 0;
        for (var i in elements) {
            var element = elements[i];
            if (eval(condition) == true) {
                count++;
            }
        }
        return count;
    }

    static sum(elements, field) {
        var sum = 0;
        for (var i in elements) {
            var element = this.parseFloat(elements[i]);
            sum = sum + element[field];
        }
        return sum;
    }

    static values(els, field) {
        var out = [];
        for (var i=0; i<els.length; i++) {
            out.push(els[i][field]);
        }
        return out;
    }

    static writeJSON(filename, contents) {
        fs.writeJSONSync(filename, contents, {spaces: 4});
    }

    /**
     * CALLED FROM:
     * - {@link Session#addApp}
     *
     * @param  {type} sourceDir description
     * @param  {type} destDir description
     * @return {type}           description
     */
    static copyFiles(sourceDir, destDir) {
        fs.ensureDirSync(destDir);
        try {
          fs.copySync(sourceDir, destDir);
        } catch (err) {
          console.error(err);
        }
    }

    /**
     * CALLED FROM:
     * - {@link Session#addApp}
     *
     * @param  {type} sourceDir description
     * @param  {type} destDir description
     * @return {type}           description
     */
    static copyFile(fn, sourceDir, destDir) {
        fs.ensureDirSync(destDir);
        try {
          fs.copySync(path.join(sourceDir, fn), path.join(destDir, fn));
        } catch (err) {
          console.error(err);
        }
    }

    // http://www.davekoelle.com/files/alphanum.js
    // Sort alphanumerically in place.
    static alphanumSort(ids) {
        let caseInsensitive = true;
        for (var z = 0, t; t = ids[z]; z++) {
            ids[z] = new Array();
            var x = 0, y = -1, n = 0, i, j;
        
            while (i = (j = t.charAt(x++)).charCodeAt(0)) {
            var m = (i == 46 || (i >=48 && i <= 57));
            if (m !== n) {
                ids[z][++y] = "";
                n = m;
            }
            ids[z][y] += j;
            }
        }
        
        ids.sort(function(a, b) {
            for (var x = 0, aa, bb; (aa = a[x]) && (bb = b[x]); x++) {
            if (caseInsensitive) {
                aa = aa.toLowerCase();
                bb = bb.toLowerCase();
            }
            if (aa !== bb) {
                var c = Number(aa), d = Number(bb);
                if (c == aa && d == bb) {
                return c - d;
                } else return (aa > bb) ? 1 : -1;
            }
            }
            return a.length - b.length;
        });
        
        for (var z = 0; z < ids.length; z++)
            ids[z] = ids[z].join("");
    }        

    static shells(list) {
        var out = [];
        for (var p in list) {
            out.push(list[p].shell());
        }
        return out;
    }

    static readJS(file) {
        // Empty white space to force conversion to UTF-8.
        return fs.readFileSync(file) + '';
    }

    static readTextFile(file) {
        // Empty white space to force conversion to UTF-8.
        return fs.readFileSync(file) + '';
    }

    /**
    *
    * @param  {type} file description
    * @return {type}      description
    */
    static readJSON(file) {
        try {
            return jsonfile.readFileSync(file);
        } catch(err) {
            return 'JSON error';
        }
    }

    static getHeaders(fields, skipFields, headers) {
        for (var f in fields) {
            var prop = fields[f];
            if (!skipFields.includes(prop) && !headers.includes(prop)) {
                headers.push(prop);
            }
        }
    }

    // Returns groups of size 'groupSize'. Last group may not be full.
    static getRandomGroups(objects, groupSize) {
        var groups = [];
        var numGroups = Math.ceil(objects.length / groupSize);
        var keys = Object.keys(objects);
        for (var i=0; i<numGroups; i++) {
            var group = [];
            for (var j=0; j<groupSize; j++) {
                if (keys.length < 1) {
                    break;
                }
                var ind = Utils.randomInt(0, keys.length);
                var obj = objects[keys[ind]];
                keys.splice(ind, 1);
                group.push(obj);
            }
            groups.push(group);
        }
        return groups;
    }

    /**
    * Is the given object a function?
    *
    * @param  {type} obj The object to check.
    * @return {boolean}     true if the object is a function, false otherwise.
    */
    static isFunction(obj) {
        return typeof obj == 'function';
    }

    /**
    * This function generates random integer between two numbers low (inclusive) and high (exclusive) ([low, high))<br>
    * Reference: <a target='_blank' href='https://blog.tompawlak.org/generate-random-values-nodejs-javascript'>https://blog.tompawlak.org/generate-random-values-nodejs-javascript</a>
    * @param  {type} low  the lower bound.
    * @param  {type} high the upper bound.
    * @return {number}      A random number from [low, high).
    */
    static randomInt(low, high) {
        return Math.floor(Math.random() * (high - low) + low);
    }

    /**
     * randomEl - description
     *
     * @param  {type} array description
     * @return {type}       description
     */
    static randomEl(array) {
        return Utils.randomEls(array, 1)[0];
    }

    // https://stackoverflow.com/questions/9960908/permutations-in-javascript
    static permutations(inputArr) {
      let result = [];
      const permute = function(arr, m, result) {
        if (arr.length === 0) {
          result.push(m);
        } else {
          for (let i = 0; i < arr.length; i++) {
            let curr = arr.slice();
            let next = curr.splice(i, 1);
            permute(curr.slice(), m.concat(next), result);
         }
       }
     }
     permute(inputArr, [], result);
     return result;
    }

    static drawRandomly(data, options) {
        let out = [];
        let elementsLeft = [];
        for (let i in data) {
            elementsLeft.push(data[i]);
        }
        for (let i=0; i<options.numDraws; i++) {
            if (options.withReplacement) {
                out.push(this.randomEl(data));
            } else {
                // let el = this.ran
                // TODO.
            }
        }
        return out;
    }

    static shuffle(array) {

        // https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
        let counter = array.length;

        // While there are elements in the array
        while (counter > 0) {
            // Pick a random index
            let index = this.randomInt(0, counter);

            // Decrease counter by 1
            counter--;

            // And swap the last element with it
            let temp = array[counter];
            array[counter] = array[index];
            array[index] = temp;
        }

        return array;
    }

    /**
    * Random draws without replacement
    */
    static randomEls(array, num) {
        var out = [];
        var indices = [];
        for (var i=0; i<array.length; i++) {
            indices.push(i);
        }
        for (var i=0; i<num; i++) {
            var ind = Utils.randomInt(0, indices.length);
            out.push(array[indices[ind]]);
            indices.splice(ind, 1);
            if (indices.length === 0) {
                break;
            }
        }
        return out;
    }

    /**
    *  sum - description
    *
    * @param  {type} items description
    * @param  {type} prop  description
    * @return {type}       description
    */
    static sum(items, prop) {
        return items.reduce( function(a, b){
            return a + b[prop];
        }, 0);
    };

    //

    /**
    * Returns date in YYYYMMDD-HHmmss-SSS
    * Reference: https://stackoverflow.com/questions/42862729/convert-date-object-in-dd-mm-yyyy-hhmmss-format
    *
    * @return {type}  The formatted date.
    */
    static getDate() {
        var date = this.getDateObject();
        return date.year + date.formattedMonth + date.formattedDay + "-" +  date.formattedHour + date.formattedMinute + date.formattedSecond + '-' + date.formattedMS;
    }

    static diffDates(d1, d2) {
        let d1Obj = this.getDateObj(d1).getTime();
        let d2Obj = this.getDateObj(d2).getTime();
        let diff = d2Obj - d1Obj;
        return diff;
    }

    static getDateObj(dateString) {
        let year = dateString.substring(0, 4) - 0;
        let month = dateString.substring(4, 6) - 0;
        let day = dateString.substring(6, 8) - 0;
        let hour = dateString.substring(9, 11) - 0;
        let minute = dateString.substring(11, 13) - 0;
        let sec = dateString.substring(13, 15) - 0;
        let millisecond = dateString.substring(16) - 0;
        return new Date(
            year,
            month,
            day,
            hour,
            minute,
            sec,
            millisecond
        );
    }

    static dateFromStr(str) {
        str = 
            str.substring(0, 4) + '-' + 
            str.substring(4, 6) + '-' + 
            str.substring(6, 8) + 'T' + 
            str.substring(9, 11) + ':' + 
            str.substring(11, 13) + ':' + 
            str.substring(13, 15) + '.' + 
            str.substring(16, 19);
        return new Date(str);
    }

    static min(array, field) {
        var out = null;
        for (var i=0; i<array.length; i++) {
            if (out === null || out > array[i][field]) {
                out = array[i][field];
            }
        }
        return out;
    }

    static getDateObject() {
        var out = {};
        var date = new Date();
        out.year = date.getFullYear();
        out.month = (date.getMonth() + 1).toString();
        out.formattedMonth = (out.month.length === 1) ? ("0" + out.month) : out.month;
        out.day = date.getDate().toString();
        out.formattedDay = (out.day.length === 1) ? ("0" + out.day) : out.day;
        out.hour = date.getHours().toString();
        out.formattedHour = (out.hour.length === 1) ? ("0" + out.hour) : out.hour;
        out.minute = date.getMinutes().toString();
        out.formattedMinute = (out.minute.length === 1) ? ("0" + out.minute) : out.minute;
        out.second = date.getSeconds().toString();
        out.formattedSecond = (out.second.length === 1) ? ("0" + out.second) : out.second;
        out.ms = date.getMilliseconds().toString();
        out.formattedMS = out.ms;
        if (out.ms.length === 1) {
            out.formattedMS = '00' + out.ms;
        } else if (out.ms.length === 2) {
            out.formattedMS = '0' + out.ms;
        }
        return out;
    }

    static getStageContents(app, stage) {
        var fn = path.join(app.session.jt.path, 'apps/' + app + '/' + stage + '.html')
        var html = fs.readFileSync(fn, 'utf8');
        return html;
    }

    static outputFields(self) {
        var fields = [];
        for (var prop in self) {
            if (
                !isFunction(self[prop]) &&
                !self.outputHide.includes(prop) &&
                !self.outputHideAuto.includes(prop)
            )
            fields.push(prop);
        }
        return fields;
    }

    // http://stackoverflow.com/questions/7364150/find-object-by-id-in-an-array-of-javascript-objects
    static findById(array, id) {
        if (array === null || array === undefined) {
            return null;
        }
        var out = $.grep(array, function(e) {
            return e !== undefined && e.id === id;
        });
        if (out.length > 0) {
            return out[0];
        }
        else {
            return null;
        }
    }

    // Find by ID without JQuery ($)
    static findByIdWOJQ(array, id) {
        for (let i in array) {
            if (array[i] !== undefined && array[i].id === id) {
                return array[i];
            }
        }
        return null;
    }

    // http://stackoverflow.com/questions/5767325/how-to-remove-a-particular-element-from-an-array-in-javascript
    static deleteById(array, id) {
        for (var i = array.length-1; i>=0; i--) {
            if(array[i].id === id) {
                array.splice(i, 1);
            }
        }
    }

    static objLength(obj) {
        return Object.keys(obj).length;
    }

    static createFile(fn) {
        fs.closeSync(fs.openSync(fn, 'w'));
    }

    // Recursively parse data fields as float numbers.
    static parseFloatRec(data, parsedObjs) {
        if (parsedObjs == null) {
            parsedObjs = [];
        }
        try {
            if (Utils.isNumeric(data)) {
                data = parseFloat(data, 10);
            } else if (typeof data === 'object') {
                parsedObjs.push(data);
                for (var i in data) {
                    if (!parsedObjs.includes(data[i])) {
                        data[i] = Utils.parseFloatRec(data[i], parsedObjs);
                    }
                }
            }
            return data;
        } catch (err) {
            return data;
        }
    }

    static decomposeId(id) {
        var out = {};
        var sesI = id.indexOf('session_');
        var sesInd = sesI + 'session_'.length;
        var appI = id.indexOf('_app_');
        if (appI === -1) {
            out.sessionId = id.substring(sesInd);
        } else {
            out.sessionId = id.substring(sesInd, appI);
            var appInd = appI + '_app_'.length;
            var prdI = id.indexOf('_period_');
            if (prdI === -1) {
                out.appId = id.substring(appInd);
            } else {
                out.appId = id.substring(appInd, prdI);
                var prdInd = prdI + '_period_'.length;
                var grpI = id.indexOf('_group_');
                if (grpI === -1) {
                    out.periodId = parseInt(id.substring(prdInd));
                } else {
                    out.periodId = parseInt(id.substring(prdInd, grpI));
                    var grpInd = grpI + '_group_'.length;
                    var plyI = id.indexOf('_player_');
                    if (plyI === -1) {
                        out.groupId = parseInt(id.substring(grpInd));
                    } else {
                        out.groupId = parseInt(id.substring(grpInd, plyI));
                        var plyInd = plyI + '_player_'.length;
                        out.playerId = id.substring(plyInd);
                    }
                }
            }
        }
        return out;
    }

    /**
     * loadContext - loads an object based on an id.
     * TODO: Add check for participant
     *
     * @param  {type} session description
     * @param  {type} id      description
     * @return {type}         description
     */
    static loadContext(session, id) {
        var ids = Utils.decomposeId(id);
        var out = null;
        if (ids.appId === null || ids.appId === undefined) {
            return session;
        } else {
            if (ids.periodId === null || ids.periodId === undefined) {
                return Utils.findByIdWOJQ(session.apps, ids.appId);
            } else {
                var app = Utils.findByIdWOJQ(session.apps, ids.appId);
                if (ids.groupId === null || ids.groupId === undefined) {
                    return Utils.findByIdWOJQ(app.periods, ids.periodId);
                } else {
                    var period = Utils.findByIdWOJQ(app.periods, ids.periodId);
                    if (ids.playerId === null || ids.playerId === undefined) {
                        return Utils.findByIdWOJQ(period.groups, ids.groupId);
                    } else {
                        var group = Utils.findByIdWOJQ(period.groups, ids.groupId);
                        return Utils.findByIdWOJQ(group.players, ids.playerId);
                    }
                }
            }
        }
    }

    static isNumeric(n) {
        return !isNaN(parseFloat(n)) && isFinite(n);
    }
}

module.exports = Utils;