require 'date'
require 'rbconfig'
require 'sys/admin'
include Config
include Sys

class File::Find
   # The version of this library
   VERSION = '0.2.3'

   # :stopdoc:
   VALID_OPTIONS = %w/
      atime
      ctime
      follow
      ftype
      inum
      group
      mtime
      name
      pattern
      path
      perm
      prune
      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.
   #
   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, where the
   # group can be either a group name or ID.
   #
   # Not currently supported on MS Windows.
   #
   attr_accessor :group

   # Controls the behavior of how symlinks are followed. If set to true (the
   # default), 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

   # Limits searches by file modification 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 :mtime

   # 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

   # Limits searches to files which have permissions that match the octal
   # value that you provide. For purposes of this comparison, only the user,
   # group, and world settings are used. Do not use a leading 0 in the values
   # that you supply, e.g. use 755 not 0755.
   #
   # Not currently supported on MS Windows.
   #
   attr_accessor :perm

   # Skips files or directories that match the string provided as an argument.
   #
   attr_accessor :prune

   # 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, where the user
   # can be either a user name or an ID.
   #
   # Not currently supported on MS Windows.
   #
   attr_accessor :user

   # The file that matched previously in the current search.
   #
   attr_reader :previous

   alias pattern name
   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.
   #
   # Example:
   #
   #    rule = File::Find.new(
   #       :name    => "*.rb",
   #       :follow  => false,
   #       :path    => ['/usr/local/lib', '/opt/local/lib']
   #    )  
   #
   def initialize(options = {})
      @options = options      

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

      validate_and_set_options(options) unless options.empty?

      @previous = nil

      @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.
   #
   # Example:
   #
   #   rule = File::Find.new(
   #      :name    => "*.rb",
   #      :follow  => false,
   #      :path    => ['/usr/local/lib', '/opt/local/lib']
   #   )   
   #
   #   rule.find{ |f|
   #      puts f
   #   }    
   #
   def find
      results = [] unless block_given?
      paths   = @path.to_a

      if @prune
         prune_regex = Regexp.new(@prune)
      else
         prune_regex = nil
      end

      paths.each{ |path|
         begin
            Dir.foreach(path){ |file|
               next if file == '.'
               next if file == '..'

               if prune_regex
                  next if prune_regex.match(file)
               end

               orig = file.dup
               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)

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

               # Dir[] doesn't like backslashes
               if File::ALT_SEPARATOR
                  file.tr!(File::ALT_SEPARATOR, File::SEPARATOR) 
                  glob.tr!(File::ALT_SEPARATOR, File::SEPARATOR) 
               end

               next unless Dir[glob].include?(file)

               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 @mtime
                  date1 = Date.parse(Time.now.to_s)
                  date2 = Date.parse(stat_info.mtime.to_s)
                  next unless (date1 - date2).numerator == @mtime
               end

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

               if @group
                  if @group.is_a?(String)
                     next unless Admin.get_group(stat_info.gid).name == @group
                  else
                     next unless stat_info.gid == @group
                  end
               end

               unless CONFIG['host_os'] =~ /windows|mswin/i
                  if @inum
                     next unless stat_info.ino == @inum
                  end
               end

               # This currently doesn't work on MS Windows, even in limited
               # fashion for 0666 and 0664, because File.stat.mode doesn't
               # return the proper value.
               #
               if @perm
                  next unless sprintf("%o", stat_info.mode & 07777) == @perm.to_s
               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
                  if @user.is_a?(String)
                     next unless Admin.get_user(stat_info.uid).name == @user
                  else
                     next unless stat_info.uid == @user
                  end
               end

               if block_given?
                  yield file
               else
                  results << file
               end

               @previous = file unless @previous == file
            }
         rescue Errno::EACCES
            next # Skip inaccessible directories
         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