module Ramaze

  module Logger

    # A customized logger (based on Informer) that creates multiple log files based on time

    class RotatingInformer
      include Innate::Traited
      include Logging

      attr_accessor :time_format, :log_levels
      attr_reader :base_dir

      # parameter for Time.now.strftime
      trait :timestamp => "%Y-%m-%d %H:%M:%S"

      # This is how the final output is arranged.
      trait :format => "[%time] %prefix  %text"

      # Create a new instance of RotatingInformer.
      #
      # base_dir is the directory where all log files will be stored
      #
      # time_format is the time format used to name the log files.
      # Possible formats are identical to those
      # accepted by Time.strftime
      #
      # log_levelse is an array describing what kind of messages
      # that the log receives. The array may contain
      # any or all of the symbols :debug, :error, :info and/or :warn
      #
      # Examples:
      #   RotatingInformer.new('logs')
      #                                   #=> Creates logs in directory called logs.
      #                                       The generated filenames will be in the
      #                                       form YYYY-MM-DD.log
      #   RotatingInformer.new('logs', '%Y-%m.txt')
      #                                   #=> Creates logs in directory called logs.
      #                                       The generated filenames will be in the
      #                                       form YYYY-MM.txt
      #   RotatingInformer.new('logs', '%Y-%m.txt', [:error])
      #                                   #=> Creates logs in directory called logs.
      #                                       The generated filenames will be in the
      #                                       form YYYY-MM.txt. Only errors will be
      #                                       logged to the files.

      def initialize(base_dir, time_format = '%Y-%m-%d.log', log_levels = [:debug, :error, :info, :warn])
        # Verify and set base directory
        send :base_dir=, base_dir, true

        @time_format = time_format
        @log_levels = log_levels

        # Keep track of log shutdown (to prevent StackErrors due to recursion)
        @in_shutdown = false
      end

      # Set the base directory for log files
      #
      # If this method is called with the raise_exception
      # parameter set to true the method will raise an exception
      # if the specified directory does not exist or is unwritable.
      #
      # If raise_exception is set to false, the method will just
      # silently fail if the specified directory does not exist
      # or is unwritable

      def base_dir=(directory, raise_exception = false)
        # Expand directory path
        base_dir = File.expand_path(directory)
        # Verify that directory path exists
        if File.exist?(base_dir)
          # Verify that directory path is a directory
          if File.directory?(base_dir)
            # Verify that directory path is writable
            if File.writable?(base_dir)
              @base_dir = base_dir
            else
              raise Exception.new("#{base_dir} is not writable") if raise_exception
            end
          else
            raise Exception.new("#{base_dir} is not a directory") if raise_exception
          end
        else
          raise Exception.new("#{base_dir} does not exist.") if raise_exception
        end
      end

      # Close the file we log to if it isn't closed already.

      def shutdown
        if @out.respond_to?(:close)
          unless @in_shutdown
            @in_shutdown = true
            Log.debug("close, #{@out.inspect}")
            @in_shutdown = false
          end
          @out.close
        end
      end

      # Integration to Logging.

      def log tag, *messages

        return unless @log_levels.include?(tag)

        # Update current log
        update_current_log

        messages.flatten!

        prefix = tag.to_s.upcase.ljust(5)

        messages.each do |message|
          @out.puts(log_interpolate(prefix, message))
        end

        @out.flush if @out.respond_to?(:flush)
      end

      # Takes the prefix (tag), text and timestamp and applies it to
      # the :format trait.

      def log_interpolate prefix, text, time = timestamp
        message = class_trait[:format].dup

        vars = { '%time' => time, '%prefix' => prefix, '%text' => text }
        vars.each{|from, to| message.gsub!(from, to) }

        message
      end

      # This uses Global.inform_timestamp or a date in the format of
      #   %Y-%m-%d %H:%M:%S
      #   # => "2007-01-19 21:09:32"

      def timestamp
        mask = class_trait[:timestamp]
        Time.now.strftime(mask || "%Y-%m-%d %H:%M:%S")
      end

      # is @out closed?

      def closed?
        @out.respond_to?(:closed?) && @out.closed?
      end

      private

      # Checks whether current filename is still valid.
      # If not, update the current log to point at the new
      # filename

      def update_current_log
        out = File.join(@base_dir, Time.now.strftime(@time_format))
        if @out.nil? || @out.path != out
          # Close old log if necessary
          shutdown unless @out.nil? || closed?

          # Start new log
          @out = File.open(out, 'ab+')
        end
      end
    end

  end
end
