# EXPRESSER APP
# -----------------------------------------------------------------------------
# The Express app server, with built-in sockets, firewall and New Relic integration.
# <!--
# @see Settings.app
# @see Firewall
# @see Sockets
# -->
class App

    express = require "express"
    fs = require "fs"
    http = require "http"
    https = require "https"
    lodash = require "lodash"
    net = require "net"
    path = require "path"

    # Current node environment and HTTP server handler are set on init.
    nodeEnv = null

    # Internal modules will be set on `init`.
    firewall = null
    logger = null
    settings = null
    sockets = null
    utils = null

    # @property [Object] Exposes the Express HTTP or HTTPS `server` object.
    server: null

    # @property [Array<Object>] Array of additional middlewares to be use by the Express server. Please note that if you're adding middlewares manually you must do it BEFORE calling `init`.
    extraMiddlewares: []

    # INIT
    # --------------------------------------------------------------------------

    # Init the Express server. If New Relic settings are set it will automatically
    # require and use the `newrelic` module. Firewall and Sockets modules will be
    # used only if enabled on the settings.
    # @param [Object] options App init options. If passed as an array, assume it's the array with extra middlewares.
    # @option options [Array] extraMiddlewares Array with extra middlewares to be loaded.
    init: (options) =>
        if lodash.isArray options
            options = {extraMiddlewares: options}
        else if not options?
            options = {}

        # Load settings and utils.
        settings = require "./settings.coffee"
        utils = require "./utils.coffee"
        nodeEnv = process.env.NODE_ENV

        # Init New Relic, if enabled, and set default error handler.
        @initNewRelic()
        @setErrorHandler()

        # Require logger.
        logger = require "./logger.coffee"
        logger.debug "App", "init", options

        # Configure Express server and start server.
        @configureServer options
        @startServer()

    # Init new Relic, depending on its settings (enabled, appName and LicenseKey).
    initNewRelic: =>
        enabled = settings.newRelic.enabled
        appName = process.env.NEW_RELIC_APP_NAME or settings.newRelic.appName
        licKey = process.env.NEW_RELIC_LICENSE_KEY or settings.newRelic.licenseKey

        # Check if New Relic settings are available, and if so, start the New Relic agent.
        if enabled and appName? and appName isnt "" and licKey? and licKey isnt ""
            targetFile = path.resolve path.dirname(require.main.filename), "newrelic.js"

            # Make sure the newrelic.js file exists on the app root, and create one if it doesn't.
            if not fs.existsSync targetFile
                if process.versions.node.indexOf(".10.") > 0
                    enc = {encoding: settings.general.encoding}
                else
                    enc = settings.general.encoding

                # Set values of newrelic.js file and write it to the app root.
                newRelicJson = "exports.config = {app_name: ['#{appName}'], license_key: '#{licKey}', logging: {level: 'trace'}};"
                fs.writeFileSync targetFile, newRelicJson, enc

                console.log "App", "Original newrelic.js file was copied to the app root, app_name and license_key were set."

            require "newrelic"
            console.log "App", "Started New Relic agent for #{appName}."

    # Log proccess termination to the console. This will force flush any buffered logs to disk.
    # Do not log the exit if running under test environment.
    setErrorHandler: =>
        process.on "exit", (sig) ->
            if nodeEnv? and nodeEnv.indexOf("test") < 0
                console.warn "App", "Terminating Expresser app...", Date(Date.now()), sig
            try
                logger.flushLocal()
            catch ex
                console.warn "App", "Could not flush buffered logs to disk.", ex.message

    # Configure the server. Set views, options, use Express modules, etc.
    configureServer: (options) =>
        midBodyParser = require "body-parser"
        midCookieParser = require "cookie-parser"
        midSession = require "cookie-session"
        midCompression = require "compression"
        midErrorHandler = require "errorhandler"

        # Create express v4 app.
        @server = express()

        settings.updateFromPaaS() if settings.app.paas

        # Set view options, use Jade for HTML templates.
        @server.set "views", settings.path.viewsDir
        @server.set "view engine", settings.app.viewEngine
        @server.set "view options", { layout: false }

        # Enable firewall?
        if settings.firewall.enabled
            firewall = require "./firewall.coffee"
            firewall.init @server

        # Use Express basic handlers.
        @server.use midBodyParser.json()
        @server.use midBodyParser.urlencoded {extended: true}
        @server.use midCookieParser settings.app.cookieSecret if settings.app.cookieEnabled
        @server.use midSession {secret: settings.app.sessionSecret} if settings.app.sessionEnabled

        # Use HTTP compression only if enabled on settings.
        @server.use midCompression if settings.app.compressionEnabled

        # Fix connect assets helper context.
        connectAssetsOptions = settings.app.connectAssets
        connectAssetsOptions.helperContext = @server.locals

        # Connect assets and dynamic compiling.
        ConnectAssets = (require "connect-assets") connectAssetsOptions
        @server.use ConnectAssets

        # Check for extra middlewares to be added.
        if options.extraMiddlewares?
            if lodash.isArray options.extraMiddlewares
                @extraMiddlewares.push mw for mw in options.extraMiddlewares
            else
                @extraMiddlewares.push options.extraMiddlewares

        # Add more middlewares, if any (for example passport for authentication).
        if @extraMiddlewares.length > 0
            @server.use mw for mw in @extraMiddlewares

        # Configure development environment to dump exceptions and show stack.
        if nodeEnv is "development"
            @server.use midErrorHandler {dumpExceptions: true, showStack: true}

        # Configure production environment.
        if nodeEnv is "production"
            @server.use midErrorHandler()

        # Use Express static routing.
        @server.use express.static settings.path.publicDir

        # If debug is on, log requests to the console.
        if settings.general.debug
            @server.use (req, res, next) =>
                ip = utils.getClientIP req
                method = req.method
                url = req.url

                # Check if request flash is present before logging.
                if req.flash? and lodash.isFunction req.flash
                    console.log "Request from #{ip}", method, url, req.flash()
                else
                    console.log "Request from #{ip}", method, url
                next() if next?

    # Start the server using HTTP or HTTPS, depending on the settings.
    startServer: =>
        if settings.app.ssl.enabled and settings.path.sslKeyFile? and settings.path.sslCertFile?
            sslKeyFile = utils.getFilePath settings.path.sslKeyFile
            sslCertFile = utils.getFilePath settings.path.sslCertFile

            # Certificate files were found? Proceed, otherwise alert the user and throw an error.
            if sslKeyFile? and sslCertFile?
                sslKey = fs.readFileSync sslKeyFile, {encoding: settings.general.encoding}
                sslCert = fs.readFileSync sslCertFile, {encoding: settings.general.encoding}
                sslOptions = {key: sslKey, cert: sslCert}
                server = https.createServer sslOptions, @server
            else
                throw new Error "The certificate files could not be found. Please check the 'Path.sslKeyFile' and 'Path.sslCertFile' settings."
        else
            server = http.createServer @server

        # Enable sockets?
        if settings.sockets.enabled
            sockets = require "./sockets.coffee"
            sockets.init server

        if settings.app.ip? and settings.app.ip isnt ""
            server.listen settings.app.port, settings.app.ip
            logger.info "App", settings.general.appTitle, "Listening on #{settings.app.ip} - #{settings.app.port}"
        else
            server.listen settings.app.port
            logger.info "App", settings.general.appTitle, "Listening on #{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.general.appTitle} 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}"
            @redirectorServer = http.createServer redirServer
            @redirectorServer.listen settings.app.ssl.redirectorPort

    # HELPER AND UTILS
    # --------------------------------------------------------------------------

    # Helper to render pages. The request, response and view are mandatory,
    # and the options argument is optional.
    # @param [Object] req The request object.
    # @param [Object] res The response object.
    # @param [String] view The view name.
    # @param [Object] options Options passed to the view, optional.
    renderView: (req, res, view, options) =>
        options = {} if not options?
        options.device = utils.getClientDevice req
        options.title = settings.general.appTitle if not options.title?
        res.render view, options

        logger.debug "App", "Render", view, options

    # Helper to send error responses. When the server can't return a valid result,
    # send an error response with the specified status code.
    # @param [Object] req The response object.
    # @param [String] message The message to be sent to the client.
    # @param [Integer] statusCode The response status code, optional, default is 500.
    renderError: (res, message, statusCode) =>
        message = JSON.stringify message
        statusCode = 500 if not statusCode?
        res.statusCode = 500
        res.send "Server error: #{message}"

        logger.error "App", "HTTP Error", statusCode, message, res

# Singleton implementation
# --------------------------------------------------------------------------
App.getInstance = ->
    @instance = new App() if not @instance?
    return @instance

module.exports = exports = App.getInstance()
