require 'rdoc/ri'
require 'rdoc/markup'

class RDoc::RI::Formatter

  attr_reader :indent
  attr_accessor :output

  FORMATTERS = { }

  def self.for(name)
    FORMATTERS[name.downcase]
  end

  def self.list
    FORMATTERS.keys.sort.join ", "
  end

  def initialize(output, width, indent)
    @output = output
    @width  = width
    @indent = indent
  end

  def draw_line(label=nil)
    len = @width
    len -= (label.size + 1) if label

    if len > 0 then
      @output.print '-' * len
      if label
        @output.print ' '
        bold_print label
      end

      @output.puts
    else
      @output.print '-' * @width
      @output.puts

      @output.puts label
    end
  end

  def wrap(txt, prefix=@indent, linelen=@width)
    return unless txt && !txt.empty?

    work = conv_markup(txt)
    textLen = linelen - prefix.length
    patt = Regexp.new("^(.{0,#{textLen}})[ \n]")
    next_prefix = prefix.tr("^ ", " ")

    res = []

    while work.length > textLen
      if work =~ patt
        res << $1
        work.slice!(0, $&.length)
      else
        res << work.slice!(0, textLen)
      end
    end
    res << work if work.length.nonzero?
    @output.puts(prefix + res.join("\n" + next_prefix))
  end

  def blankline
    @output.puts
  end

  ##
  # Called when we want to ensure a new 'wrap' starts on a newline.  Only
  # needed for HtmlFormatter, because the rest do their own line breaking.

  def break_to_newline
  end

  def bold_print(txt)
    @output.print txt
  end

  def raw_print_line(txt)
    @output.puts txt
  end

  ##
  # Convert HTML entities back to ASCII

  def conv_html(txt)
    txt = txt.gsub(/&gt;/, '>')
    txt.gsub!(/&lt;/, '<')
    txt.gsub!(/&quot;/, '"')
    txt.gsub!(/&amp;/, '&')
    txt
  end

  ##
  # Convert markup into display form

  def conv_markup(txt)
    txt = txt.gsub(%r{<tt>(.*?)</tt>}, '+\1+')
    txt.gsub!(%r{<code>(.*?)</code>}, '+\1+')
    txt.gsub!(%r{<b>(.*?)</b>}, '*\1*')
    txt.gsub!(%r{<em>(.*?)</em>}, '_\1_') 
    txt
  end

  def display_list(list)
    case list.type
    when :BULLET
      prefixer = proc { |ignored| @indent + "*   " }

    when :NUMBER, :UPPERALPHA, :LOWERALPHA then
      start = case list.type
              when :NUMBER     then 1
              when :UPPERALPHA then 'A'
              when :LOWERALPHA then 'a'
              end

      prefixer = proc do |ignored|
        res = @indent + "#{start}.".ljust(4)
        start = start.succ
        res
      end

    when :LABELED, :NOTE then
      longest = 0

      list.contents.each do |item|
        if RDoc::Markup::Flow::LI === item and item.label.length > longest then
          longest = item.label.length
        end
      end

      longest += 1

      prefixer = proc { |li| @indent + li.label.ljust(longest) }

    else
      raise ArgumentError, "unknown list type #{list.type}"
    end

    list.contents.each do |item|
      if RDoc::Markup::Flow::LI === item then
        prefix = prefixer.call item
        display_flow_item item, prefix
      else
        display_flow_item item
      end
    end
  end

  def display_flow_item(item, prefix = @indent)
    case item
    when RDoc::Markup::Flow::P, RDoc::Markup::Flow::LI
      wrap(conv_html(item.body), prefix)
      blankline

    when RDoc::Markup::Flow::LIST
      display_list(item)

    when RDoc::Markup::Flow::VERB
      display_verbatim_flow_item(item, @indent)

    when RDoc::Markup::Flow::H
      display_heading(conv_html(item.text), item.level, @indent)

    when RDoc::Markup::Flow::RULE
      draw_line

    else
      raise RDoc::Error, "Unknown flow element: #{item.class}"
    end
  end

  def display_verbatim_flow_item(item, prefix=@indent)
    item.body.split(/\n/).each do |line|
      @output.print @indent, conv_html(line), "\n"
    end
    blankline
  end

  def display_heading(text, level, indent)
    text = strip_attributes text

    case level
    when 1 then
      ul = "=" * text.length
      @output.puts
      @output.puts text.upcase
      @output.puts ul

    when 2 then
      ul = "-" * text.length
      @output.puts
      @output.puts text
      @output.puts ul
    else
      @output.print indent, text, "\n"
    end

    @output.puts
  end

  def display_flow(flow)
    flow.each do |f|
      display_flow_item(f)
    end
  end

  def strip_attributes(text)
    text.gsub(/(<\/?(?:b|code|em|i|tt)>)/, '')
  end

end

##
# Handle text with attributes. We're a base class: there are different
# presentation classes (one, for example, uses overstrikes to handle bold and
# underlining, while another using ANSI escape sequences.

class RDoc::RI::AttributeFormatter < RDoc::RI::Formatter

  BOLD      = 1
  ITALIC    = 2
  CODE      = 4

  ATTR_MAP = {
    "b"    => BOLD,
    "code" => CODE,
    "em"   => ITALIC,
    "i"    => ITALIC,
    "tt"   => CODE
  }

  AttrChar = Struct.new :char, :attr

  class AttributeString
    attr_reader :txt

    def initialize
      @txt = []
      @optr = 0
    end

    def <<(char)
      @txt << char
    end

    def empty?
      @optr >= @txt.length
    end

    # accept non space, then all following spaces
    def next_word
      start = @optr
      len = @txt.length

      while @optr < len && @txt[@optr].char != " "
        @optr += 1
      end

      while @optr < len && @txt[@optr].char == " "
        @optr += 1
      end

      @txt[start...@optr]
    end
  end

  ##
  # Overrides base class.  Looks for <tt>...</tt> etc sequences and generates
  # an array of AttrChars.  This array is then used as the basis for the
  # split.

  def wrap(txt, prefix=@indent, linelen=@width)
    return unless txt && !txt.empty?

    txt = add_attributes_to(txt)
    next_prefix = prefix.tr("^ ", " ")
    linelen -= prefix.size

    line = []

    until txt.empty?
      word = txt.next_word
      if word.size + line.size > linelen
        write_attribute_text(prefix, line)
        prefix = next_prefix
        line = []
      end
      line.concat(word)
    end

    write_attribute_text(prefix, line) if line.length > 0
  end

  protected

  def write_attribute_text(prefix, line)
    @output.print prefix
    line.each do |achar|
      @output.print achar.char
    end
    @output.puts
  end

  def bold_print(txt)
    @output.print txt
  end

  private

  def add_attributes_to(txt)
    tokens = txt.split(%r{(</?(?:b|code|em|i|tt)>)})
    text = AttributeString.new
    attributes = 0
    tokens.each do |tok|
      case tok
      when %r{^</(\w+)>$} then attributes &= ~(ATTR_MAP[$1]||0)
      when %r{^<(\w+)>$}  then attributes  |= (ATTR_MAP[$1]||0)
      else
        tok.split(//).each {|ch| text << AttrChar.new(ch, attributes)}
      end
    end
    text
  end

end

##
# This formatter generates overstrike-style formatting, which works with
# pagers such as man and less.

class RDoc::RI::OverstrikeFormatter < RDoc::RI::AttributeFormatter

  BS = "\C-h"

  def write_attribute_text(prefix, line)
    @output.print prefix

    line.each do |achar|
      attr = achar.attr
      @output.print "_", BS if (attr & (ITALIC + CODE)) != 0
      @output.print achar.char, BS if (attr & BOLD) != 0
      @output.print achar.char
    end

    @output.puts
  end

  ##
  # Draw a string in bold

  def bold_print(text)
    text.split(//).each do |ch|
      @output.print ch, BS, ch
    end
  end

end

##
# This formatter uses ANSI escape sequences to colorize stuff works with
# pagers such as man and less.

class RDoc::RI::AnsiFormatter < RDoc::RI::AttributeFormatter

  def initialize(*args)
    super
    @output.print "\033[0m"
  end

  def write_attribute_text(prefix, line)
    @output.print prefix
    curr_attr = 0
    line.each do |achar|
      attr = achar.attr
      if achar.attr != curr_attr
        update_attributes(achar.attr)
        curr_attr = achar.attr
      end
      @output.print achar.char
    end
    update_attributes(0) unless curr_attr.zero?
    @output.puts
  end

  def bold_print(txt)
    @output.print "\033[1m#{txt}\033[m"
  end

  HEADINGS = {
    1 => ["\033[1;32m", "\033[m"],
    2 => ["\033[4;32m", "\033[m"],
    3 => ["\033[32m",   "\033[m"],
  }

  def display_heading(text, level, indent)
    level = 3 if level > 3
    heading = HEADINGS[level]
    @output.print indent
    @output.print heading[0]
    @output.print strip_attributes(text)
    @output.puts heading[1]
  end

  private

  ATTR_MAP = {
    BOLD   => "1",
    ITALIC => "33",
    CODE   => "36"
  }

  def update_attributes(attr)
    str = "\033["
    for quality in [ BOLD, ITALIC, CODE]
      unless (attr & quality).zero?
        str << ATTR_MAP[quality]
      end
    end
    @output.print str, "m"
  end

end

##
# This formatter uses HTML.

class RDoc::RI::HtmlFormatter < RDoc::RI::AttributeFormatter

  def write_attribute_text(prefix, line)
    curr_attr = 0
    line.each do |achar|
      attr = achar.attr
      if achar.attr != curr_attr
        update_attributes(curr_attr, achar.attr)
        curr_attr = achar.attr
      end
      @output.print(escape(achar.char))
    end
    update_attributes(curr_attr, 0) unless curr_attr.zero?
  end

  def draw_line(label=nil)
    if label != nil
      bold_print(label)
    end
    @output.puts("<hr>")
  end

  def bold_print(txt)
    tag("b") { txt }
  end

  def blankline()
    @output.puts("<p>")
  end

  def break_to_newline
    @output.puts("<br>")
  end

  def display_heading(text, level, indent)
    level = 4 if level > 4
    tag("h#{level}") { text }
    @output.puts
  end

  def display_list(list)
    case list.type
    when :BULLET then
      list_type = "ul"
      prefixer = proc { |ignored| "<li>" }

    when :NUMBER, :UPPERALPHA, :LOWERALPHA then
      list_type = "ol"
      prefixer = proc { |ignored| "<li>" }

    when :LABELED then
      list_type = "dl"
      prefixer = proc do |li|
          "<dt><b>" + escape(li.label) + "</b><dd>"
      end

    when :NOTE then
      list_type = "table"
      prefixer = proc do |li|
          %{<tr valign="top"><td>#{li.label.gsub(/ /, '&nbsp;')}</td><td>}
      end
    else
      fail "unknown list type"
    end

    @output.print "<#{list_type}>"
    list.contents.each do |item|
      if item.kind_of? RDoc::Markup::Flow::LI
        prefix = prefixer.call(item)
        @output.print prefix
        display_flow_item(item, prefix)
      else
        display_flow_item(item)
      end
    end
    @output.print "</#{list_type}>"
  end

  def display_verbatim_flow_item(item, prefix=@indent)
    @output.print("<pre>")
    item.body.split(/\n/).each do |line|
      @output.puts conv_html(line)
    end
    @output.puts("</pre>")
  end

  private

  ATTR_MAP = {
    BOLD   => "b>",
    ITALIC => "i>",
    CODE   => "tt>"
  }

  def update_attributes(current, wanted)
    str = ""
    # first turn off unwanted ones
    off = current & ~wanted
    for quality in [ BOLD, ITALIC, CODE]
      if (off & quality) > 0
        str << "</" + ATTR_MAP[quality]
      end
    end

    # now turn on wanted
    for quality in [ BOLD, ITALIC, CODE]
      unless (wanted & quality).zero?
        str << "<" << ATTR_MAP[quality]
      end
    end
    @output.print str
  end

  def tag(code)
    @output.print("<#{code}>")
    @output.print(yield)
    @output.print("</#{code}>")
  end

  def escape(str)
    str = str.gsub(/&/n, '&amp;')
    str.gsub!(/\"/n, '&quot;')
    str.gsub!(/>/n, '&gt;')
    str.gsub!(/</n, '&lt;')
    str
  end

end

##
# This formatter reduces extra lines for a simpler output.  It improves way
# output looks for tools like IRC bots.

class RDoc::RI::SimpleFormatter < RDoc::RI::Formatter

  ##
  # No extra blank lines

  def blankline
  end

  ##
  # Display labels only, no lines

  def draw_line(label=nil)
    unless label.nil? then
      bold_print(label)
      @output.puts
    end
  end

  ##
  # Place heading level indicators inline with heading.

  def display_heading(text, level, indent)
    text = strip_attributes(text)
    case level
    when 1
      @output.puts "= " + text.upcase
    when 2
      @output.puts "-- " + text
    else
      @output.print indent, text, "\n"
    end
  end

end

RDoc::RI::Formatter::FORMATTERS['plain']  = RDoc::RI::Formatter
RDoc::RI::Formatter::FORMATTERS['simple'] = RDoc::RI::SimpleFormatter
RDoc::RI::Formatter::FORMATTERS['bs']     = RDoc::RI::OverstrikeFormatter
RDoc::RI::Formatter::FORMATTERS['ansi']   = RDoc::RI::AnsiFormatter
RDoc::RI::Formatter::FORMATTERS['html']   = RDoc::RI::HtmlFormatter
