// file      : butl/filesystem -*- C++ -*-
// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
// license   : MIT; see accompanying LICENSE file

#ifndef BUTL_FILESYSTEM
#define BUTL_FILESYSTEM

#ifndef _WIN32
#  include <dirent.h> // DIR
#else
#  include <stddef.h> // intptr_t
#endif

// VC's sys/types.h header file doesn't define mode_t type. So let's define it
// ourselves according to the POSIX specification.
//
#ifndef _MSC_VER
#  include <sys/types.h> // mode_t
#else
   typedef int mode_t;
#endif

#include <cstddef>  // ptrdiff_t
#include <cstdint>  // uint16_t
#include <utility>  // move()
#include <iterator>

#include <butl/export>

#include <butl/path>
#include <butl/timestamp>

namespace butl
{
  // Return true if the path is to an existing regular file. Note that by
  // default this function follows symlinks.
  //
  LIBBUTL_EXPORT bool
  file_exists (const char*, bool follow_symlinks = true);

  inline bool
  file_exists (const path& p, bool fs = true) {
    return file_exists (p.string ().c_str (), fs);}

  // Return true if the path is to an existing directory. Note that this
  // function follows symlinks.
  //
  LIBBUTL_EXPORT bool
  dir_exists (const char*);

  inline bool
  dir_exists (const path& p) {return dir_exists (p.string ().c_str ());}

  // Return true if the path is to an existing file system entry. Note that by
  // default this function doesn't follow symlinks.
  //
  LIBBUTL_EXPORT bool
  entry_exists (const char*, bool follow_symlinks = false);

  inline bool
  entry_exists (const path& p, bool fs = false) {
    return entry_exists (p.string ().c_str (), fs);}

  // Return true if the directory is empty. Note that the path must exist
  // and be a directory.
  //
  LIBBUTL_EXPORT bool
  dir_empty (const dir_path&);

  // Try to create a directory unless it already exists. If you expect
  // the directory to exist and performance is important, then you
  // should first call dir_exists() above since that's what this
  // implementation will do to make sure the path is actually a
  // directory.
  //
  // You should also probably use the default mode 0777 and let the
  // umask mechanism adjust it to the user's preferences.
  //
  // Errors are reported by throwing std::system_error.
  //
  enum class mkdir_status {success, already_exists};

  LIBBUTL_EXPORT mkdir_status
  try_mkdir (const dir_path&, mode_t = 0777);

  // The '-p' version of the above (i.e., it creates the parent
  // directories if necessary).
  //
  LIBBUTL_EXPORT mkdir_status
  try_mkdir_p (const dir_path&, mode_t = 0777);

  // Try to remove the directory returning not_exist if it does not exist
  // and not_empty if it is not empty. Unless ignore_error is true, all
  // other errors are reported by throwing std::system_error.
  //
  enum class rmdir_status {success, not_exist, not_empty};

  LIBBUTL_EXPORT rmdir_status
  try_rmdir (const dir_path&, bool ignore_error = false);

  // The '-r' (recursive) version of the above. Note that it will
  // never return not_empty.
  //
  LIBBUTL_EXPORT rmdir_status
  try_rmdir_r (const dir_path&, bool ignore_error = false);

  // As above but throws rather than returns not_exist if the directory
  // does not exist (unless ignore_error is true), so check before calling.
  // If the second argument is false, then the directory itself is not removed.
  //
  LIBBUTL_EXPORT void
  rmdir_r (const dir_path&, bool dir = true, bool ignore_error = false);

  // Try to remove the file (or symlinks) returning not_exist if
  // it does not exist. Unless ignore_error is true, all other
  // errors are reported by throwing std::system_error.
  //
  enum class rmfile_status {success, not_exist};

  LIBBUTL_EXPORT rmfile_status
  try_rmfile (const path&, bool ignore_error = false);

  // Automatically try to remove the path on destruction unless cancelled.
  // Since the non-cancelled destruction will normally happen as a result
  // of an exception, the failure to remove the path is silently ignored.
  //
  template <typename P>
  struct auto_rm
  {
    explicit
    auto_rm (P p = P ()): path_ (std::move (p)) {}

    void
    cancel () {path_ = P ();}

    const P&
    path () const {return path_;}

    // Movable-only type. Move-assignment cancels the lhs object.
    //
    auto_rm (auto_rm&&);
    auto_rm& operator= (auto_rm&&);
    auto_rm (const auto_rm&) = delete;
    auto_rm& operator= (const auto_rm&) = delete;

    ~auto_rm ();

  private:
    P path_;
  };

  using auto_rmfile = auto_rm<path>;
  using auto_rmdir = auto_rm<dir_path>; // Note: recursive (rm_r).

  // Create a symbolic link to a file (default) or directory (third argument
  // is true). Throw std::system_error on failures.
  //
  // Note that Windows symlinks are currently not supported.
  //
  LIBBUTL_EXPORT void
  mksymlink (const path& target, const path& link, bool dir = false);

  // Create a symbolic link to a directory. Throw std::system_error on
  // failures.
  //
  inline void
  mksymlink (const dir_path& target, const dir_path& link)
  {
    mksymlink (target, link, true);
  }

  // Create a hard link to a file (default) or directory (third argument is
  // true). Throw std::system_error on failures.
  //
  // Note that on Linix, FreeBSD and some other platforms the target can not
  // be a directory. While Windows support directories (via junktions), this
  // is currently not implemented.
  //
  LIBBUTL_EXPORT void
  mkhardlink (const path& target, const path& link, bool dir = false);

  // Create a hard link to a directory. Throw std::system_error on failures.
  //
  inline void
  mkhardlink (const dir_path& target, const dir_path& link)
  {
    mkhardlink (target, link, true);
  }

  // File copy flags.
  //
  enum class cpflags: std::uint16_t
  {
    overwrite_content     = 0x1,
    overwrite_permissions = 0x2,

    none = 0
  };

  inline cpflags operator& (cpflags, cpflags);
  inline cpflags operator| (cpflags, cpflags);
  inline cpflags operator&= (cpflags&, cpflags);
  inline cpflags operator|= (cpflags&, cpflags);

  // Copy a regular file, including its permissions. Throw std::system_error
  // on failure. Fail if the destination file exists and the overwrite_content
  // flag is not set. Leave permissions of an existing destination file intact
  // unless the overwrite_permissions flag is set. Delete incomplete copies
  // before throwing.
  //
  // Note that in case of overwriting, the existing destination file gets
  // truncated (not deleted) prior to being overwritten. As a side-effect,
  // hard link to the destination file will still reference the same file
  // system node after the copy.
  //
  // Also note that if the overwrite_content flag is not set and the
  // destination is a dangling symbolic link, then this function will still
  // fail.
  //
  LIBBUTL_EXPORT void
  cpfile (const path& from, const path& to, cpflags = cpflags::none);

  // Copy a regular file to an existing directory.
  //
  inline void
  cpfile (const path& from, const dir_path& to, cpflags fl = cpflags::none)
  {
    cpfile (from, to / from.leaf (), fl);
  }

  // Return timestamp_nonexistent if the entry at the specified path
  // does not exist or is not a path. All other errors are reported
  // by throwing std::system_error. Note that this function resolves
  // symlinks.
  //
  LIBBUTL_EXPORT timestamp
  file_mtime (const char*);

  inline timestamp
  file_mtime (const path& p) {return file_mtime (p.string ().c_str ());}

  // Path permissions.
  //
  enum class permissions: std::uint16_t
  {
    // Note: matching POSIX values.
    //
    xo = 0001,
    wo = 0002,
    ro = 0004,

    xg = 0010,
    wg = 0020,
    rg = 0040,

    xu = 0100,
    wu = 0200,
    ru = 0400,

    none = 0
  };

  inline permissions operator& (permissions, permissions);
  inline permissions operator| (permissions, permissions);
  inline permissions operator&= (permissions&, permissions);
  inline permissions operator|= (permissions&, permissions);

  // Get path permissions. Throw std::system_error on failure. Note that this
  // function resolves symlinks.
  //
  LIBBUTL_EXPORT permissions
  path_permissions (const path&);

  // Set path permissions. Throw std::system_error on failure. Note that this
  // function resolves symlinks.
  //
  LIBBUTL_EXPORT void
  path_permissions (const path&, permissions);

  // Directory entry iteration.
  //
  enum class entry_type
  {
    unknown,
    regular,
    directory,
    symlink,
    other
  };

  class LIBBUTL_EXPORT dir_entry
  {
  public:
    typedef butl::path path_type;

    // Symlink target type in case of the symlink, ltype() otherwise.
    //
    entry_type
    type () const;

    entry_type
    ltype () const;

    // Entry path (excluding the base). To get the full path, do
    // base () / path ().
    //
    const path_type&
    path () const {return p_;}

    const dir_path&
    base () const {return b_;}

    dir_entry () = default;
    dir_entry (entry_type t, path_type p, dir_path b)
        : t_ (t), p_ (std::move (p)), b_ (std::move (b)) {}

  private:
    entry_type
    type (bool link) const;

  private:
    friend class dir_iterator;

    mutable entry_type t_ = entry_type::unknown;  // Lazy evaluation.
    mutable entry_type lt_ = entry_type::unknown; // Lazy evaluation.
    path_type p_;
    dir_path b_;
  };

  class LIBBUTL_EXPORT dir_iterator
  {
  public:
    typedef dir_entry value_type;
    typedef const dir_entry* pointer;
    typedef const dir_entry& reference;
    typedef std::ptrdiff_t difference_type;
    typedef std::input_iterator_tag iterator_category;

    ~dir_iterator ();
    dir_iterator () = default;

    explicit
    dir_iterator (const dir_path&);

    dir_iterator (const dir_iterator&) = delete;
    dir_iterator& operator= (const dir_iterator&) = delete;

    dir_iterator (dir_iterator&& x);
    dir_iterator& operator= (dir_iterator&&);

    dir_iterator& operator++ () {next (); return *this;}

    reference operator* () const {return e_;}
    pointer operator-> () const {return &e_;}

    friend bool operator== (const dir_iterator&, const dir_iterator&);
    friend bool operator!= (const dir_iterator&, const dir_iterator&);

  private:
    void
    next ();

  private:
    dir_entry e_;

#ifndef _WIN32
    DIR* h_ = nullptr;
#else
    intptr_t h_ = -1;
#endif
  };

  // Range-based for loop support.
  //
  // for (const auto& de: dir_iterator (dir_path ("/tmp"))) ...
  //
  // Note that the "range" (which is the "begin" iterator), is no
  // longer usable. In other words, don't do this:
  //
  // dir_iterator i (...);
  // for (...: i) ...
  // ++i; // Invalid.
  //
  inline dir_iterator begin (dir_iterator&);
  inline dir_iterator end (const dir_iterator&);
}

#include <butl/filesystem.ixx>

#endif // BUTL_FILESYSTEM