# frozen_string_literal: true
##
# Outputs RDoc markup as RDoc markup! (mostly)

class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter
  DEFAULT_HEADINGS = {
      1 => ['= ',      ''],
      2 => ['== ',     ''],
      3 => ['=== ',    ''],
      4 => ['==== ',   ''],
      5 => ['===== ',  ''],
      6 => ['====== ', '']
  }
  DEFAULT_HEADINGS.default = []
  DEFAULT_HEADINGS.freeze

  ##
  # Current indent amount for output in characters

  attr_accessor :indent

  ##
  # Output width in characters

  attr_accessor :width

  ##
  # Stack of current list indexes for alphabetic and numeric lists

  attr_reader :list_index

  ##
  # Stack of list types

  attr_reader :list_type

  ##
  # Stack of list widths for indentation

  attr_reader :list_width

  ##
  # Prefix for the next list item.  See #use_prefix

  attr_reader :prefix

  ##
  # Output accumulator

  attr_reader :res

  ##
  # Creates a new formatter that will output (mostly) \RDoc markup

  def initialize
    super

    @markup.add_regexp_handling(/\\\S/, :SUPPRESSED_CROSSREF)
    @width = 78

    @headings = DEFAULT_HEADINGS.dup
    @hard_break = "\n"
  end

  ##
  # Adds +blank_line+ to the output

  def accept_blank_line(blank_line)
    @res << "\n"
  end

  ##
  # Adds +paragraph+ to the output

  def accept_block_quote(block_quote)
    @indent += 2

    block_quote.parts.each do |part|
      @prefix = '> '

      part.accept self
    end

    @indent -= 2
  end

  ##
  # Adds +heading+ to the output

  def accept_heading(heading)
    use_prefix or @res << ' ' * @indent
    @res << @headings[heading.level][0]
    @res << attributes(heading.text)
    @res << @headings[heading.level][1]
    @res << "\n"
  end

  ##
  # Finishes consumption of +list+

  def accept_list_end(list)
    @list_index.pop
    @list_type.pop
    @list_width.pop
  end

  ##
  # Finishes consumption of +list_item+

  def accept_list_item_end(list_item)
    width = case @list_type.last
            when :BULLET then
              2
            when :NOTE, :LABEL then
              if @prefix then
                @res << @prefix.strip
                @prefix = nil
              end

              @res << "\n"
              2
            else
              bullet = @list_index.last.to_s
              @list_index[-1] = @list_index.last.succ
              bullet.length + 2
            end

    @indent -= width
  end

  ##
  # Prepares the visitor for consuming +list_item+

  def accept_list_item_start(list_item)
    type = @list_type.last

    case type
    when :NOTE, :LABEL then
      stripped_labels = Array(list_item.label).map do |label|
        attributes(label).strip
      end

      bullets = case type
      when :NOTE
        stripped_labels.map { |b| "#{b}::" }
      when :LABEL
        stripped_labels.map { |b| "[#{b}]" }
      end

      bullets = bullets.join("\n")
      bullets << "\n" unless stripped_labels.empty?

      @prefix = ' ' * @indent
      @indent += 2
      @prefix << bullets + (' ' * @indent)
    else
      bullet = type == :BULLET ? '*' :  @list_index.last.to_s + '.'
      @prefix = (' ' * @indent) + bullet.ljust(bullet.length + 1)
      width = bullet.length + 1
      @indent += width
    end
  end

  ##
  # Prepares the visitor for consuming +list+

  def accept_list_start(list)
    case list.type
    when :BULLET then
      @list_index << nil
      @list_width << 1
    when :LABEL, :NOTE then
      @list_index << nil
      @list_width << 2
    when :LALPHA then
      @list_index << 'a'
      @list_width << list.items.length.to_s.length
    when :NUMBER then
      @list_index << 1
      @list_width << list.items.length.to_s.length
    when :UALPHA then
      @list_index << 'A'
      @list_width << list.items.length.to_s.length
    else
      raise RDoc::Error, "invalid list type #{list.type}"
    end

    @list_type << list.type
  end

  ##
  # Adds +paragraph+ to the output

  def accept_paragraph(paragraph)
    text = paragraph.text @hard_break
    wrap attributes text
  end

  ##
  # Adds +paragraph+ to the output

  def accept_indented_paragraph(paragraph)
    @indent += paragraph.indent
    text = paragraph.text @hard_break
    wrap attributes text
    @indent -= paragraph.indent
  end

  ##
  # Adds +raw+ to the output

  def accept_raw(raw)
    @res << raw.parts.join("\n")
  end

  ##
  # Adds +rule+ to the output

  def accept_rule(rule)
    use_prefix or @res << ' ' * @indent
    @res << '-' * (@width - @indent)
    @res << "\n"
  end

  ##
  # Outputs +verbatim+ indented 2 columns

  def accept_verbatim(verbatim)
    indent = ' ' * (@indent + 2)

    verbatim.parts.each do |part|
      @res << indent unless part == "\n"
      @res << part
    end

    @res << "\n"
  end

  ##
  # Adds +table+ to the output

  def accept_table(header, body, aligns)
    header = header.map { |h| attributes h }
    body = body.map { |row| row.map { |t| attributes t } }
    widths = header.zip(*body).map do |cols|
      cols.compact.map { |col| calculate_text_width(col) }.max
    end
    aligns = aligns.map do |a|
      case a
      when nil, :center
        :center
      when :left
        :ljust
      when :right
        :rjust
      end
    end
    @res << header.zip(widths, aligns).map do |h, w, a|
      extra_width = h.size - calculate_text_width(h)
      h.__send__(a, w + extra_width)
    end.join("|").rstrip << "\n"
    @res << widths.map {|w| "-" * w }.join("|") << "\n"
    body.each do |row|
      @res << widths.zip(aligns).each_with_index.map do |(w, a), i|
        t = row[i] || ""
        extra_width = t.size - calculate_text_width(t)
        t.__send__(a, w + extra_width)
      end.join("|").rstrip << "\n"
    end
  end

  def calculate_text_width(text)
    text.size
  end

  def handle_PLAIN_TEXT(text)
    add_text(text)
  end

  def handle_REGEXP_HANDLING_TEXT(text)
    add_text(text)
  end

  def handle_BOLD(target)
    on(:BOLD)
    super
    off(:BOLD)
  end

  def handle_EM(target)
    on(:EM)
    super
    off(:EM)
  end

  def handle_BOLD_WORD(word)
    on(:BOLD)
    super
    off(:BOLD)
  end

  def handle_EM_WORD(word)
    on(:EM)
    super
    off(:EM)
  end

  def handle_TT(code)
    on(:TT)
    super
    off(:TT)
  end

  def handle_STRIKE(target)
    on(:STRIKE)
    super
    off(:STRIKE)
  end

  def handle_HARD_BREAK
    add_text("\n")
  end

  def handle_TIDYLINK(label_part, url)
    super
    add_text("( #{url} )")
  end

  def handle_inline(text, initial_attributes = [])
    @attributes = Hash.new(0)
    initial_attributes.each { |attr| on(attr) }
    out = @inline_output = +''
    super(text)
    @inline_output = nil
    out
  end

  def on(attr)
    @attributes[attr] += 1
  end

  def off(attr)
    @attributes[attr] -= 1
    @attributes.delete(attr) if @attributes[attr] == 0
  end

  def add_text(text)
    emit_inline(text)
  end

  def emit_inline(text)
    @inline_output << text
  end

  ##
  # Applies attribute-specific markup to +text+ using RDoc::Markup::InlineParser

  def attributes(text)
    handle_inline(text)
  end

  ##
  # Returns the generated output

  def end_accepting
    @res.join
  end

  ##
  # Removes preceding \\ from the suppressed crossref +target+

  def handle_regexp_SUPPRESSED_CROSSREF(text)
    text.sub('\\', '')
  end

  ##
  # Prepares the visitor for text generation

  def start_accepting
    @res = [""]
    @indent = 0
    @prefix = nil

    @list_index = []
    @list_type  = []
    @list_width = []
  end

  ##
  # Adds the stored #prefix to the output and clears it.  Lists generate a
  # prefix for later consumption.

  def use_prefix
    prefix, @prefix = @prefix, nil
    @res << prefix if prefix

    prefix
  end

  ##
  # Wraps +text+ to #width

  def wrap(text)
    return unless text && !text.empty?

    text_len = @width - @indent

    text_len = 20 if text_len < 20

    next_prefix = ' ' * @indent

    prefix = @prefix || next_prefix
    @prefix = nil

    text.scan(/\G(?:([^ \n]{#{text_len}})(?=[^ \n])|(.{1,#{text_len}})(?:[ \n]|\z))/) do
      @res << prefix << ($1 || $2) << "\n"
      prefix = next_prefix
    end
  end

end
