# EXPRESSER LOGGER
# --------------------------------------------------------------------------
# Handles server logging using local files, Logentries or Loggly.
# Multiple services can be enabled at the same time.
# <!--
# @see Settings.logger
# -->
class Logger

    events = require "./events.coffee"
    fs = require "fs"
    lodash = require "lodash"
    moment = require "moment"
    path = require "path"
    settings = require "./settings.coffee"
    utils = require "./utils.coffee"

    # Local logging objects will be set on `init`.
    bufferDispatcher = null
    localBuffer = null
    flushing = false

    # Remote logging providers will be set on `init`.
    logentries = null
    loggly = null
    loggerLogentries = null
    loggerLoggly = null

    # The `serverIP` will be set on init, but only if `settings.logger.sendIP` is true.
    serverIP = null

    # Timer used for automatic logs cleaning.
    timerCleanLocal = null

    # @property [Method] Custom method to call when logs are sent to logging server or flushed to disk.
    onLogSuccess: null

    # @property [Method] Custom method to call when errors are triggered by the logging transport.
    onLogError: null

    # @property [Array] Holds a list of current active logging services.
    # @private
    activeServices = []

    # Holds a copy of emails sent for critical logs.
    criticalEmailCache: {}

    # # CONSTRUCTOR, INIT AND STOP
    # --------------------------------------------------------------------------

    # Logger constructor.
    constructor: ->
        @setEvents() if settings.events.enabled

    # Bind event listeners.
    setEvents: =>
        events.on "Logger.debug", @debug
        events.on "Logger.info", @info
        events.on "Logger.warn", @warn
        events.on "Logger.error", @error
        events.on "Logger.critical", @critical

    # Init the Logger module. Verify which services are set, and add the necessary transports.
    # IP address and timestamp will be appended to logs depending on the settings.
    # @param [Object] options Logger init options.
    init: (options) =>
        bufferDispatcher = null
        localBuffer = null
        logentries = null
        loggly = null
        serverIP = null
        activeServices = []

        # Get a valid server IP to be appended to logs.
        if settings.logger.sendIP
            serverIP = utils.getServerIP true

        # Define server IP.
        if serverIP?
            ipInfo = "IP #{serverIP}"
        else
            ipInfo = "No server IP set."

        # Init transports.
        @initLocal()
        @initLogentries()
        @initLoggly()

        # Check if uncaught exceptions should be logged. If so, try logging unhandled
        # exceptions using the logger, otherwise log to the console.
        if settings.logger.uncaughtException
            @debug "Logger.init", "Catching unhandled exceptions."

            process.on "uncaughtException", (err) =>
                try
                    @error "Unhandled exception!", err.message, err.stack
                catch ex
                    console.error "Unhandled exception!", err.message, err.stack, ex

        # Start logging!
        if not localBuffer? and not logentries? and not loggly?
            @warn "Logger.init", "No transports enabled.", "Logger module will only log to the console!"
        else
            @info "Logger.init", activeServices.join(), ipInfo

    # Init the Local transport. Check if logs should be saved locally. If so, create the logs buffer
    # and a timer to flush logs to disk every X milliseconds.
    initLocal: =>
        if settings.logger.local.enabled
            if fs.existsSync?
                folderExists = fs.existsSync settings.path.logsDir
            else
                folderExists = path.existsSync settings.path.logsDir

            # Create logs folder, if it doesn't exist.
            if not folderExists
                fs.mkdirSync settings.path.logsDir
                if settings.general.debug
                    console.log "Logger.initLocal", "Created #{settings.path.logsDir} folder."

            # Set local buffer.
            localBuffer = {info: [], warn: [], error: []}
            bufferDispatcher = setInterval @flushLocal, settings.logger.local.bufferInterval
            activeServices.push "Local"

            # Check the maxAge of local logs.
            if settings.logger.local.maxAge? and settings.logger.local.maxAge > 0
                if timerCleanLocal?
                    clearInterval timerCleanLocal
                timerCleanLocal = setInterval @cleanLocal, 86400
        else
            @stopLocal()

    # Init the Logentries transport. Check if Logentries should be used, and create the Logentries objects.
    initLogentries: =>
        if settings.logger.logentries.enabled and settings.logger.logentries.token? and settings.logger.logentries.token isnt ""
            logentries = require "node-logentries"
            loggerLogentries = logentries.logger {token: settings.logger.logentries.token, timestamp: settings.logger.sendTimestamp}
            loggerLogentries.on("log", @onLogSuccess) if lodash.isFunction @onLogSuccess
            loggerLogentries.on("error", @onLogError) if lodash.isFunction @onLogError
            activeServices.push "Logentries"
        else
            @stopLogentries()

    # Init the Loggly transport. Check if Loggly should be used, and create the Loggly objects.
    initLoggly: =>
        if settings.logger.loggly.enabled and settings.logger.loggly.subdomain? and settings.logger.loggly.token? and settings.logger.loggly.token isnt ""
            loggly = require "loggly"
            loggerLoggly = loggly.createClient {token: settings.logger.loggly.token, subdomain: settings.logger.loggly.subdomain, json: false}
            activeServices.push "Loggly"
        else
            @stopLoggly()

    # Disable and remove Local transport from the list of active services.
    stopLocal: =>
        @flushLocal()
        clearInterval bufferDispatcher if bufferDispatcher?
        bufferDispatcher = null
        localBuffer = null
        i = activeServices.indexOf "Local"
        activeServices.splice(i, 1) if i >= 0

    # Disable and remove Logentries transport from the list of active services.
    stopLogentries: =>
        logentries = null
        loggerLogentries = null
        i = activeServices.indexOf "Logentries"
        activeServices.splice(i, 1) if i >= 0

    # Disable and remove Loggly transport from the list of active services.
    stopLoggly: =>
        loggly = null
        loggerLoggly = null
        i = activeServices.indexOf "Loggly"
        activeServices.splice(i, 1) if i >= 0


    # LOG METHODS
    # --------------------------------------------------------------------------

    # Generic log method.
    # @param [String] logType The log type (for example: warning, error, info, security, etc).
    # @param [String] logFunc Optional, the logging function name to be passed to the console and Logentries.
    # @param [Array] args Array of arguments to be stringified and logged.
    log: (logType, logFunc, args) =>
        if not args? and logFunc?
            args = logFunc
            logFunc = "info"

        # Get message out of the arguments.
        msg = @getMessage args

        # Log to different transports.
        if settings.logger.local.enabled and localBuffer?
            @logLocal logType, msg
        if settings.logger.logentries.enabled and loggerLogentries?
            loggerLogentries.log logFunc, msg
        if settings.logger.loggly.enabled and loggerLoggly?
            loggerLoggly.log msg, @logglyCallback

        # Log to the console depending on `console` setting.
        if settings.logger.console
            args.unshift moment().format "HH:mm:ss.SS"
            if settings.logger.errorLogTypes.indexOf(logType) >= 0
                console.error.apply this, args
            else
                console.log.apply this, args

    # Log to the active transports as `debug`, only if the debug flag is enabled.
    # All arguments are transformed to readable strings.
    debug: =>
        return if not settings.general.debug

        args = Array.prototype.slice.call arguments
        args.unshift "DEBUG"
        @log "debug", "info", args

    # Log to the active transports as `log`.
    # All arguments are transformed to readable strings.
    info: =>
        return if settings.logger.levels.indexOf("info") < 0

        args = Array.prototype.slice.call arguments
        args.unshift "INFO"
        @log "info", "info", args

    # Log to the active transports as `warn`.
    # All arguments are transformed to readable strings.
    warn: =>
        return if settings.logger.levels.indexOf("warn") < 0

        args = Array.prototype.slice.call arguments
        args.unshift "WARN"
        @log "warn", "warning", args

    # Log to the active transports as `error`.
    # All arguments are transformed to readable strings.
    error: =>
        return if settings.logger.levels.indexOf("error") < 0

        args = Array.prototype.slice.call arguments
        args.unshift "ERROR"
        @log "error", "err", args

    # Log to the active transports as `critical`.
    # All arguments are transformed to readable strings.
    critical: =>
        return if settings.logger.levels.indexOf("critical") < 0

        args = Array.prototype.slice.call arguments
        args.unshift "CRITICAL"
        @log "critical", "err", args

        # If the `criticalEmailTo` is set, dispatch a mail send event.
        if settings.logger.criticalEmailTo? and settings.logger.criticalEmailTo isnt ""
            body = args.join ", "
            maxAge = moment().subtract(settings.logger.criticalEmailExpireMinutes, "m").unix()

            # Do not proceed if this critical email was sent recently.
            return if @criticalEmailCache[body]? and @criticalEmailCache[body] > maxAge

            # Set mail options.
            mailOptions =
                subject: "CRITICAL: #{args[1]}"
                body: body
                to: settings.logger.criticalEmailTo
                logError: false

            # Emit mail send message.
            events.emit "Mailer.send", mailOptions, (err) ->
                console.error "Logger.critical", "Can't send email!", err if err?

            # Save to critical email cache.
            @criticalEmailCache[body] = moment().unix()

    # LOCAL LOGGING
    # --------------------------------------------------------------------------

    # Log locally. The path is defined on `Settings.Path.logsDir`.
    # @param [String] logType The log type (info, warn, error, debug, etc).
    # @param [String] message Message to be logged.
    # @private
    logLocal: (logType, message) ->
        now = moment()
        message = now.format("HH:mm:ss.SSS") + " - " + message
        localBuffer[logType] = [] if not localBuffer[logType]?
        localBuffer[logType].push message

    # Flush all local buffered log messages to disk. This is usually called by the `bufferDispatcher` timer.
    flushLocal: ->
        return if flushing

        # Set flushing and current date.
        flushing = true
        now = moment()
        date = now.format "YYYYMMDD"

        # Flush all buffered logs to disk. Please note that messages from the last seconds of the previous day
        # can be saved to the current day depending on how long it takes for the bufferDispatcher to run.
        # Default is every 10 seconds, so messages from 23:59:50 onwards could be saved on the next day.
        for key, logs of localBuffer
            if logs.length > 0
                writeData = logs.join("\n")
                filePath = path.join settings.path.logsDir, "#{date}.#{key}.log"
                successMsg = "#{logs.length} records logged to disk."

                # Reset this local buffer.
                localBuffer[key] = []

                # Only use `appendFile` on new versions of Node.
                if fs.appendFile?
                    fs.appendFile filePath, writeData, (err) =>
                        flushing = false
                        if err?
                            console.error "Logger.flushLocal", err
                            @onLogError err if @onLogError?
                        else
                            @onLogSuccess successMsg if @onLogSuccess?

                else
                    fs.open filePath, "a", 666, (err1, fd) =>
                        if err1?
                            flushing = false
                            console.error "Logger.flushLocal.open", err1
                            @onLogError err1 if @onLogError?
                        else
                            fs.write fd, writeData, null, settings.general.encoding, (err2) =>
                                flushing = false
                                if err2?
                                    console.error "Logger.flushLocal.write", err2
                                    @onLogError err2 if @onLogError?
                                else
                                    @onLogSuccess successMsg if @onLogSuccess?
                                fs.closeSync fd

    # Delete old log files from disk. The maximum date is defined on the settings.
    cleanLocal: ->
        maxDate = moment().subtract settings.logger.local.maxAge, "d"

        fs.readdir settings.path.logsDir, (err, files) ->
            if err?
                console.error "Logger.cleanLocal", err
            else
                for f in files
                    date = moment f.split(".")[1], "yyyyMMdd"
                    if date.isBefore maxDate
                        fs.unlink path.join(settings.path.logsDir, f), (err) ->
                            console.error "Logger.cleanLocal", err if err?

    # HELPER METHODS
    # --------------------------------------------------------------------------

    # Returns a human readable message out of the arguments.
    # @return [String] The human readable, parsed JSON message.
    # @private
    getMessage: ->
        separated = []
        args = arguments
        args = args[0] if args.length is 1

        # Parse all arguments and stringify objects. Please note that fields defined
        # on the `Settings.logger.removeFields` won't be added to the message.
        for a in args
            if settings.logger.removeFields.indexOf(a) < 0
                if lodash.isArray a
                    for b in a
                        separated.push b if settings.logger.removeFields.indexOf(b) < 0
                else if lodash.isObject a
                    try
                        separated.push JSON.stringify a
                    catch ex
                        separated.push a
                else
                    separated.push a

        # Append IP address, if `serverIP` is set.
        separated.push "IP #{serverIP}" if serverIP?

        # Return single string log message.
        return separated.join " | "

    # Wrapper callback for `onLogSuccess` and `onLogError` to be used by Loggly.
    # @param [String] err The Loggly error.
    # @param [String] result The Loggly logging result.
    # @private
    logglyCallback: (err, result) =>
        if err? and @onLogError?
            @onLogError err
        else if @onLogSuccess?
            @onLogSuccess result

# Singleton implementation
# --------------------------------------------------------------------------
Logger.getInstance = ->
    @instance = new Logger() if not @instance?
    return @instance

module.exports = exports = Logger.getInstance()
