require 'English'

module FakeFS
  # FakeFs Dir class
  class Dir
    include Enumerable
    attr_reader :path

    def self._check_for_valid_file(path)
      raise Errno::ENOENT, path.to_s unless FileSystem.find(path)
    end

    def initialize(string)
      self.class._check_for_valid_file(string)

      @path     = FileSystem.normalize_path(string)
      @open     = true
      @pointer  = 0
      @contents = ['.', '..'] + FileSystem.find(@path).entries
      @inode    = FakeInode.new(self)
    end

    def close
      @open = false
      @pointer = nil
      @contents = nil
      nil
    end

    def each
      if block_given?
        while (f = read)
          yield f
        end
      else
        @contents.map { |entry| entry_to_relative_path(entry) }.each
      end
    end

    def children
      each.to_a
    end

    def pos
      @pointer
    end

    def pos=(integer)
      @pointer = integer
    end

    def read
      raise IOError, 'closed directory' unless @pointer
      entry = @contents[@pointer]
      @pointer += 1
      entry_to_relative_path(entry) if entry
    end

    def rewind
      @pointer = 0
    end

    def seek(integer)
      raise IOError, 'closed directory' if @pointer.nil?
      @pointer = integer
      @contents[integer]
    end

    def self.[](*pattern)
      glob pattern
    end

    def self.exist?(path)
      File.exist?(path) && File.directory?(path)
    end

    def self.chdir(dir, &blk)
      FileSystem.chdir(dir, &blk)
    end

    def self.chroot(_string)
      raise NotImplementedError
    end

    def self.delete(string)
      _check_for_valid_file(string)
      raise Errno::ENOTEMPTY, string.to_s unless FileSystem.find(string).empty?

      FileSystem.delete(string)
    end

    def self.entries(dirname, _options = nil)
      _check_for_valid_file(dirname)

      Dir.new(dirname).map { |file| File.basename(file) }
    end

    def self.children(dirname, _options = nil)
      entries(dirname) - ['.', '..']
    end

    def self.each_child(dirname, &_block)
      Dir.open(dirname) do |dir|
        dir.each do |file|
          next if ['.', '..'].include?(file)
          yield file
        end
      end
    end

    if RUBY_VERSION >= '2.4'
      def self.empty?(dirname)
        _check_for_valid_file(dirname)
        if File.directory?(dirname)
          Dir.new(dirname).count <= 2
        else
          false
        end
      end
    end

    def self.foreach(dirname, &_block)
      Dir.open(dirname) do |dir|
        dir.each do |file|
          yield file
        end
      end
    end

    def self.glob(pattern, _flags = 0, flags: _flags, base: nil, &block) # rubocop:disable Lint/UnderscorePrefixedVariableName
      pwd = FileSystem.normalize_path(base || Dir.pwd)
      matches_for_pattern = lambda do |matcher|
        [FileSystem.find_with_glob(matcher, flags, true, dir: pwd) || []].flatten.map do |e|
          pwd_regex = %r{\A#{pwd.gsub('+') { '\+' }}/?}
          if pwd.match(%r{\A/?\z}) ||
             !e.to_s.match(pwd_regex)
            e.to_s
          else
            e.to_s.match(pwd_regex).post_match
          end
        end.sort
      end

      files =
        if pattern.is_a?(Array)
          pattern.map do |matcher|
            matches_for_pattern.call matcher
          end.flatten
        else
          matches_for_pattern.call pattern
        end

      block_given? ? files.each { |file| block.call(file) } : files
    end

    def self.home(user = nil)
      RealDir.home(user)
    end

    def self.mkdir(string, _integer = 0)
      FileUtils.mkdir(string)
    end

    def self.open(string, &_block)
      dir = Dir.new(string)
      if block_given?
        result = yield(dir)
        dir.close
        result
      else
        dir
      end
    end

    def self.tmpdir
      '/tmp'
    end

    def self.pwd
      FileSystem.current_dir.to_s
    end

    # Tmpname module
    module Tmpname # :nodoc:
      module_function

      def tmpdir
        Dir.tmpdir
      end

      def make_tmpname(prefix_suffix, suffix)
        case prefix_suffix
        when String
          prefix = prefix_suffix
          suffix = ''
        when Array
          prefix = prefix_suffix[0]
          suffix = prefix_suffix[1]
        else
          raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
        end
        t = Time.now.strftime('%Y%m%d')
        path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
        path << "-#{suffix}" if suffix
        path << suffix
      end

      def create(basename, *rest)
        if (opts = Hash.try_convert(rest[-1]))
          opts = opts.dup if rest.pop.equal?(opts)
          max_try = opts.delete(:max_try)
        else
          opts = {}
        end
        tmpdir, = *rest
        tmpdir ||= self.tmpdir
        Dir.mkdir(tmpdir) unless Dir.exist?(tmpdir)

        n = nil
        begin
          path = File.join(tmpdir, make_tmpname(basename, n))
          yield(path, n, opts)
        rescue Errno::EEXIST
          n ||= 0
          n += 1
          retry if !max_try || n < max_try
          raise "cannot generate temporary name using `#{basename}' " \
            "under `#{tmpdir}'"
        end
        path
      end
    end

    def ino
      @inode.inode_num
    end

    # This code has been borrowed from Rubinius
    def self.mktmpdir(prefix_suffix = nil, tmpdir = nil)
      case prefix_suffix
      when nil
        prefix = 'd'
        suffix = ''
      when String
        prefix = prefix_suffix
        suffix = ''
      when Array
        prefix = prefix_suffix[0]
        suffix = prefix_suffix[1]
      else
        raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
      end

      t = Time.now.strftime('%Y%m%d')
      n = nil

      begin
        path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
        path << "-#{n}" if n
        path << suffix
        mkdir(path, 0o700)
      rescue Errno::EEXIST
        n ||= 0
        n += 1
        retry
      end

      if block_given?
        begin
          yield path
        ensure
          require 'fileutils'
          # This here was using FileUtils.remove_entry_secure instead of just
          # .rm_r. However, the security concerns that apply to
          # .rm_r/.remove_entry_secure shouldn't apply to a test fake
          # filesystem. :^)
          FileUtils.rm_r path
        end
      else
        path
      end
    end

    private

    def entry_to_relative_path(entry)
      filename = entry.to_s
      filename.start_with?("#{path}/") ? filename[path.size + 1..-1] : filename
    end

    class << self
      alias getwd pwd
      alias rmdir delete
      alias unlink delete
    end
  end
end
