#!/usr/bin/env ruby
# fri: access RI documentation through DRb
# Copyright (C) 2006 Mauricio Fernandez <mfp@acm.org>
#

require 'fastri/util'
require 'fastri/full_text_index'

default_local_mode = File.basename($0)[/^qri/] ? true : false

# we bind to 127.0.0.1 by default, because otherwise Ruby will try with
# 0.0.0.0, which results in a DNS request, adding way too much latency
options = {
  :addr   => "127.0.0.1",
  :format =>
    case RUBY_PLATFORM
    when /win/
      if /darwin|cygwin/ =~ RUBY_PLATFORM
        "ansi"
      else
        "plain"
      end
    else
      "ansi"
    end,
  :width => 72,
  :lookup_order => [
    :exact, :exact_ci, :nested, :nested_ci, :partial, :partial_ci, 
    :nested_partial, :nested_partial_ci,
  ],
  :show_matches => false,
  :do_full_text => false, 
  :full_text_dir => File.join(FastRI::Util.find_home, ".fastri-fulltext"),
  :use_pager => nil,
  :pager => nil,
  :list_classes => nil,
  :list_methods => nil,
  :extended => false,
  :index_file => File.join(FastRI::Util.find_home, ".fastri-index"),
  :local_mode => default_local_mode,
  :do_second_guess => true,
  :expand_choices => false,
}

override_addr_env = false


# only load optparse if actually needed
# saves ~0.01-0.04s.
if (arg = ARGV[0]) && arg[0, 1] == "-" or ARGV.empty?
  require 'optparse'
  optparser = OptionParser.new do |opts|
    opts.version = FastRI::FASTRI_VERSION
    opts.release = FastRI::FASTRI_RELEASE_DATE
    opts.banner = "Usage: #{File.basename($0)} [options] <query>"

    opts.on("-L", "--local", "Try to use local index instead of DRb service.",
                             *[("(default)" if default_local_mode)].compact) do
      options[:local_mode] = true
    end
    opts.on("--index-file=FILE", "Use index file (forces --local mode).",
            "(default: #{options[:index_file]})") do |file|
      options[:index_file] = file
      options[:local_mode] = true
    end
    opts.on("-R", "--remote", "Use DRb service. #{'(default)' unless default_local_mode}") do
      options[:local_mode] = false
    end
    opts.on("-s", "--bind ADDR", "Bind to ADDR for incoming DRb connections.",
            "(default: 127.0.0.1)") do |addr|
      options[:addr] = addr
      override_addr_env = true
    end

    order_mapping = {
      'e' => :exact,   'E' => :exact_ci,   'n' => :nested, 'N' => :nested_ci,
      'p' => :partial, 'P' => :partial_ci, 'x' => :nested_partial,
      'X' => :nested_partial_ci, 'a' => :anywhere, 'A' => :anywhere_ci,
      'm' => :namespace_partial, 'M' => :namespace_partial_ci,
      'f' => :full_partial, 'F' => :full_partial_ci,
    }
    opts.on("-O", "--order ORDER", "Specify lookup order.",
            "(default: eEnNpPxX)", "Uppercase: case-indep.",
            "e:exact n:nested p:partial (completion)",
            "x:nested and partial m:complete namespace",
            "f:complete both class and method",
            "a:match method name anywhere") do |order|
      options[:lookup_order] = order.split(//).map{|x| order_mapping[x]}.compact
    end

    opts.on("-1", "--exact", "Does not do second guess(exact query).") do
      options[:do_second_guess] = false
      options[:lookup_order] = [ :exact ]
    end
    opts.on("-a", "--all", "Show info for all methods in Multiple choices",
            "(default: don't)") do
      options[:expand_choices] = true
    end

    opts.on("-e", "--extended", "Show all methods for given namespace.") do
      options[:extended]  = true
      options[:use_pager] = true
      options[:format]    = "plain"
    end

    opts.on("--show-matches", "Only show matching entries."){ options[:show_matches] = true }

    opts.on("--classes", "List all known classes/modules.") do 
      options[:use_pager]    = true
      options[:list_classes] = true
    end
    opts.on("--methods", "List all known methods.") do 
      options[:use_pager]    = true
      options[:list_methods] = true
    end
    opts.on("-l", "--list-names", "List all known namespaces/methods.") do
      options[:use_pager]    = true
      options[:list_classes] = true
      options[:list_methods] = true
    end

    opts.on("-S", "--full-text", "Perform full-text search.") do 
      options[:do_full_text] = true
      options[:use_pager]    = true if options[:use_pager].nil?
      options[:format]       = "plain"
    end

    opts.on("-F", "--full-text-dir DIR", "Use full-text index in DIR",
            "(default: #{options[:full_text_dir]})") do |dir|
      options[:full_text_dir] = dir if dir
      options[:do_full_text]  = true
      options[:use_pager]     = true
      options[:format]        = "plain"
    end

    opts.on("-f", "--format FMT", "Format to use when displaying output:",
            "   ansi, plain (default: #{options[:format]})") do |format|
      options[:format] = format
    end

    opts.on("-P", "--[no-]pager", "Use pager.", "(default: don't)") do |usepager|
      options[:use_pager] = usepager
      options[:format] = "plain" if usepager
    end

    opts.on("-T", "Don't use a pager."){ options[:use_pager] = false }

    opts.on("--pager-cmd PAGER", "Use pager PAGER.", "(default: don't)") do |pager|
      options[:pager]     = pager
      options[:use_pager] = true
      options[:format]    = "plain"
    end

    opts.on("-w", "--width WIDTH", "Set the width of the output.") do |width|
      w = width.to_i
      options[:width] = w > 0 ? w : options[:width]
    end

    opts.on("-h", "--help", "Show this help message") do 
      puts opts
      exit
    end
  end
  optparser.parse!

  if !options[:list_classes] && !options[:list_methods] && ARGV.empty?
    puts optparser
    exit
  end
end # lazy optparse loading

# {{{ try to find where the method comes from exactly
include FastRI::Util::MagicHelp

MAX_CONTEXT_LINES = 20
def context_wrap(text, width)
  "... " + 
  text.gsub(/(.{1,#{width-4}})( +|$\n?)|(.{1,#{width-4}})/, "\\1\\3\n").chomp
end

def display_fulltext_search_results(results, gem_dir_info = FastRI::Util.gem_directories_unique,
                                    width = 78)
  return if results.empty?
  path = File.expand_path(results[0].path)
  gem_name, version, gem_path = FastRI::Util.gem_info_for_path(path, gem_dir_info)
  if gem_name
    rel_path = path[/#{Regexp.escape(gem_path)}\/(.*)/, 1]
    if rel_path
      entry_name = FastRI::Util.gem_relpath_to_full_name(rel_path)
    end
    puts "Found in #{gem_name} #{version}  #{entry_name}"
  else
    rdoc_system_path = File.expand_path(RI::Paths::SYSDIR)
    if path.index(rdoc_system_path)
      rel_path = path[/#{Regexp.escape(rdoc_system_path)}\/(.*)/, 1]
      puts "Found in system  #{FastRI::Util.gem_relpath_to_full_name(rel_path)}"
    else
      puts "Found in #{path}:"
    end
  end
  text = results.map do |result|
    context = result.context(120)
    from = (context.rindex("\n", context.index(result.query)) || -1) + 1
    to   = (context.index("\n", context.index(result.query)) || 0) - 1
    context_wrap(context[from..to], width)
  end
  puts
  puts text.uniq[0...MAX_CONTEXT_LINES]
  puts
end

def perform_fulltext_search(options)
  fulltext = File.join(options[:full_text_dir], "full_text.dat")
  suffixes = File.join(options[:full_text_dir], "suffixes.dat")
  begin
    index = FastRI::FullTextIndex.new_from_filenames(fulltext, suffixes)
  rescue Exception
    puts <<EOF
Couldn't open the full-text index:
#{fulltext}
#{suffixes}

The index needs to be rebuilt with
   fastri-server -B
EOF
    exit(-1)
  end
  gem_dir_info = FastRI::Util.gem_directories_unique
  match_sets = ARGV.map do |query|
    result = index.lookup(query)
    if result
      index.next_matches(result) + [result]
    else
      []
    end
  end
  path_map = Hash.new{|h,k| h[k] = []}
  match_sets.each{|matches| matches.each{|m| path_map[m.path] << m} }
  paths = match_sets[1..-1].inject(match_sets[0].map{|x| x.path}.uniq) do |s,x|
    s & x.map{|y| y.path}.uniq
  end
  if paths.empty?
    puts "nil"
  else
    puts "#{paths.size} hits"
    paths.sort_by{|path| 1.0 * -path_map[path].size / path_map[path].first.metadata[:size] ** 0.5}.map do |path|
      puts "=" * options[:width]
      puts 1.0 * path_map[path].size / path_map[path].first.metadata[:size] ** 0.5
      display_fulltext_search_results(path_map[path], gem_dir_info, options[:width])
    end
  end
  
  exit 0
end

#{{{ set up pager
if options[:use_pager]
  [options[:pager], ENV["PAGER"], "less", "more", "pager"].compact.uniq.each do |pager|
    begin
      $stdout = IO.popen(pager, "w")
      at_exit{ $stdout.close }
      break
    rescue Exception
    end
  end
end

#{{{ perform full text search if asked to
perform_fulltext_search(options) if options[:do_full_text]

#{{{ normal query
if options[:local_mode]
  require 'fastri/ri_service'
  ri_reader = open(options[:index_file], "rb"){|io| Marshal.load io } rescue nil
  unless ri_reader
    puts <<EOF
Couldn't open the index:
#{options[:index_file]}

The index needs to be rebuilt with
   fastri-server -b
EOF
    exit(-1)
  end
  service = FastRI::RiService.new(ri_reader)
else # remote
  require 'rinda/ring'

  #{{{ determine the address to bind to
  if override_addr_env
    addr_spec = options[:addr]
  else
    addr_spec = ENV["FASTRI_ADDR"] || options[:addr]
  end

  ip   = addr_spec[/^[^:]+/]    || "127.0.0.1"
  port = addr_spec[/:(\d+)/, 1] || 0
  addr = "druby://#{ip}:#{port}"

  #{{{ start DRb and perform request
  begin
    DRb.start_service(addr)
    ring_server = Rinda::RingFinger.primary
  rescue Exception
    $stderr.puts <<EOF
Couldn't initialize DRb and locate the Ring server.

Please make sure that:
 * the fastri-server is running, the server is bound to the correct interface,
   and the ACL setup allows connections from this host
 * fri is using the correct interface for incoming DRb requests:
   either set the FASTRI_ADDR environment variable, or use --bind ADDR, e.g
      export FASTRI_ADDR="192.168.1.12"
      fri Array
EOF
    exit(-1)
  end
  service = ring_server.read([:name, :FastRI, nil, nil])[2]
end


info_options = {
  :formatter => options[:format],
  :width     => options[:width],
  :lookup_order => options[:lookup_order],
  :extended  => options[:extended],
  :expand_choices => options[:expand_choices],
}

# {{{ list classes or methods
puts service.all_classes if options[:list_classes]
puts service.all_methods if options[:list_methods]
exit if options[:list_classes] || options[:list_methods]

# {{{ normal query

ARGV.each do |term|
  help_query = magic_help(term)
  if options[:show_matches]
    puts service.matches(help_query, info_options).sort
  else
    result = service.info(help_query, info_options)
    # second-guess the correct method type only as the last resort.
    if result
      puts result
    elsif options[:do_second_guess] and (new_query = FastRI::Util.change_query_method_type(help_query)) != help_query
      puts service.info(new_query)
    else
      puts nil
    end
  end
end
# vi: set sw=2 expandtab:
