
require 'pp'
require 'stringio'

class Loquacious::Configuration

  # Generate nicely formatted help messages for a configuration. The Help
  # class iterates over all the attributes in a configuration and outputs
  # the name, value, and description to an IO stream. The format of the
  # messages can be configured, and the description and/or value of the
  # attribute can be shown or hidden independently.
  #
  class Help

    # :stopdoc:
    @@defaults = {
      :io => $stdout,
      :name_leader => '  - '.freeze,
      :name_length => 0,
      :name_value_sep => ' => '.freeze,
      :desc_leader => ' '.freeze,
      :nesting_nodes => true,
      :colorize => false,
      :colors => {
        :name => :white,
        :value => :cyan,
        :description => :green,
        :leader => :yellow
      }.freeze
    }.freeze

    class Error < StandardError; end
    # :startdoc:

    # Create a new Help instance for the given configuration where _config_
    # can be either a Configuration instance or a configuration name or
    # symbol. Several options can be provided to determine how the
    # configuration information will be printed to the IO stream.
    #
    #   :name_leader      String appearing before the attribute name
    #   :name_length      Maximum length for an attribute name
    #   :name_value_sep   String separating the attribute name from the value
    #   :desc_leader      String appearing before the description
    #   :io               The IO object where help will be written
    #   :nesting_nodes    Flag to enable or disable output of nesting nodes
    #                     (this does not affect display of attributes
    #                     contained by the nesting nodes)
    #   :colorize         Flag to colorize the output or not
    #   :colors           Hash of colors for the name, value, description
    #       :name           Name color
    #       :value          Value color
    #       :description    Description color
    #       :leader         Leader and spacer color
    #
    # The description is printed before each attribute name and value on its
    # own line.
    #
    def initialize( config, opts = {} )
      opts = @@defaults.merge opts
      @config = config.kind_of?(::Loquacious::Configuration) ? config :
                ::Loquacious::Configuration.for(config)

      @io = opts[:io]
      @name_length = Integer(opts[:name_length])
      @desc_leader = opts[:desc_leader]
      @nesting_nodes = opts[:nesting_nodes]
      @colorize = opts[:colorize]
      @colors = opts[:colors]

      unless @name_length > 0
        Iterator.new(@config).each do |node|
          length = node.name.length
          @name_length = length if length > @name_length
        end
      end

      name_leader = opts[:name_leader]
      name_value_sep = opts[:name_value_sep]
      extra_length = name_leader.length + name_value_sep.length
      name_value_sep = name_value_sep.gsub('%', '%%')

      @value_length = 78 - @name_length - extra_length
      @value_leader = "\n" + ' '*(@name_length + extra_length)
      @format = "#{name_leader}%-#{@name_length}s#{name_value_sep}%s"
      @name_format = "#{name_leader}%s"

      if colorize?
        @desc_leader = self.__send__(@colors[:leader], @desc_leader)
        name_leader = self.__send__(@colors[:leader], name_leader)
        name_value_sep = self.__send__(@colors[:leader], name_value_sep)

        @format = name_leader.dup
        @format << self.__send__(@colors[:name], "%-#{@name_length}s")
        @format << name_value_sep.dup
        @format << self.__send__(@colors[:value], "%s")

        @name_format = name_leader.dup
        @name_format << self.__send__(@colors[:name], "%s")
      end

      @desc_leader.freeze
      @value_leader.freeze
      @format.freeze
      @name_format.freeze
    end

    # Returns +true+ if the help instance is configured to colorize the
    # output messages. Returns +false+ otherwise.
    #
    def colorize?
      @colorize
    end

    # Returns +true+ if the help instance is configured to show nesting
    # configuration nodes when iterating over the attributes. This only
    # prevents the nesting node name from being displayed. The attributes
    # nested under the node are still displayed regardless of this setting.
    #
    def show_nesting_nodes?
      @nesting_nodes
    end

    # call-seq:
    #    show_attribute( name = nil, opts = {} )
    #
    # Use this method to show the description for a single attribute or for
    # all the attributes if no _name_ is given. The options allow you to
    # show the values along with the attributes and to hide the descriptions
    # (if all you want to see are the values).
    #
    #    :descriptions => true to show descriptions and false to hide them
    #    :values       => true to show values and false to hide them
    #
    def show_attribute( name = nil, opts = {} )
      name, opts = nil, name if name.is_a?(Hash)
      opts = {
        :descriptions => true,
        :values => false
      }.merge!(opts)

      rgxp = Regexp.new(normalize_attr(name))
      show_description = opts[:descriptions]
      show_value = opts[:values]

      Iterator.new(@config).each do |node|
        next unless rgxp =~ node.name
        next if !show_nesting_nodes? and node.config?
        print_node(node, show_description, show_value)
      end
    end
    alias :show :show_attribute

    # Show all attributes for the configuration. The same options allowed by
    # the +show+ method are also supported by this method.
    #
    def show_all( opts = {} )
      show_attribute(nil, opts)
    end
    alias :show_attributes :show_all

    # Normalize the attribute _name_.
    #
    def normalize_attr( name )
      case name
      when String, nil; name.to_s
      when Symbol; name.to_s
      when Array;  name.join('.')
      else
        raise Error, "cannot convert #{name.inspect} into an attribute identifier"
      end
    end

    # Format the attribute name, value, and description and print the
    # results. The value can be printed or not by setting the _show_value_
    # flag to either +true+ or +false+. The description can be printed or
    # not by setting the _show_description_ flag to either +true+ or
    # +false+.
    #
    def print_node( node, show_description, show_value )
      desc = node.desc.to_s.dup
      show_description = false if desc.empty?

      if show_description
        if colorize?
          desc = desc.gsub(%r/([^\n]+)/,
                           self.__send__(@colors[:description], '\1'))
        end
        @io.puts(desc.indent(@desc_leader))
      end

      @io.puts(format_name(node, show_value))
      @io.puts if show_description
    end

    # Format the name of the attribute pointed at by the given _node_. If
    # the _show_value_ flag is set to +true+, then the attribute value will
    # also be included in the returned string.
    #
    def format_name( node, show_value )
      name = node.name.reduce @name_length
      return @name_format % name if node.config? or !show_value

      sio = StringIO.new
      PP.pp(node.obj, sio, @value_length)
      sio.seek 0
      obj = sio.read.chomp.gsub("\n", @value_leader)
      @format % [name, obj]
    end

    [ [ :clear        ,   0 ],
      [ :reset        ,   0 ],     # synonym for :clear
      [ :bold         ,   1 ],
      [ :dark         ,   2 ],
      [ :italic       ,   3 ],     # not widely implemented
      [ :underline    ,   4 ],
      [ :underscore   ,   4 ],     # synonym for :underline
      [ :blink        ,   5 ],
      [ :rapid_blink  ,   6 ],     # not widely implemented
      [ :negative     ,   7 ],     # no reverse because of String#reverse
      [ :concealed    ,   8 ],
      [ :strikethrough,   9 ],     # not widely implemented
      [ :black        ,  30 ],
      [ :red          ,  31 ],
      [ :green        ,  32 ],
      [ :yellow       ,  33 ],
      [ :blue         ,  34 ],
      [ :magenta      ,  35 ],
      [ :cyan         ,  36 ],
      [ :white        ,  37 ],
      [ :on_black     ,  40 ],
      [ :on_red       ,  41 ],
      [ :on_green     ,  42 ],
      [ :on_yellow    ,  43 ],
      [ :on_blue      ,  44 ],
      [ :on_magenta   ,  45 ],
      [ :on_cyan      ,  46 ],
      [ :on_white     ,  47 ] ].each do |name,code|

      class_eval <<-CODE
        def #{name.to_s}( str )
          "\e[#{code}m\#{str}\e[0m"
        end
      CODE
    end

  end  # class Help
end  # module Loquacious

# EOF
