require 'rdoc/generator'
require 'rdoc/markup/to_flow'

require 'rdoc/ri/cache'
require 'rdoc/ri/reader'
require 'rdoc/ri/writer'
require 'rdoc/ri/descriptions'

class RDoc::Generator::RI

  ##
  # Generator may need to return specific subclasses depending on the
  # options they are passed. Because of this we create them using a factory

  def self.for(options)
    new(options)
  end

  class << self
    protected :new
  end

  ##
  # Set up a new RDoc::Generator::RI.

  def initialize(options) #:not-new:
    @options   = options
    @ri_writer = RDoc::RI::Writer.new "."
    @markup    = RDoc::Markup.new
    @to_flow   = RDoc::Markup::ToFlow.new

    @generated = {}
  end

  ##
  # Build the initial indices and output objects based on an array of
  # TopLevel objects containing the extracted information.

  def generate(toplevels)
    RDoc::TopLevel.all_classes_and_modules.each do |cls|
      process_class cls
    end
  end

  def process_class(from_class)
    generate_class_info(from_class)

    # now recure into this classes constituent classess
    from_class.each_classmodule do |mod|
      process_class(mod)
    end
  end

  def generate_class_info(cls)
    if cls === RDoc::NormalModule
      cls_desc = RDoc::RI::ModuleDescription.new
    else
      cls_desc = RDoc::RI::ClassDescription.new
      cls_desc.superclass  = cls.superclass
    end

    cls_desc.name        = cls.name
    cls_desc.full_name   = cls.full_name
    cls_desc.comment     = markup(cls.comment)

    cls_desc.attributes = cls.attributes.sort.map do |a|
      RDoc::RI::Attribute.new(a.name, a.rw, markup(a.comment))
    end

    cls_desc.constants = cls.constants.map do |c|
      RDoc::RI::Constant.new(c.name, c.value, markup(c.comment))
    end

    cls_desc.includes = cls.includes.map do |i|
      RDoc::RI::IncludedModule.new(i.name)
    end

    class_methods, instance_methods = method_list(cls)

    cls_desc.class_methods = class_methods.map do |m|
      RDoc::RI::MethodSummary.new(m.name)
    end

    cls_desc.instance_methods = instance_methods.map do |m|
      RDoc::RI::MethodSummary.new(m.name)
    end

    update_or_replace(cls_desc)

    class_methods.each do |m|
      generate_method_info(cls_desc, m)
    end

    instance_methods.each do |m|
      generate_method_info(cls_desc, m)
    end
  end

  def generate_method_info(cls_desc, method)
    meth_desc = RDoc::RI::MethodDescription.new
    meth_desc.name = method.name
    meth_desc.full_name = cls_desc.full_name
    if method.singleton
      meth_desc.full_name += "::"
    else
      meth_desc.full_name += "#"
    end
    meth_desc.full_name << method.name

    meth_desc.comment = markup(method.comment)
    meth_desc.params = params_of(method)
    meth_desc.visibility = method.visibility.to_s
    meth_desc.is_singleton = method.singleton
    meth_desc.block_params = method.block_params

    meth_desc.aliases = method.aliases.map do |a|
      RDoc::RI::AliasName.new(a.name)
    end

    @ri_writer.add_method(cls_desc, meth_desc)
  end

  private

  ##
  # Returns a list of class and instance methods that we'll be documenting

  def method_list(cls)
    list = cls.method_list
    unless @options.show_all
      list = list.find_all do |m|
        m.visibility == :public || m.visibility == :protected || m.force_documentation
      end
    end

    c = []
    i = []
    list.sort.each do |m|
      if m.singleton
        c << m
      else
        i << m
      end
    end
    return c,i
  end

  def params_of(method)
    if method.call_seq
      method.call_seq
    else
      params = method.params || ""

      p = params.gsub(/\s*\#.*/, '')
      p = p.tr("\n", " ").squeeze(" ")
      p = "(" + p + ")" unless p[0] == ?(

      if (block = method.block_params)
        block.gsub!(/\s*\#.*/, '')
        block = block.tr("\n", " ").squeeze(" ")
        if block[0] == ?(
          block.sub!(/^\(/, '').sub!(/\)/, '')
        end
        p << " {|#{block.strip}| ...}"
      end
      p
    end
  end

  def markup(comment)
    return nil if !comment || comment.empty?

    # Convert leading comment markers to spaces, but only
    # if all non-blank lines have them

    if comment =~ /^(?>\s*)[^\#]/
      content = comment
    else
      content = comment.gsub(/^\s*(#+)/)  { $1.tr('#',' ') }
    end
    @markup.convert(content, @to_flow)
  end

  ##
  # By default we replace existing classes with the same name. If the
  # --merge option was given, we instead merge this definition into an
  # existing class. We add our methods, aliases, etc to that class, but do
  # not change the class's description.

  def update_or_replace(cls_desc)
    old_cls = nil

    if @options.merge
      rdr = RDoc::RI::Reader.new RDoc::RI::Cache.new(@options.op_dir)

      namespace = rdr.top_level_namespace
      namespace = rdr.lookup_namespace_in(cls_desc.name, namespace)
      if namespace.empty?
        $stderr.puts "You asked me to merge this source into existing "
        $stderr.puts "documentation. This file references a class or "
        $stderr.puts "module called #{cls_desc.name} which I don't"
        $stderr.puts "have existing documentation for."
        $stderr.puts
        $stderr.puts "Perhaps you need to generate its documentation first"
        exit 1
      else
        old_cls = namespace[0]
      end
    end

    prev_cls = @generated[cls_desc.full_name]

    if old_cls and not prev_cls then
      old_desc = rdr.get_class old_cls
      cls_desc.merge_in old_desc
    end

    if prev_cls then
      cls_desc.merge_in prev_cls
    end

    @generated[cls_desc.full_name] = cls_desc

    @ri_writer.remove_class cls_desc
    @ri_writer.add_class cls_desc
  end

end

