"use strict";
var task_resolve_1 = require("./task.resolve");
var _ = require('../lodash.custom');
var Rx = require('rx');
var Observable = Rx.Observable;
var adaptors = require("./adaptors");
var Immutable = require("immutable");
var task_sequence_factories_1 = require("./task.sequence.factories");
var task_runner_1 = require("./task.runner");
var task_utils_1 = require("./task.utils");
function createFlattenedSequence(tasks, trigger) {
    return flatten(tasks, []);
    function flatten(items, initial, options, viaName) {
        return items.reduce(function (all, task) {
            /**
             * If the current task has child tasks, we build a tree of
             * nested observables for it (a task with children cannot itself
             * be a task that should be run)
             */
            if (task.type === task_resolve_1.TaskTypes.TaskGroup) {
                /**
                 * If we're looking at a group of tasks that was run
                 * with sub-tasks, we need to resolve differently to
                 * allow things such as parallel running to work as expected
                 */
                if (task.subTasks.length && task.tasks.length) {
                    /**
                     * Build the list of tasks/groups
                     * @type {Array}
                     */
                    var output = resolveGroupOfTasks(task);
                    /**
                     * Wrap as parallel group if this task has a runMode of 'parallel'
                     */
                    if (task.runMode === task_resolve_1.TaskRunModes.parallel) {
                        return all.concat(task_sequence_factories_1.createSequenceParallelGroup({
                            taskName: task.taskName,
                            items: output,
                            skipped: task.skipped
                        }));
                    }
                    /**
                     * Wrap as a series group if this task has a runMode of 'series'
                     */
                    if (task.runMode === task_resolve_1.TaskRunModes.series) {
                        return all.concat(task_sequence_factories_1.createSequenceSeriesGroup({
                            taskName: task.taskName,
                            items: output,
                            skipped: task.skipped
                        }));
                    }
                }
                /**
                 * If the current task was marked as `parallel`, all immediate children
                 * of (this task) will be run in `parallel`
                 */
                if (task.runMode === task_resolve_1.TaskRunModes.parallel) {
                    return all.concat(resolveGroup(task, task_sequence_factories_1.createSequenceParallelGroup));
                }
                /**
                 * If the current task was marked as `series`, all immediate child tasks
                 * will be queued and run in series - each waiting until the previous
                 * one has completed
                 */
                if (task.runMode === task_resolve_1.TaskRunModes.series) {
                    return all.concat(resolveGroup(task, task_sequence_factories_1.createSequenceSeriesGroup));
                }
            }
            /**
             * At this point, we must be dealing with a task that should be run,
             * so we first check if it's an adaptor @ task first
             */
            if (task.type === task_resolve_1.TaskTypes.Adaptor) {
                return all.concat(getSequenceItemWithOptions(task, trigger, adaptors[task.adaptor].create(task, trigger), {}));
            }
            /**
             * Finally, if the does not have children tasks & is not an
             * adaptor task it must have at least 1 associated module
             * (or an inline function) so we can begin working with it
             * by first resolving the top-level options object for it.
             */
            var localOptions = _.assign({}, loadTopLevelOptions(task, trigger), options);
            /**
             * Decide where the callable function is coming from
             * (inline function, external task etc)
             * @type {CBFunction}
             */
            var callable = (function () {
                if (task.type === task_resolve_1.TaskTypes.InlineFunction) {
                    return task.inlineFunctions[0];
                }
                return require(task.externalTasks[0].resolved);
            })();
            /**
             * Take the callable and create items with it + options
             */
            return all.concat(resolveFromFunction(task, callable, trigger, localOptions, viaName));
        }, initial);
    }
    /**
     * Resolve a group of tasks
     * @param task
     * @param groupCreatorFn
     * @param continueFn
     * @returns {any}
     */
    function resolveGroup(task, groupCreatorFn) {
        /**
         * If the group contains no subtasks
         */
        if (!task.subTasks.length) {
            /**
             * If a group has _default options,
             * but here no 'subTasks' were given, use the
             * default options always
             */
            var parentOptions = (function () {
                if (task.options._default !== undefined) {
                    return _.merge({}, task.options._default, task.query, task.flags);
                }
                return task.options;
            })();
            /**
             * Here the group had no direct 'sub tasks', so just return the item
             */
            return [groupCreatorFn({
                    taskName: task.taskName,
                    items: flatten(task.tasks, [], parentOptions),
                    skipped: task.skipped
                })];
        }
        /**
         * Use either subtasks directly, or if '*' was given, use
         * each key in the object to create a task
         */
        var lookupKeys = getLookupKeys(task.subTasks, task.options);
        /**
         * Now for each sub-task create a separate task item
         */
        return lookupKeys.map(function (subTaskName) {
            /**
             * When things like options, flags or query strings
             * were present on this task-group - pass them into the upcoming task instead
             * Order of presedence
             *   flags -> query -> options -> shared
             */
            var taskOptions = _.merge({}, task.options._default, _.get(task.options, subTaskName, {}), task.query, task.flags);
            return groupCreatorFn({
                taskName: task.taskName,
                items: flatten(task.tasks, [], taskOptions, task.taskName + ':' + subTaskName),
                skipped: task.skipped,
                subTaskName: subTaskName
            });
        });
    }
    function resolveGroupOfTasks(task) {
        var lookupKeys = getLookupKeys(task.subTasks, task.options);
        return lookupKeys.reduce(function (acc, subTaskName) {
            var opts = _.merge({}, task.options._default, _.get(task.options, subTaskName, {}), task.query, task.flags);
            return acc.concat(flatten(task.tasks, [], opts, task.taskName + ':' + subTaskName));
        }, []);
    }
}
exports.createFlattenedSequence = createFlattenedSequence;
function resolveFromFunction(task, callable, trigger, localOptions, viaName) {
    /**
     * If the current item has no sub-tasks, we can return early
     * with a simple task creation using the global options
     *
     * eg:
     *      $ crossbow run sass
     *
     * options:
     *      sass:
     *        input:  "core.scss"
     *        output: "core.css"
     *
     * -> `sass` task will be run with the options
     *    {input: "core.scss", output: "core.css"}
     */
    if (!task.subTasks.length) {
        return getSequenceItemWithOptions(task, trigger, callable, localOptions, viaName);
    }
    /**
     * Get lookup keys for this task
     */
    var lookupKeys = getLookupKeys(task.subTasks, localOptions);
    /**
     * Now generate 1 task per lookup key.
     */
    var group = lookupKeys.reduce(function (acc, optionKey) {
        /**
         * `optionKey` here will be a string that represented the subTask
         * name, so we use that to try and find a child key
         * in the options that matched it.
         * */
        var currentOptionObject = _.merge({}, localOptions._default, _.get(localOptions, optionKey));
        var sequenceItems = getSequenceItemWithOptions(task, trigger, callable, currentOptionObject, optionKey)
            .map(function (seqItem) {
            seqItem.subTaskName = optionKey;
            return seqItem;
        });
        return acc.concat(sequenceItems);
    }, []);
    /**
     * Don't create a 'group' if we're only talking about 1 item
     */
    if (group.length === 1) {
        return group;
    }
    if (task.runMode === task_resolve_1.TaskRunModes.parallel) {
        return [task_sequence_factories_1.createSequenceParallelGroup({
                taskName: task.taskName,
                items: group,
                skipped: task.skipped
            })];
    }
    /**
     * If the current task was marked as `series`, all immediate child tasks
     * will be queued and run in series - each waiting until the previous
     * one has completed
     */
    if (task.runMode === task_resolve_1.TaskRunModes.series) {
        return [task_sequence_factories_1.createSequenceSeriesGroup({
                taskName: task.taskName,
                items: group,
                skipped: task.skipped
            })];
    }
}
function getSequenceItemWithOptions(task, trigger, imported, options, viaName) {
    /**
     * Merge incoming options with query + flags
     * eg:
     *     $  sass?input=css/core.css --production
     *     -> sass
     *          input: css/core.css
     *          production: true
     */
    var mergedOptionsWithQuery = _.merge({}, options, task.options, task.query, task.flags);
    /**
     * If the module did not export a function, but has a 'tasks'
     * property that is an array, use each function from it
     * eg:
     *  module.exports.tasks = [sass, cssmin, version-rev]
     */
    if (imported.tasks && Array.isArray(imported.tasks)) {
        return imported.tasks.map(function (importedFn, i) {
            return task_sequence_factories_1.createSequenceTaskItem({
                fnName: getFunctionName(imported, i + 1),
                factory: importedFn,
                task: task,
                options: mergedOptionsWithQuery,
                viaName: viaName
            });
        });
    }
    /**
     * If the module exported a function, use that as the factory
     * and return a single task for it.
     * eg:
     *  module.exports = function runSass() {}
     */
    if (typeof imported === 'function') {
        return [task_sequence_factories_1.createSequenceTaskItem({
                fnName: getFunctionName(imported, 0),
                factory: imported,
                task: task,
                options: mergedOptionsWithQuery,
                viaName: viaName
            })];
    }
}
/**
 * For reporting purposes, try to 'name' a function
 */
function getFunctionName(fn, count) {
    if (count === void 0) { count = 0; }
    if (fn.name === undefined) {
        return "Anonymous Function " + count;
    }
    return fn.name;
}
/**
 *           ******************
 * Where the **--~~Magic~~--** happens!!!
 *           ******************
 *
 * Creating a task runner in crossbow is really about
 * wrapping the process of running the tasks in a way
 * that allows comprehensive logging/reporting
 *
 * Series & Parallel have different semantics and are
 * therefor handled separately.
 *
 * Note that everything here is completely lazy and
 * nothing will be executed until a user calls subscribe
 */
function createRunner(items, trigger) {
    return {
        sequence: items,
        series: function (ctx) {
            if (!ctx)
                ctx = Immutable.Map({});
            var flattened = createObservableTree(items, [], false, ctx);
            var run = Observable
                .from(flattened)
                .concatAll()
                .catch(function (x) { return Rx.Observable.empty(); });
            return run;
        },
        parallel: function (ctx) {
            if (!ctx)
                ctx = Immutable.Map({});
            var flattened = createObservableTree(items, [], true, ctx);
            var run = Observable.from(flattened).mergeAll();
            return run;
        }
    };
    /**
     * Any task in 'Parallel' run mode that throws an
     * error should not adversely affect sibling tasks
     */
    function shouldCatch(trigger) {
        return trigger.config.runMode === task_resolve_1.TaskRunModes.parallel;
    }
    /**
     * Create a nested tree of Observables that can contain tasks
     * alongside parallel/series groups. To understand how this works
     * you can think of the following to be an accurate representation of
     * what this function produces:
     *
     * const out = [
         Observable.concat(
             task1(),
             task2()
         ),
         Observable.concat(
             task3(),
             task4(),
             Observable.concat(
                 task5(),
                 task6(),
                 task7()
             )
         )
     ];
     *
     */
    function createObservableTree(items, initial, addCatch, ctx) {
        if (addCatch === void 0) { addCatch = false; }
        return items.reduce(function (all, item) {
            var output;
            /**
             * If the current task was marked as `parallel`, all immediate children
             * of (this task) will be run in `parallel`
             */
            if (item.type === task_sequence_factories_1.SequenceItemTypes.ParallelGroup) {
                output = Observable.merge(createObservableTree(item.items, [], shouldCatch(trigger), ctx));
            }
            /**
             * If the current task was marked as `series`, all immediate child tasks
             * will be queued and run in series - each waiting until the previous
             * one has completed
             */
            if (item.type === task_sequence_factories_1.SequenceItemTypes.SeriesGroup) {
                output = Observable.concat(createObservableTree(item.items, [], false, ctx));
            }
            /**
             * Finally is item is a task, create an observable for it.
             */
            if (item.type === task_sequence_factories_1.SequenceItemTypes.Task && item.factory) {
                output = task_runner_1.createObservableFromSequenceItem(item, trigger, ctx);
            }
            /**
             * Should we add a catch clause to this item to enable
             * siblings to continue when a task errors
             */
            if (addCatch || !trigger.config.fail) {
                return all.concat(output.catch(function (x) { return Rx.Observable.empty(); }));
            }
            return all.concat(output);
        }, initial);
    }
}
exports.createRunner = createRunner;
/**
 * From user input, try to locate a options object
 */
function loadTopLevelOptions(task, trigger) {
    // todo - more robust way of matching options -> tasks
    var fullMatch = _.get(trigger.input.options, [task.taskName]);
    if (fullMatch !== undefined) {
        /**
         * If this item was given as top-level + options
         * just return the options here
         */
        if (fullMatch.options && fullMatch.tasks) {
            return fullMatch.options;
        }
        /**
         * If this task has a _default key, don't pass
         * all the options in, just pass the stuff under default
         */
        if (task.subTasks.length === 0 && fullMatch._default !== undefined) {
            return fullMatch._default;
        }
        return fullMatch;
    }
    if (task_utils_1.isInternal(task.rawInput)) {
        var lookup = task.taskName.replace(/(.+?)_internal_fn_\d{0,10}/, '');
        var fromInternal = _.get(trigger.input.options, [lookup]);
        if (fromInternal !== undefined) {
            return fromInternal;
        }
    }
    return {};
}
/**
 * After a bunch of tasks have run, we need to link up task-ended reports
 * with their original position in the sequence. This will allow us to
 * reconstruct the task render-tree but also show any tasks that errored
 * or did not complete
 * @param sequence
 * @param reports
 * @returns {*}
 */
function decorateSequenceWithReports(sequence, reports) {
    return addMany(sequence, []);
    function addMany(sequence, initial) {
        return sequence.reduce(function (all, item) {
            var c = _.assign({}, item);
            if (item.type === task_sequence_factories_1.SequenceItemTypes.Task) {
                c.stats = getMergedStats(item, reports);
                return all.concat(c);
            }
            else {
                c.items = addMany(item.items, []);
                return all.concat(c);
            }
        }, initial);
    }
}
exports.decorateSequenceWithReports = decorateSequenceWithReports;
/**
 * Look at every item in the sequence tree and count how many
 * error have occured
 */
function countSequenceErrors(items) {
    return items.reduce(function (acc, item) {
        if (item.type === task_sequence_factories_1.SequenceItemTypes.Task) {
            var errors = _.get(item, 'stats.errors', []);
            if (errors.length) {
                return acc + errors.length;
            }
            return acc;
        }
        return acc + countSequenceErrors(item.items);
    }, 0);
}
exports.countSequenceErrors = countSequenceErrors;
function collectSkippedTasks(items, initial) {
    return items.reduce(function (acc, item) {
        if (item.type === task_sequence_factories_1.SequenceItemTypes.Task) {
            if (item.stats.skipped) {
                return acc.concat(item);
            }
            return acc;
        }
        return acc.concat(collectSkippedTasks(item.items, []));
    }, initial);
}
exports.collectSkippedTasks = collectSkippedTasks;
function collectRunnableTasks(items, initial) {
    return items.reduce(function (acc, item) {
        if (item.type === task_sequence_factories_1.SequenceItemTypes.Task) {
            return acc.concat(item);
        }
        return acc.concat(collectRunnableTasks(item.items, []));
    }, initial);
}
exports.collectRunnableTasks = collectRunnableTasks;
/**
 * Look at the reports array to find stats linked to a
 * given task
 */
function getMergedStats(item, reports) {
    var match = reports.filter(function (report) {
        return report.item.seqUID === item.seqUID;
    });
    var start = match.filter(function (x) { return x.type === task_runner_1.TaskReportType.start; })[0];
    var error = match.filter(function (x) { return x.type === task_runner_1.TaskReportType.error; })[0];
    var end = match.filter(function (x) { return x.type === task_runner_1.TaskReportType.end; })[0];
    if (start && end) {
        return _.assign({}, start.stats, end.stats);
    }
    if (start && error) {
        var duration = error.stats.endTime - start.stats.startTime;
        return _.assign({}, start.stats, error.stats, { duration: duration });
    }
    if (start) {
        return _.assign({}, start.stats);
    }
    return { item: item, errors: [] };
}
/**
 * When we know a task has `subTasks` we need to check if
 * if the first entry in the subTasks array is a `*` - then
 * the user wants to run all tasks under this options
 * object. So we need to get the keys and use each one as a lookup
 * on the local options. (minus any excluded tasks)
 *
 * eg:
 *     $ crossbow run sass:*
 *
 * options:
 *   sass:
 *     site:  {input: "core.scss"}
 *     debug: {input: "debug.scss"}
 *
 * lookupKeys = ['site', 'debug']
 */
var blacklistedSubTaskNames = ['_default'];
function getLookupKeys(subTasks, topLevelObject) {
    if (subTasks[0] === '*') {
        return Object.keys(topLevelObject)
            .filter(function (x) { return blacklistedSubTaskNames.indexOf(x) === -1; });
    }
    return subTasks;
}
//# sourceMappingURL=task.sequence.js.map