#
# database.rb
#
# Copyright (c) 2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#

require 'refe/fsdbm'
require 'refe/completiontable'
require 'refe/fileutils'
require 'refe/traceutils'
begin
  require 'refe/config'
rescue LoadError
end


module ReFe

  class CompletionError < StandardError; end
  class LookupError < StandardError; end


  class Database

    include ReFe::FileUtils

    def initialize( rootdir = nil, init = false )
      @init = init
      if rootdir
        @rootdir = rootdir
      elsif ENV['REFE_DATA_DIR']
        @rootdir = ENV['REFE_DATA_DIR']
      elsif defined?(REFE_DATA_DIR)
        @rootdir = REFE_DATA_DIR
      else
        raise ArgumentError, 'ReFe database directory not given'
      end
      isdbdir @rootdir

      @class_document_dbm          = nil
      @class_document_comptable    = nil
      @method_document_dbm         = nil
      @method_document_comptable   = nil
      @mf_relation_dbm             = nil
      @mf_relation_comptable       = nil
      @function_document_dbm       = nil
      @function_document_comptable = nil
      @function_source_dbm         = nil
      @function_source_comptable   = nil
    end

    #
    # facades
    #

    def class_document
      ClassTable.new(class_document_dbm(),
                     class_document_comptable())
    end

    def method_document
      MethodTable.new(method_document_dbm(),
                      method_document_comptable())
    end

    def mf_relation
      MFRelationTable.new(method_document(),
                          mf_relation_dbm(),
                          mf_relation_comptable(),
                          function_source())
    end

    def function_document
      FunctionTable.new(function_document_dbm(),
                        function_document_comptable())
    end

    def function_source
      FunctionTable.new(function_source_dbm(),
                        function_source_comptable())
    end

    #
    # low level database
    #

    private

    def class_document_dbm
      @class_document_dbm ||= FSDBM.new(isdbdir(@rootdir + '/class_document'))
    end

    def class_document_comptable
      @class_document_comptable ||=
        CompletionTable.new(isdbfile(@rootdir + '/class_document_comp'), @init)
    end

    def method_document_dbm
      @method_document_dbm ||= FSDBM.new(isdbdir(@rootdir + '/method_document'))
    end

    def method_document_comptable
      @method_document_comptable ||=
        CompletionTable.new(isdbfile(@rootdir + '/method_document_comp'), @init)
    end

    def mf_relation_dbm
      @mf_relation_dbm ||= FSDBM.new(isdbdir(@rootdir + '/mf_relation'))
    end

    def mf_relation_comptable
      @mf_relation_comptable ||=
        CompletionTable.new(isdbfile(@rootdir + '/mf_relation_comp'), @init)
    end

    def function_document_dbm
      @function_document_dbm ||=
        FSDBM.new(isdbdir(@rootdir + '/function_document'))
    end

    def function_document_comptable
      @function_document_comptable ||=
        CompletionTable.new(isdbfile(@rootdir+'/function_document_comp'), @init)
    end

    def function_source_dbm
      @function_source_dbm ||=
        FSDBM.new(isdbdir(@rootdir + '/function_source'))
    end

    def function_source_comptable
      @function_source_comptable ||=
        CompletionTable.new(isdbfile(@rootdir + '/function_source_comp'), @init)
    end

    private

    def isdbdir( dir )
      unless File.directory?(dir)
        raise ArgumentError, "database not initialized: #{dir}" unless @init
        mkdir_p dir
      end
      dir
    end

    def isdbfile( file )
      unless File.file?(file)
        raise ArgumentError, "database not initialized: #{file}" unless @init
      end
      file
    end
  
  end


  class ClassTable

    include TraceUtils

    def initialize( dbm, comp )
      @dbm = dbm
      @comp = comp
    end

    def flush
      @comp.flush
    end

    def classes
      @comp.list
    end

    def []( name )
      @dbm[name] or raise LookupError, "class not found: #{name}"
    end

    def []=( name, content )
      @comp.add name
      @dbm[name] = content
    end

    def complete( pattern )
      list = @comp.expand(compile_pattern(pattern))
      raise CompletionError, "not match: #{pattern}" if list.empty?
      trace "class comp/1: list.size = #{list.size}"
      return list if list.size == 1

      list.each do |n|
        if n == pattern
          trace "class comp/2: exact match"
          return [n]
        end
      end

      trace "class comp: completion failed"
      list
    end

    private

    def compile_pattern( pattern )
      flags = (/[A-Z]/ === pattern ? 'n' : 'ni')
      pat = pattern.gsub(/\\.|\[\]|\*|\?/) {|s|
          case s
          when /\A\\/
            s
          when '[]'
            '\\[\\]'
          when '*'
            '.*'
          when '?'
            '.'
          end
      }
      Regexp.compile("\\A#{pat}", flags)
    end

  end


  module MethodCompletion

    include TraceUtils

    private

    def complete0( table, comptable, c, t, m )
      list = comptable.expand(compile_pattern(c,t,m))
      trace "method comp/1: list.size = #{list.size}"
      raise CompletionError, "not match: #{c}#{t ? t : ' '}#{m}" if list.empty?
      return list if list.size == 1

      #
      # Normal expantion failed. Try reducing.
      #

      # Method name is exactly equal to parameter m.
      #
      alt1 = list.select {|name|
          c2, t2, m2 = name.split(/([\.\#])/, 2)
          m2 == m
      }
      trace "method comp/2: alt1.size = #{alt1.size}"
      return alt1 if alt1.size == 1

      # Both of method name and class name are exactly equal to parameters.
      #
      if c
        alt2 = list.select {|name|
            c2, t2, m2 = name.split(/([\.\#])/, 2)
            c2.downcase == c.downcase and m2 == m
        }
        trace "method comp/3: alt2.size = #{alt2.size}"
        return alt2 if alt2.size == 1
      else
        trace 'method comp/3: reducing scheme #2 is useless (class not given)'
      end

      # differ only the '!' or '?' character
      #
      alt3 = list.map {|n| n.sub(/[!?]\z/, '') }.uniq
      trace "method comp/4: alt3.size = #{alt3.size}"
      return alt3 if alt3.size == 1

      # document is same
      #
      if belongs_to_one_class?(list)
        h = {}
        list.each {|n| (h[table[n]] ||= []).push n }
        trace "mtable comp/5: alt4.size = #{h.size}"
        return [h.to_a[0][1][0]] if h.size == 1
      else
        trace "method comp/5: reducing scheme #4 is useless (class mixed)"
      end

      trace "method comp: completion failed"
      list
    end

    def compile_pattern( c, t, m )
      /#{c ? '\\A' : ''}#{word(c)}#{c ? '.*' : ''}#{type(t)}#{word(m,true)}/
    end

    def word( pattern, is_method = false )
      return '' unless pattern
      ignore_case_p = !pattern.index(/[A-Z]/)
      pattern.gsub(/\\.|\[\]|\[[^\]]+\]|[a-z]|([!?]\z)|\*|\?/) {|s|
          on_tail = $1
          case s
          when /\A\\/
            s
          when '[]'
            '\\[\\]'
          when '*'
            '.*'
          when '?'
            (is_method && on_tail) ? '.*\\?\\z' : '.'
          when '!'
            '.*!\\z'
          when /\A\[/
            s
          when /[a-z]/
            ignore_case_p ? "[#{s}#{s.upcase}]" : s
          end
      }
    end

    def type( type )
      return '[\\.\\#]' unless type
      case type
      when '.' then '\\.'
      when '#' then '#'
      else
        raise ArgumentError, "wrong type: #{type.inspect}"
      end
    end

    def belongs_to_one_class?( list )
      list.map {|n| n.slice(/\A[\w:]+[\.\#]/) }.uniq.size == 1
    end
  
  end


  class MethodTable

    include MethodCompletion

    def initialize( dbm, comp )
      @dbm = dbm
      @comp = comp
    end

    def flush
      @comp.flush
    end

    def singleton_methods_of( c )
      @comp.expand(/\A#{c}\./).map {|n| n.sub(/\A[\w:]+\./, '') }
    end

    def instance_methods_of( c )
      @comp.expand(/\A#{c}#/).map {|n| n.sub(/\A[\w:]+#/, '') }
    end

    def []( name )
      c, t, m = name.split(/([\.\#])/, 2)
      @dbm[c + t, m] or
              raise LookupError, "method not found: #{name}"
    end

    def []=( name, content )
      c, t, m = name.split(/([\.\#])/, 2)
      @dbm[c + t, m] = content
      @comp.add name
      content
    end

    def complete( c, t, m )
      complete0(self, @comp, c, t, m)
    end

  end


  class MFRelationTable

    include MethodCompletion

    def initialize( mdoc, mtof_dbm, mtof_comp, fsrc )
      @mdoc = mdoc
      @mtof_dbm = mtof_dbm
      @mtof_comp = mtof_comp
      @fsrc = fsrc
    end

    def flush
      @mtof_comp.flush
    end

    def []( method )
      c, t, m = method.split(/([\.\#])/, 2)
      f = @mtof_dbm[c + t, m] or
              raise LookupError, "cannot convert method to function: #{method}"
      @fsrc[f]
    end

    def []=( method, function )
      c, t, m = method.split(/([\.\#])/, 2)
      @mtof_dbm[c + t, m] = function
      @mtof_comp.add method
      function
    end

    def complete( c, t, m )
      complete0(self, @mtof_comp, c, t, m)
    end

  end


  class FunctionTable
  
    include TraceUtils

    def initialize( dbm, comp )
      @dbm = dbm
      @comp = comp
    end

    def flush
      @comp.flush
    end

    def []( name )
      @dbm[*name.split(/_/)] or
              raise LookupError, "function not found: #{name}"
    end

    def []=( name, content )
      @comp.add name
      @dbm[*name.split(/_/)] = content
    end

    def complete( words )
      pattern = normalize(words)

      list = @comp.expand(compile_pattern(pattern))
      trace "function: comp/1: list.size = #{list.size}"
      raise CompletionError, "not match: #{words.join(' ')}" if list.empty?
      return list if list.size == 1

      words = pattern.split(/_/)
      alt1 = list.select {|func|
          tmp = func.split(/_/)
          (tmp.size == words.size) and (tmp[-1] == words[-1])
      }
      trace "function: comp/2: alt1.size = #{alt1.size}"
      return alt1 if alt1.size == 1

      list
    end

    private

    def normalize( words )
      words.join('_').tr('-', '_').squeeze('_').sub(/\A_/, 'rb_')
    end

    def compile_pattern( pattern )
      Regexp.compile('\\A' + pattern.split(/_/).join('[^_]*_'),
                     (pattern.index(/[A-Z]/) ? 'n' : 'in'))
    end

  end

end
