#--
# Copyright (C)2009 Tony Arcieri
# You can redistribute this under the terms of the MIT license
# See file LICENSE for details
#++

module RequireAll
  LoadError = Class.new(::LoadError)

  # A wonderfully simple way to load your code.
  #
  # The easiest way to use require_all is to just point it at a directory
  # containing a bunch of .rb files.  These files can be nested under 
  # subdirectories as well:
  #
  #  require_all 'lib'
  #
  # This will find all the .rb files under the lib directory and load them.
  #
  # If a file required by require_all references a constant that is not yet
  # loaded, a RequireAll::LoadError will be thrown.
  #
  # You can also give it a glob, which will enumerate all the matching files: 
  #
  #  require_all 'lib/**/*.rb'
  #
  # It will also accept an array of files:
  #
  #  require_all Dir.glob("blah/**/*.rb").reject { |f| stupid_file(f) }
  # 
  # Or if you want, just list the files directly as arguments:
  #
  #  require_all 'lib/a.rb', 'lib/b.rb', 'lib/c.rb', 'lib/d.rb'
  #
  def require_all(*args)
    # Handle passing an array as an argument
    args.flatten!

    options = {method: :require}
    options.merge!(args.pop) if args.last.is_a?(Hash)

    if args.empty?
      puts "no files were loaded due to an empty Array" if $DEBUG
      return false
    end

    if args.size > 1
      # Expand files below directories
      files = args.map do |path|
        if File.directory? path
          Dir[File.join(path, '**', '*.rb')]
        else
          path
        end
      end.flatten
    else
      arg = args.first
      begin
        # Try assuming we're doing plain ol' require compat
        stat = File.stat(arg)

        if stat.file?
          files = [arg]
        elsif stat.directory?
          files = Dir.glob File.join(arg, '**', '*.rb')
        else
          raise ArgumentError, "#{arg} isn't a file or directory"
        end
      rescue SystemCallError
        # If the stat failed, maybe we have a glob!
        files = Dir.glob arg

        # Maybe it's an .rb file and the .rb was omitted
        if File.file?(arg + '.rb')
          file = arg + '.rb'
          options[:method] != :autoload ? __require(options[:method], file) : __autoload(file, file, options)
          return true
        end

        # If we ain't got no files, the glob failed
        raise LoadError, "no such file to load -- #{arg}" if files.empty?
      end
    end

    return if files.empty?

    if options[:method] == :autoload
      files.map! { |file_| [file_, File.expand_path(file_)] }
      files.each do |file_, full_path|
        __autoload(file_, full_path, options)
      end

      return true
    end

    files.map { |file_| File.expand_path file_ }.sort.each do |file_|
      begin
        __require(options[:method], file_)
      rescue NameError => e
        # Only wrap NameError exceptions for uninitialized constants
        raise e unless e.instance_of?(NameError) && e.message.include?('uninitialized constant')
        raise LoadError, "Could not require #{file_} (#{e}). Please require the necessary files"
      end
    end

    true
  end

  # Works like require_all, but paths are relative to the caller rather than 
  # the current working directory
  def require_rel(*paths)
    # Handle passing an array as an argument
    paths.flatten!
    return false if paths.empty?

    source_directory = File.dirname caller.first.sub(/:\d+$/, '')
    paths.each do |path|
      require_all File.join(source_directory, path)
    end
  end

  # Loads all files like require_all instead of requiring
  def load_all(*paths)
    require_all paths, method: :load
  end

  # Loads all files by using relative paths of the caller rather than
  # the current working directory
  def load_rel(*paths)
    paths.flatten!
    return false if paths.empty?

    source_directory = File.dirname caller.first.sub(/:\d+$/, '')
    paths.each do |path|
      require_all File.join(source_directory, path), method: :load
    end
  end

  # Performs Kernel#autoload on all of the files rather than requiring immediately.
  #
  # Note that all Ruby files inside of the specified directories should have same module name as
  # the directory itself and file names should reflect the class/module names.
  # For example if there is a my_file.rb in directories dir1/dir2/ then
  # there should be a declaration like this in my_file.rb:
  #   module Dir1
  #     module Dir2
  #       class MyFile
  #         ...
  #       end
  #     end
  #  end
  #
  # If the filename and namespaces won't match then my_file.rb will be loaded into wrong module!
  # Better to fix these files.
  #
  # Set $DEBUG=true to see how files will be autoloaded if experiencing any problems.
  #
  # If trying to perform autoload on some individual file or some inner module, then you'd have
  # to always specify *:base_dir* option to specify where top-level namespace resides.
  # Otherwise it's impossible to know the namespace of the loaded files.
  #
  # For example loading only my_file.rb from dir1/dir2 with autoload_all:
  #
  #   autoload_all File.dirname(__FILE__) + '/dir1/dir2/my_file',
  #                base_dir: File.dirname(__FILE__) + '/dir1'
  #
  # WARNING: All modules will be created even if files themselves aren't loaded yet, meaning
  # that all the code which depends of the modules being loaded or not will not work, like usages
  # of define? and it's friends.
  #
  # Also, normal caveats of using Kernel#autoload apply - you have to remember that before
  # applying any monkey-patches to code using autoload, you'll have to reference the full constant
  # to load the code before applying your patch!

  def autoload_all(*paths)
    paths.flatten!
    return false if paths.empty?
    require "pathname"

    options = {method: :autoload}
    options.merge!(paths.pop) if paths.last.is_a?(Hash)

    paths.each do |path|
      require_all path, {base_dir: path}.merge(options)
    end
  end

  # Performs autoloading relatively from the caller instead of using current working directory
  def autoload_rel(*paths)
    paths.flatten!
    return false if paths.empty?
    require "pathname"

    options = {method: :autoload}
    options.merge!(paths.pop) if paths.last.is_a?(Hash)

    source_directory = File.dirname caller.first.sub(/:\d+$/, '')
    paths.each do |path|
      file_path = Pathname.new(source_directory).join(path).to_s
      require_all file_path, {method: :autoload,
                              base_dir: source_directory}.merge(options)
    end
  end

  private

  def __require(method, file)
    Kernel.send(method, file)
  end

  def __autoload(file, full_path, options)
    last_module = "Object" # default constant where namespaces are created into
    begin
      base_dir = Pathname.new(options[:base_dir]).realpath
    rescue Errno::ENOENT
      raise LoadError, ":base_dir doesn't exist at #{options[:base_dir]}"
    end
    Pathname.new(file).realpath.descend do |entry|
      # skip until *entry* is same as desired directory
      # or anything inside of it avoiding to create modules
      # from the top-level directories
      next if (entry <=> base_dir) < 0

      # get the module into which a new module is created or
      # autoload performed
      mod = Object.class_eval(last_module)

      without_ext = entry.basename(entry.extname).to_s

      const =
        if defined? ActiveSupport::Inflector
          ActiveSupport::Inflector.camelize(without_ext)
        else
          without_ext.split("_").map {|word| word.capitalize}.join
        end

      if entry.file? || (entry.directory? && entry.sub_ext('.rb').file?)
        mod.class_eval do
          puts "autoloading #{mod}::#{const} from #{full_path}" if $DEBUG
          autoload const, full_path
        end
      else
        mod.class_eval "module #{const} end" if entry.directory?
      end

      last_module += "::#{const}" if entry.directory?
    end
  end

end

include RequireAll
