# NOTE: To use `FileUtils`, you must explicitly import it with `require "file_utils"`
module FileUtils
  extend self

  # Changes the current working directory of the process to the given string *path*.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.cd("/tmp")
  # ```
  #
  # NOTE: Alias of `Dir.cd`
  def cd(path : Path | String) : Nil
    Dir.cd(path)
  end

  # Changes the current working directory of the process to the given string *path*
  # and invoked the block, restoring the original working directory when the block exits.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.cd("/tmp") { Dir.current } # => "/tmp"
  # ```
  #
  # NOTE: Alias of `Dir.cd` with block
  def cd(path : Path | String, &)
    Dir.cd(path) { yield }
  end

  # Compares two files *filename1* to *filename2* to determine if they are identical.
  # Returns `true` if content are the same, `false` otherwise.
  #
  # ```
  # require "file_utils"
  #
  # File.write("file.cr", "1")
  # File.write("bar.cr", "1")
  # FileUtils.cmp("file.cr", "bar.cr") # => true
  # ```
  #
  # NOTE: Alias of `File.same_content?`
  def cmp(filename1 : Path | String, filename2 : Path | String) : Bool
    File.same_content?(filename1, filename2)
  end

  # Attempts to set the access and modification times of the file named
  # in the *path* parameter to the value given in *time*.
  #
  # If the file does not exist, it will be created.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.touch("afile.cr")
  # ```
  #
  # NOTE: Alias of `File.touch`
  def touch(path : Path | String, time : Time = Time.utc) : Nil
    File.touch(path, time)
  end

  # Attempts to set the access and modification times of each file given
  # in the *paths* parameter to the value given in *time*.
  #
  # If the file does not exist, it will be created.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.touch(["foo", "bar"])
  # ```
  def touch(paths : Enumerable(Path | String), time : Time = Time.utc) : Nil
    paths.each do |path|
      touch(path, time)
    end
  end

  # Copies the file *src_path* to the file or directory *dest*.
  # If *dest* is a directory, a file with the same basename as *src_path* is created in *dest*
  # Permission bits are copied too.
  #
  # ```
  # require "file_utils"
  #
  # File.touch("afile")
  # File.chmod("afile", 0o600)
  # FileUtils.cp("afile", "afile_copy")
  # File.info("afile_copy").permissions.value # => 0o600
  # ```
  def cp(src_path : Path | String, dest : Path | String) : Nil
    dest = Path[dest, File.basename(src_path)] if Dir.exists?(dest)
    File.copy(src_path, dest)
  end

  # Copies a list of files *src* to *dest*.
  # *dest* must be an existing directory.
  #
  # ```
  # require "file_utils"
  #
  # Dir.mkdir("files")
  # FileUtils.cp({"bar.cr", "afile"}, "files")
  # ```
  def cp(srcs : Enumerable(Path | String), dest : Path | String) : Nil
    raise ArgumentError.new("No such directory : #{dest}") unless Dir.exists?(dest)
    srcs.each do |src|
      cp(src, dest)
    end
  end

  # Copies a file or directory *src_path* to *dest_path*.
  # If *src_path* is a directory, this method copies all its contents recursively.
  # If *dest* is a directory, copies src to dest/src.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.cp_r("files", "dir")
  # ```
  def cp_r(src_path : Path | String, dest_path : Path | String) : Nil
    dest_path = File.join(dest_path, File.basename(src_path)) if File.directory?(dest_path)
    cp_recursive(src_path, dest_path)
  end

  private def cp_recursive(src_path : Path | String, dest_path : Path | String)
    if Dir.exists?(src_path)
      Dir.mkdir(dest_path) unless Dir.exists?(dest_path)
      Dir.each_child(src_path) do |entry|
        src = File.join(src_path, entry)
        dest = File.join(dest_path, entry)
        cp_recursive(src, dest)
      end
    else
      cp(src_path, dest_path)
    end
  end

  # Creates a hard link *dest_path* which points to *src_path*.
  # If *dest_path* already exists and is a directory, creates a link *dest_path/src_path*.
  #
  # ```
  # require "file_utils"
  #
  # # Create a hard link, pointing from /usr/bin/emacs to /usr/bin/vim
  # FileUtils.ln("/usr/bin/vim", "/usr/bin/emacs")
  # # Create a hard link, pointing from /tmp/foo.c to foo.c
  # FileUtils.ln("foo.c", "/tmp")
  # ```
  def ln(src_path : Path | String, dest_path : Path | String) : Nil
    if Dir.exists?(dest_path)
      File.link(src_path, File.join(dest_path, File.basename(src_path)))
    else
      File.link(src_path, dest_path)
    end
  end

  # Creates a hard link to each path in *src_paths* inside the *dest_dir* directory.
  # If *dest_dir* is not a directory, raises an `ArgumentError`.
  #
  # ```
  # require "file_utils"
  #
  # # Create /usr/bin/vim, /usr/bin/emacs, and /usr/bin/nano as hard links
  # FileUtils.ln(["vim", "emacs", "nano"], "/usr/bin")
  # ```
  def ln(src_paths : Enumerable(Path | String), dest_dir : Path | String) : Nil
    raise ArgumentError.new("No such directory : #{dest_dir}") unless Dir.exists?(dest_dir)

    src_paths.each do |path|
      ln(path, dest_dir)
    end
  end

  # Creates a symbolic link *dest_path* which points to *src_path*.
  # If *dest_path* already exists and is a directory, creates a link *dest_path/src_path*.
  #
  # ```
  # require "file_utils"
  #
  # # Create a symbolic link pointing from logs to /var/log
  # FileUtils.ln_s("/var/log", "logs")
  # # Create a symbolic link pointing from /tmp/src to src
  # FileUtils.ln_s("src", "/tmp")
  # ```
  def ln_s(src_path : Path | String, dest_path : Path | String) : Nil
    if Dir.exists?(dest_path)
      File.symlink(src_path, File.join(dest_path, File.basename(src_path)))
    else
      File.symlink(src_path, dest_path)
    end
  end

  # Creates a symbolic link to each path in *src_paths* inside the *dest_dir* directory.
  # If *dest_dir* is not a directory, raises an `ArgumentError`.
  #
  # ```
  # require "file_utils"
  #
  # # Create symbolic links in src/ pointing to every .c file in the current directory
  # FileUtils.ln_s(Dir["*.c"], "src")
  # ```
  def ln_s(src_paths : Enumerable(Path | String), dest_dir : Path | String) : Nil
    raise ArgumentError.new("No such directory : #{dest_dir}") unless Dir.exists?(dest_dir)

    src_paths.each do |path|
      ln_s(path, dest_dir)
    end
  end

  # Like `#ln_s(Path | String, Path | String)`, but overwrites `dest_path` if it exists and is not a directory
  # or if `dest_path/src_path` exists.
  #
  # ```
  # require "file_utils"
  #
  # # Create a symbolic link pointing from bar.c to foo.c, even if bar.c already exists
  # FileUtils.ln_sf("foo.c", "bar.c")
  # ```
  def ln_sf(src_path : Path | String, dest_path : Path | String) : Nil
    if File.directory?(dest_path)
      dest_path = File.join(dest_path, File.basename(src_path))
    end

    rm_rf(dest_path) if File.exists?(dest_path)
    File.symlink(src_path, dest_path)
  end

  # Creates a symbolic link to each path in *src_paths* inside the *dest_dir* directory,
  # ignoring any overwritten paths.
  #
  # If *dest_dir* is not a directory, raises an `ArgumentError`.
  #
  # ```
  # require "file_utils"
  #
  # # Create symbolic links in src/ pointing to every .c file in the current directory,
  # # even if it means overwriting files in src/
  # FileUtils.ln_sf(Dir["*.c"], "src")
  # ```
  def ln_sf(src_paths : Enumerable(Path | String), dest_dir : Path | String) : Nil
    raise ArgumentError.new("No such directory : #{dest_dir}") unless Dir.exists?(dest_dir)

    src_paths.each do |path|
      ln_sf(path, dest_dir)
    end
  end

  # Creates a new directory at the given *path*. The linux-style permission *mode*
  # can be specified, with a default of 777 (0o777).
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.mkdir("src")
  # ```
  #
  # NOTE: Alias of `Dir.mkdir`
  def mkdir(path : Path | String, mode = 0o777) : Nil
    Dir.mkdir(path, mode)
  end

  # Creates a new directory at the given *paths*. The linux-style permission *mode*
  # can be specified, with a default of 777 (0o777).
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.mkdir(["foo", "bar"])
  # ```
  def mkdir(paths : Enumerable(Path | String), mode = 0o777) : Nil
    paths.each do |path|
      Dir.mkdir(path, mode)
    end
  end

  # Creates a new directory at the given *path*, including any non-existing
  # intermediate directories. The linux-style permission *mode* can be specified,
  # with a default of 777 (0o777).
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.mkdir_p("foo")
  # ```
  #
  # NOTE: Alias of `Dir.mkdir_p`
  def mkdir_p(path : Path | String, mode = 0o777) : Nil
    Dir.mkdir_p(path, mode)
  end

  # Creates a new directory at the given *paths*, including any non-existing
  # intermediate directories. The linux-style permission *mode* can be specified,
  # with a default of 777 (0o777).
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.mkdir_p(["foo", "bar", "baz", "dir1", "dir2", "dir3"])
  # ```
  def mkdir_p(paths : Enumerable(Path | String), mode = 0o777) : Nil
    paths.each do |path|
      Dir.mkdir_p(path, mode)
    end
  end

  # Moves *src_path* to *dest_path*.
  #
  # NOTE: If *src_path* and *dest_path* exist on different mounted filesystems,
  # the file at *src_path* is copied to *dest_path* and then removed.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.mv("afile", "afile.cr")
  # ```
  def mv(src_path : Path | String, dest_path : Path | String) : Nil
    if error = Crystal::System::File.rename(src_path.to_s, dest_path.to_s)
      raise error unless error.os_error.in?(Errno::EXDEV, Errno::EPERM, WinError::ERROR_NOT_SAME_DEVICE)
      cp_r(src_path, dest_path)
      rm_r(src_path)
    end
  end

  # Moves every *srcs* to *dest*.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.mv(["foo", "bar"], "src")
  # ```
  def mv(srcs : Enumerable(Path | String), dest : Path | String) : Nil
    raise ArgumentError.new("No such directory : #{dest}") unless Dir.exists?(dest)
    srcs.each do |src|
      mv(src, File.join(dest, File.basename(src)))
    rescue File::Error
    end
  end

  # Returns the current working directory.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.pwd
  # ```
  #
  # NOTE: Alias of `Dir.current`
  def pwd : String
    Dir.current
  end

  # Deletes the *path* file given, raises if the path does not exist.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.rm("afile.cr")
  # ```
  #
  # NOTE: Alias of `File.delete`
  def rm(path : Path | String) : Nil
    File.delete(path)
  end

  # Deletes all *paths* files given, raises if any of the paths doesn't exist.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.rm(["dir/afile", "afile_copy"])
  # ```
  def rm(paths : Enumerable(Path | String)) : Nil
    paths.each do |path|
      File.delete(path)
    end
  end

  # Deletes the given *path* file, ignoring it if it does not exist.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.rm_f("afile.cr")
  # ```
  #
  # NOTE: Alias of `File.delete?`
  def rm_f(path : Path | String) : Nil
    File.delete?(path)
  end

  # Deletes all *paths* files given, ignoring non-existing ones.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.rm_f(["dir/afile", "afile_copy"])
  # ```
  def rm_f(paths : Enumerable(Path | String)) : Nil
    paths.each do |path|
      File.delete?(path)
    end
  end

  # Deletes a file or directory *path*.
  # If *path* is a directory, this method removes all its contents recursively.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.rm_r("dir")
  # FileUtils.rm_r("file.cr")
  # ```
  def rm_r(path : Path | String) : Nil
    if Dir.exists?(path) && !File.symlink?(path)
      Dir.each_child(path) do |entry|
        src = File.join(path, entry)
        rm_r(src)
      end
      Dir.delete(path)
    else
      File.delete(path)
    end
  end

  # Deletes a list of files or directories *paths*.
  # If one path is a directory, this method removes all its contents recursively.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.rm_r(["files", "bar.cr"])
  # ```
  def rm_r(paths : Enumerable(Path | String)) : Nil
    paths.each do |path|
      rm_r(path)
    end
  end

  # Deletes a file or directory *path*.
  # If *path* is a directory, this method removes all its contents recursively.
  # Ignores all errors.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.rm_rf("dir")
  # FileUtils.rm_rf("file.cr")
  # FileUtils.rm_rf("non_existent_file")
  # ```
  def rm_rf(path : Path | String) : Nil
    rm_r(path)
  rescue File::Error
  end

  # Deletes a list of files or directories *paths*.
  # If one path is a directory, this method removes all its contents recursively.
  # Ignores all errors.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.rm_rf(["dir", "file.cr", "non_existent_file"])
  # ```
  def rm_rf(paths : Enumerable(Path | String)) : Nil
    paths.each do |path|
      rm_r(path)
    rescue File::Error
    end
  end

  # Removes the directory at the given *path*.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.rmdir("baz")
  # ```
  #
  # NOTE: Alias of `Dir.delete`
  def rmdir(path : Path | String) : Nil
    Dir.delete(path)
  end

  # Removes all directories at the given *paths*.
  #
  # ```
  # require "file_utils"
  #
  # FileUtils.rmdir(["dir1", "dir2", "dir3"])
  # ```
  def rmdir(paths : Enumerable(Path | String)) : Nil
    paths.each do |path|
      Dir.delete(path)
    end
  end
end
