(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "@dojo/core/lang"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    var lang_1 = require("@dojo/core/lang");
    var configPathSeparator = '@';
    /**
     * Return a string with all lines prefixed with a given prefix.
     */
    function prefix(message, prefix) {
        return message.split('\n').map(function (line) { return prefix + line; }).join('\n');
    }
    exports.prefix = prefix;
    /**
     * Load config data from a given path, using a given text loader, and mixing args and/or a childConfig into the final
     * config value if provided.
     */
    function loadConfig(configPath, loadText, args, childConfig) {
        return _loadConfig(configPath, loadText, args, childConfig).then(function (config) {
            // 'config' and 'extends' are only applicable to the config loader, not the Executors
            delete config.config;
            delete config.extends;
            if (!args || !args.showConfigs) {
                // 'configs' is only relevant if we're showing configs
                delete config.configs;
            }
            return config;
        });
    }
    exports.loadConfig = loadConfig;
    function _loadConfig(configPath, loadText, args, childConfig) {
        return loadText(configPath).then(function (text) {
            var config = parseJson(text);
            // extends paths are assumed to be relative and use '/'
            if (config.extends) {
                var parts = configPath.split('/');
                var _a = splitConfigPath(config.extends), configFile = _a.configFile, childConfig_1 = _a.childConfig;
                var extensionPath = parts.slice(0, parts.length - 1).concat(configFile).join('/');
                return _loadConfig(extensionPath, loadText, undefined, childConfig_1).then(function (extension) {
                    // Copy all keys except 'configs' from the config to the thing its extending (shallow mixin)
                    Object.keys(config).filter(function (key) { return key !== 'configs'; }).forEach(function (key) {
                        extension[key] = config[key];
                    });
                    // If config has a 'configs' property, mix its values into extension.configs (slightly deeper mixin)
                    if (config.configs) {
                        if (extension.configs == null) {
                            extension.configs = {};
                        }
                        Object.keys(config.configs).forEach(function (key) {
                            extension.configs[key] = config.configs[key];
                        });
                    }
                    return extension;
                });
            }
            return config;
        }).then(function (config) {
            if (args && args.showConfigs) {
                // If we're showing the configs, don't mix in children
                return config;
            }
            if (childConfig) {
                var mixinConfig_1 = function (childConfig) {
                    var configs = Array.isArray(childConfig) ? childConfig : [childConfig];
                    configs.forEach(function (childConfig) {
                        var child = config.configs[childConfig];
                        if (!child) {
                            throw new Error("Unknown child config \"" + childConfig + "\"");
                        }
                        if (child.extends) {
                            mixinConfig_1(child.extends);
                        }
                        // Mix the child into the current config. Properties other than the environment resource keys
                        // ('node' and 'browser') will replace values on the parent. The environment resource objects will
                        // be mixed into the corresponding objects on the parent.
                        Object.keys(child)
                            .filter(function (key) { return key !== 'node' && key !== 'browser'; })
                            .forEach(function (key) {
                            config[key] = child[key];
                        });
                        ['node', 'browser'].forEach(function (key) {
                            if (child[key]) {
                                if (config[key]) {
                                    lang_1.mixin(config[key], child[key]);
                                }
                                else {
                                    config[key] = child[key];
                                }
                            }
                        });
                    });
                };
                mixinConfig_1(childConfig);
            }
            return config;
        }).then(function (config) {
            if (args) {
                lang_1.mixin(config, args);
                // If any non-additive resources are specified in args, they will apply to all environments and will
                // override any environment specific resources.
                ['plugins', 'reporters', 'require', 'suites'].filter(function (property) {
                    return args[property];
                }).forEach(function (property) {
                    ['node', 'browser'].filter(function (environment) {
                        return config[environment];
                    }).forEach(function (environment) {
                        delete config[environment][property];
                    });
                });
            }
            return config;
        });
    }
    function getConfigDescription(config) {
        var description = '';
        if (config.description) {
            description += config.description + "\n\n";
        }
        if (config.configs) {
            description += "Configs:\n";
            var width_1 = Object.keys(config.configs).reduce(function (width, name) {
                return Math.max(width, name.length);
            }, 0);
            var lines = Object.keys(config.configs).map(function (name) {
                var child = config.configs[name];
                while (name.length < width_1) {
                    name += ' ';
                }
                var line = "  " + name;
                if (child.description) {
                    line += " (" + child.description + ")";
                }
                return line;
            });
            description += lines.join('\n');
        }
        return description;
    }
    exports.getConfigDescription = getConfigDescription;
    /**
     * Normalize a path such that it ends in '/'
     */
    function normalizePathEnding(path) {
        if (path && path.length > 0 && path[path.length - 1] !== '/') {
            return path + "/";
        }
        return path;
    }
    exports.normalizePathEnding = normalizePathEnding;
    /**
     * Parse an array of name=value arguments into an object
     */
    function parseArgs(rawArgs) {
        var args = {};
        rawArgs.forEach(function (arg) {
            var _a = arg.split('=', 2), name = _a[0], value = _a[1];
            if (typeof value === 'undefined') {
                args[name] = true;
            }
            else {
                if (!(name in args)) {
                    args[name] = value;
                }
                else if (!Array.isArray(args[name])) {
                    args[name] = [args[name], value];
                }
                else {
                    args[name].push(value);
                }
            }
        });
        return args;
    }
    exports.parseArgs = parseArgs;
    /**
     * Parse a JSON string that may contain comments
     */
    function parseJson(json) {
        return JSON.parse(removeComments(json));
    }
    exports.parseJson = parseJson;
    /**
     * Parse a particular type of value from a given value
     *
     * @param name The 'name' of the value being parsed (used for error messages)
     * @param value A value to parse something from
     * @param parser The type of thing to parse, or a parser function
     * @param requiredProperty Only used with 'object' and 'object[]' parsers
     */
    function parseValue(name, value, parser, requiredProperty) {
        switch (parser) {
            case 'boolean':
                if (typeof value === 'boolean') {
                    return value;
                }
                if (value === 'true') {
                    return true;
                }
                if (value === 'false') {
                    return false;
                }
                throw new Error("Non-boolean value \"" + value + "\" for " + name);
            case 'number':
                var numValue = Number(value);
                if (!isNaN(numValue)) {
                    return numValue;
                }
                throw new Error("Non-numeric value \"" + value + "\" for " + name);
            case 'regexp':
                if (typeof value === 'string') {
                    return new RegExp(value);
                }
                if (value instanceof RegExp) {
                    return value;
                }
                throw new Error("Non-regexp value \"" + value + "\" for " + name);
            case 'object':
                if (typeof value === 'string') {
                    try {
                        value = JSON.parse(value);
                    }
                    catch (error) {
                        if (!requiredProperty) {
                            throw new Error("Non-object value \"" + value + "\" for " + name);
                        }
                        value = (_a = {}, _a[requiredProperty] = value, _a);
                    }
                }
                // A value of type 'object' should be a simple object, not a built-in type like RegExp or Array
                if (Object.prototype.toString.call(value) === '[object Object]') {
                    if (requiredProperty && !value[requiredProperty]) {
                        throw new Error("Invalid value \"" + JSON.stringify(value) + "\" for " + name + ": missing '" + requiredProperty + "' property");
                    }
                    return value;
                }
                throw new Error("Non-object value \"" + value + "\" for " + name);
            case 'object[]':
                if (!value) {
                    value = [];
                }
                if (!Array.isArray(value)) {
                    value = [value];
                }
                return value.map(function (item) {
                    return parseValue(name, item, 'object', requiredProperty);
                });
            case 'string':
                if (typeof value === 'string') {
                    return value;
                }
                throw new Error("Non-string value \"" + value + "\" for " + name);
            case 'string[]':
                if (!value) {
                    value = [];
                }
                if (typeof value === 'string') {
                    value = [value];
                }
                if (Array.isArray(value) && value.every(function (v) { return typeof v === 'string'; })) {
                    return value;
                }
                throw new Error("Non-string[] value \"" + value + "\" for " + name);
            default:
                if (typeof parser === 'function') {
                    return parser(value);
                }
                else {
                    throw new Error('Parser must be a valid type name');
                }
        }
        var _a;
    }
    exports.parseValue = parseValue;
    /**
     * Remove all instances of of an item from any array and return the removed instances.
     */
    function pullFromArray(haystack, needle) {
        var removed = [];
        var i = 0;
        while ((i = haystack.indexOf(needle, i)) > -1) {
            removed.push(haystack.splice(i, 1)[0]);
        }
        return removed;
    }
    exports.pullFromArray = pullFromArray;
    /**
     * Split a config path into a file name and a child config name.
     */
    function splitConfigPath(path) {
        var _a = path.split(configPathSeparator, 2), configFile = _a[0], childConfig = _a[1];
        return { configFile: configFile, childConfig: childConfig };
    }
    exports.splitConfigPath = splitConfigPath;
    /**
     * Convert an object to JSON, handling non-primitive properties
     *
     * @param object The object to serialise.
     * @returns A JSON string
     */
    function stringify(object, indent) {
        return JSON.stringify(object, serializeReplacer, indent);
    }
    exports.stringify = stringify;
    /**
     * Replacer function used in stringify
     */
    function serializeReplacer(_key, value) {
        if (!value) {
            return value;
        }
        if (value instanceof RegExp) {
            return value.source;
        }
        if (typeof value === 'function') {
            return value.toString();
        }
        return value;
    }
    /**
     * Remove JS-style line and block comments from a string
     */
    function removeComments(text) {
        var state = 'default';
        var i = 0;
        // Create an array of chars from the text, the blank out anything in a comment
        var chars = text.split('');
        while (i < chars.length) {
            switch (state) {
                case 'block-comment':
                    if (chars[i] === '*' && chars[i + 1] === '/') {
                        chars[i] = ' ';
                        chars[i + 1] = ' ';
                        state = 'default';
                        i += 2;
                    }
                    else if (chars[i] !== '\n') {
                        chars[i] = ' ';
                        i += 1;
                    }
                    else {
                        i += 1;
                    }
                    break;
                case 'line-comment':
                    if (chars[i] === '\n') {
                        state = 'default';
                    }
                    else {
                        chars[i] = ' ';
                    }
                    i += 1;
                    break;
                case 'string':
                    if (chars[i] === '"') {
                        state = 'default';
                        i += 1;
                    }
                    else if (chars[i] === '\\' && chars[i + 1] === '\\') {
                        i += 2;
                    }
                    else if (chars[i] === '\\' && chars[i + 1] === '"') {
                        i += 2;
                    }
                    else {
                        i += 1;
                    }
                    break;
                default:
                    if (chars[i] === '"') {
                        state = 'string';
                        i += 1;
                    }
                    else if (chars[i] === '/' && chars[i + 1] === '*') {
                        chars[i] = ' ';
                        chars[i + 1] = ' ';
                        state = 'block-comment';
                        i += 2;
                    }
                    else if (chars[i] === '/' && chars[i + 1] === '/') {
                        chars[i] = ' ';
                        chars[i + 1] = ' ';
                        state = 'line-comment';
                        i += 2;
                    }
                    else {
                        i += 1;
                    }
            }
        }
        return chars.join('');
    }
});
//# sourceMappingURL=util.js.map