# EXPRESSER APP
# -----------------------------------------------------------------------------
express = require "express"
errors = require "./errors.coffee"
events = require "./events.coffee"
fs = require "fs"
http = require "http"
https = require "https"
lodash = require "lodash"
logger = require "./logger.coffee"
path = require "path"
settings = require "./settings.coffee"
util = require "util"
utils = require "./utils.coffee"

nodeEnv = null

###
# This is the "core" of an Expresser based application. The App wraps the
# Express server and its middlewares / routes, along with a few extra helpers.
###
class App
    newInstance: -> return new App()

    ##
    # Exposes the Express app object to the outside.
    # @property
    # @type express-Application
    expressApp: null

    ##
    # The underlying HTTP(S) server.
    # @property
    # @type http-Server
    webServer: null

    ##
    # The HTTP to HTTPS redirector server (only if settings.app.ssl.redirectorPort is set).
    # @property
    # @type http-Server
    redirectorServer: null

    ##
    # Additional middlewares to be used by the Express server.
    # These will be called before the default middlewares.
    # @property
    # @type Array
    prependMiddlewares: []

    ##
    # Additional middlewares to be used by the Express server.
    # These will be called after the default middlewares.
    # @property
    # @type Array
    appendMiddlewares: []

    # INIT
    # --------------------------------------------------------------------------

    ###
    # Create, configure and run the Express application. In most cases this should be
    # the last step of you app loading, after loading custom modules, setting custom
    # configuration, etc. Please note that this will be called automatically if
    # you call the main `expresser.init()`.
    ###
    init: ->
        logger.debug "App.init"
        events.emit "App.before.init"

        nodeEnv = process.env.NODE_ENV

        # Get version from main app's package.json (NOT Expresser, but the actual app using it!)
        try
            if @expresser?.rootPath
                @version = require(@expresser.rootPath + "/package.json")?.version
            else
                @version = require(__dirname + "../../package.json")?.version
        catch ex
            logger.error "App.init", "Could not fetch version from package.json.", ex

        # Configure the Express server.
        @configure()

        # Start web server!
        @start()

        events.emit "App.on.init"
        delete @init

    ###
    # Configure the server. Set views, options, use Express modules, etc.
    # Called automatically on `init()`, so normally you should never need
    # to call `configure()` on your own.
    # @private
    ###
    configure: ->
        midBodyParser = require "body-parser"
        midCookieParser = require "cookie-parser"
        midCompression = require "compression"
        midSession = require "express-session"
        memoryStore = require("memorystore") midSession

        if settings.general.debug or nodeEnv is "test"
            midErrorHandler = require "errorhandler"

        # Create express v4 app.
        @expressApp = express()

        # DEPRECATED! The "server" was renamed to "expressApp".
        @server = @expressApp

        # BRAKING! Alert if user is still using old ./views default path for views.
        if not fs.existsSync(settings.app.viewPath)
            logger.warn "Attention!", "Views path not found: #{settings.app.viewPath}", "Note that the default path has changed from ./views/ to ./assets/views/"

        # Set view options, use Pug for HTML templates.
        @expressApp.set "views", settings.app.viewPath
        @expressApp.set "view engine", settings.app.viewEngine
        @expressApp.set "view options", { layout: false }

        # Prepend middlewares, if any was specified.
        if @prependMiddlewares.length > 0
            @expressApp.use mw for mw in @prependMiddlewares

        # Use Express basic handlers.
        @expressApp.use midBodyParser.json {limit: settings.app.bodyParser.limit}
        @expressApp.use midBodyParser.urlencoded {extended: settings.app.bodyParser.extended, limit: settings.app.bodyParser.limit}

        if settings.app.cookie.enabled
            @expressApp.use midCookieParser settings.app.cookie.secret

        if settings.app.session.enabled
            @expressApp.use midSession {store: new memoryStore(), secret: settings.app.session.secret, resave: false, saveUninitialized: false, cookie: {httpOnly: settings.app.session.httpOnly, maxAge: new Date(Date.now() + (settings.app.session.maxAge * 1000))}}

        # Use HTTP compression only if enabled on settings.
        if settings.app.compressionEnabled
            @expressApp.use midCompression

        # Fix connect assets helper context.
        connectAssetsOptions = lodash.cloneDeep settings.app.connectAssets
        connectAssetsOptions.helperContext = @expressApp.locals

        # Connect assets and dynamic compiling.
        ConnectAssets = (require "./app/connect-assets.js") connectAssetsOptions
        @expressApp.use ConnectAssets

        # Append extra middlewares, if any was specified.
        if @appendMiddlewares.length > 0
            @expressApp.use mw for mw in @appendMiddlewares

        # Configure development environment to dump exceptions and show stack.
        if settings.general.debug or nodeEnv is "test"
            @expressApp.use midErrorHandler {dumpExceptions: true, showStack: true}

        # Use Express static routing.
        @expressApp.use express.static settings.app.publicPath

        # Log all requests if debug is true.
        if settings.general.debug
            @expressApp.use (req, res, next) ->
                ip = utils.browser.getClientIP req
                method = req.method
                url = req.url

                console.log "Request from #{ip}", method, url

                next() if next?

                return url

        # We should not call configure more than once!
        delete @configure

    # START AND KILL
    # --------------------------------------------------------------------------

    ###
    # Start the server using HTTP or HTTPS, depending on the settings.
    ###
    start: ->
        if @webServer?
            return logger.warn "App.start", "Application has already started (webServer is not null). Abort!"

        events.emit "App.before.start"

        if settings.app.ssl.enabled and settings.app.ssl.keyFile? and settings.app.ssl.certFile?
            sslKeyFile = utils.io.getFilePath settings.app.ssl.keyFile
            sslCertFile = utils.io.getFilePath settings.app.ssl.certFile

            # Certificate files were found? Proceed, otherwise alert the user and throw an error.
            if sslKeyFile? and sslCertFile?
                if fs.existsSync(sslKeyFile) and fs.existsSync(sslCertFile)
                    sslKey = fs.readFileSync sslKeyFile, {encoding: settings.general.encoding}
                    sslCert = fs.readFileSync sslCertFile, {encoding: settings.general.encoding}
                    sslOptions = {key: sslKey, cert: sslCert}
                    serverRef = https.createServer sslOptions, @expressApp
                else
                    return errors.throw "certificatesNotFound", "Please check paths defined on settings.app.ssl."
            else
                return errors.throw "certificatesNotFound", "Please check paths defined on settings.app.ssl."
        else
            serverRef = http.createServer @expressApp

        # Expose the web server.
        @webServer = serverRef

        # Start the app!
        if settings.app.ip? and settings.app.ip isnt ""
            serverRef.listen settings.app.port, settings.app.ip
            logger.info "App", settings.app.title, "Listening on #{settings.app.ip} port #{settings.app.port}"
        else
            serverRef.listen settings.app.port
            logger.info "App", settings.app.title, "Listening on port #{settings.app.port}"

        # Using SSL and redirector port is set? Then create the http server.
        if settings.app.ssl.enabled and settings.app.ssl.redirectorPort > 0
            logger.info "App", "#{settings.app.title} will redirect HTTP #{settings.app.ssl.redirectorPort} to HTTPS on #{settings.app.port}."

            redirServer = express()
            redirServer.get "*", (req, res) -> res.redirect "https://#{req.hostname}:#{settings.app.port}#{req.url}"

            # Log all redirector requests if debug is true.
            if settings.general.debug
                redirServer.use @requestLogger

            @redirectorServer = http.createServer redirServer
            @redirectorServer.listen settings.app.ssl.redirectorPort

        # Pass the HTTP(s) server created to external modules.
        events.emit "App.on.start", serverRef

    ###
    # Kill the underlying HTTP(S) server(s).
    ###
    kill: ->
        events.emit "App.before.kill"

        try
            @webServer?.close()
            @redirectorServer?.close()
        catch ex
            logger.error "App.kill", ex

        webServer = null
        @redirectorServer = null

        events.emit "App.on.kill"

    # BRIDGED EXPRESS METHODS
    # --------------------------------------------------------------------------

    ##
    # Helper to call the Express App .all().
    all: =>
        return errors.throw "expressNotInit" if not @expressApp?
        logger.debug "App.all", util.inspect(arguments[0]), util.inspect(arguments[1])
        @expressApp.all.apply @expressApp, arguments

    ##
    # Helper to call the Express App .get().
    get: =>
        return errors.throw "expressNotInit" if not @expressApp?
        logger.debug "App.get", util.inspect(arguments[0]), util.inspect(arguments[1])
        @expressApp.get.apply @expressApp, arguments

    ##
    # Helper to call the Express App .post().
    post: =>
        return errors.throw "expressNotInit" if not @expressApp?
        logger.debug "App.post", util.inspect(arguments[0]), util.inspect(arguments[1])
        @expressApp.post.apply @expressApp, arguments

    ##
    # Helper to call the Express App .put().
    put: =>
        return errors.throw "expressNotInit" if not @expressApp?
        logger.debug "App.put", util.inspect(arguments[0]), util.inspect(arguments[1])
        @expressApp.put.apply @expressApp, arguments

    ##
    # Helper to call the Express App .patch().
    patch: =>
        return errors.throw "expressNotInit" if not @expressApp?
        logger.debug "App.patch", util.inspect(arguments[0]), util.inspect(arguments[1])
        @expressApp.patch.apply @expressApp, arguments

    ##
    # Helper to call the Express App .delete().
    delete: =>
        return errors.throw "expressNotInit" if not @expressApp?
        logger.debug "App.delete", util.inspect(arguments[0]), util.inspect(arguments[1])
        @expressApp.delete.apply @expressApp, arguments

    ##
    # Helper to call the Express App .listen().
    listen: =>
        return errors.throw "expressNotInit" if not @expressApp?
        logger.debug "App.listen", util.inspect(arguments[0]), util.inspect(arguments[1])
        @expressApp.listen.apply @expressApp, arguments

    ##
    # Helper to call the Express App .route().
    route: =>
        return errors.throw "expressNotInit" if not @expressApp?
        logger.debug "App.route", arguments[0]
        @expressApp.route.apply @expressApp, arguments

    ##
    # Helper to call the Express App .use().
    use: =>
        return errors.throw "expressNotInit" if not @expressApp?
        logger.debug "App.use", util.inspect(arguments[0]), util.inspect(arguments[1])
        @expressApp.use.apply @expressApp, arguments

    # HELPER AND UTILS
    # --------------------------------------------------------------------------

    ###
    # Return an array with all routes registered on the Express application.
    # @param {Boolean} asString If true, returns the route strings only, otherwise returns full objects.
    # @return {Array} Array with the routes (as object or as string if asString = true).
    ###
    listRoutes: (asString = false) =>
        result = []

        for r in @expressApp._router.stack
            if r.route?.path? and r.route.path isnt ""
                if asString
                    result.push r.route.path
                else
                    result.push {route: r.route.path, methods: lodash.keys(r.route.methods)}

        return result

    ###
    # Render a Pug view and send to the client.
    # @param {Object} req The Express request object, mandatory.
    # @param {Object} res The Express response object, mandatory.
    # @param {String} view The Pug view filename, mandatory.
    # @param {Object} options Options passed to the view, optional.
    ###
    renderView: (req, res, view, options) ->
        logger.debug "App.renderView", req.originalUrl, view, options

        try
            options = {} if not options?
            options.device = utils.browser.getDeviceDetails req
            options.title = settings.app.title if not options.title?

            # View filename must jave .pug extension.
            view += ".pug" if view.indexOf(".pug") < 0

            # Send rendered view to client.
            res.render view, options

        catch ex
            logger.error "App.renderView", view, ex
            @renderError req, res, ex

        events.emit "App.on.renderView", req, res, view, options

    ###
    # Render response as JSON data and send to the client.
    # @param {Object} req The Express request object, mandatory.
    # @param {Object} res The Express response object, mandatory.
    # @param {Object} data The JSON data to be sent, mandatory.
    ###
    renderJson: (req, res, data) ->
        logger.debug "App.renderJson", req.originalUrl, data

        if lodash.isString data
            try
                data = JSON.parse data
            catch ex
                return @renderError req, res, ex, 500

        # Remove methods from JSON before rendering.
        cleanJson = (obj, depth) ->
            if depth > settings.logger.maxDepth
                return

            if lodash.isArray obj
                for i in obj
                    cleanJson i, depth + 1
            else if lodash.isObject obj
                for k, v of obj
                    if lodash.isFunction v
                        delete obj[k]
                    else
                        cleanJson v, depth + 1

        cleanJson data, 0

        # Add Access-Control-Allow-Origin to all when debug is true.
        if settings.general.debug
            res.setHeader "Access-Control-Allow-Origin", "*"

        # Send JSON response.
        res.json data

        events.emit "App.on.renderJson", req, res, data

    ###
    # Render an image from the speficied file, and send to the client.
    # @param {Object} req The Express request object, mandatory.
    # @param {Object} res The Express response object, mandatory.
    # @param {String} filename The full path to the image file, mandatory.
    # @param {Object} options Options passed to the image renderer, for example the "mimetype".
    ###
    renderImage: (req, res, filename, options) ->
        logger.debug "App.renderImage", req.originalUrl, filename, options

        mimetype = options?.mimetype

        # Try to figure out the mime type in case it wasn't passed along the options.
        if not mimetype?
            extname = path.extname(filename).toLowerCase().replace(".","")
            extname = "jpeg" if extname is "jpg"
            mimetype = "image/#{extname}"

        # Send image to client.
        res.contentType mimetype
        res.sendFile filename

        events.emit "App.on.renderImage", req, res, filename, options

    ###
    # Sends error response as JSON.
    # @param {Object} req The Express request object, mandatory.
    # @param {Object} res The Express response object, mandatory.
    # @param {Object} error The error object or message to be sent to the client, mandatory.
    # @param {Number} status The response status code, optional, default is 500.
    ###
    renderError: (req, res, error, status) ->
        logger.debug "App.renderError", req.originalUrl, status, error

        # Status defaults to 500.
        status = error?.statusCode or 500 if not status?
        status = 408 if status is "ETIMEDOUT"

        # Helper to build message out of the error object.
        getMessage = (obj) ->
            msg = {}

            if lodash.isString obj
                msg.message = obj
            else
                msg.message = obj.message if obj.message?
                msg.friendlyMessage = obj.friendlyMessage if obj.friendlyMessage?
                msg.reason = obj.reason if obj.reason?
                msg.code = obj.code if obj.code?

            # Nothing taken out of error objec? Return null then.
            if lodash.keys(msg).length is 0
                return null

            return msg

        try
            message = getMessage error

            # Error might be encapsulated inside another "error" property.
            if not mesage? and error.error?
                message = getMessage error.error

        catch ex
            logger.error "App.renderError", ex

        # Can't figure it out? Just pass error as string then.
        if not message?
            message = error.toString()

        # Send error JSON to client.
        res.status(status).json {error: message, url: req.originalUrl}

        events.emit "App.on.renderError", req, res, error, status

# Singleton implementation
# -----------------------------------------------------------------------------
App.getInstance = ->
    @instance = new App() if not @instance?
    return @instance

module.exports = App.getInstance()
