"use strict";
var hooks = require("./hooks");
var asyncTasks = require("./async-tasks");
var config = require("./config");
var connectUtils = require("./connect-utils");
var utils = require("./utils");
var logger = require("./logger");
var chalk = require("chalk");
var eachSeries = utils.eachSeries;
var _ = require("./lodash.custom");
var EE = require("easy-extender");
/**
 * Required internal plugins.
 * Any of these can be overridden by deliberately
 * causing a name-clash.
 */
var defaultPlugins = {
    logger: logger,
    socket: require("./sockets"),
    "file:watcher": require("./file-watcher"),
    server: require("./server"),
    tunnel: require("./tunnel"),
    "client:script": require("browser-sync-client"),
    UI: require("browser-sync-ui")
};
/**
 * @constructor
 */
var BrowserSync = function (emitter) {
    var bs = this;
    bs.cwd = process.cwd();
    bs.active = false;
    bs.paused = false;
    bs.config = config;
    bs.utils = utils;
    bs.events = bs.emitter = emitter;
    bs._userPlugins = [];
    bs._reloadQueue = [];
    bs._cleanupTasks = [];
    bs._browserReload = false;
    // Plugin management
    bs.pluginManager = new EE(defaultPlugins, hooks);
};
/**
 * Call a user-options provided callback
 * @param name
 */
BrowserSync.prototype.callback = function (name) {
    var bs = this;
    var cb = bs.options.getIn(["callbacks", name]);
    if (_.isFunction(cb)) {
        cb.apply(bs.publicInstance, _.toArray(arguments).slice(1));
    }
};
/**
 * @param {Map} options
 * @param {Function} cb
 * @returns {BrowserSync}
 */
BrowserSync.prototype.init = function (options, cb) {
    /**
     * Safer access to `this`
     * @type {BrowserSync}
     */
    var bs = this;
    /**
     * Set user-provided callback, or assign a noop
     * @type {Function}
     */
    bs.cb = cb || utils.defaultCallback;
    /**
     * Verify provided config.
     * Some options are not compatible and will cause us to
     * end the process.
     */
    if (!utils.verifyConfig(options, bs.cb)) {
        return;
    }
    /**
     * Save a reference to the original options
     * @type {Map}
     * @private
     */
    bs._options = options;
    /**
     * Set additional options that depend on what the
     * user may of provided
     * @type {Map}
     */
    bs.options = options;
    /**
     * Kick off default plugins.
     */
    bs.pluginManager.init();
    /**
     * Create a base logger & debugger.
     */
    bs.logger = bs.pluginManager.get("logger")(bs.events, bs);
    bs.debugger = bs.logger.clone({ useLevelPrefixes: true });
    bs.debug = bs.debugger.debug;
    /**
     * Run each setup task in sequence
     */
    eachSeries(asyncTasks, taskRunner(bs), tasksComplete(bs));
    return this;
};
/**
 * Run 1 setup task.
 * Each task is a pure function.
 * They can return options or instance properties to set,
 * but they cannot set them directly.
 * @param {BrowserSync} bs
 * @returns {Function}
 */
function taskRunner(bs) {
    return function (item, cb) {
        bs.debug("-> %s", chalk.yellow("Starting Step: " + item.step));
        /**
         * Execute the current task.
         */
        item.fn(bs, executeTask);
        function executeTask(err, out) {
            /**
             * Exit early if any task returned an error.
             */
            if (err) {
                return cb(err);
            }
            /**
             * Act on return values (such as options to be set,
             * or instance properties to be set
             */
            if (out) {
                handleOut(bs, out);
            }
            bs.debug("+  %s", chalk.green("Step Complete: " + item.step));
            cb();
        }
    };
}
/**
 * @param bs
 * @param out
 */
function handleOut(bs, out) {
    /**
     * Set a single/many option.
     */
    if (out.options) {
        setOptions(bs, out.options);
    }
    /**
     * Any options returned that require path access?
     */
    if (out.optionsIn) {
        out.optionsIn.forEach(function (item) {
            bs.setOptionIn(item.path, item.value);
        });
    }
    /**
     * Any instance properties returned?
     */
    if (out.instance) {
        Object.keys(out.instance).forEach(function (key) {
            bs[key] = out.instance[key];
        });
    }
}
/**
 * Update the options Map
 * @param bs
 * @param options
 */
function setOptions(bs, options) {
    /**
     * If multiple options were set, act on the immutable map
     * in an efficient way
     */
    if (Object.keys(options).length > 1) {
        bs.setMany(function (item) {
            Object.keys(options).forEach(function (key) {
                item.set(key, options[key]);
                return item;
            });
        });
    }
    else {
        Object.keys(options).forEach(function (key) {
            bs.setOption(key, options[key]);
        });
    }
}
/**
 * At this point, ALL async tasks have completed
 * @param {BrowserSync} bs
 * @returns {Function}
 */
function tasksComplete(bs) {
    return function (err) {
        if (err) {
            bs.logger.setOnce("useLevelPrefixes", true).error(err.message);
        }
        /**
         * Set active flag
         */
        bs.active = true;
        /**
         * @deprecated
         */
        bs.events.emit("init", bs);
        /**
         * This is no-longer needed as the Callback now only resolves
         * when everything (including slow things, like the tunnel) is ready.
         * It's here purely for backwards compatibility.
         * @deprecated
         */
        bs.events.emit("service:running", {
            options: bs.options,
            baseDir: bs.options.getIn(["server", "baseDir"]),
            type: bs.options.get("mode"),
            port: bs.options.get("port"),
            url: bs.options.getIn(["urls", "local"]),
            urls: bs.options.get("urls").toJS(),
            tunnel: bs.options.getIn(["urls", "tunnel"])
        });
        /**
         * Call any option-provided callbacks
         */
        bs.callback("ready", null, bs);
        /**
         * Finally, call the user-provided callback given as last arg
         */
        bs.cb(null, bs);
    };
}
/**
 * @param module
 * @param opts
 * @param cb
 */
BrowserSync.prototype.registerPlugin = function (module, opts, cb) {
    var bs = this;
    bs.pluginManager.registerPlugin(module, opts, cb);
    if (module["plugin:name"]) {
        bs._userPlugins.push(module);
    }
};
/**
 * Get a plugin by name
 * @param name
 */
BrowserSync.prototype.getUserPlugin = function (name) {
    var bs = this;
    var items = bs.getUserPlugins(function (item) {
        return item["plugin:name"] === name;
    });
    if (items && items.length) {
        return items[0];
    }
    return false;
};
/**
 * @param {Function} [filter]
 */
BrowserSync.prototype.getUserPlugins = function (filter) {
    var bs = this;
    filter =
        filter ||
            function () {
                return true;
            };
    /**
     * Transform Plugins option
     */
    bs.userPlugins = bs._userPlugins.filter(filter).map(function (plugin) {
        return {
            name: plugin["plugin:name"],
            active: plugin._enabled,
            opts: bs.pluginManager.pluginOptions[plugin["plugin:name"]]
        };
    });
    return bs.userPlugins;
};
/**
 * Get middleware
 * @returns {*}
 */
BrowserSync.prototype.getMiddleware = function (type) {
    var types = {
        connector: connectUtils.socketConnector(this.options)
    };
    if (type in types) {
        return function (req, res) {
            res.setHeader("Content-Type", "text/javascript");
            res.end(types[type]);
        };
    }
};
/**
 * Shortcut for pushing a file-serving middleware
 * onto the stack
 * @param {String} path
 * @param {{type: string, content: string}} props
 */
var _serveFileCount = 0;
BrowserSync.prototype.serveFile = function (path, props) {
    var bs = this;
    var mode = bs.options.get("mode");
    var entry = {
        handle: function (req, res) {
            res.setHeader("Content-Type", props.type);
            res.end(props.content);
        },
        id: "Browsersync - " + _serveFileCount++,
        route: path
    };
    bs._addMiddlewareToStack(entry);
};
/**
 * Add middlewares on the fly
 */
BrowserSync.prototype._addMiddlewareToStack = function (entry) {
    var bs = this;
    /**
     * additional middlewares are always appended -1,
     * this is to allow the proxy middlewares to remain,
     * and the directory index to remain in serveStatic/snippet modes
     */
    bs.app.stack.splice(bs.app.stack.length - 1, 0, entry);
};
var _addMiddlewareCount = 0;
BrowserSync.prototype.addMiddleware = function (route, handle, opts) {
    var bs = this;
    if (!bs.app) {
        return;
    }
    opts = opts || {};
    if (!opts.id) {
        opts.id = "bs-mw-" + _addMiddlewareCount++;
    }
    if (route === "*") {
        route = "";
    }
    var entry = {
        id: opts.id,
        route: route,
        handle: handle
    };
    if (opts.override) {
        entry.override = true;
    }
    bs.options = bs.options.update("middleware", function (mw) {
        if (bs.options.get("mode") === "proxy") {
            return mw.insert(mw.size - 1, entry);
        }
        return mw.concat(entry);
    });
    bs.resetMiddlewareStack();
};
/**
 * Remove middlewares on the fly
 * @param {String} id
 * @returns {Server}
 */
BrowserSync.prototype.removeMiddleware = function (id) {
    var bs = this;
    if (!bs.app) {
        return;
    }
    bs.options = bs.options.update("middleware", function (mw) {
        return mw.filter(function (mw) {
            return mw.id !== id;
        });
    });
    bs.resetMiddlewareStack();
};
/**
 * Middleware for socket connection (external usage)
 * @param opts
 * @returns {*}
 */
BrowserSync.prototype.getSocketConnector = function (opts) {
    var bs = this;
    return function (req, res) {
        res.setHeader("Content-Type", "text/javascript");
        res.end(bs.getExternalSocketConnector(opts));
    };
};
/**
 * Socket connector as a string
 * @param {Object} opts
 * @returns {*}
 */
BrowserSync.prototype.getExternalSocketConnector = function (opts) {
    var bs = this;
    return connectUtils.socketConnector(bs.options.withMutations(function (item) {
        item.set("socket", item.get("socket").merge(opts));
        if (!bs.options.getIn(["proxy", "ws"])) {
            item.set("mode", "snippet");
        }
    }));
};
/**
 * Callback helper
 * @param name
 */
BrowserSync.prototype.getOption = function (name) {
    this.debug("Getting option: {magenta:%s", name);
    return this.options.get(name);
};
/**
 * Callback helper
 * @param path
 */
BrowserSync.prototype.getOptionIn = function (path) {
    this.debug("Getting option via path: {magenta:%s", path);
    return this.options.getIn(path);
};
/**
 * @returns {BrowserSync.options}
 */
BrowserSync.prototype.getOptions = function () {
    return this.options;
};
/**
 * @returns {BrowserSync.options}
 */
BrowserSync.prototype.getLogger = logger.getLogger;
/**
 * @param {String} name
 * @param {*} value
 * @returns {BrowserSync.options|*}
 */
BrowserSync.prototype.setOption = function (name, value, opts) {
    var bs = this;
    opts = opts || {};
    bs.debug("Setting Option: {cyan:%s} - {magenta:%s", name, value.toString());
    bs.options = bs.options.set(name, value);
    if (!opts.silent) {
        bs.events.emit("options:set", {
            path: name,
            value: value,
            options: bs.options
        });
    }
    return this.options;
};
/**
 * @param path
 * @param value
 * @param opts
 * @returns {Map|*|BrowserSync.options}
 */
BrowserSync.prototype.setOptionIn = function (path, value, opts) {
    var bs = this;
    opts = opts || {};
    bs.debug("Setting Option: {cyan:%s} - {magenta:%s", path.join("."), value.toString());
    bs.options = bs.options.setIn(path, value);
    if (!opts.silent) {
        bs.events.emit("options:set", {
            path: path,
            value: value,
            options: bs.options
        });
    }
    return bs.options;
};
/**
 * Set multiple options with mutations
 * @param fn
 * @param opts
 * @returns {Map|*}
 */
BrowserSync.prototype.setMany = function (fn, opts) {
    var bs = this;
    opts = opts || {};
    bs.debug("Setting multiple Options");
    bs.options = bs.options.withMutations(fn);
    if (!opts.silent) {
        bs.events.emit("options:set", { options: bs.options.toJS() });
    }
    return this.options;
};
BrowserSync.prototype.addRewriteRule = function (rule) {
    var bs = this;
    bs.options = bs.options.update("rewriteRules", function (rules) {
        return rules.concat(rule);
    });
    bs.resetMiddlewareStack();
};
BrowserSync.prototype.removeRewriteRule = function (id) {
    var bs = this;
    bs.options = bs.options.update("rewriteRules", function (rules) {
        return rules.filter(function (rule) {
            return rule.id !== id;
        });
    });
    bs.resetMiddlewareStack();
};
BrowserSync.prototype.setRewriteRules = function (rules) {
    var bs = this;
    bs.options = bs.options.update("rewriteRules", function (_) {
        return rules;
    });
    bs.resetMiddlewareStack();
};
/**
 * Add a new rewrite rule to the stack
 * @param {Object} rule
 */
BrowserSync.prototype.resetMiddlewareStack = function () {
    var bs = this;
    var middlewares = require("./server/utils").getMiddlewares(bs, bs.options);
    bs.app.stack = middlewares;
};
/**
 * @param fn
 */
BrowserSync.prototype.registerCleanupTask = function (fn) {
    this._cleanupTasks.push(fn);
};
/**
 * Instance Cleanup
 */
BrowserSync.prototype.cleanup = function (cb) {
    var bs = this;
    if (!bs.active) {
        return;
    }
    // Remove all event listeners
    if (bs.events) {
        bs.debug("Removing event listeners...");
        bs.events.removeAllListeners();
    }
    // Close any core file watchers
    if (bs.watchers) {
        Object.keys(bs.watchers).forEach(function (key) {
            bs.watchers[key].watchers.forEach(function (watcher) {
                watcher.close();
            });
        });
    }
    // Run any additional clean up tasks
    bs._cleanupTasks.forEach(function (fn) {
        if (_.isFunction(fn)) {
            fn(bs);
        }
    });
    // Reset the flag
    bs.debug("Setting {magenta:active: false");
    bs.active = false;
    bs.paused = false;
    bs.pluginManager.plugins = {};
    bs.pluginManager.pluginOptions = {};
    bs.pluginManager.defaultPlugins = defaultPlugins;
    bs._userPlugins = [];
    bs.userPlugins = [];
    bs._reloadTimer = undefined;
    bs._reloadQueue = [];
    bs._cleanupTasks = [];
    if (_.isFunction(cb)) {
        cb(null, bs);
    }
};
module.exports = BrowserSync;
//# sourceMappingURL=browser-sync.js.map