"use strict";
var task_transforms_1 = require("./task.transforms");
var _ = require('../lodash.custom');
var debug = require('debug')('cb:task.resolve');
var task_errors_1 = require("./task.errors");
var task_utils_1 = require("./task.utils");
var adaptors = require("./adaptors");
var task_preprocess_1 = require("./task.preprocess");
var task_tree_transforms_1 = require("./task.tree.transforms");
(function (TaskTypes) {
    TaskTypes[TaskTypes["ExternalTask"] = "ExternalTask"] = "ExternalTask";
    TaskTypes[TaskTypes["Adaptor"] = "Adaptor"] = "Adaptor";
    TaskTypes[TaskTypes["TaskGroup"] = "TaskGroup"] = "TaskGroup";
    TaskTypes[TaskTypes["InlineFunction"] = "InlineFunction"] = "InlineFunction";
})(exports.TaskTypes || (exports.TaskTypes = {}));
var TaskTypes = exports.TaskTypes;
(function (TaskOriginTypes) {
    TaskOriginTypes[TaskOriginTypes["CrossbowConfig"] = "CrossbowConfig"] = "CrossbowConfig";
    TaskOriginTypes[TaskOriginTypes["NpmScripts"] = "NpmScripts"] = "NpmScripts";
    TaskOriginTypes[TaskOriginTypes["FileSystem"] = "FileSystem"] = "FileSystem";
    TaskOriginTypes[TaskOriginTypes["Adaptor"] = "Adaptor"] = "Adaptor";
    TaskOriginTypes[TaskOriginTypes["InlineFunction"] = "InlineFunction"] = "InlineFunction";
    TaskOriginTypes[TaskOriginTypes["InlineArray"] = "InlineArray"] = "InlineArray";
    TaskOriginTypes[TaskOriginTypes["InlineObject"] = "InlineObject"] = "InlineObject";
})(exports.TaskOriginTypes || (exports.TaskOriginTypes = {}));
var TaskOriginTypes = exports.TaskOriginTypes;
(function (TaskRunModes) {
    TaskRunModes[TaskRunModes["series"] = "series"] = "series";
    TaskRunModes[TaskRunModes["parallel"] = "parallel"] = "parallel";
})(exports.TaskRunModes || (exports.TaskRunModes = {}));
var TaskRunModes = exports.TaskRunModes;
var defaultTask = {
    baseTaskName: '',
    valid: false,
    query: {},
    flags: {},
    subTasks: [],
    inlineFunctions: [],
    externalTasks: [],
    tasks: [],
    parents: [],
    errors: [],
    cbflags: [],
    description: '',
    rawInput: '',
    env: {},
    taskName: undefined,
    runMode: TaskRunModes.series,
    skipped: false,
    ifChanged: [],
    options: {}
};
/**
 * Entry point for resolving the task tree from any given point
 */
function createFlattenedTask(taskItem, parents, trigger) {
    /** DEBUG **/
    debug("resolving ('" + typeof taskItem + "') " + taskItem);
    /** DEBUG-END **/
    /**
     * We first 'preprocess' the task in order to
     * handle different types of task input. Supported:
     *  - string
     *  - function
     *  - object literal
     * @type {Task}
     */
    var incoming = task_preprocess_1.preprocessTask(taskItem, trigger, parents);
    /** DEBUG **/
    debug("preprocessed '" + taskItem + "'", incoming);
    /** DEBUG-END **/
    /**
     * We exit very quickly if the pre-process step has delivered
     * an 'adaptor' task - which means that's nothing left to determine.
     */
    if (incoming.type === TaskTypes.Adaptor) {
        return incoming;
    }
    /**
     * A 'toplevelValue' is when the current baseTaskName matches a key
     * in the given task definitions. This is classed an alias and we
     * may want to handle sub-task resolution differently depending
     * on when that value is.
     *
     * eg: If the user ran
     *   $ crossbow run js
     *
     * and there's task config like:
     *   js: ['task1', 'task2']
     *
     * -> Then the value is an array (2 items) that need resolving
     * themselves and setting as children.
     * @type {any}
     */
    var toplevelValue = getTopLevelValue(incoming, trigger);
    /**
     * There's a special case where the toplevel property of a task
     * maybe a group definition, like
     *
     * js:
     *  - tasks: ['one', 'two']
     *    description: 'Some description'
     *    runMode: 'parallel'
     *
     * In this case, we want to resolve 'one' and 'two' as children,
     * and not the object literal itself. A common use-case is when
     * you want to be able to provide a description for group-level tasks
     * but normally this is only possible on a per-task basis
     * @type {Task|Task}
     */
    incoming = (function () {
        if (task_utils_1.isPlainObject(toplevelValue) && toplevelValue.tasks) {
            return createTask(_.merge(incoming, toplevelValue));
        }
        return incoming;
    })();
    /**
     * Determine which sub tasks need converting as children.
     * Based on what was explained above, we may want to pass the
     * top level Value, or may want to use the tasks value directly
     * @type {Array}
     */
    var toConvert = (function () {
        if (incoming.tasks.length && incoming.origin === TaskOriginTypes.InlineArray) {
            return incoming.tasks;
        }
        if (incoming.tasks.length && incoming.origin === TaskOriginTypes.InlineObject) {
            return incoming.tasks;
        }
        if (toplevelValue == undefined)
            return [];
        if (task_utils_1.isPlainObject(toplevelValue) && toplevelValue.tasks) {
            return [].concat(toplevelValue.tasks);
        }
        if (typeof toplevelValue === 'function') {
            return [];
        }
        return [].concat(toplevelValue);
    })();
    /**
     * Add child tasks
     * @type {Array}
     */
    incoming.tasks = getTasks(toConvert, incoming, trigger, parents);
    /**
     * @type {CBFunction[]}
     */
    incoming.inlineFunctions = (function () {
        if (incoming.inlineFunctions.length)
            return incoming.inlineFunctions;
        if (incoming.tasks.length)
            return [];
        var toplevel = getTopLevelValue(incoming, trigger);
        if (typeof toplevel === 'function') {
            return [toplevel];
        }
        return [];
    })();
    /**
     * @type {ExternalTask[]}
     */
    incoming.externalTasks = (function () {
        if (incoming.tasks.length)
            return [];
        if (incoming.inlineFunctions.length)
            return [];
        return task_utils_1.locateModule(trigger.config, incoming.baseTaskName);
    })();
    debug("externalTasks: " + JSON.stringify(incoming.externalTasks[0]));
    /**
     * Set task types
     * @type {TaskTypes}
     */
    incoming.type = (function () {
        if (incoming.externalTasks.length) {
            return TaskTypes.ExternalTask;
        }
        if (incoming.inlineFunctions.length) {
            return TaskTypes.InlineFunction;
        }
        return TaskTypes.TaskGroup;
    })();
    debug("type: " + incoming.type);
    /**
     * @type {boolean}
     */
    incoming.valid = (function () {
        if (incoming.type === TaskTypes.TaskGroup)
            return true;
        if (incoming.type === TaskTypes.InlineFunction)
            return true;
        if (incoming.type === TaskTypes.ExternalTask)
            return true;
        return false;
    })();
    debug("valid: " + incoming.valid);
    /**
     * Now apply any transformations
     * @type {Task}
     */
    incoming = task_transforms_1.applyTransforms(incoming);
    /**
     * Collect errors
     * @type {TaskError[]}
     */
    incoming.errors = task_errors_1.gatherTaskErrors(incoming, trigger);
    debug("errors: " + incoming.errors);
    /**
     * Add parents array (for detecting circular references);
     * @type {string[]}
     */
    incoming.parents = parents;
    return incoming;
}
/**
 * Set child tasks
 * @type {Task[]}
 */
function getTasks(items, incoming, trigger, parents) {
    if (!items.length) {
        return [];
    }
    return items.reduce(function (acc, taskItem) {
        if (Array.isArray(taskItem)) {
            var out = task_preprocess_1.handleArrayInput(taskItem, trigger.input, parents);
            out.tasks = out.tasks.map(function (item) { return createFlattenedTask(item, parents.concat(incoming.baseTaskName), trigger); });
            return acc.concat(out);
        }
        if (parents.indexOf(taskItem) > -1) {
            return acc.concat(createCircularReferenceTask(incoming, parents));
        }
        var flattenedTask = createFlattenedTask(taskItem, parents.concat(incoming.baseTaskName), trigger);
        /**
         * if a user has
         *   tasks:
         *      js: 'sometask:op1:op2'
         *
         *  And calls
         *    $ cb js@p
         *
         *  The @p flag (parallel) need to be applied the resulting tasks
         *  that are created from the above call.
         *
         */
        if (flattenedTask.subTasks.length && !flattenedTask.cbflags.length) {
            flattenedTask.runMode = incoming.runMode;
        }
        return acc.concat(flattenedTask);
    }, []);
}
/**
 * Factory for creating a new Task Item
 * @param {object} obj
 * @returns {object}
 */
function createTask(obj) {
    return _.mergeWith({}, defaultTask, obj, function customizer(objValue, srcValue) {
        if (_.isArray(objValue)) {
            return objValue.concat(srcValue);
        }
    });
}
exports.createTask = createTask;
/**
 * When a circular reference is detected, exit with the appropriate error
 */
function createCircularReferenceTask(incoming, parents) {
    return _.merge({}, defaultTask, incoming, {
        errors: [{
                type: task_errors_1.TaskErrorTypes.CircularReference,
                incoming: incoming,
                parents: parents
            }]
    });
}
exports.createCircularReferenceTask = createCircularReferenceTask;
/**
 * Match a task name with a top-level value from 'tasks'
 */
function getTopLevelValue(incoming, trigger) {
    var exactMatch = trigger.input.tasks[incoming.baseTaskName];
    if (exactMatch !== undefined) {
        return exactMatch;
    }
    var maybes = Object.keys(trigger.input.tasks).filter(function (taskName) { return taskName.match(new RegExp("^" + incoming.baseTaskName + "($|@(.+?))")) !== null; });
    if (maybes.length) {
        return trigger.input.tasks[maybes[0]];
    }
    return undefined;
}
/**
 * Anything that begins @ is always an adaptor and will skip
 * file i/o etc.
 * @param taskName
 * @param parents
 * @returns {Task}
 */
function createAdaptorTask(taskName, parents) {
    taskName = task_utils_1.removeTrailingNewlines(taskName);
    /**
     * Strip the first part of the task name.
     *  eg: `@npm eslint`
     *   ->  eslint
     * @type {string}
     */
    var commandInput = taskName.replace(/^@(.+?) /, '');
    /**
     * Get a valid adaptors adaptor name
     * @type {string|undefined}
     */
    var validAdaptorName = Object.keys(adaptors).filter(function (x) {
        return taskName.match(new RegExp("^@" + x + " "));
    })[0];
    /**
     * If it was not valid, return a simple
     * task that will be invalid
     */
    if (!validAdaptorName) {
        return createTask({
            rawInput: taskName,
            taskName: taskName,
            type: TaskTypes.Adaptor,
            origin: TaskOriginTypes.Adaptor,
            adaptor: taskName.split(' ')[0],
            errors: [{
                    type: task_errors_1.TaskErrorTypes.AdaptorNotFound,
                    taskName: taskName
                }]
        });
    }
    return createTask({
        baseTaskName: taskName,
        valid: true,
        adaptor: validAdaptorName,
        taskName: taskName,
        rawInput: taskName,
        parents: parents,
        command: commandInput,
        runMode: TaskRunModes.series,
        origin: TaskOriginTypes.Adaptor,
        type: TaskTypes.Adaptor,
    });
}
exports.createAdaptorTask = createAdaptorTask;
/**
 * Look at a hash and determine if the incoming 'taskName'
 * could match a valid taskName.
 * eg:
 *  $ crossbow run shane
 *
 * -> matches:   'shane' & 'shane@p'
 */
function maybeTaskNames(tasks, taskName) {
    return Object.keys(tasks).reduce(function (all, key) {
        var match = key.match(new RegExp("^" + taskName + "@(.+)"));
        if (match) {
            return tasks[key];
        }
        return all;
    }, []);
}
exports.maybeTaskNames = maybeTaskNames;
/**
 * Attempt to match a task-name from within the various 'inputs'
 * given
 */
function pullTaskFromInput(taskName, input) {
    if (input.tasks[taskName] !== undefined) {
        return { items: [].concat(input.tasks[taskName]), origin: TaskOriginTypes.CrossbowConfig };
    }
    /**
     * Next, look at the top-level input,
     * is this taskname going to match, and if so, does it contain any flags?
     */
    var maybes = maybeTaskNames(input.tasks, taskName);
    if (maybes.length) {
        return { items: maybes, origin: TaskOriginTypes.CrossbowConfig };
    }
    return { items: [], origin: TaskOriginTypes.CrossbowConfig };
}
/**
 * A task is valid if every child eventually resolves to
 * having a module or has a adaptors helper
 */
function validateTask(task, trigger) {
    /**
     * Return early if a task has previously
     * been marked as invalid
     */
    if (task.valid === false) {
        return false;
    }
    /**
     * If the task has errors attached, it's always invalid
     */
    if (task.errors.length) {
        return false;
    }
    /**
     * If this task has subtasks in the `task.tasks` Array
     * return the result of validating each of those.
     */
    if (task.tasks.length) {
        return task.tasks.every(function (t) {
            return validateTask(t, trigger);
        });
    }
    /**
     * The final chance for a task to be deemed valid
     * is when `task.adaptors` is set to a string.
     *  eg: lint: '@npm eslint'
     *   -> true
     */
    if (typeof task.adaptor === 'string') {
        /**
         * task.adaptor is a string, but does it match any adaptors?
         * If it does, return the result of running the
         * validate method from the adaptor
         */
        if (adaptors[task.adaptor]) {
            return adaptors[task.adaptor].validate.apply(null, [task, trigger]);
        }
    }
    /**
     * If this task was set to InlineFunction, it's always valid
     * (as there's a function to call)
     */
    if (task.type === TaskTypes.InlineFunction) {
        return true;
    }
    /**
     * If the task has external modules associated
     * with it, it's always valid (as it has things to run)
     */
    if (task.type === TaskTypes.ExternalTask) {
        return true;
    }
    /**
     * In the case where `task.modules` has a length (meaning a JS file was found)
     * and there are NO sub tasks to validate, this means the current
     * task is ALWAYS valid so we can return true;
     */
    if (task.externalTasks.length && !task.tasks.length) {
        return true;
    }
}
function resolveTasks(taskCollection, trigger) {
    /**
     * Now begin making the nested task tree
     */
    var taskList = taskCollection
        .map(function (task) {
        return createFlattenedTask(task, [], trigger);
    });
    /**
     * Now apply any last-minute tree transformations
     */
    taskList = task_tree_transforms_1.applyTreeTransforms(taskList);
    /**
     * Return both valid & invalid tasks. We want to let consumers
     * handle errors/successes
     * @type {{valid: Array, invalid: Array}}
     */
    var output = {
        valid: taskList.filter(function (x) { return validateTask(x, trigger); }),
        invalid: taskList.filter(function (x) { return !validateTask(x, trigger); }),
        all: taskList
    };
    return output;
}
exports.resolveTasks = resolveTasks;
//# sourceMappingURL=task.resolve.js.map