#!/usr/local/bin/ruby
# -*- ruby -*-

# Copyright 2000 by Jim Weirich (jweirich@one.net).  All rights reserved.
# Permission is granted for use, modification and distribution as
# long as the above copyright notice is included.

require 'tk'

REVISION = "$Revision: 1.8 $"

# Regular expression matching classes to omit from class lists
Class_reject_filter = /^(Errno::.*|fatal)$/

# Modules whose methods should be omitted from the flat method list
Flat_reject_list = [Object, Kernel]


# ====================================================================
# (Extensions)
module Kernel
  def when_missing
    self
  end
end


# ====================================================================
# (Extensions)
class NilClass
  def when_missing
    yield
  end
end


# ====================================================================
# (Extensions)
module Enumerable
  def inject (initial_value)
    result = initial_value
    each { |item| result = yield (item, result) }
    result
  end
end


# ====================================================================
class BasicListing

  attr_reader :list, :selection
  attr_writer :select_action

  @@packing = {'fill'=>'both', 'expand'=>true, 'side'=>'left'}

  def initialize (parent, title, container=nil)
    p = @@packing
    @widget = (container ? container : TkFrame.new(parent) { pack p } )
    TkLabel.new(@widget) {
      text title
      pack ( 'fill'=>'none', 'expand'=>false, 'side'=>'top', 'anchor'=>'w' )
    }
    @list = TkListbox.new(@widget) { height 3; width 15; pack p }
    sb = TkScrollbar.new (@widget)
    sb.command (proc { |*args| @list.yview *args })
    sb.pack ('fill'=>'y', 'side'=>'right')
    @list.yscrollcommand (proc { |a,b| sb.set(a,b) })
    @select_action = proc { puts "Do Nothing" }
    @list.bind ("ButtonRelease-1") { 
      index = @list.curselection[0]
      sel = @list.get(index) if index.is_a?(Integer)
      select (sel)
    }
  end

  def select (aString)
    @selection = aString
  end

  def fill (key)
  end

  def highlight (index)
    list.selection_clear (0, 'end')
    list.selection_set (index)
    list.see (index)
  end

  private

  def class_for_name (class_name)
    begin
      result = eval(class_name)
    rescue
      result = nil
    end
    result
  end

  def filtered_classes (class_list)
    class_list.reject {
      |c| c.name =~ Class_reject_filter
    }.sort {
      |a,b| a.name <=> b.name
    }
  end
end


# ====================================================================
class ClassListing < BasicListing
  
  attr_accessor :class_label, :parent_label

  def initialize (parent, title)
    super (parent, title)
    @target_list = []
    @include_system = true
  end

  def highlight_name (class_name)
    index = @classes.index(class_name).
      when_missing { @classes.index(class_name + " (M)") }
    highlight (index) if index.is_a?(Integer)
  end

  def select (aString)
    begin
      return unless (String === aString) and (aString != "")
      @selection = aString
      aString =~ /^(\S+)/
      class_name = $1
      highlight_name(class_name)
      update_labels(class_name)
      update_targets(class_name)
    rescue ScriptError
      update_class_label ("<error>")
    end
  end

  def fill (key)
    @list.delete (0, "end")
    @classes = []
    ObjectSpace.each_object(Module) { |n|
      if n.name !~ Class_reject_filter then
	@classes << (n.name + ((Class === n) ? "" : " (M)"))
      end
    }
    if not @include_system then
      @classes = @classes.reject { |m|
	name = (m =~ /^(\w+)/) ? $1 : m
	$system_names[name]
      }
    end
    @classes.sort!
    @classes.each { |n| @list.insert ("end", n) }
  end

  def add_target (target)
    @target_list << target
  end

  def toggle_system_include
    @include_system = (not @include_system)
  end

  private

  def update_labels (class_name)
    update_class_label (class_name)
    update_parent_label (class_name)
  end

  def update_class_label (class_name)
    if @class_label then
      @class_label.text (class_name)
    end
  end

  def update_parent_label (class_name)
    if @parent_label then
      @parent_label.text (parent_of(class_name))
    end
  end

  def update_targets (class_name)
    @target_list.each { |t| t.fill(class_name) }
  end

  def parent_of (class_name)
    class_obj = class_for_name(class_name)
    if (not (Class === class_obj)) or (class_obj == Object) then
      parent_name = ""
    else
      parent_name = class_obj.superclass.name
    end
  end
end


# ====================================================================
class NameListing < BasicListing
  
  attr_accessor :target, :selection
  
  def initialize (parent, title, container=nil)
    super (parent, title, container)
    @names = []
    @target = nil
    @selection = nil
  end

  def highlight_name (name)
    index = @names.index(name)
    highlight (index) if index.is_a?(Integer)
  end

  def select (name)
    begin
      @selection = name
      highlight_name (name)
      @target.select(@selection) if @target
    rescue ScriptError
      update_class_label ("<error>")
    end
  end

  def fill (name)
    @names = find_names_for (name)
    @list.delete (0, "end")
    @names.each { |n| @list.insert ("end", n) }
  end
end


# ====================================================================
class ModuleListing < NameListing
  private

  def find_names_for (class_name)
    class_obj = class_for_name(class_name)
    filtered_classes (class_obj.included_modules.sort)
  end
end


# ====================================================================
class ChildListing < NameListing
  private

  def find_names_for (class_name)
    class_obj = class_for_name(class_name)
    list = []
    ObjectSpace.each_object(Class) { |c|
      list << c if child_of? (class_obj, c)
    }
    filtered_classes (list.sort)
  end

  def child_of? (class_obj, candidate_child)
    return (candidate_child.superclass == class_obj or
	    candidate_child.included_modules.member?(class_obj))
  end
end


# ====================================================================
class MethodListing < BasicListing

  def initialize (parent, title)
    super(parent, title)
    @flat_view = false
    @show_class_methods = false
  end

  def select (class_name)
  end

  def toggle_flat_view
    @flat_view = (not @flat_view)
  end

  def toggle_class_methods
    @show_class_methods = (not @show_class_methods)
  end

  def fill (class_name)
    return if not class_name
    @list.delete (0, "end")
    methods = find_methods(class_name)
    methods.sort.each { |m|
      @list.insert ("end", m)
    }
  end

  def find_methods (class_name)
    result = []
    class_obj = class_for_name(class_name)
    return [] if class_obj == nil
    if @show_class_methods and @flat_view then
      result = find_flat_class_methods(class_obj)
    elsif @show_class_methods then
      result = find_class_methods(class_obj)
    elsif @flat_view then
      result = find_flat_instance_methods (class_obj)
    else
      result = find_instance_methods(class_obj)
    end
    result
  end    

  def find_instance_methods (class_obj)
    class_obj.instance_methods
  end

  def find_flat_instance_methods (class_obj)
    class_obj.ancestors.reject {
      |m| Flat_reject_list.member?(m)
    }.inject([]) {
      |m, result| result | m.instance_methods 
    }
  end

  def find_flat_class_methods (class_obj)
    class_obj.methods
  end
    
  def find_class_methods (class_obj)
    result = class_obj.methods
    if class_obj.is_a?(Class) then
      result = result - class_obj.superclass.methods
    end
    result
  end
end


# Top Level Methods ==================================================

def create_gui
  root = TkRoot.new { title "Gem Hunter" }
  
  buttons = TkFrame.new {
    pack ( 'fill'=>'x', 'expand'=>false, 'side'=>'bottom' )
  }
  TkButton.new(buttons) {
    text 'Exit'
    command proc { exit }
    pack ( 'fill'=>'x', 'expand'=>false, 'side'=>'right')
  }
  
  flat_button = TkCheckButton.new (buttons) {
    text 'Flat View'
    pack ( 'fill'=>'x', 'expand'=>false, 'side'=>'left')
  }
  
  cm_button = TkCheckButton.new (buttons) {
    text 'Class Methods'
    variable TkVariable.new ($checked)
    pack ( 'fill'=>'x', 'expand'=>false, 'side'=>'left')
  }
  
  xs_button = TkCheckButton.new (buttons) {
    text 'System'
    pack ( 'fill'=>'x', 'expand'=>false, 'side'=>'left')
  }
  xs_button.select
  
  labels = TkFrame.new { pack ( 'fill'=>'x', 'expand'=>false ) }
  class_label = TkLabel.new(labels) {
    text "Class: "
    pack ( 'fill'=>'none',
	  'expand'=>false,
	  'side'=>'left',
	  'anchor'=>'w')
  }
  class_label = TkLabel.new (labels) {
    relief 'groove'
    width 15
    pack ( 'fill'=>'x',
	  'expand'=>true,
	  'side'=>'left',
	  'anchor'=>'w')
  }
  parent_label = TkButton.new (labels) {
    text ""
    width 15
    pack ( 'fill'=>'x',
	  'expand'=>true,
	  'side'=>'right',
	  'anchor'=>'w')
  }
  TkLabel.new(labels) {
    text "Parent: "
    pack ( 'fill'=>'none',
	  'expand'=>false,
	  'side'=>'right',
	  'anchor'=>'w')
  }

  windows = TkFrame.new { pack ('fill'=>'both', 'expand'=>true) }

  class_list  = ClassListing.new (windows, "Classes")
  center_frame = TkFrame.new(windows) {
    pack ('fill'=>'both', 'expand'=>true, 'side'=>'left')
  }
  center_top_frame = TkFrame.new (center_frame) {
    pack ('fill'=>'both', 'expand'=>true, 'side'=>'top')
  }
  center_bottom_frame = TkFrame.new (center_frame) {
    pack ('fill'=>'both', 'expand'=>true, 'side'=>'top')
  }
  child_list  = ChildListing.new (center_top_frame,
				  "Subclasses",
				   center_top_frame)
  module_list = ModuleListing.new (center_bottom_frame,
				   "Included Modules",
				   center_bottom_frame)
  method_list = MethodListing.new (windows, "Methods")
  parent_label.command proc {
    class_name = parent_label.text
    if class_name.is_a?(String) and (class_name!="") then
      class_list.select(parent_label.text)
    end
  }

  class_list.add_target (method_list)
  class_list.add_target (module_list)
  class_list.add_target (child_list)
  class_list.class_label = class_label
  class_list.parent_label = parent_label
  class_list.fill("")

  module_list.target = class_list
  child_list.target  = class_list

  cm_button.command proc {
    method_list.toggle_class_methods
    class_list.select(class_list.selection)
  }
  flat_button.command proc {
    method_list.toggle_flat_view
    class_list.select(class_list.selection)
  }
  xs_button.command proc {
    class_list.toggle_system_include
    class_list.fill("")
  }
end


def load_libraries
  ARGV.each { |fn|
    begin
      require fn
    rescue LoadError => ex
      puts "Can't find '#{fn}.rb', trying plain '#{fn}'"
      load fn
    end
  }
end

def record_system_modules
  $system_modules = []
  ObjectSpace.each_object (Module) { |m| $system_modules << m }
  
  $system_names = {}
  $system_modules.each { |m| $system_names[m.name] = true }
end


# Main ===============================================================

def main
  record_system_modules
  load_libraries
  create_gui

  Tk.mainloop
end

if __FILE__ == $0 then
  main
end
