require 'date'

class File::Find
   # The version of this package
   VERSION = '0.1.1'

   # :stopdoc:
   VALID_OPTIONS = %w/
      atime
      ctime
      follow
      ftype
      inum
      group
      name
      path
      size
      user
   /
   # :startdoc:

   # The starting path(s) for the search. The default is the current directory.
   # This can be a single path or an array of paths.
   #
   attr_accessor :path

   # The list of options passed to the constructor and/or used by the
   # File::Find#find method.
   #
   attr_accessor :options

   # Limits searches by file access time, where the value you supply is the
   # number of days back from the time that the File::Find#find method was
   # called. Note that the File::Find#find method itself alters the access
   # times.
   #
   attr_accessor :atime

   # Limits searches by file change time, where the value you supply is the
   # number of days back from the time that the File::Find#find method was
   # called.
   #
   attr_accessor :ctime

   # Limits searches to files that belong to a specific group ID.
   #
   attr_accessor :group

   # Controls the behavior of how symlinks are followed. If set to true, then
   # follows the file pointed to. If false, it considers the symlink itself.
   #
   attr_accessor :follow

   # Limits searches to specific types of files. The possible values here are
   # those returned by the File.ftype method.
   #
   attr_accessor :ftype

   # Limits search to a file with a specific inode number. Ignored on MS
   # Windows.
   #
   attr_accessor :inum

   # The name pattern used to limit file searches. The patterns that are legal
   # for Dir.glob are legal here. The default is '*', i.e. everything.
   #
   attr_accessor :name

   # If the value passed is an integer, this option limits searches to files
   # that match the size, in bytes, exactly. If a string is passed, you can
   # use the standard comparable operators to match files, e.g. ">= 200" would
   # limit searches to files greater than or equal to 200 bytes.
   #
   attr_accessor :size

   # Limits searches to files that belong to a specific user ID.
   #
   attr_accessor :user

   alias pattern name

   # Creates and returns a new File::Find object. The options set for this
   # object serve as the rules for determining what files the File::Find#find
   # method will search for.
   #
   def initialize(options = {})
      @options = options      

      @atime  = nil
      @ctime  = nil
      @ftype  = nil
      @group  = nil
      @follow = true
      @inum   = nil
      @size   = nil
      @user   = nil

      validate_and_set_options(options) unless options.empty?

      @path ||= Dir.pwd
      @name ||= '*'
   end

   # Executes the find based on the rules you set for the File::Find object.
   # In block form, yields each file in turn that matches the specified rules.
   # In non-block form it will return an array of matches instead.
   #
   def find
      results = [] unless block_given?
      paths   = [@path]

      catch(:loop) do
         paths.each{ |path|
            Dir.foreach(path){ |file|
               next if file == '.'
               next if file == '..'

               file = File.join(path, file)

               stat_method = @follow ? :lstat : :stat

               # Skip files we cannot access, stale links, etc.
               begin
                  stat_info = File.send(stat_method, file)
               rescue Errno::ENOENT, Errno::EACCES
                  next
               rescue Errno::ELOOP
                  stat_method = :lstat # Handle recursive symlinks
                  retry
               end

               glob = File.join(File.dirname(file), @name)
               next unless Dir[glob].include?(file)

               # Add directories back onto the list of paths to search unless
               # they've already been added.
               #
               if stat_info.directory?
                  unless paths.include?(file)
                     paths << file
                  end
               end

               if @atime
                  date1 = Date.parse(Time.now.to_s)
                  date2 = Date.parse(stat_info.atime.to_s)
                  next unless (date1 - date2).numerator == @atime
               end

               if @ctime
                  date1 = Date.parse(Time.now.to_s)
                  date2 = Date.parse(stat_info.ctime.to_s)
                  next unless (date1 - date2).numerator == @ctime
               end

               if @ftype
                  next unless File.ftype(file) == @ftype
               end

               if @group
                  next unless stat_info.gid == @group
               end

               unless RUBY_PLATFORM.match('mswin')
                  if @inum
                     next unless stat_info.ino == @inum
                  end
               end

               # Allow plain numbers, or strings for comparison operators.
               if @size
                  if @size.is_a?(String)
                     regex = /^([><=]+)\s*?(\d+)$/
                     match = regex.match(@size)

                     if match.nil? || match.captures.include?(nil)
                        raise ArgumentError, "invalid size string: '#{@size}'"
                     end

                     operator = match.captures.first.strip
                     number   = match.captures.last.strip.to_i

                     next unless stat_info.size.send(operator, number)
                  else
                     next unless stat_info.size == @size
                  end
               end

               if @user
                  next unless stat_info.uid == @user
               end

               if block_given?
                  yield file
               else
                  results << file
               end
            }
         }
      end

      block_given? ? nil : results
   end

   private

   # This validates that the keys are valid. If they are, it sets the value
   # of that key's corresponding method to the given value.
   #
   def validate_and_set_options(options)
      options.each do |key, value|
         key = key.to_s.downcase
         unless VALID_OPTIONS.include?(key)
            raise ArgumentError, "invalid option '#{key}'"
         end
         send("#{key}=", value)
      end
   end
end