# frozen_string_literal: true

module FakeFS
  # FileUtils module
  module FileUtils
    extend self

    def mkdir_p(list, options = {})
      list = [list] unless list.is_a?(Array)
      list.each do |path|
        # FileSystem.add call adds all the necessary parent directories but
        # can't set their mode. Thus, we have to collect created directories
        # here and set the mode later.
        if options[:mode]
          created_dirs = []
          dir = path

          until Dir.exist?(dir)
            created_dirs << dir
            dir = File.dirname(dir)
          end
        end

        FileSystem.add(path, FakeDir.new)

        next unless options[:mode]
        created_dirs.each do |d|
          File.chmod(options[:mode], d)
        end
      end
    end

    alias mkpath mkdir_p
    alias makedirs mkdir_p

    def mkdir(list, _ignored_options = {})
      list = [list] unless list.is_a?(Array)
      list.each do |path|
        parent = path.to_s.split('/')
        parent.pop
        unless parent.join == '' || parent.join == '.' || FileSystem.find(parent.join('/'))
          raise Errno::ENOENT, path.to_s
        end
        raise Errno::EEXIST, path.to_s if FileSystem.find(path)
        FileSystem.add(path, FakeDir.new)
      end
    end

    def rmdir(list, _options = {})
      list = [list] unless list.is_a?(Array)
      list.each do |l|
        parent = l.to_s.split('/')
        parent.pop
        raise Errno::ENOENT, l.to_s unless parent.join == '' || FileSystem.find(parent.join('/'))
        raise Errno::ENOENT, l.to_s unless FileSystem.find(l)
        raise Errno::ENOTEMPTY, l.to_s unless FileSystem.find(l).empty?
        rm(l)
      end
    end

    def rm(list, options = {})
      Array(list).each do |path|
        FileSystem.delete(path) ||
          (!options[:force] && raise(Errno::ENOENT, path.to_s))
      end
    end
    alias rm_r rm
    alias remove rm

    def rm_f(list, options = {})
      rm(list, options.merge(force: true))
    end

    def rm_rf(list, options = {})
      rm_r(list, options.merge(force: true))
    end
    alias rmtree rm_rf
    alias safe_unlink rm_f

    def remove_entry_secure(path, force = false)
      rm_rf(path, force: force)
    end

    def ln_s(target, path, options = {})
      options = { force: false }.merge(options)
      raise(Errno::EEXIST, path.to_s) if FileSystem.find(path) && !options[:force]
      FileSystem.delete(path)

      if !options[:force] && !Dir.exist?(File.dirname(path))
        raise Errno::ENOENT, path.to_s
      end

      FileSystem.add(path, FakeSymlink.new(target))
    end

    def ln_sf(target, path)
      ln_s(target, path, force: true)
    end

    alias symlink ln_s

    def cp(src, dest, options = {})
      raise Errno::ENOTDIR, dest.to_s if src.is_a?(Array) && !File.directory?(dest)

      raise Errno::ENOENT, dest.to_s unless File.exist?(dest) || File.exist?(File.dirname(dest))

      # handle `verbose' flag
      RealFileUtils.cp src, dest, **options.merge(noop: true)

      # handle `noop' flag
      return if options[:noop]

      Array(src).each do |source|
        dst_file = FileSystem.find(dest)
        src_file = FileSystem.find(source)

        raise Errno::ENOENT, source.to_s unless src_file

        if dst_file && File.directory?(dst_file)
          FileSystem.add(
            File.join(dest, File.basename(source)), src_file.entry.clone(dst_file)
          )
        else
          FileSystem.delete(dest)
          FileSystem.add(dest, src_file.entry.clone)
        end
      end

      nil
    end

    alias copy cp

    def copy_file(src, dest, _preserve = false, _dereference = true)
      # Not a perfect match, but similar to what regular FileUtils does.
      cp(src, dest)
    end

    def copy_entry(src, dest, preserve = false, dereference_root = false, remove_destination = false)
      cp_r(
        src, dest,
        preserve: preserve,
        dereference_root: dereference_root,
        remove_destination: remove_destination
      )
    end

    def cp_r(src, dest, options = {})
      # handle `verbose' flag
      RealFileUtils.cp_r src, dest, **options.merge(noop: true)

      # handle `noop' flag
      return if options[:noop]

      Array(src).each do |source|
        dir = FileSystem.find(source)
        raise Errno::ENOENT, source.to_s unless dir

        new_dir = FileSystem.find(dest)
        raise Errno::EEXIST, dest.to_s if new_dir && !File.directory?(dest)
        raise Errno::ENOENT, dest.to_s if !new_dir && !FileSystem.find(dest.to_s + '/../')

        update_times = proc { |f| f.atime = f.mtime = Time.now }
        # This last bit is a total abuse and should be thought hard
        # about and cleaned up.
        if new_dir
          if src.to_s.end_with?('/.')
            dir.entries.each do |f|
              copy = f.clone(new_dir)
              walk_hierarchy(copy, &update_times) unless options[:preserve]
              new_dir[f.name] = copy
            end
          else
            copy = dir.entry.clone(new_dir)
            walk_hierarchy(copy, &update_times) unless options[:preserve]
            new_dir[dir.name] = copy
          end
        else
          copy = dir.entry.clone
          walk_hierarchy(copy, &update_times) unless options[:preserve]
          FileSystem.add(dest, copy)
        end
      end

      nil
    end

    def mv(src, dest, options = {})
      # handle `verbose' flag
      RealFileUtils.mv src, dest, **options.merge(noop: true)

      # handle `noop' flag
      return if options[:noop]

      Array(src).each do |path|
        if (target = FileSystem.find(path))
          dest_path =
            if File.directory?(dest)
              File.join(dest, File.basename(path))
            else
              dest
            end
          if File.directory?(dest_path)
            raise Errno::EEXIST, dest_path.to_s unless options[:force]
          elsif File.directory?(File.dirname(dest_path))
            FileSystem.delete(dest_path)
            FileSystem.delete(path)
            FileSystem.add(dest_path, target.entry.clone)
          else
            raise Errno::ENOENT, dest_path.to_s unless options[:force]
          end
        else
          raise Errno::ENOENT, path.to_s
        end
      end

      nil
    end

    alias move mv

    def chown(user, group, list, _options = {})
      list = Array(list)
      list.each do |f|
        if File.exist?(f)
          uid =
            if user
              user.to_s =~ /\d+/ ? user.to_i : Etc.getpwnam(user).uid
            end
          gid =
            if group
              group.to_s =~ /\d+/ ? group.to_i : Etc.getgrnam(group).gid
            end
          File.chown(uid, gid, f)
        else
          raise Errno::ENOENT, f.to_s
        end
      end
      list
    end

    def chown_R(user, group, list, _options = {})
      list = Array(list)
      list.each do |file|
        chown(user, group, file)
        [FileSystem.find_with_glob("#{file}/**/**")].flatten.each do |f|
          chown(user, group, f.to_s)
        end
      end
      list
    end

    def chmod(mode, list, _options = {})
      list = Array(list)
      list.each do |f|
        if File.exist?(f)
          File.chmod(mode, f)
        else
          raise Errno::ENOENT, f.to_s
        end
      end
      list
    end

    def chmod_R(mode, list, _options = {})
      list = Array(list)
      list.each do |file|
        chmod(mode, file)
        [FileSystem.find_with_glob("#{file}/**/**")].flatten.each do |f|
          chmod(mode, f.to_s)
        end
      end
      list
    end

    def touch(list, options = {})
      Array(list).each do |f|
        if (fs = FileSystem.find(f))
          now = Time.now
          fs.mtime = options[:mtime] || now
          fs.atime = now
        else
          file = File.open(f, 'w')
          file.close

          if (mtime = options[:mtime])
            fs = FileSystem.find(f)
            fs.mtime = mtime
          end
        end
      end
    end

    def cd(dir, &block)
      FileSystem.chdir(dir, &block)
    end
    alias chdir cd

    def compare_file(file1, file2)
      # we do a strict comparison of both files content
      File.readlines(file1) == File.readlines(file2)
    end

    alias cmp compare_file
    alias identical? compare_file

    def uptodate?(new, old_list)
      return false unless File.exist?(new)
      new_time = File.mtime(new)
      old_list.each do |old|
        if File.exist?(old)
          return false unless new_time > File.mtime(old)
        end
      end
      true
    end

    private

    # Walks through the file system hierarchy recursively, starting from the given entry,
    # and calls the given block with it.
    def walk_hierarchy(entry, &block)
      yield entry
      if entry.is_a? FakeDir
        entry.entries.each { |child| walk_hierarchy(child, &block) }
      end
    end
  end
end
