# Defines the log level for a Padrino project.
PADRINO_LOG_LEVEL = ENV['PADRINO_LOG_LEVEL'] unless defined?(PADRINO_LOG_LEVEL)

# Defines the logger used for a Padrino project.
PADRINO_LOGGER = ENV['PADRINO_LOGGER'] unless defined?(PADRINO_LOGGER)

module Padrino
  ##
  # @return [Padrino::Logger]
  #
  # @example
  #   logger.debug "foo"
  #   logger.warn "bar"
  #
  def self.logger
    Padrino::Logger.logger
  end

  ##
  # Set the padrino logger
  #
  # @param [Object] value
  #   an object that respond to <<, write, puts, debug, warn etc..
  #
  # @return [Object]
  #   the given value
  #
  # @example using ruby default logger
  #   require 'logger'
  #   Padrino.logger = Logger.new(STDOUT)
  #
  # @example using ActiveSupport
  #   require 'active_support/buffered_logger'
  #   Padrino.logger = Buffered.new(STDOUT)
  #
  def self.logger=(value)
    Padrino::Logger.logger = value
  end

  ##
  # Padrinos internal logger, using all of Padrino log extensions.
  #
  class Logger
    ##
    # Ruby (standard) logger levels:
    #
    # :fatal:: An unhandleable error that results in a program crash
    # :error:: A handleable error condition
    # :warn:: A warning
    # :info:: generic (useful) information about system operation
    # :debug:: low-level information for developers
    # :devel:: Development-related information that is unnecessary in debug mode
    #
    Levels = {
      :fatal =>  7,
      :error =>  6,
      :warn  =>  4,
      :info  =>  3,
      :debug =>  0,
      :devel => -1,
    } unless defined?(Levels)

    module Extensions
      ##
      # Generate the logging methods for {Padrino.logger} for each log level.
      #
      Padrino::Logger::Levels.each_pair do |name, number|
        define_method(name) do |*args|
          return if number < level
          if args.size > 1
            bench(args[0], args[1], args[2], name)
          else
            push(args * '', name)
          end
        end

        define_method(:"#{name}?") do
          number >= level
        end
      end

      ##
      # Append a to development logger a given action with time
      #
      # @param [string] action
      #   The action
      #
      # @param [float] time
      #   Time duration for the given action
      #
      # @param [message] string
      #   The message that you want to log
      #
      # @example
      #   logger.bench 'GET', started_at, '/blog/categories'
      #   # => DEBUG - GET (0.0056s) - /blog/categories
      #
      def bench(action, began_at, message, level=:debug, color=:yellow)
        @_pad  ||= 8
        @_pad    = action.to_s.size if action.to_s.size > @_pad
        duration = Time.now - began_at
        color    = :red if duration > 1
        action   = colorize(action.to_s.upcase.rjust(@_pad), color)
        duration = colorize('%0.4fs' % duration, :bold, color)
        push "#{action} (#{duration}) #{message}", level
      end

      ##
      # Appends a message to the log. The methods yield to an optional block and
      # the output of this block will be appended to the message.
      #
      # @param [String] message
      #   The message that you want write to your stream
      #
      # @param [String] level
      #   The level one of :debug, :warn etc...
      #
      #
      def push(message = nil, level = nil)
        add(Padrino::Logger::Levels[level], format(message, level))
      end

      ##
      # Formats the log message. This method is a noop and should be implemented by other
      # logger components such as {Padrino::Logger}.
      #
      # @param [String] message
      #   The message to format
      #
      # @param [String,Symbol] level
      #   The log level, one of :debug, :warn...
      def format(message, level)
        message
      end

      ##
      # The debug level, with some style added. May be reimplemented.
      #
      # @example
      #   stylized_level(:debug) => DEBUG
      #
      # @param [String,Symbol] level
      #   The log level
      #
      def stylized_level(level)
        level.to_s.upcase.rjust(7)
      end

      ##
      # Colorizes a string for colored console output. This is a noop and can be reimplemented
      # to colorize the string as needed.
      #
      # @see
      #   ColorizedLogger
      #
      # @param [string]
      #   The string to be colorized.
      #
      # @param [Array<Symbol>]
      #   The colors to use. Should be applied in the order given.
      def colorize(string, *colors)
        string
      end

      ##
      # Turns a logger with LoggingExtensions into a logger with colorized output.
      #
      # @example
      #   Padrino.logger = Logger.new($stdout)
      #   Padrino.logger.colorize!
      #   Padrino.logger.debug("Fancy Padrino debug string")
      def colorize!
        self.extend(Colorize)
      end
    end

    module Colorize
      # Colors for levels
      ColoredLevels = {
        :fatal => [:bold, :red],
        :error => [:red],
        :warn  => [:yellow],
        :info  => [:green],
        :debug => [:cyan],
        :devel => [:magenta]
      } unless defined?(ColoredLevels)

      ##
      # Colorize our level
      #
      # @param [String, Symbol] level
      #
      # @see Padrino::Logging::ColorizedLogger::ColoredLevels
      #
      def colorize(string, *colors)
        colors.each do |c|
          string = string.send(c)
        end
        string
      end

      def stylized_level(level)
        style = ColoredLevels[level].map { |c| "\e[%dm" % String.colors[c] } * ''
        [style, super, "\e[0m"] * ''
      end
    end

    include Extensions

    attr_accessor :auto_flush, :level, :log_static
    attr_reader   :buffer, :colorize_logging, :init_args, :log

    ##
    # Configuration for a given environment, possible options are:
    #
    # :log_level:: Once of [:fatal, :error, :warn, :info, :debug]
    # :stream:: Once of [:to_file, :null, :stdout, :stderr] our your custom stream
    # :log_level::
    #   The log level from, e.g. :fatal or :info. Defaults to :warn in the
    #   production environment and :debug otherwise.
    # :auto_flush::
    #   Whether the log should automatically flush after new messages are
    #   added. Defaults to true.
    # :format_datetime:: Format of datetime. Defaults to: "%d/%b/%Y %H:%M:%S"
    # :format_message:: Format of message. Defaults to: ""%s - - [%s] \"%s\"""
    # :log_static:: Whether or not to show log messages for static files. Defaults to: false
    # :colorize_logging:: Whether or not to colorize log messages. Defaults to: true
    #
    # @example
    #   Padrino::Logger::Config[:development] = { :log_level => :debug, :stream => :to_file }
    #   # or you can edit our defaults
    #   Padrino::Logger::Config[:development][:log_level] = :error
    #   # or you can use your stream
    #   Padrino::Logger::Config[:development][:stream] = StringIO.new
    #
    # Defaults are:
    #
    #   :production  => { :log_level => :warn, :stream => :to_file }
    #   :development => { :log_level => :debug, :stream => :stdout }
    #   :test        => { :log_level => :fatal, :stream => :null }
    #
    # In some cases, configuring the loggers before loading the framework is necessary.
    # You can do so by setting PADRINO_LOGGER:
    #
    #   PADRINO_LOGGER = { :staging => { :log_level => :debug, :stream => :to_file }}
    #
    Config = {
      :production  => { :log_level => :warn,  :stream => :to_file },
      :development => { :log_level => :debug, :stream => :stdout, :format_datetime => '' },
      :test        => { :log_level => :debug, :stream => :null }
    }
    Config.merge!(PADRINO_LOGGER) if PADRINO_LOGGER

    @@mutex = Mutex.new
    def self.logger
      @_logger || setup!
    end

    def self.logger=(logger)
      logger.extend(Padrino::Logger::Extensions)

      @_logger = logger
    end

    ##
    # Setup a new logger
    #
    # @return [Padrino::Logger]
    #   A {Padrino::Logger} instance
    #
    def self.setup!
      self.logger = begin
        config_level = (PADRINO_LOG_LEVEL || Padrino.env || :test).to_sym # need this for PADRINO_LOG_LEVEL
        config = Config[config_level]

        unless config
          warn("No logging configuration for :#{config_level} found, falling back to :production")
          config = Config[:production]
        end

        stream = case config[:stream]
          when :to_file
            FileUtils.mkdir_p(Padrino.root('log')) unless File.exists?(Padrino.root('log'))
            File.new(Padrino.root('log', "#{Padrino.env}.log"), 'a+')
          when :null   then StringIO.new
          when :stdout then $stdout
          when :stderr then $stderr
          else config[:stream] # return itself, probabilly is a custom stream.
        end

        Padrino::Logger.new(config.merge(:stream => stream))
      end
    end

    ##
    # To initialize the logger you create a new object, proxies to set_log.
    #
    # @param [Hash] options
    #
    # @option options [Symbol] :stream ($stdout)
    #   Either an IO object or a name of a logfile. Defaults to $stdout
    #
    # @option options [Symbol] :log_level (:production in the production environment and :debug otherwise)
    #   The log level from, e.g. :fatal or :info.
    #
    # @option options [Symbol] :auto_flush (true)
    #   Whether the log should automatically flush after new messages are
    #   added. Defaults to true.
    #
    # @option options [Symbol] :format_datetime (" [%d/%b/%Y %H:%M:%S] ")
    #   Format of datetime
    #
    # @option options [Symbol] :format_message ("%s -%s%s")
    #    Format of message
    #
    # @option options [Symbol] :log_static (false)
    #   Whether or not to show log messages for static files.
    #
    # @option options [Symbol] :colorize_logging (true)
    #   Whether or not to colorize log messages. Defaults to: true
    #
    def initialize(options={})
      @buffer          = []
      @auto_flush      = options.has_key?(:auto_flush) ? options[:auto_flush] : true
      @level           = options[:log_level] ? Padrino::Logger::Levels[options[:log_level]] : Padrino::Logger::Levels[:debug]
      @log             = options[:stream]  || $stdout
      @log.sync        = true
      @format_datetime = options[:format_datetime] || "%d/%b/%Y %H:%M:%S"
      @format_message  = options[:format_message]  || "%s - %s %s"
      @log_static      = options.has_key?(:log_static) ? options[:log_static] : false
      @colorize_logging = options.has_key?(:colorize_logging) ? options[:colorize_logging] : true
      colorize! if @colorize_logging
    end

    ##
    # Flush the entire buffer to the log object.
    #
    def flush
      return unless @buffer.size > 0
      @@mutex.synchronize do
        @log.write(@buffer.join(''))
        @buffer.clear
      end
    end

    ##
    # Close and remove the current log object.
    #
    # @return [NilClass]
    #
    def close
      flush
      @log.close if @log.respond_to?(:close) && !@log.tty?
      @log = nil
    end

    ##
    # Adds a message to the log - for compatibility with other loggers.
    #
    # @private
    def add(level, message = nil)
      write(message)
    end

    ##
    # Directly append message to the log.
    #
    # @param [String] message
    #   The message
    #
    def <<(message = nil)
      message << "\n" unless message[-1] == ?\n
      @@mutex.synchronize {
        @buffer << message
      }
      flush if @auto_flush
      message
    end
    alias :write :<<

    def format(message, level)
      @format_message % [stylized_level(level), colorize(Time.now.strftime(@format_datetime), :yellow), message.to_s.strip]
    end

    ##
    # Padrino::Loggger::Rack forwards every request to an +app+ given, and
    # logs a line in the Apache common log format to the +logger+, or
    # rack.errors by default.
    #
    class Rack

      def initialize(app, uri_root) # @private
        @app = app
        @uri_root = uri_root.sub(/\/$/,"")
      end

      def call(env) # @private
        env['rack.logger'] = Padrino.logger
        began_at = Time.now
        status, header, body = @app.call(env)
        log(env, status, header, began_at) if logger.debug?
        [status, header, body]
      end

      private
      def log(env, status, header, began_at)
        return if env['sinatra.static_file'] && (!logger.respond_to?(:log_static) || !logger.log_static)
        logger.bench(
          env["REQUEST_METHOD"],
          began_at,
          [
            @uri_root.to_s,
            env["PATH_INFO"],
            env["QUERY_STRING"].empty? ? "" : "?" + env["QUERY_STRING"],
            ' - ',
            logger.colorize(status.to_s[0..3], :bold),
            ' ',
            code_to_name(status)
          ] * '',
          :debug,
          :magenta
        )
      end

      def code_to_name(status)
        ::Rack::Utils::HTTP_STATUS_CODES[status.to_i] || ''
      end
    end # Rack
  end # Logger
end # Padrino

module Kernel # @private
  ##
  # Define a logger available every where in our app
  #
  def logger
    Padrino.logger
  end
end # Kernel
