module Logging::Layouts

  # Accessor / Factory for the Pattern layout.
  #
  # Returns a new Pattern layout instance
  def self.pattern( *args )
    return ::Logging::Layouts::Pattern if args.empty?
    ::Logging::Layouts::Pattern.new(*args)
  end

  # A flexible layout configurable via a conversion pattern string.
  #
  # The goal of this class is to format a LogEvent and return the results as
  # a String. The results depend on the conversion pattern.
  #
  # The conversion pattern is closely related to the conversion pattern of
  # the sprintf function. A conversion pattern is composed of literal text
  # and format control expressions called conversion specifiers.
  #
  # You are free to insert any literal text within the conversion pattern.
  #
  # Each conversion specifier starts with a percent sign (%) and is followed
  # by optional format modifiers and a conversion character. The conversion
  # character specifies the type of data, e.g. logger, level, date, thread
  # ID. The format modifiers control such things as field width, padding,
  # left and right justification. The following is a simple example.
  #
  # Let the conversion pattern be "%-5l [%c]: %m\n" and assume that the
  # logging environment was set to use a Pattern layout. Then the statements
  #
  #    root = Logging.logger[:root]
  #    root.debug("Message 1")
  #    root.warn("Message 2")
  #
  # would yield the output
  #
  #    DEBUG [root]: Message 1
  #    WARN  [root]: Message 2
  #
  # Note that there is no explicit separator between text and conversion
  # specifiers. The pattern parser knows when it has reached the end of a
  # conversion specifier when it reads a conversion character. In the example
  # above the conversion specifier %-5l means the level of the logging event
  # should be left justified to a width of five characters. The recognized
  # conversion characters are
  #
  #  [c]  Used to output the name of the logger that generated the log
  #       event. Supports an optional "precision" described further below.
  #  [d]  Used to output the date of the log event. The format of the
  #       date is specified using the :date_pattern option when the Layout
  #       is created. ISO8601 format is assumed if not date pattern is given.
  #  [F]  Used to output the file name where the logging request was issued.
  #  [l]  Used to output the level of the log event.
  #  [L]  Used to output the line number where the logging request was
  #       issued.
  #  [m]  Used to output the application supplied message associated with
  #       the log event.
  #  [M]  Used to output the method name where the logging request was
  #       issued.
  #  [h]  Used to output the hostname
  #  [p]  Used to output the process ID of the currently running program.
  #  [r]  Used to output the number of milliseconds elapsed from the
  #       construction of the Layout until creation of the log event.
  #  [t]  Used to output the object ID of the thread that generated the
  #       log event.
  #  [T]  Used to output the name of the thread that generated the log event.
  #       Name can be specified using Thread.current[:name] notation. Output
  #       empty string if name not specified. This option helps to create
  #       more human readable output for multi-threaded application logs.
  #  [X]  Used to output values from the Mapped Diagnostic Context. Requires
  #       a key name to lookup the value from the context. More details are
  #       listed below.
  #  [x]  Used to output values from the Nested Diagnostic Context. Supports
  #       an optional context separator string. More details are listed below.
  #  [%]  The sequence '%%' outputs a single percent sign.
  #
  # The logger name directive 'c' accepts an optional precision that will
  # only print the rightmost number of name space identifiers for the logger.
  # By default the logger name is printed in full. For example, for the
  # logger name "Foo::Bar::Baz" the pattern %c{2} will output "Bar::Baz".
  #
  # The directives F, L, and M will only work if the Logger generating the
  # events is configured to generate tracing information. If this is not
  # the case these fields will always be empty.
  #
  # The directives for include diagnostic context information in the log
  # messages are X and x. For the Mapped Diagnostic Context the directive must
  # be accompanied by the key identifying the value to insert into the log
  # message. The X directive can appear multiple times to include multiple
  # values from the mapped context.
  #
  #   %X{Cookie}      Insert the current session cookie
  #   %X{X-Session}   Insert a session identifier
  #
  # For the Nested Diagnostic Context you need only include the directive
  # once. All contexts currently in the stack will be added to the log message
  # separated by spaces. If spaces are not your style, a separator string can
  # be given, too.
  #
  #   %x      Insert all contexts separated by spaces
  #   %x{, }  Insert all contexts separate by a comma and a space
  #
  # By default the relevant information is output as is. However, with the
  # aid of format modifiers it is possible to change the minimum field width,
  # the maximum field width and justification.
  #
  # The optional format modifier is placed between the percent sign and the
  # conversion character.
  #
  # The first optional format modifier is the left justification flag which
  # is just the minus (-) character. Then comes the optional minimum field
  # width modifier. This is a decimal constant that represents the minimum
  # number of characters to output. If the data item requires fewer
  # characters, it is padded on either the left or the right until the
  # minimum width is reached. The default is to pad on the left (right
  # justify) but you can specify right padding with the left justification
  # flag. The padding character is space. If the data item is larger than the
  # minimum field width, the field is expanded to accommodate the data. The
  # value is never truncated.
  #
  # This behavior can be changed using the maximum field width modifier which
  # is designated by a period followed by a decimal constant. If the data
  # item is longer than the maximum field, then the extra characters are
  # removed from the end of the data item.
  #
  # Below are various format modifier examples for the category conversion
  # specifier.
  #
  #   %20c       Left pad with spaces if the logger name is less than 20
  #              characters long
  #   %-20c      Right pad with spaces if the logger name is less than 20
  #              characters long
  #   %.30c      Truncates the logger name if it is longer than 30 characters
  #   %20.30c    Left pad with spaces if the logger name is shorter than
  #              20 characters. However, if the logger name is longer than
  #              30 characters, then truncate the name.
  #   %-20.30c   Right pad with spaces if the logger name is shorter than
  #              20 characters. However, if the logger name is longer than
  #              30 characters, then truncate the name.
  #
  # Below are examples of some conversion patterns.
  #
  #    %.1l, [%d] %5l -- %c: %m\n
  #
  # This is how the Logger class in the Ruby standard library formats
  # messages. The main difference will be in the date format (the Pattern
  # Layout uses the ISO8601 date format). Set the :date_method on the
  # Pattern Layout to be 'to_s' and then the date formats will agree.
  #
  class Pattern < ::Logging::Layout

    # :stopdoc:

    # default date format
    ISO8601 = "%Y-%m-%dT%H:%M:%S".freeze

    # call-seq:
    #    Pattern.create_date_format_methods( pl )
    #
    # This method will create the +date_format+ method in the given Pattern
    # Layout _pl_ based on the configured date pattern and/or date method
    # specified by the user.
    #
    def self.create_date_format_methods( pl )
      code = "undef :format_date if method_defined? :format_date\n"
      code << "def format_date( time )\n"
      if pl.date_method.nil?
        if pl.date_pattern =~ %r/%s/
          code << <<-CODE
            dp = '#{pl.date_pattern}'.gsub('%s','%06d' % time.usec)
            time.strftime dp
          CODE
        else
          code << "time.strftime '#{pl.date_pattern}'\n"
        end
      else
        code << "time.#{pl.date_method}\n"
      end
      code << "end\n"
      ::Logging.log_internal(0) {code}

      pl._meta_eval(code, __FILE__, __LINE__)
    end

    # call-seq:
    #    Pattern.create_format_method( pl )
    #
    # This method will create the `format` method in the given Pattern
    # Layout `pl` based on the configured format pattern specified by the
    # user.
    #
    def self.create_format_method( pl )
      builder = FormatMethodBuilder.new(pl)
      code = builder.build_code

      ::Logging.log_internal(0) { code }

      pl._meta_eval(code, __FILE__, __LINE__)
    end
    # :startdoc:

    # call-seq:
    #    Pattern.new( opts )
    #
    # Creates a new Pattern layout using the following options.
    #
    #    :pattern       =>  "[%d] %-5l -- %c : %m\n"
    #    :date_pattern  =>  "%Y-%m-%d %H:%M:%S"
    #    :date_method   =>  'usec' or 'to_s'
    #    :color_scheme  =>  :default
    #
    # If used, :date_method will supersede :date_pattern.
    #
    # The :color_scheme is used to apply color formatting to the log messages.
    # Individual tokens can be colorized witch the level token [%l] receiving
    # distinct colors based on the level of the log event. The entire
    # generated log message can also be colorized based on the level of the
    # log event. See the ColorScheme documentation for more details.
    #
    def initialize( opts = {} )
      super
      @created_at = Time.now

      @date_pattern = opts.fetch(:date_pattern, nil)
      @date_method = opts.fetch(:date_method, nil)
      @date_pattern = ISO8601 if @date_pattern.nil? && @date_method.nil?

      @pattern = opts.fetch(:pattern,
          "[%d] %-#{::Logging::MAX_LEVEL_LENGTH}l -- %c : %m\n")

      cs_name = opts.fetch(:color_scheme, nil)
      @color_scheme =
          case cs_name
          when false, nil; nil
          when true; ::Logging::ColorScheme[:default]
          else ::Logging::ColorScheme[cs_name] end

      self.class.create_date_format_methods(self)
      self.class.create_format_method(self)
    end

    attr_reader :pattern, :date_pattern, :date_method, :color_scheme

    # call-seq:
    #    appender.pattern = "[%d] %-5l -- %c : %m\n"
    #
    # Set the message formatting pattern to be used by the layout.
    #
    def pattern=( var )
      @pattern = var
      Pattern.create_format_method(self)
    end

    # call-seq:
    #    appender.date_pattern = "%Y-%m-%d %H:%M:%S"
    #
    # Set the date formatting pattern to be used when outputting timestamps
    # in the log messages.
    #
    def date_pattern=( var )
      @date_pattern = var
      Pattern.create_date_format_methods(self)
    end

    # call-seq:
    #    appender.date_method = 'to_s'
    #    appender.date_method = :usec
    #
    # Set the date method to be used when outputting timestamps in the log
    # messages. If a date method is configured, the output of that method
    # will be used in leu of the date pattern.
    #
    def date_method=( var )
      @date_method = var
      Pattern.create_date_format_methods(self)
    end

    # :stopdoc:

    # Evaluates the given string of `code` if the singleton class of this
    # Pattern Layout object.
    #
    # Returns this Pattern Layout instance.
    def _meta_eval( code, file = nil, line = nil )
      meta = class << self; self end
      meta.class_eval code, file, line
      self
    end

    # This class is used to build the `format` method for the Pattern layout. It
    # parses the user defined pattern and emits Ruby source code (as a string)
    # that can be `eval`d in the context of the Pattern layout instance.
    class FormatMethodBuilder
      # Matches the first directive encountered and the stuff around it.
      #
      # * $1 is the stuff before directive or "" if not applicable
      # * $2 is the %#.# match within directive group
      # * $3 is the directive letter
      # * $4 is the precision specifier for the logger name
      # * $5 is the stuff after the directive or "" if not applicable
      DIRECTIVE_RGXP = %r/([^%]*)(?:(%-?\d*(?:\.\d+)?)([a-zA-Z%])(?:\{([^\}]+)\})?)?(.*)/m

      # Arguments to sprintf keyed to directive letters
      DIRECTIVE_TABLE = {
        'c' => 'event.logger'.freeze,
        'd' => 'format_date(event.time)'.freeze,
        'F' => 'event.file'.freeze,
        'l' => '::Logging::LNAMES[event.level]'.freeze,
        'L' => 'event.line'.freeze,
        'm' => 'format_obj(event.data)'.freeze,
        'M' => 'event.method'.freeze,
        'h' => "'#{Socket.gethostname}'".freeze,
        'p' => 'Process.pid'.freeze,
        'r' => 'Integer((event.time-@created_at)*1000).to_s'.freeze,
        't' => 'Thread.current.object_id.to_s'.freeze,
        'T' => 'Thread.current[:name]'.freeze,
        'X' => :placeholder,
        'x' => :placeholder,
        '%' => :placeholder
      }.freeze

      # Human name aliases for directives - used for colorization of tokens
      COLOR_ALIAS_TABLE = {
        'c' => :logger,
        'd' => :date,
        'm' => :message,
        'h' => :hostname,
        'p' => :pid,
        'r' => :time,
        'T' => :thread,
        't' => :thread_id,
        'F' => :file,
        'L' => :line,
        'M' => :method,
        'X' => :mdc,
        'x' => :ndc
      }.freeze

      attr_reader :layout
      attr_accessor :pattern
      attr_reader :color_scheme
      attr_reader :sprintf_args
      attr_reader :format_string
      attr_accessor :name_map_count

      # Creates the format method builder and initializes some variables from
      # the given Patter layout instance.
      #
      # pattern_layout - The Pattern Layout instance
      #
      def initialize( pattern_layout )
        @layout         = pattern_layout
        @pattern        = layout.pattern.dup
        @color_scheme   = layout.color_scheme

        @sprintf_args   = []
        @format_string  = '"'
        @name_map_count = 0
      end

      # Returns `true` if the log messages should be colorized.
      def colorize?
        color_scheme && !color_scheme.lines?
      end

      # Returns `true` if the log messages should be colorized by line.
      def colorize_lines?
        color_scheme && color_scheme.lines?
      end

      # Returns `true` if the log levels have special colorization defined.
      def colorize_levels?
        color_scheme && color_scheme.levels?
      end

      # This method returns a String which can be `eval`d in the context of the
      # Pattern layout. When it is `eval`d, a `format` method is defined in the
      # Pattern layout.
      #
      # At the heart of the format method is `sprintf`. The conversion pattern
      # specified in the Pattern layout is parsed and converted into a format
      # string and corresponding arguments list. The format string and arguments
      # are then processed by `sprintf` to format log events.
      #
      # Returns a Ruby code as a String.
      def build_code
        build_format_string

        sprintf = "sprintf("
        sprintf << format_string
        sprintf << ', ' + sprintf_args.join(', ') unless sprintf_args.empty?
        sprintf << ")"

        if colorize_lines?
          sprintf = "color_scheme.color(#{sprintf}, ::Logging::LNAMES[event.level])"
        end

        code = "undef :format if method_defined? :format\n"
        code << "def format( event )\n#{sprintf}\nend\n"
      end

      # This method builds the format string used by `sprintf` to format log
      # events. The conversion pattern given by the user is iteratively parsed
      # by a regular expression into separate format directives. Each directive
      # builds up the format string and the corresponding arguments list that
      # will be formatted.
      #
      # The actual building of the format string is handled by separate
      # directive specific methods. Those handlers also populate the arguments
      # list passed to `sprintf`.
      #
      # Returns the format String.
      def build_format_string
        while true
          match = DIRECTIVE_RGXP.match(pattern)
          _, pre, format, directive, precision, post = *match

          format_string << pre unless pre.empty?

          case directive
          when '%'; format_string << '%%'
          when 'c'; handle_logger( format, directive, precision )
          when 'l'; handle_level(  format, directive, precision )
          when 'X'; handle_mdc(    format, directive, precision )
          when 'x'; handle_ndc(    format, directive, precision )

          when *DIRECTIVE_TABLE.keys
            handle_directives(format, directive, precision)

          when nil; break
          else
            raise ArgumentError, "illegal format character - '#{directive}'"
          end

          break if post.empty?
          self.pattern = post
        end

        format_string << '"'
      end

      # Add the logger name to the `format_string` and the `sprintf_args`. The
      # `slice` argument is a little interesting - this is the number of logger
      # name segments to keep. If we have a logger named "Foo::Bar::Baz" and our
      # `slice` is 2, then "Bar::Baz" will appear in the generated log message.
      # So the `slice` selects the last two parts of the logger name.
      #
      # format    - format String
      # directive - the directive character ('c')
      # slice     - the number of name segments to keep
      #
      # Returns nil
      def handle_logger( format, directive, slice )
        fmt = format + 's'
        fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[directive]) if colorize?

        format_string << fmt
        sprintf_args << DIRECTIVE_TABLE[directive].dup

        if slice
          numeric = Integer(slice) rescue nil
          if numeric
            raise ArgumentError, "logger name slice must be an integer greater than zero: #{numeric}" unless numeric > 0
            sprintf_args.last <<
                ".split(::Logging::Repository::PATH_DELIMITER)" \
                ".last(#{slice}).join(::Logging::Repository::PATH_DELIMITER)"
          else
            format_string << "{#{slice}}"
          end
        end

        nil
      end

      # Add the log event level to the `format_string` and the `sprintf_args`.
      # The color scheme is taken into account when formatting the log event
      # level.
      #
      # format    - format String
      # directive - the directive character ('l')
      # precision - added back to the format string
      #
      # Returns nil
      def handle_level( format, directive, precision )
        if colorize_levels?
          name_map = ::Logging::LNAMES.map { |name| color_scheme.color(("#{format}s" % name), name) }
          var = "@name_map_#{name_map_count}"
          layout.instance_variable_set(var.to_sym, name_map)
          self.name_map_count += 1

          format_string << '%s'
          format_string << "{#{precision}}" if precision
          sprintf_args << "#{var}[event.level]"
        else
          format_string << format + 's'
          format_string << "{#{precision}}" if precision
          sprintf_args << DIRECTIVE_TABLE[directive]
        end

        nil
      end

      # Add a Mapped Diagnostic Context to the `format_string` and the
      # `sprintf_args`. Only one MDC value is added at a time, so this directive
      # can appear multiple times using various keys.
      #
      # format    - format String
      # directive - the directive character ('X')
      # key       - which MDC value to add to the log message
      #
      # Returns nil
      def handle_mdc( format, directive, key )
        raise ArgumentError, "MDC must have a key reference" unless key
        fmt = format + 's'
        fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[directive]) if colorize?

        format_string << fmt
        sprintf_args << "::Logging.mdc['#{key}']"

        nil
      end

      # Add a Nested Diagnostic Context to the `format_string` and the
      # `sprintf_args`. Since the NDC is an Array of values, the directive will
      # appear only once in the conversion pattern. A `separator` is inserted
      # between the values in generated log message.
      #
      # format    - format String
      # directive - the directive character ('x')
      # separator - used to separate the values in the NDC array
      #
      # Returns nil
      def handle_ndc( format, directive, separator )
        fmt = format + 's'
        fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[directive]) if colorize?

        format_string << fmt
        separator = separator.to_s
        separator = ' ' if separator.empty?
        sprintf_args << "::Logging.ndc.context.join('#{separator}')"

        nil
      end

      # Handles the rest of the directives; none of these need any special
      # handling.
      #
      # format    - format String
      # directive - the directive character
      # precision - added back to the format string
      #
      # Returns nil
      def handle_directives( format, directive, precision )
        fmt = format + 's'
        fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[directive]) if colorize?

        format_string << fmt
        format_string << "{#{precision}}" if precision
        sprintf_args << DIRECTIVE_TABLE[directive]

        nil
      end
    end
    # :startdoc:

  end
end
