"use strict";
var fs = require("fs");
var path = require("path");
var join = require("path").join;
var connect = require("connect");
var Immutable = require("immutable");
var http = require("http");
var https = require("https");
var Map = require("immutable").Map;
var fromJS = require("immutable").fromJS;
var List = require("immutable").List;
var snippet = require("./../snippet").utils;
var _ = require("../lodash.custom");
var serveStatic = require("serve-static");
var serveIndex = require("serve-index");
var logger = require("../logger");
var snippetUtils = require("../snippet").utils;
var lrSnippet = require("resp-modifier");
var certPath = join(__dirname, "..", "..", "certs");
function getCa(options) {
    var caOption = options.getIn(["https", "ca"]);
    // if not provided, use Browsersync self-signed
    if (typeof caOption === "undefined") {
        return fs.readFileSync(join(certPath, "server.csr"));
    }
    // if a string was given, read that file from disk
    if (typeof caOption === "string") {
        return fs.readFileSync(caOption);
    }
    // if an array was given, read all
    if (List.isList(caOption)) {
        return caOption.toArray().map(function (x) {
            return fs.readFileSync(x);
        });
    }
}
function getKey(options) {
    return fs.readFileSync(options.getIn(["https", "key"]) || join(certPath, "server.key"));
}
function getCert(options) {
    return fs.readFileSync(options.getIn(["https", "cert"]) || join(certPath, "server.crt"));
}
function getHttpsServerDefaults(options) {
    return fromJS({
        key: getKey(options),
        cert: getCert(options),
        ca: getCa(options),
        passphrase: ""
    });
}
function getPFXDefaults(options) {
    return fromJS({
        pfx: fs.readFileSync(options.getIn(["https", "pfx"]))
    });
}
var serverUtils = {
    /**
     * @param options
     * @returns {{key, cert}}
     */
    getHttpsOptions: function (options) {
        var userOption = options.get("https");
        if (Map.isMap(userOption)) {
            if (userOption.has("pfx")) {
                return userOption.mergeDeep(getPFXDefaults(options));
            }
            return userOption.mergeDeep(getHttpsServerDefaults(options));
        }
        return getHttpsServerDefaults(options);
    },
    /**
     * Get either http or https server
     * or use the httpModule provided in options if present
     */
    getServer: function (app, options) {
        return {
            server: (function () {
                var httpModule = serverUtils.getHttpModule(options);
                if (options.get("scheme") === "https" ||
                    options.get("httpModule") === "http2") {
                    var opts = serverUtils.getHttpsOptions(options);
                    return httpModule.createServer(opts.toJS(), app);
                }
                return httpModule.createServer(app);
            })(),
            app: app
        };
    },
    getHttpModule: function (options) {
        /**
         * Users may provide a string to be used by nodes
         * require lookup.
         */
        var httpModule = options.get("httpModule");
        if (typeof httpModule === "string") {
            /**
             * Note, this could throw, but let that happen as
             * the error message good enough.
             */
            var maybe = path.resolve(options.get("cwd"), "node_modules", httpModule);
            return require(maybe);
        }
        if (options.get("scheme") === "https") {
            return https;
        }
        return http;
    },
    getMiddlewares: function (bs) {
        var clientJs = bs.pluginManager.hook("client:js", {
            port: bs.options.get("port"),
            options: bs.options
        });
        var scripts = bs.pluginManager.get("client:script")(bs.options.toJS(), clientJs, "middleware");
        var defaultMiddlewares = [
            {
                id: "Browsersync HTTP Protocol",
                route: require("../config").httpProtocol.path,
                handle: require("../http-protocol").middleware(bs)
            },
            {
                id: "Browsersync IE8 Support",
                route: "",
                handle: snippet.isOldIe(bs.options.get("excludedFileTypes").toJS())
            },
            {
                id: "Browsersync Response Modifier",
                route: "",
                handle: serverUtils.getSnippetMiddleware(bs)
            },
            {
                id: "Browsersync Client - versioned",
                route: bs.options.getIn(["scriptPaths", "versioned"]),
                handle: scripts
            },
            {
                id: "Browsersync Client",
                route: bs.options.getIn(["scriptPaths", "path"]),
                handle: scripts
            }
        ];
        /**
         * Add cors middleware to the front of the stack
         * if a user provided a 'cors' flag
         */
        if (bs.options.get("cors")) {
            defaultMiddlewares.unshift({
                id: "Browsersync CORS support",
                route: "",
                handle: serverUtils.getCorsMiddlewware()
            });
        }
        /**
         * Add connect-history-api-fallback if 'single' argument given
         */
        if (bs.options.get("single")) {
            defaultMiddlewares.unshift({
                id: "Browsersync SPA support",
                route: "",
                handle: require("connect-history-api-fallback")()
            });
        }
        /**
         * Add serve static middleware
         */
        if (bs.options.get("serveStatic")) {
            var ssMiddlewares = serverUtils.getServeStaticMiddlewares(bs.options.get("serveStatic"), bs.options.get("serveStaticOptions", Immutable.Map({})).toJS());
            var withErrors = ssMiddlewares.filter(function (x) {
                return x.get("errors").size > 0;
            });
            var withoutErrors = ssMiddlewares.filter(function (x) {
                return x.get("errors").size === 0;
            });
            if (withErrors.size) {
                withErrors.forEach(function (item) {
                    logger.logger.error("{red:Warning!} %s", item.getIn(["errors", 0, "data", "message"]));
                });
            }
            if (withoutErrors.size) {
                withoutErrors.forEach(function (item) {
                    defaultMiddlewares.push.apply(defaultMiddlewares, item.get("items").toJS());
                });
            }
        }
        /**
         * Add user-provided middlewares
         */
        var userMiddlewares = bs.options
            .get("middleware")
            .map(normaliseMiddleware)
            .toArray();
        var beforeMiddlewares = userMiddlewares.filter(function (x) {
            return x.override;
        });
        var afterMiddlewares = userMiddlewares
            .filter(function (x) {
            return !x.override;
        })
            .concat(bs.options.get("mode") !== "proxy" &&
            userMiddlewares.length === 0 && {
            id: "Browsersync 404/index support",
            route: "",
            handle: serveIndex(bs.options.get("cwd"), {
                icons: true,
                view: "details"
            })
        });
        var mwStack = []
            .concat(beforeMiddlewares, defaultMiddlewares, afterMiddlewares)
            .filter(Boolean);
        return mwStack;
        function normaliseMiddleware(item) {
            /**
             * Object given in options, which
             * ended up being a Map
             */
            if (Map.isMap(item)) {
                return item.toJS();
            }
            /**
             * Single function
             */
            if (typeof item === "function") {
                return {
                    route: "",
                    handle: item
                };
            }
            /**
             * Plain obj
             */
            if (item.route !== undefined && item.handle) {
                return item;
            }
        }
    },
    getBaseApp: function (bs) {
        var app = connect();
        var middlewares = serverUtils.getMiddlewares(bs);
        /**
         * Add all internal middlewares
         */
        middlewares.forEach(function (item) {
            app.stack.push(item);
        });
        return app;
    },
    getSnippetMiddleware: function (bs) {
        var rules = [];
        var blacklist = List([])
            .concat(bs.options.getIn(["snippetOptions", "ignorePaths"]))
            .concat(bs.options.getIn(["snippetOptions", "blacklist"]))
            .filter(Boolean);
        var whitelist = List([]).concat(bs.options.getIn(["snippetOptions", "whitelist"]));
        // Snippet
        rules.push(snippetUtils.getRegex(bs.options.get("snippet"), bs.options.get("snippetOptions")));
        // User
        bs.options.get("rewriteRules").forEach(function (rule) {
            if (Map.isMap(rule)) {
                rules.push(rule.toJS());
            }
            if (_.isPlainObject(rule)) {
                rules.push(rule);
            }
        });
        // Proxy
        if (bs.options.get("proxy")) {
            var proxyRule = require("./proxy-utils").rewriteLinks(bs.options.getIn(["proxy", "url"]).toJS());
            rules.push(proxyRule);
        }
        var lr = lrSnippet.create({
            rules: rules,
            blacklist: blacklist.toArray(),
            whitelist: whitelist.toArray()
        });
        return lr.middleware;
    },
    getCorsMiddlewware: function () {
        return function (req, res, next) {
            // Website you wish to allow to connect
            res.setHeader("Access-Control-Allow-Origin", "*");
            // Request methods you wish to allow
            res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
            // Request headers you wish to allow
            res.setHeader("Access-Control-Allow-Headers", "X-Requested-With,content-type");
            // Set to true if you need the website to include cookies in the requests sent
            // to the API (e.g. in case you use sessions)
            res.setHeader("Access-Control-Allow-Credentials", true);
            next();
        };
    },
    /**
     * @param ssOption
     * @param serveStaticOptions
     * @returns {*}
     */
    getServeStaticMiddlewares: function (ssOption, serveStaticOptions) {
        return ssOption.map(function (dir, i) {
            /**
             * When a user gives a plain string only, eg:
             *   serveStatic: ['./temp']
             *     ->
             *       This means a middleware will be created with
             *         route: ''
             *         handle: serveStatic('./temp', options)
             */
            if (_.isString(dir)) {
                return getFromString(dir);
            }
            /**
             * If a user gave an object eg:
             *   serveStatic: [{route: "", dir: ["test", "./tmp"]}]
             *     ->
             *       This means we need to create a middle for each route + dir combo
             */
            if (Immutable.Map.isMap(dir)) {
                return getFromMap(dir, i);
            }
            /**
             * At this point, an item in the serveStatic array was not a string
             * or an object so we return an error that can be logged
             */
            return fromJS({
                items: [],
                errors: [
                    {
                        type: "Invalid Type",
                        data: {
                            message: "Only strings and Objects (with route+dir) are supported for the ServeStatic option"
                        }
                    }
                ]
            });
        });
        /**
         * @param {string} x
         * @returns {string}
         */
        function getRoute(x) {
            if (x === "")
                return "";
            return x[0] === "/" ? x : "/" + x;
        }
        /**
         * @param dir
         * @returns {Map}
         */
        function getFromString(dir) {
            return fromJS({
                items: [
                    {
                        route: "",
                        handle: serveStatic(dir, serveStaticOptions)
                    }
                ],
                errors: []
            });
        }
        /**
         * @param dir
         * @returns {Map}
         */
        function getFromMap(dir) {
            var ssOptions = (function () {
                if (dir.get("options")) {
                    return dir.get("options").toJS();
                }
                return {};
            })();
            var route = Immutable.List([])
                .concat(dir.get("route"))
                .filter(_.isString);
            var _dir = Immutable.List([])
                .concat(dir.get("dir"))
                .filter(_.isString);
            if (_dir.size === 0) {
                return fromJS({
                    items: [],
                    errors: [
                        {
                            type: "Invalid Object",
                            data: {
                                message: "Serve Static requires a 'dir' property when using an Object"
                            }
                        }
                    ]
                });
            }
            var ssItems = (function () {
                /**
                 * iterate over every 'route' item
                 * @type {Immutable.List<any>|Immutable.List<*>|Immutable.List<any>|*}
                 */
                var routeItems = (function () {
                    /**
                     * If no 'route' was given, assume we want to match all
                     * paths
                     */
                    if (route.size === 0) {
                        return _dir.map(function (dirString) {
                            return Map({
                                route: "",
                                dir: dirString
                            });
                        });
                    }
                    return route.reduce(function (acc, routeString) {
                        /**
                         * For each 'route' item, also iterate through 'dirs'
                         * @type {Immutable.Iterable<K, M>}
                         */
                        var perDir = _dir.map(function (dirString) {
                            return Map({
                                route: getRoute(routeString),
                                dir: dirString
                            });
                        });
                        return acc.concat(perDir);
                    }, List([]));
                })();
                /**
                 * Now create a serverStatic Middleware for each item
                 */
                return routeItems.map(function (routeItem) {
                    return routeItem.merge({
                        handle: serveStatic(routeItem.get("dir"), ssOptions)
                    });
                });
            })();
            return fromJS({
                items: ssItems,
                errors: []
            });
        }
    }
};
module.exports = serverUtils;
//# sourceMappingURL=utils.js.map