Source: infernalUtils.js


module.exports = {
    equals: equals,
    parseParameters: parseFactPaths,
    getContext: getContext,
    compilePath: compilePath,
    typeof: _typeof
};

//https://regex101.com/r/zYhguP/6/
const paramRegex = /(?:\/\*?@ *([\w/.]+?) *\*\/ *\w+,?)|[(,]? *(\w+) *[,)]?/g;
const singleParamLambdaRegex = /^((?:\/\*@[^*]*\*\/\s*)?[\w$]+)\s*=>/;
const trailingTermRemovalRegex = /[^/]+?$/;
const pathCompactionRegex = /\/[^/.]+\/\.\./;
const currentPathRemoval = /\.\//g;

/**
 * Compare two values and return true if they are equal. Fix date comparison issue and insure
 * both types are the same.
 * 
 * @param {*} a The first value used to compare.
 * @param {*} b The second value used to caompare.
 */
function equals(a, b) {
    var aValue = a;
    if (a instanceof Date) {
        aValue = a.getTime(); 
    }
    var bValue = b;
    if (b instanceof Date) {
        bValue = b.getTime();
    }
    return (aValue === bValue);
}


function _typeof(o) {
    if (o instanceof Date) {
        return "date";
    }
    else if (o instanceof Array) {
        return "array";
    }
    else {
        return typeof o;
    }
}

/**
 * Generator that parses all fact paths derived from the given rule parameters. Either return the
 * parameter name or the path contained by the optional attribute comment. Supports regular
 * functions as well as lambda (arrow) functions, with or without parenthesis around a single
 * parameter.
 * 
 * @example
 * 
 * Using an attribute comment:
 * 
 *     async function(/¤@ /path/to/fact ¤/ param1, param2) {}
 *     async (/¤@ /path/to/fact ¤/ param1, param2) => {}
 *     async /¤@ /path/to/fact ¤/ param1 => {}
 * 
 * In the above example: 
 * 
 *   1) ¤ replaces * because using * and / one after the other ends the documentation comment.
 *   2) returned fact paths would be ['/path/to/fact', 'param2'] (or ['/path/to/fact'] for the
 *      single parameter lambda).
 * 
 * @param {AsyncFunction} rule The rule 
 */
function* parseFactPaths(rule) {
    let allParams = extractParameterList(rule.toString());
    if (!allParams) return;

    paramRegex.lastIndex = 0;
    let paramMatch = paramRegex.exec(allParams);
    while (paramMatch) {
        let param = paramMatch[1] || paramMatch[2];
        if (param) yield param;
        paramMatch = paramRegex.exec(allParams);
    }
}

/**
 * Extracts the raw parameter list (without the surrounding parenthesis) from the source code of
 * a function. Returns an empty string when the function has no parameter.
 * 
 * @param {String} code The function source code.
 */
function extractParameterList(code) {
    let header = code.replace(/^\s*async\b\s*/, "");

    // Lambda with a single parameter not enclosed in parenthesis: 'x => ...' or
    // '/*@ /path */ x => ...'
    let singleParamMatch = singleParamLambdaRegex.exec(header);
    if (singleParamMatch) {
        return singleParamMatch[1].replace(/\s+/g, " ");
    }

    // Regular function, method or lambda with parenthesis: take what is inside the first pair.
    let start = header.indexOf("(");
    if (start < 0) return "";
    let end = header.indexOf(")", start);
    if (end < 0) return "";
    return header.substring(start + 1, end).replace(/\s+/g, " ").trim();
}


/**
 * Extracts the context from the fact or rule name. Must be a compiled path, not a path that
 * contains "../" or "./".
 * 
 * @example
 * 
 * Given the rule path: '/path/to/some/rule'
 * The returned value would be: '/path/to/some'
 * 
 * @param {String} compiledPath The compiled path to extract the context from.
 */
function getContext(compiledPath) {
    return compiledPath.replace(trailingTermRemovalRegex, "");
}

/**
 * Create a compacted fact name from a given fact name. Path do not end with a trailing '/'.
 * 
 *  '../' Denote the parent term. Move up one term in the context or path.
 *  './'  Denote the current term. Removed from the context or path.
 * 
 * Examples: 
 * 
 *  '/a/given/../correct/./path' is compiled to '/a/correct/path'
 * 
 * @param {String} path The path or context to compile into a context.
 */
function compilePath(path) {
    let current = path;
    let next = path.replace(pathCompactionRegex, "");
    while (next != current) {
        current = next;
        next = current.replace(pathCompactionRegex, "");
    }
    if (current.startsWith("/..")) {
        throw new Error(`Unable to compile the path '${path}' properly.`);
    }
    return current.replace(currentPathRemoval, "");
}