# check for and avoid a double load...because we're afraid to load twice, since we override require et al

if(defined?($already_using_faster_require))
  p 'warning: faster_require double load--expected?' if $FAST_REQUIRE_DEBUG
  local_version = File.read(File.dirname(__FILE__) + "/../VERSION")
  raise "mismatched faster_require versions! #{local_version} != #{FastRequire::VERSION}" unless local_version == FastRequire::VERSION
else
  
$already_using_faster_require = true


require 'rbconfig' # maybe could cache this one's loc, too? probably not...

module FastRequire
  
  $FAST_REQUIRE_DEBUG ||= $DEBUG # can set this via $DEBUG, or on its own previously
  
  VERSION = File.read(File.dirname(__FILE__) + "/../VERSION")

  def self.sanitize filename
    filename.gsub(/[\/:]/, '_')
  end
  
  if RUBY_VERSION >= '1.9.0'
    # appears 1.9.x has inconsistent string hashes...so roll our own...
    def self.string_array_cruddy_hash strings
      # we only call this method once, so overflowing to a bignum is ok
      hash = 1;
      for string in strings
        hash = hash * 31
        string.each_byte{|b|
          hash += b
        }
      end
      hash # probably a Bignum (sigh)
    end
    
  else
    
    def self.string_array_cruddy_hash strings
      strings.hash
    end
    
  end

  def self.setup
    begin
     @@dir = File.expand_path('~/.ruby_faster_require_cache')
    rescue ArgumentError => e # couldn't find HOME environment or the like
     whoami = `whoami`.strip
     if File.directory?(home = "/home/#{whoami}")
      @@dir = home + '/.ruby_faster_require_cache'
     else
       raise e.to_s + " and couldnt infer it from whoami"
     end
    end

    unless File.directory?(@@dir)
      Dir.mkdir @@dir 
      raise 'unable to create user dir for faster_require ' + @@dir unless File.directory?(@@dir)
    end
    
    config = RbConfig::CONFIG
    
    # try to be a unique, but not too long, filename, for restrictions on filename length in doze
    ruby_bin_name = config['bindir'] + config['ruby_install_name'] # needed if you have two rubies, same box, same ruby description [version, patch number]
    parts = [File.basename($0), RUBY_PATCHLEVEL.to_s, RUBY_PLATFORM, RUBY_VERSION, RUBY_VERSION, File.expand_path(File.dirname($0)), ruby_bin_name]
    unless defined?($faster_require_ignore_pwd_for_cache)
      # add in Dir.pwd
      parts << File.basename(Dir.pwd)
      parts << Dir.pwd
    else
      p 'ignoring dirpwd for cached file location' if $FAST_REQUIRE_DEBUG
    end
    
    sanitized_parts = parts.map{|part| sanitize(part)}

    full_parts_hash = string_array_cruddy_hash(parts).to_s
    
    loc_name = (sanitized_parts.map{|part| part[0..5] + (part[-5..-1] || '')}).join('-') + '-' + full_parts_hash + '.marsh'
    
    @@loc = @@dir + '/' + loc_name
    
    if File.exist?(@@loc)
      FastRequire.load @@loc
    else
      @@require_locs = {}
    end
      
    @@already_loaded = {}
  
    $LOADED_FEATURES.each{|already_loaded|
      # in 1.8 they might be partial paths
      # in 1.9, they might be non collapsed paths
      # so we have to sanitize them here...
      # XXXX File.exist? is a bit too loose, here...
      if File.exist?(already_loaded)
        key = File.expand_path(already_loaded)
      else
        key = FastRequire.guess_discover(already_loaded) || already_loaded
      end
      @@already_loaded[key] = true
    }
  
    @@already_loaded[File.expand_path(__FILE__)] = true # this file itself isn't in loaded features, yet, but very soon will be..
    # a special case--I hope...
  
    # also disallow re-loading $0
    @@require_locs[$0] = File.expand_path($0) # so when we run into $0 on a freak require, we will skip it...
    @@already_loaded[File.expand_path($0)] = true
    
  end
  
  def self.load filename
    cached_marshal_data = File.open(filename, 'rb') {|f| f.read}
    begin
      @@require_locs = Marshal.restore( cached_marshal_data )
    rescue ArgumentError
      @@require_locs= {}
    end
  end


  # try to see where this file was loaded from, from $:
  # partial_name might be abc.rb, or might be abc
  # partial_name might be a full path, too
  def self.guess_discover partial_name, add_dot_rb = false

    # test for full path first
    # unfortunately it has to be a full separate test
    # for windoze sake, as drive letter could be different than slapping a '/' on the dir to test list...
    tests = [partial_name]

    if add_dot_rb
      tests << partial_name + '.rb'
      tests << partial_name + '.' + RbConfig::CONFIG['DLEXT']
    end

    tests.each{|b|
      # assume that .rb.rb is...valid...?
      if File.file?(b) && ((b[-3..-1] == '.rb') || (b[-3..-1] == '.' + RbConfig::CONFIG['DLEXT']))
        return File.expand_path(b)
      end
    }

    for dir in $:
      if File.file?(b = (dir + '/' + partial_name))
        # make sure we require a file that has the right suffix...
        if (b[-3..-1] == '.rb')  || (b[-3..-1] == '.' + RbConfig::CONFIG['DLEXT'])
          return File.expand_path(b)
        end

      end
    end

    if add_dot_rb && (partial_name[-3..-1] != '.rb') && (partial_name[-3..-1] != '.' + RbConfig::CONFIG['DLEXT'])
      guess_discover(partial_name + '.rb') || guess_discover(partial_name + '.')
    else
      nil
    end
  end
  
  FastRequire.setup
  
  def self.already_loaded
    @@already_loaded
  end

  def self.require_locs
    @@require_locs
  end

  def self.dir
    @@dir
  end
  
  def self.loc
    @@loc
  end

  at_exit {
    FastRequire.default_save
  }

  def self.default_save
    self.save @@loc
  end

  def self.save to_file
    File.open(to_file, 'wb'){|f| f.write Marshal.dump(@@require_locs)}
  end

  # for testing use only, basically
  def self.clear_all!
    require 'fileutils'
    success = false
    if File.exist? @@dir
      FileUtils.rm_rf @@dir 
      success = true
    end
    @@require_locs.clear
    setup
    success
  end
  
  private
  def last_caller
   caller[-2]
  end
  
  IN_PROCESS = []
  ALL_IN_PROCESS = []
  @@count = 0
  
  public
  
  def require_cached lib
    lib = lib.to_s # might not be zactly 1.9 compat... to_path ??
    ALL_IN_PROCESS << [lib, @@count += 1]
    begin
      p 'doing require ' + lib + ' from ' + caller[-1] if $FAST_REQUIRE_DEBUG
      if known_loc = @@require_locs[lib]
        if @@already_loaded[known_loc]
          p 'already loaded ' + known_loc + ' ' + lib if $FAST_REQUIRE_DEBUG
          return false 
        end
        @@already_loaded[known_loc] = true
        if known_loc =~ /\.#{RbConfig::CONFIG['DLEXT']}$/
          puts 'doing original_non_cached_require on .so full path ' + known_loc if $FAST_REQUIRE_DEBUG
          original_non_cached_require known_loc # not much we can do there...too bad...well at least we pass it a full path though :P
        else
          unless $LOADED_FEATURES.include? known_loc
            if known_loc =~ /rubygems.rb$/
              puts 'requiring rubygems ' + lib if $FAST_REQUIRE_DEBUG
              original_non_cached_require(lib) # revert to normal require so rubygems doesn't freak out when it finds itself already in $LOADED_FEATURES with rubygems > 1.6 :P
            else
              IN_PROCESS << known_loc
              begin
                if $FAST_REQUIRE_DEBUG
                  puts 'doing cached loc eval on ' + lib + '=>' + known_loc + " with stack:" + IN_PROCESS.join(' ')
                end
                $LOADED_FEATURES << known_loc
                # fakely add the load path, too, so that autoload for the same file/path in gems will work <sigh> [rspec2]
                no_suffix_full_path = known_loc.gsub(/\.[^.]+$/, '')
                no_suffix_lib = lib.gsub(/\.[^.]+$/, '')
                libs_path = no_suffix_full_path.gsub(no_suffix_lib, '')
                libs_path = File.expand_path(libs_path) # strip off trailing '/'
                
                $: << libs_path unless $:.index(libs_path) # add in this ones real require path, so that neighboring autoloads will work
                known_locs_dir = File.dirname(known_loc)
                $: << known_locs_dir unless $:.index(known_locs_dir) # attempt to avoid the regin loading bug...yipes.
                
                # try some more autoload conivings...so that it won't attempt to autoload if it runs into it later...
                relative_full_path = known_loc.sub(libs_path, '')[1..-1]
                $LOADED_FEATURES << relative_full_path unless $LOADED_FEATURES.index(relative_full_path) # add in with .rb, too, for autoload 
                  
                # load(known_loc, false) # too slow
                
                # use eval: if this fails to load breaks re-save the offending file in binary mode, or file an issue on the faster_require tracker...
                contents = File.open(known_loc, 'rb:utf-8') {|f| f.read} # read only costs 0.34/10.0 s...
                if contents =~ /require_relative/ # =~ is faster apparently faster than .include?
                  load(known_loc, false) # load is slow, but overcomes a ruby core bug: http://redmine.ruby-lang.org/issues/4487
                else
                  eval(contents, TOPLEVEL_BINDING, known_loc)
                end
              ensure
                raise 'unexpected' unless IN_PROCESS.pop == known_loc
              end
              return true
            end
          else
            puts 'ignoring already loaded [circular require?] ' + known_loc + ' ' + lib if $FAST_REQUIRE_DEBUG
          end
        end
      else
        # we don't know the location--let Ruby's original require do the heavy lifting for us here
        old = $LOADED_FEATURES.dup
        p 'doing old non-known location require ' + lib if $FAST_REQUIRE_DEBUG
        if(original_non_cached_require(lib))
          # debugger might land here the first time you run a script and it doesn't have a require
          # cached yet...
          new = $LOADED_FEATURES - old
          found = new.last
  
          # incredibly, in 1.8.x, this doesn't always get set to a full path.
          if RUBY_VERSION < '1.9'
            if !File.file?(found)
              # discover the full path.
              dir = $:.find{|path| File.file?(path + '/' + found)}
              return true unless dir # give up, case jruby socket.jar "mysterious"
              found = dir + '/' + found
            end
            found = File.expand_path(found);
          end
          puts 'found new loc:' + lib + '=>' + found if $FAST_REQUIRE_DEBUG
          @@require_locs[lib] = found
          @@already_loaded[found] = true
          return true
        else
        
          # this is expected if it's for libraries required before faster_require was [like rbconfig]
          # raise 'actually expected' + lib if RUBY_VERSION >= '1.9.0'
          puts 'already loaded, apparently [require returned false], trying to discover how it was redundant... ' + lib if $FAST_REQUIRE_DEBUG
          # this probably was something like
          # the first pass was require 'regdeferred'
          # now it's a different require 'regdeferred.rb'
          # which fails (or vice versa)
          # so figure out why
          # calc location, expand, map back
          where_found = FastRequire.guess_discover(lib, true)
          if where_found
            puts 'inferred lib loc:' + lib + '=>' + where_found if $FAST_REQUIRE_DEBUG
            @@require_locs[lib] = where_found
            # unfortunately if it's our first pass
            # and we are in the middle of a "real" require
            # that is circular
            # then $LOADED_FEATURES or (AFAIK) nothing will have been set
            # for us to be able to assert that
            # so...I think we'll end up
            # just fudging for a bit
            #	raise 'not found' unless @@already_loaded[where_found] # should have already been set...I think...
          else
            if $FAST_REQUIRE_DEBUG
              # happens for enumerator XXXX
              puts 'unable to infer ' + lib + ' location' if $FAST_REQUIRE_DEBUG
              @@already_loaded[found] = true # so hacky...
            end
          end
          return false # XXXX test all these return values
        end
      end
    ensure
      raise 'huh' unless ALL_IN_PROCESS.pop[0] == lib
    end
  end

end

module Kernel

  # overwrite old require...
  include FastRequire
  if defined?(gem_original_require)
    class << self
      alias :original_remove_method :remove_method
      
      def remove_method method # I think this actually might be needed <sigh>
        if method.to_s == 'require'
          #p 'not removing old require, since that\'s ours now'
        else
          original_remove_method method
        end
      end
    
    end
    
    # similarly overwrite this one...I guess...1.9.x...rubygems uses this as its default...I think...
    alias :original_non_cached_require :gem_original_require
    alias :gem_original_require :require_cached
  else
    alias :original_non_cached_require :require
    alias :require :require_cached
  end
end

end