require 'rdoc/markup'
require 'rdoc/markup/lines'

class RDoc::Markup

  ##
  # A Fragment is a chunk of text, subclassed as a paragraph, a list
  # entry, or verbatim text.

  class Fragment
    attr_reader   :level, :param, :txt
    attr_accessor :type

    ######
    # This is a simple factory system that lets us associate fragement
    # types (a string) with a subclass of fragment

    TYPE_MAP = {}

    def self.type_name(name)
      TYPE_MAP[name] = self
    end

    def self.for(line)
      klass =  TYPE_MAP[line.type] ||
        raise("Unknown line type: '#{line.type.inspect}:' '#{line.text}'")
      return klass.new(line.level, line.param, line.flag, line.text)
    end

    def initialize(level, param, type, txt)
      @level = level
      @param = param
      @type  = type
      @txt   = ""
      add_text(txt) if txt
    end

    def add_text(txt)
      @txt << " " if @txt.length > 0
      @txt << txt.tr_s("\n ", "  ").strip
    end

    def to_s
      "L#@level: #{self.class.name.split('::')[-1]}\n#@txt"
    end

  end

  ##
  # A paragraph is a fragment which gets wrapped to fit. We remove all
  # newlines when we're created, and have them put back on output.

  class Paragraph < Fragment
    type_name :PARAGRAPH
  end

  class BlankLine < Paragraph
    type_name :BLANK
  end

  class Heading < Paragraph
    type_name :HEADING

    def head_level
      @param.to_i
    end
  end

  ##
  # A List is a fragment with some kind of label

  class ListBase < Paragraph
    LIST_TYPES = [
      :BULLET,
      :NUMBER,
      :UPPERALPHA,
      :LOWERALPHA,
      :LABELED,
      :NOTE,
    ]
  end

  class ListItem < ListBase
    type_name :LIST

    def to_s
      text = if [:NOTE, :LABELED].include? type then
               "#{@param}: #{@txt}"
             else
               @txt
             end

      "L#@level: #{type} #{self.class.name.split('::')[-1]}\n#{text}"
    end

  end

  class ListStart < ListBase
    def initialize(level, param, type)
      super(level, param, type, nil)
    end
  end

  class ListEnd < ListBase
    def initialize(level, type)
      super(level, "", type, nil)
    end
  end

  ##
  # Verbatim code contains lines that don't get wrapped.

  class Verbatim < Fragment
    type_name  :VERBATIM

    def add_text(txt)
      @txt << txt.chomp << "\n"
    end

  end

  ##
  # A horizontal rule

  class Rule < Fragment
    type_name :RULE
  end

  ##
  # Collect groups of lines together. Each group will end up containing a flow
  # of text.

  class LineCollection

    def initialize
      @fragments = []
    end

    def add(fragment)
      @fragments << fragment
    end

    def each(&b)
      @fragments.each(&b)
    end

    def to_a # :nodoc:
      @fragments.map {|fragment| fragment.to_s}
    end

    ##
    # Factory for different fragment types

    def fragment_for(*args)
      Fragment.for(*args)
    end

    ##
    # Tidy up at the end

    def normalize
      change_verbatim_blank_lines
      add_list_start_and_ends
      add_list_breaks
      tidy_blank_lines
    end

    def to_s
      @fragments.join("\n----\n")
    end

    def accept(am, visitor)
      visitor.start_accepting

      @fragments.each do |fragment|
        case fragment
        when Verbatim
          visitor.accept_verbatim(am, fragment)
        when Rule
          visitor.accept_rule(am, fragment)
        when ListStart
          visitor.accept_list_start(am, fragment)
        when ListEnd
          visitor.accept_list_end(am, fragment)
        when ListItem
          visitor.accept_list_item(am, fragment)
        when BlankLine
          visitor.accept_blank_line(am, fragment)
        when Heading
          visitor.accept_heading(am, fragment)
        when Paragraph
          visitor.accept_paragraph(am, fragment)
        end
      end

      visitor.end_accepting
    end

    private

    # If you have:
    #
    #    normal paragraph text.
    #
    #       this is code
    #   
    #       and more code
    #
    # You'll end up with the fragments Paragraph, BlankLine, Verbatim,
    # BlankLine, Verbatim, BlankLine, etc.
    #
    # The BlankLine in the middle of the verbatim chunk needs to be changed to
    # a real verbatim newline, and the two verbatim blocks merged

    def change_verbatim_blank_lines
      frag_block = nil
      blank_count = 0
      @fragments.each_with_index do |frag, i|
        if frag_block.nil?
          frag_block = frag if Verbatim === frag
        else
          case frag
          when Verbatim
            blank_count.times { frag_block.add_text("\n") }
            blank_count = 0
            frag_block.add_text(frag.txt)
            @fragments[i] = nil    # remove out current fragment
          when BlankLine
            if frag_block
              blank_count += 1
              @fragments[i] = nil
            end
          else
            frag_block = nil
            blank_count = 0
          end
        end
      end
      @fragments.compact!
    end

    ##
    # List nesting is implicit given the level of indentation. Make it
    # explicit, just to make life a tad easier for the output processors

    def add_list_start_and_ends
      level = 0
      res = []
      type_stack = []

      @fragments.each do |fragment|
        # $stderr.puts "#{level} : #{fragment.class.name} : #{fragment.level}"
        new_level = fragment.level
        while (level < new_level)
          level += 1
          type = fragment.type
          res << ListStart.new(level, fragment.param, type) if type
          type_stack.push type
          # $stderr.puts "Start: #{level}"
        end

        while level > new_level
          type = type_stack.pop
          res << ListEnd.new(level, type) if type
          level -= 1
          # $stderr.puts "End: #{level}, #{type}"
        end

        res << fragment
        level = fragment.level
      end
      level.downto(1) do |i|
        type = type_stack.pop
        res << ListEnd.new(i, type) if type
      end

      @fragments = res
    end

    ##
    # Inserts start/ends between list entries at the same level that have
    # different element types

    def add_list_breaks
      res = @fragments

      @fragments = []
      list_stack = []

      res.each do |fragment|
        case fragment
        when ListStart
          list_stack.push fragment
        when ListEnd
          start = list_stack.pop
          fragment.type = start.type
        when ListItem
          l = list_stack.last
          if fragment.type != l.type
            @fragments << ListEnd.new(l.level, l.type)
            start = ListStart.new(l.level, fragment.param, fragment.type)
            @fragments << start
            list_stack.pop
            list_stack.push start
          end
        else
          ;
        end
        @fragments << fragment
      end
    end

    ##
    # Tidy up the blank lines:
    # * change Blank/ListEnd into ListEnd/Blank
    # * remove blank lines at the front

    def tidy_blank_lines
      (@fragments.size - 1).times do |i|
        if BlankLine === @fragments[i] and ListEnd === @fragments[i+1] then
          @fragments[i], @fragments[i+1] = @fragments[i+1], @fragments[i]
        end
      end

      # remove leading blanks
      @fragments.each_with_index do |f, i|
        break unless f.kind_of? BlankLine
        @fragments[i] = nil
      end

      @fragments.compact!
    end

  end

end

