require 'rdoc/ri/ri_driver'
require 'rexml/document'

# Ri bindings for interactive use from within Ruby.
# Does a bit of second-guessing (Instance method? Class method? 
# Try both unless explicitly defined. Not found in this class? Try the 
# ancestor classes.)
#
# Goal is that help is given for all methods that have help.
#
# Examples:
#
#   require 'ihelp'
#
#   a = "string"
#   a.help
#   a.help :reverse
#   a.help :map
#   String.help
#   String.help :new
#   String.help :reverse
#   String.help :map
#   String.instance_help :reverse
#   String.instance_help :new # => No help found.
#   a.help :new
#   help "String#reverse"
#   help "String.reverse"
#   a.method(:reverse).help # gets help for Method
#   help "Hash#map"
#
# Custom help renderers:
#
# The help-method calls IHelp::Renderer's method defined by
# IHelp.renderer with the RI info object. You can print help
# out the way you want by defining your own renderer method
# in IHelp::Renderer and setting IHelp.renderer to the name
# of the method.
#
# Example:
# 
#   require 'ihelp'
#
#   class IHelp::Renderer
#     def print_name(info)
#       puts info.full_name
#     end
#   end
#
#   IHelp.renderer = :print_name
#
#   [1,2,3].help:reject
#   # Array#reject
#   # => nil
#
# The current renderers are:
#
#   ri -- the default renderer
#   html -- creates a HTML document for the help and opens it
#           with the program named in IHelp::WWW_BROWSER
#   rubydoc_org -- opens the corresponding www.ruby-doc.org class 
#                  documentation page with the program named in
#                  IHelp::WWW_BROWSER
#   rubytoruby_src -- uses RubyToRuby to print the source for the method
#                     (highly experimental)
#
#
# License: Ruby's
#
# Author: Ilmari Heikkinen <kig misfiring net>
#
module IHelp
  HELP_VERSION = "0.3.1"
end


module IHelp

  IHelp::WWW_BROWSER = 'firefox'  

  # Contains the help renderer methods to be used by IHelp#help.
  # The help-method creates a new instance of Renderer and calls
  # the method defined by IHelp.renderer with the RI info object.
  #
  class Renderer
  
    # Default renderer method, opens the help using the IHelpDriver
    # gotten from IHelp.ri_driver.
    #
    def ri(info) 
      IHelp.ri_driver.display_info(info) 
    end
    
    # Opens the class documentation page on www.ruby-doc.org using
    # the program defined in IHelp::WWW_BROWSER.
    #
    def rubydoc_org(info)
      require 'uri'
      class_name = parse_ruby_doc_url(info.full_name)
      puts "Opening help for: #{class_name.gsub(/\//,"::")}"
      system("#{IHelp::WWW_BROWSER} 'http://www.ruby-doc.org/core/classes/#{class_name}.html'")
    end

    # Experimental show sources -renderer using RubyToRuby. 
    # Prints the generated source with puts.
    # See http://blog.zenspider.com/archives/2005/02/rubytoruby.html
    # for more info about RubyToRuby
    #
    def rubytoruby_src(info)
      require 'rubytoruby'
      class_name = info.full_name.split(/[#\.]/).first
      klass = class_name.split("::").inject(Object){|o,i| o.const_get(i)}
      args = [klass]
      args << info.name if info.is_a? RI::MethodDescription
      puts RubyToRuby.translate(*args)
    end
    
    def html(info)
      puts "Opening help for: #{info.full_name}"
      doc = REXML::Document.new
      root = doc.add_element("html")
      head = root.add_element("head")
      title = head.add_element("title")
      title.add_text("#{info.full_name} - RI Documentation")
      body = root.add_element("body")
      body.add_element(info.to_html.root)
      tmp = Tempfile.new("#{info.full_name.gsub(/\W/,"_")}_doc.html")
      tmp.write( doc.to_s(2) )
      tmp.flush
      pid = fork{
        system("#{IHelp::WWW_BROWSER} 'file://#{tmp.path}'")
        tmp.close!
      }
      Process.detach(pid)
      pid
    end

  private
    def parse_ruby_doc_url(item_name)
      item_name.split(/\.|#/,2).first.gsub(/::/,"/")
    end
    
  end

      
  # Print out help for self.
  #
  #   If method_name is given, prints help for that method.
  #   If instance is true, tries to find help only for the instance method.
  #   If instance is false, tries to find help for the object's method only.
  #   If instance is nil, checks object's method first, then instance method.
  #
  # Uses help_description(method_name, instance).
  #
  def help(method_name=nil, instance=nil)
    info = help_description(method_name, instance)
    if not info
      puts "No help found."
      return
    end 
    IHelp.render(info)
  end

  # Print out help for instance method method_name.
  # If no method_name given, behaves like #help.
  #
  def instance_help(method_name = nil)
    help(method_name, true)
  end

  # Returns help string in YAML for self.
  #
  #   If method_name is given, prints help for that method.
  #   If instance is true, tries to find help only for the instance method.
  #   If instance is false, tries to find help for the object's method only.
  #   If instance is nil, checks object's method first, then instance method.
  #   Returns nil if there is no help to be found.
  #
  def help_yaml(method_name=nil, instance=nil)
    info = help_description(method_name, instance)
    info.to_yaml if info
  end
  
  # Returns help string as a HTML REXML::Document with a DIV element as the root.
  #
  #   If method_name is given, prints help for that method.
  #   If instance is true, tries to find help only for the instance method.
  #   If instance is false, tries to find help for the object's method only.
  #   If instance is nil, checks object's method first, then instance method.
  #   Returns nil if there is no help to be found.
  #
  def help_html(method_name=nil, instance=nil)
    info = help_description(method_name, instance)
    info.to_html if info
  end

  # Return RI::ClassDescription / RI::MethodDescription for self
  # or its method meth, or its instance method meth if instance == true.
  #
  def help_description(method_name=nil, instance=nil)
    IHelp.generate_help_description(self, method_name, instance)
  end


  class << self

    attr_accessor :renderer
    
    IHelp.renderer ||= :ri

    IHelp::RI_ARGS = []
    if ENV["RI"]
      IHelp::RI_ARGS = ENV["RI"].split.concat(ARGV)
    end
    
    # Render the RI info object a renderer method in IHelp::Renderer.
    # The name of the renderer method to use is returned by IHelp.renderer,
    # and can be set with IHelp.renderer=.
    #
    def render(info)
      IHelp::Renderer.new.send(renderer, info)
    end
    
    def ri_driver
      @ri_driver ||= IHelpDriver.new(RI_ARGS)
    end
    
    # Return RI::ClassDescription / RI::MethodDescription for klass
    # or its method meth, or its instance method meth if instance == true.
    #
    def generate_help_description(klass, meth=nil, instance=nil)
      meth_str = nil
      double_colon = false
      if meth
        meth_str = meth.to_s
        if /::|\.|#/ === meth_str # called with e.g."Array#str","String.new"
          meth_str, klass_name, instance_help, double_colon = 
            get_help_klass_info_for_name(meth_str)
            klass_ancs = find_ancestors(klass_name, instance)  
        else
          klass_name, klass_ancs, instance_help = 
            get_help_klass_info(klass, instance)
        end
      else
        klass_name, klass_ancs, instance_help = 
          get_help_klass_info(klass, instance)
      end
      info = get_help_info(meth_str, klass_name, klass_ancs, instance_help,
                           instance)
      # Retry with method as class if double_colon-splitted and no info
      if info.nil? and double_colon 
        klass_name = [klass_name, meth_str].join("::")
        meth_str = nil
        klass_ancs = find_ancestors(klass_name, instance)
        info = get_help_info(
                 meth_str, klass_name, klass_ancs, instance_help, instance)
      end
      info
    end
    
  private
    def get_help_klass_info(klass,instance)
        if klass.is_a? Class or klass.is_a? Module
          klass_ancs = klass.ancestors + klass.class.ancestors
          klass_ancs -= [klass, klass.class]
          instance = false if instance.nil?
        # If we are an instance, set klass to our class
        #
        else
          klass = klass.class
          klass_ancs = klass.ancestors - [klass]
          instance = true if instance.nil?
        end
        klass_name = klass.name
        [klass_name, klass_ancs, instance]
    end
  
    def get_help_klass_info_for_name(meth_str)
      double_colon = false
      # Maybe we are being called with something like "Array#slice"
      if /#/ === meth_str
        klass_name, meth_str = meth_str.split(/#/, 2)
        instance = true
  
      # Or maybe the requested item is "Ri::RiDriver.new"
      elsif /\./ === meth_str
        klass_name, meth_str = meth_str.reverse.split(/\./, 2).
                                        reverse.map{|i| i.reverse}
        instance = false
  
      # And the problematic case of "Test::Unit" (is Unit a class name or
      # a method name? Why does Ri even care?)
      else
        klass_name, meth_str = meth_str.reverse.split(/::/, 2).
                                        reverse.map{|i| i.reverse}
        double_colon = true
        instance = false
      end
      [meth_str, klass_name, instance, double_colon]
    end
  
    # Find ancestors for klass_name (works if the class has been loaded)
    def find_ancestors(klass_name, instance)
      similarily_named_class = nil
      ObjectSpace.each_object(Class){|k| 
        similarily_named_class = k if k.name == klass_name
        break if similarily_named_class
      }
      if similarily_named_class
        klass_ancs = similarily_named_class.ancestors
        klass_ancs += similarily_named_class.class.ancestors unless instance
      else
        klass_ancs = []
      end
      klass_ancs
    end
  
    def get_help_info(meth_str, klass_name, klass_ancs, instance_help, instance)
      info = get_help_info_str(meth_str, klass_name, klass_ancs, instance_help)
      # If instance is undefined, try both the class methods and instance
      # methods.
      if info.nil? and instance.nil?
        info = get_help_info_str(
                 meth_str, klass_name, klass_ancs, (not instance_help))
      end
      info
    end
  
    def get_help_info_str(meth_str, klass_name, klass_ancs, instance)
        info_str = ri_driver.get_info_str(klass_name, meth_str, instance)
        if not info_str
          # Walk through class hierarchy to find an inherited method
          ancest = klass_ancs.find{|anc|
            info_str = ri_driver.get_info_str(anc.name, meth_str, instance)
          }
          # Avoid returning Object in case of no help.
          if ancest == Object and meth_str.nil? and klass_name != Object.name
            info_str = nil 
          end
        end
        info_str
    end
  end


  # Version of RiDriver that takes its options
  # as parameter to #initialize.
  #
  class IHelpDriver < RiDriver
  
    # Create new IHelpDriver, with the given args 
    # passed to @options, which is a RI::Options.instance
    #
    def initialize(args = [])
      @options = RI::Options.instance
      @options.parse(args)
  
      paths = @options.paths || RI::Paths::PATH
      if paths.empty?
        report_missing_documentation(paths)
      end
      @ri_reader = RI::RiReader.new(RI::RiCache.new(paths))
      @display   = @options.displayer
    end    

    # Get info string from ri database for klass_name [method_name]
    #
    def get_info_str(klass_name, method_name = nil, instance = false)
      is_class_method = (not instance)
      top_level_namespace = @ri_reader.top_level_namespace
      namespaces = klass_name.split(/::/).inject(top_level_namespace){
        |ns, current_name|
        @ri_reader.lookup_namespace_in(current_name, ns)
      }
      return nil if namespaces.empty?
      if method_name.nil?
        get_class_info_str(namespaces)
      else
        methods = @ri_reader.find_methods(
                    method_name, is_class_method, namespaces)
        return nil if methods.empty?
        get_method_info_str(method_name, methods)
      end
    end

    # Display the info based on if it's
    # for a class or a method. Using ri's pager.
    #
    def display_info(info)
      case [info.class] # only info.class doesn't work
      when [RI::ClassDescription]
        @display.display_class_info(info, @ri_reader)
      when [RI::MethodDescription]
        @display.display_method_info(info)
      end
    end

    # Get info for the class in the given namespaces.
    #
    def get_class_info_str(namespaces)
      return nil if namespaces.empty?
      klass = nil
      namespaces.find{|ns| 
        begin
          klass = @ri_reader.get_class(ns)
        rescue TypeError
          nil
        end
      }
      klass
    end

    # Get info for the method in the given methods.
    #
    def get_method_info_str(requested_method_name, methods)
      if methods.size == 1
        @ri_reader.get_method(methods.first)
      else
        entries = methods.find_all {|m| m.name == requested_method_name}
        return nil if entries.empty?
        method = nil
        entries.find{|entry| method = @ri_reader.get_method(entry)}
        method
      end
    end

  end


end


module RI

  class MethodDescription
  
    # Creates HTML element from the MethodDescription.
    # Uses container_tag as the root node name and header_tag
    # as the tag for the header element that contains the method's name.
    #
    # Returns a REXML document with container_tag as the root element name.
    #
    def to_html(container_tag="div", header_tag="h1")
      doc = REXML::Document.new
      root = doc.add_element(container_tag)
      header = root.add_element(header_tag)
      header.add_text(full_name)
      comment.each{|c|
        tag = c.class.to_s.split("::").last
        tag = "PRE" if tag == "VERB" 
        xmlstr = "<#{tag}>#{c.body}</#{tag}>"
        c_doc = REXML::Document.new(xmlstr)
        root.add_element( c_doc.root )
      }
      doc
    end
    
  end

  
  class ClassDescription
  
    # Creates HTML element from the ClassDescription.
    # Uses container_tag as the root node name and header_tag
    # as the tag for the header element that contains the classes name.
    # Uses methods_header_tag as the tag for the "Class/Instance Methods"
    # method list headers.
    # Uses methods_tag as the tag for the method lists.
    #
    # Returns a REXML document with container_tag as the root element name.
    #
    def to_html(container_tag="div", header_tag="h1",
                methods_header_tag="h2", methods_tag="p")
      doc = REXML::Document.new
      root = doc.add_element(container_tag)
      header = root.add_element(header_tag)
      header.add_text(full_name)
      comment.each{|c|
        tag = c.class.to_s.split("::").last
        tag = "PRE" if tag == "VERB" 
        xmlstr = "<#{tag}>#{c.body}</#{tag}>"
        c_doc = REXML::Document.new(xmlstr)
        root.add_element( c_doc.root )
      }
      root.add_element(methods_header_tag).add_text("Class Methods")
      cmethods = root.add_element(methods_tag)
      class_methods[0...-1].each{|m|
        cmethods.add(m.to_html.root)
        cmethods.add_text(", ")
      }
      cmethods.add(class_methods.last.to_html.root)
      root.add_element(methods_header_tag).add_text("Instance Methods")
      imethods = root.add_element(methods_tag)
      instance_methods[0...-1].each{|m|
        imethods.add(m.to_html.root)
        imethods.add_text(", ")
      }
      imethods.add(instance_methods.last.to_html.root)
      doc
    end
    
  end
  
  
  class MethodSummary

    # Creates HTML element from the ClassDescription.
    # Puts the method's name inside the tag named in 
    # container_tag.
    #
    # Returns a REXML document with container_tag as the root element name.
    #
    def to_html(container_tag="em")
      doc = REXML::Document.new
      doc.add_element(container_tag).add_text(name)
      doc
    end
    
  end
    
end


class Object
  include IHelp
  extend IHelp
end


if __FILE__ == $0
  require 'test/unit'

  # to get around rdoc documenting NoHelp
  eval("module NoHelp; end")

  class HelpTest < Test::Unit::TestCase
  
    def no_warn
      old_w = $-w
      $-w = nil
      yield
      $-w = old_w
    end

    def setup
      no_warn{
        Object.const_set("ARGV", ["--readline", "--prompt-mode", "simple"])
      }
      IHelp.instance_variable_set(
        :@ri_driver, 
        IHelp::IHelpDriver.new(IHelp::RI_ARGS))
    end
  
    def test_simple_help
      assert("string".help_yaml)
    end

    def test_method_help
      assert("string".help_yaml(:reverse))
    end

    def test_inherited_method_help
      assert("string".help_yaml(:map))
    end

    def test_class_help
      assert(String.help_yaml)
    end

    def test_class_method_help
      assert(String.help_yaml(:new))
    end

    def test_class_inherited_method_help
      assert(String.help_yaml(:map))
    end

    def test_method_equalities
      assert(String.help_yaml(:new) == 
             "string".help_yaml(:new))
      assert(String.help_yaml(:reverse) == 
             "string".help_yaml(:reverse))
    end

    def test_method_constraints
      assert((not "string".help_yaml(:new,true)))
      assert((not "string".help_yaml(:reverse,false)))
      assert((not String.help_yaml(:new,true)))
      assert((not String.help_yaml(:reverse,false)))
    end

    def test_help_yamlings
      assert("string".help_yaml(:reverse) == 
             help_yaml("String#reverse"))
      assert(String.help_yaml(:new) == 
             help_yaml("String::new"))
    end

    def test_multipart_namespaces
      assert(Test::Unit.help_yaml)
      assert(help_yaml("Test::Unit"))
      assert(Test::Unit.help_yaml("run?"))
      assert(help_yaml("Test::Unit.run?"))
      assert(help_yaml("Test::Unit::run?"))
      assert(help_yaml("Test::Unit#run?"))
    end

    def test_not_found
      assert((NoHelp.help_yaml == nil))
      assert((String.help_yaml(:nonexistent) == nil))
    end

  end


end

