'use strict';

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

var _toConsumableArray = _interopDefault(require('@babel/runtime/helpers/toConsumableArray'));
var _regeneratorRuntime = _interopDefault(require('@babel/runtime/regenerator'));
var _asyncToGenerator = _interopDefault(require('@babel/runtime/helpers/asyncToGenerator'));
var _slicedToArray = _interopDefault(require('@babel/runtime/helpers/slicedToArray'));
var _classCallCheck = _interopDefault(require('@babel/runtime/helpers/classCallCheck'));
var _createClass = _interopDefault(require('@babel/runtime/helpers/createClass'));
var _defineProperty = _interopDefault(require('@babel/runtime/helpers/defineProperty'));
var defaultConfig = _interopDefault(require('../.server'));
var BodyParser = _interopDefault(require('./BodyParser'));
var Engine = _interopDefault(require('./Engine'));
var FileServer = _interopDefault(require('./FileServer'));
var Logger = _interopDefault(require('./Logger'));
var Router = _interopDefault(require('./Router'));
var fs = require('fs');
var http = require('http');
var https = require('https');
var path = require('path');
var Response = _interopDefault(require('./Response'));
var Request = _interopDefault(require('./Request'));
var utils = require('@forensic-js/utils');
var nodeUtils = require('@forensic-js/node-utils');
var Wrapper = _interopDefault(require('./Wrapper'));
var EntityTooLargeException = _interopDefault(require('../Exceptions/EntityTooLargeException'));
var Utils = require('./Utils');

var App =
/*#__PURE__*/
function () {
  function App(config) {
    var _this = this;

    _classCallCheck(this, App);

    _defineProperty(this, "httpServer", http.createServer({
      ServerResponse: Response,
      IncomingMessage: Request
    }));

    _defineProperty(this, "httpsServer", null);

    _defineProperty(this, "entryPath", '');

    _defineProperty(this, "config", void 0);

    _defineProperty(this, "router", new Router(false));

    _defineProperty(this, "mountedRouters", []);

    _defineProperty(this, "logger", void 0);

    _defineProperty(this, "bodyParser", void 0);

    _defineProperty(this, "activeServers", 0);

    _defineProperty(this, "closeCallback", function () {});

    _defineProperty(this, "errorCallback", null);

    /* istanbul ignore else */
    this.entryPath = nodeUtils.getEntryPath();
    this.config = this.resolveConfig(this.entryPath, config);
    this.logger = new Logger(this.config);
    this.bodyParser = new BodyParser(this.config); //setup https server if it is enabled

    var httpsConfig = this.config.https;

    if (httpsConfig.enabled) {
      var options = {
        ServerResponse: Response,
        IncomingMessage: Request
      };
      Object.entries(httpsConfig.credentials).reduce(function (result, _ref) {
        var _ref2 = _slicedToArray(_ref, 2),
            key = _ref2[0],
            value = _ref2[1];

        result[key] = key === 'passphrase' ? key : fs.readFileSync(path.resolve(_this.entryPath, value));
        return result;
      }, options);
      this.httpsServer = https.createServer(options);
    }
  }
  /**
   * returns server intro
   */


  _createClass(App, [{
    key: "getServerIntro",
    value: function getServerIntro(server) {
      var result = {
        name: server instanceof https.Server ? 'Https' : 'Http',
        address: ''
      };

      if (server.listening) {
        var _ref3 = server.address(),
            address = _ref3.address,
            port = _ref3.port;
        /* istanbul ignore else */


        if (address === '::') {
          result.address = "".concat(result.name.toLowerCase(), "://localhost:").concat(port, "/");
        } else {
          result.address = "".concat(result.name.toLowerCase(), "://").concat(address, ":").concat(port, "/");
        }
      }

      return result;
    }
    /**
     * resolves and merges the configuration objects
     */

  }, {
    key: "resolveConfig",
    value: function resolveConfig(entryPath, config) {
      if (utils.isString(config)) {
        var absPath = path.resolve(entryPath, config);

        if (fs.existsSync(absPath)) {
          config = require(absPath);
        } else {
          config = {};
        }
      }

      var resolvedConfig = utils.copy({}, defaultConfig, config); // resolve to numeric value

      resolvedConfig.maxMemory = utils.expandToNumeric(resolvedConfig.maxMemory); // prioritize node_env setting to config file setting

      var NODE_ENV = process.env.NODE_ENV;

      if (utils.isString(NODE_ENV) && NODE_ENV !== '') {
        if (NODE_ENV.toLowerCase().indexOf('prod') > -1) {
          resolvedConfig.env = 'prod';
        } else if (NODE_ENV.toLowerCase().indexOf('dev') > -1) {
          resolvedConfig.env = 'dev';
        }
      }

      var HTTPS_PORT = process.env.HTTPS_PORT;

      if (utils.isNumber(HTTPS_PORT) || utils.isString(HTTPS_PORT) && /^\d{3}/.test(HTTPS_PORT)) {
        resolvedConfig.https.port = Number.parseInt(HTTPS_PORT);
      }

      resolvedConfig.entryPath = entryPath;
      return resolvedConfig;
    }
    /**
     * runs the array of route instances until a matched route is found
     */

  }, {
    key: "runRoutes",
    value: function () {
      var _runRoutes = _asyncToGenerator(
      /*#__PURE__*/
      _regeneratorRuntime.mark(function _callee(engine, api, routes) {
        var _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, route;

        return _regeneratorRuntime.wrap(function _callee$(_context) {
          while (1) {
            switch (_context.prev = _context.next) {
              case 0:
                _iteratorNormalCompletion = true;
                _didIteratorError = false;
                _iteratorError = undefined;
                _context.prev = 3;
                _iterator = routes[Symbol.iterator]();

              case 5:
                if (_iteratorNormalCompletion = (_step = _iterator.next()).done) {
                  _context.next = 14;
                  break;
                }

                route = _step.value;
                _context.next = 9;
                return engine[api](route);

              case 9:
                if (!_context.sent) {
                  _context.next = 11;
                  break;
                }

                return _context.abrupt("return", true);

              case 11:
                _iteratorNormalCompletion = true;
                _context.next = 5;
                break;

              case 14:
                _context.next = 20;
                break;

              case 16:
                _context.prev = 16;
                _context.t0 = _context["catch"](3);
                _didIteratorError = true;
                _iteratorError = _context.t0;

              case 20:
                _context.prev = 20;
                _context.prev = 21;

                if (!_iteratorNormalCompletion && _iterator.return != null) {
                  _iterator.return();
                }

              case 23:
                _context.prev = 23;

                if (!_didIteratorError) {
                  _context.next = 26;
                  break;
                }

                throw _iteratorError;

              case 26:
                return _context.finish(23);

              case 27:
                return _context.finish(20);

              case 28:
                return _context.abrupt("return", false);

              case 29:
              case "end":
                return _context.stop();
            }
          }
        }, _callee, null, [[3, 16, 20, 28], [21,, 23, 27]]);
      }));

      function runRoutes(_x, _x2, _x3) {
        return _runRoutes.apply(this, arguments);
      }

      return runRoutes;
    }()
    /**
     * cordinates how routes are executed, including mounted routes
     */

  }, {
    key: "cordinateRoutes",
    value: function () {
      var _cordinateRoutes = _asyncToGenerator(
      /*#__PURE__*/
      _regeneratorRuntime.mark(function _callee2(url, method, request, response) {
        var engine, routes, _iteratorNormalCompletion2, _didIteratorError2, _iteratorError2, _iterator2, _step2, mountedRouter, middlewares, mountedRoutes;

        return _regeneratorRuntime.wrap(function _callee2$(_context2) {
          while (1) {
            switch (_context2.prev = _context2.next) {
              case 0:
                method = method.toLowerCase(); //create the engine, with zero middlewares yet

                engine = new Engine(url, method, request, response, this.logger, this.errorCallback);
                routes = this.router.getRoutes(); //run on the main router thread

                engine.use(this.router.getMiddlewares());
                _context2.next = 6;
                return this.runRoutes(engine, 'all', routes.all);

              case 6:
                if (!_context2.sent) {
                  _context2.next = 8;
                  break;
                }

                return _context2.abrupt("return", true);

              case 8:
                _context2.next = 10;
                return this.runRoutes(engine, method, routes[method]);

              case 10:
                if (!_context2.sent) {
                  _context2.next = 12;
                  break;
                }

                return _context2.abrupt("return", true);

              case 12:
                //run on the mounted routers' thread
                _iteratorNormalCompletion2 = true;
                _didIteratorError2 = false;
                _iteratorError2 = undefined;
                _context2.prev = 15;
                _iterator2 = this.mountedRouters[Symbol.iterator]();

              case 17:
                if (_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done) {
                  _context2.next = 33;
                  break;
                }

                mountedRouter = _step2.value;
                middlewares = mountedRouter.shouldInheritMiddlewares() ? [].concat(_toConsumableArray(this.router.getMiddlewares()), _toConsumableArray(mountedRouter.getMiddlewares())) : mountedRouter.getMiddlewares();
                mountedRoutes = mountedRouter.getRoutes();
                engine.use(middlewares);
                _context2.next = 24;
                return this.runRoutes(engine, 'all', mountedRoutes.all);

              case 24:
                if (!_context2.sent) {
                  _context2.next = 26;
                  break;
                }

                return _context2.abrupt("return", true);

              case 26:
                _context2.next = 28;
                return this.runRoutes(engine, method, mountedRoutes[method]);

              case 28:
                if (!_context2.sent) {
                  _context2.next = 30;
                  break;
                }

                return _context2.abrupt("return", true);

              case 30:
                _iteratorNormalCompletion2 = true;
                _context2.next = 17;
                break;

              case 33:
                _context2.next = 39;
                break;

              case 35:
                _context2.prev = 35;
                _context2.t0 = _context2["catch"](15);
                _didIteratorError2 = true;
                _iteratorError2 = _context2.t0;

              case 39:
                _context2.prev = 39;
                _context2.prev = 40;

                if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
                  _iterator2.return();
                }

              case 42:
                _context2.prev = 42;

                if (!_didIteratorError2) {
                  _context2.next = 45;
                  break;
                }

                throw _iteratorError2;

              case 45:
                return _context2.finish(42);

              case 46:
                return _context2.finish(39);

              case 47:
                return _context2.abrupt("return", false);

              case 48:
              case "end":
                return _context2.stop();
            }
          }
        }, _callee2, this, [[15, 35, 39, 47], [40,, 42, 46]]);
      }));

      function cordinateRoutes(_x4, _x5, _x6, _x7) {
        return _cordinateRoutes.apply(this, arguments);
      }

      return cordinateRoutes;
    }()
    /**
     * perform house keeping
     */

  }, {
    key: "onResponseFinish",
    value: function onResponseFinish(request, response) {
      response.endedAt = new Date();
      this.bodyParser.cleanUpTempFiles(request.files);
      this.logger.profile(request, response);
    }
    /**
     * handle on response error event
     */

  }, {
    key: "onResponseError",
    value: function onResponseError(err, request, response) {
      Utils.handleError(err, this.errorCallback, this.logger, request, response);
    }
    /**
     * parse all request data
     */

  }, {
    key: "parseRequestData",
    value: function parseRequestData(request, url) {
      //parse query
      request.query = this.bodyParser.parseQueryString(url); //parse the request body

      if (request.buffer.length > 0) {
        var contentType = 'text/plain';
        /* istanbul ignore else */

        if (request.headers['content-type']) {
          contentType = request.headers['content-type'];
        }

        var result = this.bodyParser.parse(request.buffer, contentType);
        request.body = result.body;
        request.files = result.files;
      }

      utils.copy(request.data, request.query, request.body);
    }
    /**
     * handle onrequest end event
     */

  }, {
    key: "onRequestEnd",
    value: function onRequestEnd(request, response) {
      var _this2 = this;

      if (!request.error) {
        request.endedAt = response.startedAt = new Date();
        response.config = this.config;
        response.request = request;
        response.errorCallback = this.errorCallback;
        var url = request.url,
            method = request.method;
        this.parseRequestData(request, url);
        return this.cordinateRoutes(url, method, request, response).then(function (status) {
          if (!status) {
            var fileServer = new FileServer(_this2.config, _this2.logger, request, response, _this2.errorCallback);
            return fileServer.serve(url).then(function (status) {
              if (!status) {
                return fileServer.serveHttpErrorFile(404);
              }

              return true;
            });
          }

          return true;
        });
      }
    }
    /**
     * handle on request error event
     */

  }, {
    key: "onRequestError",
    value: function onRequestError(err, request, response) {
      request.error = true;
      Utils.handleError(err, this.errorCallback, this.logger, request, response, 400);
    }
    /**
     * handle request data event
     */

  }, {
    key: "onRequestData",
    value: function onRequestData(chunk, request) {
      if (!request.entityTooLarge) {
        var newEntityLength = request.buffer.length + chunk.length;

        if (newEntityLength <= this.config.maxMemory) {
          request.buffer = Buffer.concat([request.buffer, chunk]);
        } else {
          request.entityTooLarge = true;
          request.emit('error', new EntityTooLargeException());
        }
      }
    }
    /**
     * handles onrequest events
     */

  }, {
    key: "onRequest",
    value: function onRequest(request, response, server) {
      request.method = request.method.toLowerCase();
      request.startedAt = new Date();
      request.hostname = request.headers['host'].replace(/:\d+$/, '');
      request.encrypted = server instanceof https.Server; //enforce https if set

      var httpsConfig = this.config.https;

      if (httpsConfig.enabled && !request.encrypted && httpsConfig.enforce) {
        response.redirect("https://".concat(path.join(request.hostname + ':' + httpsConfig.port, request.url)));
      } else {
        //handle on request error
        request.on('error', utils.scopeCallback(this.onRequestError, this, [request, response])); //handle on data event

        request.on('data', utils.scopeCallback(this.onRequestData, this, request)); //handle on end event

        request.on('end', utils.scopeCallback(this.onRequestEnd, this, [request, response])); //handle on response error

        response.on('error', utils.scopeCallback(this.onResponseError, this, response)); //clean up resources once the response has been sent out

        response.on('finish', utils.scopeCallback(this.onResponseFinish, this, [request, response]));
      }
    }
    /**
     * handle server close event
     */

  }, {
    key: "onClose",
    value: function onClose(server) {
      this.activeServers -= 1;
      var intro = this.getServerIntro(server);
      this.logger.info("".concat(intro.name, " connection closed successfully"));

      if (this.activeServers === 0 || this.httpsServer === null) {
        var callback = this.closeCallback;
        this.logger.close();
        callback();
      }
    }
    /**
     * handles server listening event
     */

  }, {
    key: "onListening",
    value: function onListening(server, callback) {
      this.activeServers += 1;
      var intro = this.getServerIntro(server);
      this.logger.info("".concat(intro.name, " server started at ").concat(intro.address));

      if (this.activeServers === 2 || this.httpsServer === null) {
        callback();
      }
    }
    /**
     * handles client error events
     */
    // private onClientError(err, socket: Socket) {
    //     socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
    // }

    /**
     * handle server error events. server errors are majorly startup errors, log error and shut server down
     */

  }, {
    key: "onServerError",
    value: function onServerError(err, server) {
      var intro = this.getServerIntro(server);
      this.logger.warn("".concat(intro.name, " Server Error: ").concat(err.code, " ").concat(err.message));
      this.close(function () {});
    }
    /**
     * binds all event handlers on the server
     */

  }, {
    key: "initServer",
    value: function initServer(server, callback) {
      //handle on error event
      server.on('error', utils.scopeCallback(this.onServerError, this, server)) //handle server client error
      //.on('clientError', scopeCallback(this.onClientError, this))
      //handle server listening event
      .on('listening', utils.scopeCallback(this.onListening, this, [server, callback])) //handle server close event
      .on('close', utils.scopeCallback(this.onClose, this, server)) //handle server request
      .on('request', utils.scopeCallback(this.onRequest, this, server));
    }
    /**
     * returns boolean indicating if the server is listening
     */

  }, {
    key: "getRouter",

    /**
     * returns the server instance router
     */
    value: function getRouter() {
      return this.router;
    }
    /**
     * returns the server instance mounted routers
     */

  }, {
    key: "getMountedRouters",
    value: function getMountedRouters() {
      return this.mountedRouters;
    }
    /**
     * returns the resolved server config object
     */

  }, {
    key: "getConfig",
    value: function getConfig() {
      return this.config;
    }
    /**
     * sets routing base path that gets prepended to all route and middleware urls
     */

  }, {
    key: "setBasePath",
    value: function setBasePath(basePath) {
      this.router.setBasePath(basePath);
    }
    /**
     * sets the app intance callback error handler
     * @param errorCallback app instance error callback
     */

  }, {
    key: "setErrorCallback",
    value: function setErrorCallback(errorCallback) {
      this.errorCallback = errorCallback;
    }
    /**
     * stores route rules for http OPTIONS method
     *
     * @param url - route url
     * @param callback - route callback handler
     * @param options - route configuration object or middleware or array of middlewares
     */

  }, {
    key: "options",
    value: function options(url, callback, _options) {
      return this.router.options(url, callback, _options);
    }
    /**
     * stores route rules for http HEAD method
     *
     * @param url - route url
     * @param callback - route callback handler
     * @param options - route configuration object or middleware or array of middlewares
     */

  }, {
    key: "head",
    value: function head(url, callback, options) {
      return this.router.head(url, callback, options);
    }
    /**
     * stores route rules for http GET method
     *
     * @param url - route url
     * @param callback - route callback handler
     * @param options - route configuration object or middleware or array of middlewares
     */

  }, {
    key: "get",
    value: function get(url, callback, options) {
      return this.router.get(url, callback, options);
    }
    /**
     * stores route rules for http POST method
     *
     * @param url - route url
     * @param callback - route callback handler
     * @param options - route configuration object or middleware or array of middlewares
     */

  }, {
    key: "post",
    value: function post(url, callback, options) {
      return this.router.post(url, callback, options);
    }
    /**
     * stores route rules for http PUT method
     * @param url - route url
     * @param callback - route callback handler
     * @param options - route configuration object or middleware or array of middlewares
     */

  }, {
    key: "put",
    value: function put(url, callback, options) {
      return this.router.put(url, callback, options);
    }
    /**
     * stores route rules for http DELETE method
     *
     * @param url - route url
     * @param callback - route callback handler
     * @param options - route configuration object or middleware or array of middlewares
     */

  }, {
    key: "delete",
    value: function _delete(url, callback, options) {
      return this.router.delete(url, callback, options);
    }
    /**
     * stores route rules for all http methods
     *
     * @param url - route url
     * @param callback - route callback handler
     * @param options - route configuration object or middleware or array of middlewares
     */

  }, {
    key: "all",
    value: function all(url, callback, options) {
      this.router.all(url, callback, options);
    }
    /**
     * returns a route wrapper for the given url
     */

  }, {
    key: "route",
    value: function route(url) {
      return new Wrapper(this.router, url);
    }
    /**
     * removes a given route
     * @param id route id
     */

  }, {
    key: "removeRoute",
    value: function removeRoute(id) {
      if (!this.router.removeRoute(id)) {
        var _iteratorNormalCompletion3 = true;
        var _didIteratorError3 = false;
        var _iteratorError3 = undefined;

        try {
          for (var _iterator3 = this.mountedRouters[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
            var mountedRouter = _step3.value;

            if (mountedRouter.removeRoute(id)) {
              return true;
            }
          }
        } catch (err) {
          _didIteratorError3 = true;
          _iteratorError3 = err;
        } finally {
          try {
            if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
              _iterator3.return();
            }
          } finally {
            if (_didIteratorError3) {
              throw _iteratorError3;
            }
          }
        }

        return false;
      } else {
        return true;
      }
    }
    /**
     * removes a given middleware
     * @param id middleware id
     */

  }, {
    key: "removeMiddleware",
    value: function removeMiddleware(id) {
      if (!this.router.removeMiddleware(id)) {
        var _iteratorNormalCompletion4 = true;
        var _didIteratorError4 = false;
        var _iteratorError4 = undefined;

        try {
          for (var _iterator4 = this.mountedRouters[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
            var mountedRouter = _step4.value;

            if (mountedRouter.removeMiddleware(id)) {
              return true;
            }
          }
        } catch (err) {
          _didIteratorError4 = true;
          _iteratorError4 = err;
        } finally {
          try {
            if (!_iteratorNormalCompletion4 && _iterator4.return != null) {
              _iterator4.return();
            }
          } finally {
            if (_didIteratorError4) {
              throw _iteratorError4;
            }
          }
        }

        return false;
      } else {
        return true;
      }
    }
    /**
     * registers a middleware to be called whenever the given url is visited
     *
     * @param url - url to apply middleware to. use * to appy to all urls
     * @param middleware - the middleware or array of middlewares
     * @param options - middleware configuration option. here, you can specify the http method
     * that the middleware will run against
     */

  }, {
    key: "use",
    value: function use(url, middleware, options) {
      return this.router.use(url, middleware, options);
    }
    /**
     * mounts a router to the server instance
     */

  }, {
    key: "mount",
    value: function mount(baseUrl, router) {
      var mainRouterBasePath = this.router.getBasePath();

      var resolve = function resolve(instance) {
        instance[1] = nodeUtils.joinPaths(mainRouterBasePath, baseUrl, instance[1]);
      }; //resolve all routes, each apiRoutes is of the form [url, callback, options]


      var routes = router.getRoutes();

      for (var _i = 0, _Object$keys = Object.keys(routes); _i < _Object$keys.length; _i++) {
        var api = _Object$keys[_i];
        var apiRoutes = routes[api];
        apiRoutes.forEach(resolve, this);
      } //resolve all middlewares. each middleware is of the format [url, callback, options]


      var middlewares = router.getMiddlewares();
      middlewares.forEach(resolve);
      this.mountedRouters.push(router);
    }
    /**
     * starts the server at a given port
     */

  }, {
    key: "listen",
    value: function listen(port) {
      var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {};
      var closeCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {};
      var envPort = Number.parseInt(process.env.PORT || '0');

      if (this.listening) {
        this.logger.warn('Server already started. You must close the server first');
      } else {
        this.closeCallback = closeCallback;
        this.initServer(this.httpServer, callback);
        this.httpServer.listen(envPort || port || 8000);

        if (this.httpsServer !== null) {
          this.initServer(this.httpsServer, callback);
          this.httpsServer.listen(this.config.https.port);
        }
      }
    }
    /**
     * closes server
     * @param callback callback function to execute when connection closes
     */

  }, {
    key: "close",
    value: function close(callback) {
      if (this.listening) {
        this.closeCallback = callback || this.closeCallback;
        this.httpServer.close();

        if (this.httpsServer && this.httpsServer.listening) {
          this.httpsServer.close();
        }
      }
    }
    /**
     * returns http and https server address
     */

  }, {
    key: "address",
    value: function address() {
      var result = {
        http: null,
        https: null
      };

      if (this.httpServer.listening) {
        result.http = this.httpServer.address();
      }

      if (this.httpsServer && this.httpsServer.listening) {
        result.https = this.httpsServer.address();
      }

      return result;
    }
  }, {
    key: "listening",
    get: function get() {
      return this.httpsServer && this.httpsServer.listening || this.httpServer.listening;
    }
  }]);

  return App;
}();

module.exports = App;
//# sourceMappingURL=App.js.map
