#
# librefe.rb
#
#   Copyright (c) 2001 Minero Aoki <aamine@cd.xdsl.ne.jp>
#
#   This program is free software.
#   You can distribute/modify this program under the terms of
#   the GNU General Public License version 2 or later.
#

module ReFe

  Version   = '0.3.1'
  Copyright = 'Copyright (c) 2001 Minero Aoki'


  class SearchError < StandardError; end


  class Database

    INDEX_FILE = 'index.refe'

    unless defined? DATA_DIR then
      DATA_DIR = File.expand_path('~/share')
    end

    def self.load( from = nil )
      File.open( from || "#{DATA_DIR}/#{INDEX_FILE}" ) do |f|
        return Marshal.load(f)
      end
    end

    def save( to = nil )
      File.open( to || "#{DATA_DIR}/#{INDEX_FILE}", 'w' ) do |f|
        Marshal.dump self, f
      end
    end


    def initialize( tab )
      @table = tab
      reduce_table tab

      @inv_table = invert_table( tab )

      @c_abbr_table = create_abbr_table( class_names )
      @m_abbr_table = create_abbr_table( @inv_table.keys )
    end

    private

    def reduce_table( table )
      del = {}
      nomethod = {}
      table.each do |k,mt|
        if mt.empty? then
          k = k.chop
          if del.key? k then
            nomethod[k] = true
          end
          del[k] = true
        end
      end
      @no_method_classes = nomethod

      table.delete_if do |k,mt|
        mt.empty?
      end
    end

    def invert_table( table )
      it = {}
      table.each do |cname, methods|
        methods.each_key do |name|
          (it[name] ||= []).push cname
        end
      end

      it
    end

    def create_abbr_table( cand )
      table = {}
      cand.each do |orig|
        str = orig.downcase
        begin
          (table[str] ||= []).push orig
          str.chop!
        end until str.empty?
      end

      table
    end


    public

    def registered_classes
      class_names.collect {|c| /[\.\#]/ === c[-1,1] ? c.chop : c }.uniq
    end

    def method_description( name )
      c, m = name.split( /[\.\#]/, 2 )
      @table[c + $&][m]
    end

    def methods( c )
      @table[c].keys
    end

    private

    def class_names
      @table.keys + @no_method_classes.keys
    end


    public

    def lookup( klass, method, type )
      ccand = select_class( klass, type )
      ccand.empty? and notfound 'class', klass

      mcand = select_method( method )
      mcand.empty? and notfound 'method', method

      ent = ccand.collect {|c|
          m = mcand.find_all {|m| @table[c] and @table[c][m] }
          m.empty? ? nil : [c,m]
      }.compact
      ent.empty? and notfound 'method', "#{klass}#{type ? type : ' '}#{method}"

      SearchResultMethod.new( self,
              ent.collect {|c,mt| mt.collect {|m| c + m } }.flatten )
    end

    def lookup_class( klass, type )
      ccand = select_class( klass, type, true )
      ccand.empty? and notfound 'class', klass

      SearchResultClass.new( self, ccand )
    end

    def lookup_method( method )
      mcand = select_method( method )
      mcand.empty? and notfound 'method', method

      ent = mcand.collect {|m|
          @inv_table[m].collect {|c| c + m }
      }.flatten

      SearchResultMethod.new( self, ent )
    end

    private

    def select_class( name, type, usenom = false )
      if type then
        if @table.key? name + type then
          return [name + type]
        end

        t = @c_abbr_table[name.downcase] or return []
        t.find_all {|c| c[-1,1] == type }
      else
        t = %w( # . ).find_all {|t| @table.key? name + t }
        unless t.empty? then
          return t.collect {|t| name + t }
        end

        @c_abbr_table[name.downcase].to_a +
            (usenom and
            t = @c_abbr_table[name.downcase] and
            t.find_all {|c| not /[\.\#]\z/ === c }).to_a
      end
    end

    def select_method( name )
      if @inv_table.key? name then
        return [name]
      end
      if mcand = @m_abbr_table[name.downcase] then
        return mcand
      end

      if /[?!=]\z/ === name then
        tail = name[-1]
        name = name.chop.downcase
        if mcand = @m_abbr_table[name] then
          return mcand.find_all {|m| m[-1] == tail }
        end
      end
      []
    end


    def notfound( label, name )
      raise SearchError, "no such #{label}: #{name}"
    end

  end


  class SearchResult

    def initialize( db, ent )
      @db = db
      @result = ent
    end

    def size
      @result.size
    end

    def name( i = 0 )
      @result[i]
    end

    def names
      @result.dup
    end

    def determined?
      @result.size == 1
    end

    def each
      @result.each_with_index do |n,i|
        yield n, description(i)
      end
    end

    def each_name( &block )
      @result.each &block
    end

    def each_description
      @result.each do |i|
        yield description i
      end
    end
  
  end

  class SearchResultMethod < SearchResult

    def description( i = 0 )
      @db.method_description( * @result[i] )
    end

    def class?
      false
    end

    def method?
      true
    end
  
  end

  class SearchResultClass < SearchResult

    def initialize( db, ent )
      @db = db
      @result = ent.collect {|c| /[\.\#]/ === c ? c.chop : c }.uniq
      @expand = {}
      ent.each do |c|
        case c
        when /\.\z/ then (@expand[c.chop] ||= [])[0] = '.'
        when /\#\z/ then (@expand[c.chop] ||= [])[1] = '#'
        end
      end
    end

    def methods( key )
      ret = []
      if e = @expand[key] then
        2.times do |i|
          ret[i] = e[i] ? @db.methods(key + e[i]) : nil
        end
      end
      ret
    end

    def description( i = 0 )
      ''
    end

    def class?
      true
    end

    def method?
      false
    end
  
  end

end
