# jEdit :folding=explicit:collapseFolds=1:indentSize=2:tabSize=2:

objectgraph = true
# add requires here #{{{ 

require "startup.rb"

#}}}


# handle command line #{{{
def usage #{{{
  puts <<-USAGE
    Ruby Class Inheritance Graph, version 1.0.1.
    Generates a png and an HTML map or the ruby
  
    ruby [-r libs] graph.rb <layout> <options>
    Use the -r lib to require extra libraries into the namespace (or edit the
    script file).
    'layout' is at least one of the following GraphViz layout engines:
             -neato   neato engine ('spring' model layout. recommended :)
             -dot     dot engine (hierarchical tree)
             -circo   circo engine (circular layout. generates large pics)
             -twopi   twopi engine (oval layout)
             -all     generate graphs with all layouts 
    Refer to http://www.research.att.com/sw/tools/graphviz/ for more details.
    
    'options' are:
             -skip-errno    Does not show the Errno classes
             -font <font>   Uses <font> in the nodes texts.
             -output <fmt>  Output format as suppoted by GraphViz (defaults to PNG).
             -base-class <ClassName>  Display only classes that inherit from <ClassName>.
                                      Cannot be use for inheritance trees based in class Module.
             -name-space <namespace>  Display only classes in <namespace>
             -out-name <name>         Ouput file name. Only useful is only one format is specified.
  USAGE
end #}}}

# manual handling to avoid requiring extra libs.
$genDot     = ARGV.include? "-dot"
$genNeato   = ARGV.include? "-neato"
$genTwopi   = ARGV.include? "-twopi"
$genCirco   = ARGV.include? "-circo"
$genDot = $genNeato = $genTwopi = $genCirco = true if ARGV.include? "-all" 

if not ($genDot or $genNeato or $genTwopi or $genCirco) or not (ARGV & ["-help", "-h", "-?"]).empty?
  usage
  exit(-1)
end

$skipErrno  = ARGV.include? "-skip-errno"

$font = ""
$font = ARGV[ARGV.index("-font")+1] if ARGV.include? "-font"

$outfmt = "png"
$outfmt = ARGV[ARGV.index("-output")+1] if ARGV.include? "-output"

$outname = ""
$outname = ARGV[ARGV.index("-out-name")+1] if ARGV.include? "-out-name"

$useBaseClass = ARGV.include? "-base-class"
if $useBaseClass
  baseClassName = ARGV[ARGV.index("-base-class")+1]
  $baseClass = ObjectSpace.each_object(Class) { |k| break k if k.name == baseClassName }
end

$useNamespace = ARGV.include? "-name-space"
if $useNamespace
  $nameSpace = ARGV[ARGV.index("-name-space")+1]
end

#}}}

# node and link formating utility #{{{ 

# Core classes docos: {{{
# This is ugly, but i can't think of another way short of checking the site.
$core_classes = %w{
 ArgumentError Array Benchmark Benchmark::Job Benchmark::Report
 Benchmark::Tms Bignum Binding CGI CGI::Cookie CGI::HtmlExtension CGI::QueryExtension
 CGI::QueryExtension::Value Class Comparable Complex ConditionVariable Continuation
 Data Date DateTime Dir EOFError Enumerable Errno Exception ExceptionForMatrix FalseClass
 File File::Constants File::Stat FileTest FileUtils FileUtils::NoWrite FileUtils::Verbose
 Find Fixnum Float FloatDomainError GC Generator Hash IO IOError IndexError Integer
 Interrupt Kernel LoadError LocalJumpError Logger Logger::Application Logger::Error
 Logger::LogDevice Logger::Severity Logger::ShiftingError Marshal MatchData Math
 Matrix Method Module Mutex NameError NilClass NoMemoryError NoMethodError NotImplementedError
 Numeric Object ObjectSpace Observable Pathname Precision Proc Process Process::GID Process::Status
 Process::Sys Process::UID Queue Range RangeError Regexp RegexpError RuntimeError
 ScriptError SecurityError Set Shellwords Signal SignalException Singleton Singleton
 SingletonClassMethods SizedQueue SortedSet StandardError String Struct Symbol SyncEnumerator
 SyntaxError SystemCallError SystemExit SystemStackError Tempfile Test Test::Unit
 Thread ThreadError ThreadGroup ThreadsWait Time TrueClass TypeError UnboundMethod
 Vector YAML ZeroDivisionError} #}}}

def get_class_url(klass)
  if $core_classes.include? klass.name
    "http://www.ruby-doc.org/docs/rdoc/1.9/classes/#{klass.name.sub(/::/,'/')}.html"
  else
    # mod = klass.name[0...(klass.name.index('::') || klass.name.length)].downcase
    # "http://www.ruby-doc.org/stdlib/libdoc/#{mod}/rdoc/classes/#{klass.name.sub(/::/,'/')}.html"
    
    # until we can better guess the module:
    "http://www.ruby-doc.org/stdlib/"
  end
end

def print_info(fmt, klass, inverse=false)
  # mask StringIO if it's undefined {{{
  # TODO: make this more efficient
  s = nil
  if defined? StringIO
    s = StringIO.new
  else
    s = String.new
    class << s
      def puts(str)
        self.replace(self + str + "\n")
      end
      def string
        self.to_s
      end
    end
  end
  #}}}

  begin
    oname = klass.name.gsub(/:/, '_')
    sname = klass.superclass.name.gsub(/:/, '_')
  rescue
    return
  end
  url = get_class_url(klass)
  s.puts "#{oname} [label=\"#{klass.name}\",URL=\"#{url}\"];"
  names = [oname, sname]
  names.reverse! if inverse
  s.puts fmt % names
  s.string
end #}}}

# Main loop over the object space {{{
def objectspace_loop(fmt, io, inverse) 
  ObjectSpace.each_object(Class) do |klass|
    next if $skipErrno and klass.name =~ /^Errno/
    begin
      if $useBaseClass
        # do not :allocate objects of class Class
        next if [Object, $baseClass, Class, Module].include? klass
        next unless klass.allocate.kind_of? $baseClass
      end
      if $useNamespace
        next unless klass.name =~ /^#{$nameSpace}/
      end
    rescue => detail
      next if detail.message =~ /allocator undefined/
    end

    io.puts( print_info(fmt, klass, inverse) )
  end
end #}}}

# Graph generation routine #{{{
def genGraph(prog, graphParams = "", inverse=false)
  baseName = ($outname == "" ? prog : $outname + "_" + prog)
  graphFile = baseName + '.ObjectGraph'
  picFile = baseName + 'OG.' + $outfmt
  htmlFile = baseName + 'OG.html'
  htmlMapFile = baseName + 'OG.map'

  # create graph file:
  File.open(graphFile, "w") do |file|
    # Graph properties:
    file.puts "digraph G {"
    file.puts 'concentrate = true;'
    file.puts "node [fontsize=8,fontname=\"#{$font}\",height=0.2];"
                  
    # Page Special nodes properties:
    file.puts graphParams
    
    if $useBaseClass
      url = get_class_url($baseClass)
      file.puts "#{$baseClass.name.gsub(/:/, '_')} [color=green,style=bold,label=\"#{$baseClass.name}\",URL=\"#{url}\"];"
    else
      url = get_class_url(Object)
      file.puts "Object [color=green,style=bold,label=\"Object\",URL=\"#{url}\"];"
    end
    
    # Links and node properties:
    objectspace_loop("%s -> %s;", file, inverse)
    
    file.puts "}"
  end

  # call GraphViz:
  system("#{prog} -Tcmap  #{graphFile} -o #{htmlMapFile}")       # genereate html map
  system("#{prog} -T#$outfmt #{graphFile} -o #{picFile}")      # generate png (or other output)

  # generate HTML file:
  File.open(htmlFile, "w") do |html|
    html.puts '<html><head></head><body>'
    html.puts "<img src='#{picFile}') usemap='#graph.map'>"
    html.puts '<map name="graph.map">'
    html.puts File.read(htmlMapFile)
    html.puts '</map></body></html>'
  end
  
end #}}}

# Create graphs and HTML files #{{{ 
# sizes are optimised for a full screen view at 1280 x 1024.
genGraph('dot', "edge [dir=back];", true) if $genDot
genGraph('neato', 'edge [len=2.0];') if $genNeato
genGraph('twopi', 'ranksep=2.0;') if $genTwopi
genGraph('circo', 'size="12.5,12.5";') if $genCirco
#}}}