RI_VERSION = "0.7"


###########################################################################
USAGE = <<_EOU_

usage:
      #$0 [opts] name ...

 opts:
      --version, -v           display version and exit
      --line-length, -l nn    set the line length for the output 
                              (minimum 30 chars)
      --synopsis,    -s       display just a synopsis
      --format, -f name       use the 'name' module (default 'Plain')
                              for output formatting. Available modules:
!MODULELIST!

 name:
      Class | Class::method | Class#method | Class.method | method


'ri' displays documentation for the named classes or methods. All names 
can be abbreviated to their minimum non-ambiguous size.
_EOU_
############################################################################


require 'ri/refdoc.rb'
require 'rbconfig'

include Config

$sitedir = File.join(CONFIG["sitedir"], CONFIG["MAJOR"] + "." + CONFIG["MINOR"])
$datadir = File.join($sitedir, "ri")
$opdir   = File.join($datadir, "op")


def moduleList()
  res = ""
  leader = "                                  "
  begin
    Dir.foreach($opdir) do |name|
      next unless name =~ /(.*)\.rb$/
      require File.join("ri/op", $1)
      klass = eval $1
      modDesc = klass::desc
      res << leader << $1 << ":  " << modDesc << "\n"
    end
  rescue
    puts $!
    res = leader + "no modules found..."
  end
  res
end

##
# Manage the list of known classes
#

class ClassIndex
  @@classes = nil

  def ClassIndex.loadClasses
    Dir.open($datadir) do |dir|
      @@classes = dir.grep(/^[A-Z]/)
    end
  end

  def ClassIndex.classes
    ClassIndex.loadClasses unless @@classes
    @@classes
  end

  def ClassIndex.findClasses(name)
    ClassIndex.loadClasses unless @@classes
    name = Regexp.escape(name)
    @@classes.grep(/^#{name}/)
  end
end

##
# Manage the index mapping method names to classes
#

class MethodIndex
  @@index = nil
  @@names = nil

  def MethodIndex.loadIndex
    begin
      File.open(File.join($datadir, "index")) do |f|
        @@index = Marshal.load(f)
        @@names = @@index.names
      end
    rescue
      @@index = {}
    end
  end

  # If the given name exactly matches a known method, return an
  # array of classes implementing that method, otherwise
  # use the name as a glob and search for matches
  def MethodIndex.findMethods(name)
    MethodIndex.loadIndex unless @@index
    res = @@index[name]
    if !res
      name = Regexp.escape(name)
      nameList = @@names.grep(/^#{name}/)
      res = []
      nameList.each {|n| res.concat @@index[n]}
      res.sort
    end
    res
  end
end

##
# Remove the simple HTML markup in the text.
#
def stripFormatting(str)
  res = str.dup
  res.gsub!("<br></br>", "\n")
  1 while res.gsub!(/<([^>]+)>(.*?)<\/\1>/m, '\2')
  res
end


class RI

  attr_accessor :synopsis

  def initialize
    @synopsis = false
  end

  def version
    RI_VERSION
  end

  def setOutputFormatter(op)
    @op = op
  end

  ##
  # Read in a serialized class description
  #
  def findClass(name)
    fname = name.tr(':', '_')
    
    # First, assume it's a non-abbreviated name
    begin
      File.open(File.join($datadir, fname)) do |f|
        return Marshal.load(f)
      end
    rescue
      cl = ClassIndex.findClasses(name)
      
      case cl.size
      when 0
        @op.error("Couldn't find class/module `#{name}'.\n" +
                  "Use #$0 with no parameter for a list")
        throw(:exit, 1)
      when 1
        fname = cl[0]
        retry
      else
        return cl
      end  
    end
  end
  
  ##
  # Print a list of fragments in a nice pretty way
  #
  
  def printFragments(source)

    @op.putFragmentBlock do 
      source.eachFragment do |f|
        
        case f
        when Verbatim
          @op.putVerbatim(f.to_s)
          
        when Paragraph
          @op.putParagraph(f.to_s)
        end
      end
    end
  end
  
  
  ##
  # Print a simple list of classes and exit
  #
  def usage

    unless @synopsis
      @op.error(USAGE.sub(/!MODULELIST!/, moduleList()))
      
      @op.error("\nI have documentation for the classes and modules:\n\n")
    end

    names = ClassIndex.classes
    if names.size.zero?
      @op.error("Configuration error: could not find class list")
    else
      @op.putMethodList(names)
    end
    throw :exit, 2
  end
  
  ##
  # The user asked for X.y, and the 'X' part matches more than
  # one class. For each, find all potential matching methods
  # and report on each
  
  def matchMethodsInClasses(classList, type, mname)
    res = []
    
    for cname in classList
      cl = findClass(cname)
      meths = cl.findMethods(mname, type == "::")
      meths.each {|m| res << "#{cname}#{m.type=='class'?'::':'#'}#{m.name}" }
    end
    
    @op.putListOfMethodsMatchingName(mname) do
      @op.putMethodList(res)
    end
  end
  
  
  ##
  #
  # Describe a method in a known class
  #
  
  def describeMethod(cname, type, mname)
    cl = findClass(cname)
    
    # If the class name part is ambiguous, then we have a join to
    # do
    
    case cl
    when Array
      matchMethodsInClasses(cl, type, mname)
      
    when ClassModule
      meths = cl.findMethods(mname, type == "::")
      
      case meths.size
        
      when 0
        @op.error("Cannot find method `#{cname}#{type}#{mname}'")
        throw :exit, 3
        
      when 1
        meth = meths[0]
        @op.putMethodDescription do
          @op.putMethodHeader(cl.name, meth.typeAsSeparator, meth.name, meth.callseq)
          printFragments(meth) unless @synopsis
        end
        
      else
        @op.putListOfMethodsMatchingName(cl.name + type + mname) do
          @op.putMethodList(meths.collect {|m| 
                              "#{cl.name}#{m.typeAsSeparator}#{m.name}" 
                            })
        end
      end
    end
  end
  
  ##
  # Produce a class description
  #
  
  def describeClass(name)
    cl = findClass(name)
    
    case cl
      
    when Array
      @op.putListOfClassesMatchingName(name) do
        @op.putMethodList(cl)
      end

    when ClassModule
      @op.putClassDescription do 
        @op.putClassHeader(cl.type, cl.name, cl.super, cl.subclasses) unless @synopsis
        printFragments(cl) unless @synopsis
        @op.putClassMethods(cl.methods.collect{|m| m.name})
      end
    end
  end
  
  ##
  # Find a method given it's name, and then describe it
  #
  
  def findAndDescribe(name)
    methods = MethodIndex.findMethods(name)
    
    if methods.size.zero?
      @op.error("Don't know anything about a method called `#{name}'.")
      throw :exit, 4
    end
    
    if methods.size == 1
      methods[0] =~ /^([A-Z]\w*)(\.|\#|::)(.+)/
      describeMethod($1, $2, $3)
    else
      @op.putListOfMethodsMatchingName(name) do
        @op.putMethodList(methods)
      end
    end
  end


  # With no parameters, list know classes and modules

  def handle(args)

    return catch(:exit) do
      if args.size.zero?
        usage
      end
      
      cnPattern = '[A-Z]\w*(::[A-Z]\w+)?'
      
      args.each do |name|
        
        case name
          
        when /^#{cnPattern}$/o
          describeClass(name)
          
        when /^(#{cnPattern})(\.|\#|::)(.+)/o
          describeMethod($1, $3, $4)
          
        else
          findAndDescribe(name)
        end
      end

      return 0                  # normal exit
    end
  end

end


###################################################################

# Local Variables:
# mode: ruby
# End:
