// file      : bpkg/utility.cxx -*- C++ -*-
// license   : MIT; see accompanying LICENSE file

#include <bpkg/utility.hxx>

#include <cerrno>  // ERANGE
#include <cstdlib> // strtoull()

#include <libbutl/prompt.hxx>
#include <libbutl/fdstream.hxx>

#include <bpkg/diagnostics.hxx>
#include <bpkg/common-options.hxx>

using namespace std;
using namespace butl;

namespace bpkg
{
  const string empty_string;
  const path empty_path;
  const dir_path empty_dir_path;

  const dir_path bpkg_dir (".bpkg");

  // Keep these directory names short, lowering the probability of hitting the
  // path length limit on Windows.
  //
  const dir_path certs_dir (dir_path (bpkg_dir) /= "certs");
  const dir_path repos_dir (dir_path (bpkg_dir) /= "repos");

  // Standard and alternative build file/directory naming schemes.
  //
  // build:
  //
  const dir_path std_build_dir      ("build");
  const dir_path std_config_dir     (dir_path (std_build_dir) /= "config");
  const path     std_bootstrap_file (dir_path (std_build_dir) /= "bootstrap.build");
  const path     std_root_file      (dir_path (std_build_dir) /= "root.build");
  const path     std_config_file    (dir_path (std_build_dir) /= "config.build");
  const dir_path std_bootstrap_dir  (dir_path (std_build_dir) /= "bootstrap");
  const path     std_src_root_file  (dir_path (std_bootstrap_dir) /= "src-root.build");
  const string   std_build_ext      ("build");

  // build2:
  //
  const dir_path alt_build_dir      ("build2");
  const dir_path alt_config_dir     (dir_path (alt_build_dir) /= "config");
  const path     alt_bootstrap_file (dir_path (alt_build_dir) /= "bootstrap.build2");
  const path     alt_root_file      (dir_path (alt_build_dir) /= "root.build2");
  const path     alt_config_file    (dir_path (alt_build_dir) /= "config.build2");
  const dir_path alt_bootstrap_dir  (dir_path (alt_build_dir) /= "bootstrap");
  const path     alt_src_root_file  (dir_path (alt_bootstrap_dir) /= "src-root.build2");
  const string   alt_build_ext      ("build2");

  const dir_path current_dir (".");

  const target_triplet host_triplet (BPKG_HOST_TRIPLET);

  map<dir_path, dir_path> tmp_dirs;

  bool keep_tmp;

  auto_rmfile
  tmp_file (const dir_path& cfg, const string& p)
  {
    auto i (tmp_dirs.find (cfg));
    assert (i != tmp_dirs.end ());
    return auto_rmfile (i->second / path::traits_type::temp_name (p),
                        !keep_tmp);
  }

  auto_rmdir
  tmp_dir (const dir_path& cfg, const string& p)
  {
    auto i (tmp_dirs.find (cfg));
    assert (i != tmp_dirs.end ());
    return auto_rmdir (i->second / dir_path (path::traits_type::temp_name (p)),
                       !keep_tmp);
  }

  void
  init_tmp (const dir_path& cfg)
  {
    // Whether the configuration is required or optional depends on the
    // command so if the configuration directory does not exist or it is not a
    // bpkg configuration directory, we simply create tmp in a system one and
    // let the command complain if necessary.
    //
    dir_path d (cfg.empty () ||
                !exists (cfg / bpkg_dir, true /* ignore_error */)
                ? dir_path::temp_path ("bpkg")
                : cfg / bpkg_dir / dir_path ("tmp"));

    if (exists (d))
      rm_r (d, true /* dir_itself */, 2);

    mk (d); // We shouldn't need mk_p().

    tmp_dirs[cfg] = move (d);
  }

  void
  clean_tmp (bool ignore_error)
  {
    for (const auto& d: tmp_dirs)
    {
      const dir_path& td (d.second);

      if (exists (td))
      {
        rm_r (td,
              true /* dir_itself */,
              3,
              ignore_error ? rm_error_mode::ignore : rm_error_mode::fail);
      }
    }

    tmp_dirs.clear ();
  }

  path&
  normalize (path& f, const char* what)
  {
    try
    {
      if (!f.complete ().normalized ())
        f.normalize ();
    }
    catch (const invalid_path& e)
    {
      fail << "invalid " << what << " path " << e.path;
    }
    catch (const system_error& e)
    {
      fail << "unable to obtain current directory: " << e;
    }

    return f;
  }

  dir_path&
  normalize (dir_path& d, const char* what)
  {
    try
    {
      if (!d.complete ().normalized ())
        d.normalize ();
    }
    catch (const invalid_path& e)
    {
      fail << "invalid " << what << " directory " << e.path;
    }
    catch (const system_error& e)
    {
      fail << "unable to obtain current directory: " << e;
    }

    return d;
  }

  dir_path
  current_directory ()
  {
    try
    {
      return dir_path::current_directory ();
    }
    catch (const system_error& e)
    {
      fail << "unable to obtain current directory: " << e << endf;
    }
  }

  optional<const char*> stderr_term = nullopt;
  bool stderr_term_color = false;

  bool
  yn_prompt (const string& p, char d)
  {
    try
    {
      return butl::yn_prompt (p, d);
    }
    catch (io_error&)
    {
      fail << "unable to read y/n answer from stdin" << endf;
    }
  }

  bool
  exists (const path& f, bool ignore_error)
  {
    try
    {
      return file_exists (f, true /* follow_symlinks */, ignore_error);
    }
    catch (const system_error& e)
    {
      fail << "unable to stat path " << f << ": " << e << endf;
    }
  }

  bool
  exists (const dir_path& d, bool ignore_error)
  {
    try
    {
      return dir_exists (d, ignore_error);
    }
    catch (const system_error& e)
    {
      fail << "unable to stat path " << d << ": " << e << endf;
    }
  }

  bool
  empty (const dir_path& d)
  {
    try
    {
      return dir_empty (d);
    }
    catch (const system_error& e)
    {
      fail << "unable to scan directory " << d << ": " << e << endf;
    }
  }

  void
  mk (const dir_path& d)
  {
    if (verb >= 3)
      text << "mkdir " << d;

    try
    {
      try_mkdir (d);
    }
    catch (const system_error& e)
    {
      fail << "unable to create directory " << d << ": " << e;
    }
  }

  void
  mk_p (const dir_path& d)
  {
    if (verb >= 3)
      text << "mkdir -p " << d;

    try
    {
      try_mkdir_p (d);
    }
    catch (const system_error& e)
    {
      fail << "unable to create directory " << d << ": " << e;
    }
  }

  void
  rm (const path& f, uint16_t v)
  {
    if (verb >= v)
      text << "rm " << f;

    try
    {
      if (try_rmfile (f) == rmfile_status::not_exist)
        fail << "unable to remove file " << f << ": file does not exist";
    }
    catch (const system_error& e)
    {
      fail << "unable to remove file " << f << ": " << e;
    }
  }

  void
  rm_r (const dir_path& d, bool dir, uint16_t v, rm_error_mode m)
  {
    if (verb >= v)
      text << (dir ? "rmdir -r " : "rm -r ") << (dir ? d : d / dir_path ("*"));

    try
    {
      rmdir_r (d, dir, m == rm_error_mode::ignore);
    }
    catch (const system_error& e)
    {
      bool w (m == rm_error_mode::warn);

      (w ? warn : error) << "unable to remove " << (dir ? "" : "contents of ")
                         << "directory " << d << ": " << e;

      if (!w)
        throw failed ();
    }
  }

  bool
  mv (const dir_path& from, const dir_path& to, bool fail)
  {
    if (verb >= 3)
      text << "mv " << from << ' ' << to; // Prints trailing slashes.

    try
    {
      mvdir (from, to);
    }
    catch (const system_error& e)
    {
      error << "unable to move directory " << from << " to " << to << ": "
            << e;

      if (fail)
        throw failed ();

      return false;
    }

    return true;
  }

  bool
  mv (const path& from, const path& to, bool fail)
  {
    if (verb >= 3)
      text << "mv " << from << ' ' << to;

    try
    {
      mvfile (from, to,
              cpflags::overwrite_content | cpflags::overwrite_permissions);
    }
    catch (const system_error& e)
    {
      error << "unable to move file " << from << " to " << to << ": " << e;

      if (fail)
        throw failed ();

      return false;
    }

    return true;
  }

  void
  hardlink (const path& target, const path& link)
  {
    // Note that this implementation is inspired by libbutl's mkanylink()
    // function.
    //
    try
    {
      mkhardlink (target, link);
    }
    catch (const system_error& e)
    {
      if (e.code ().category () == generic_category ())
      {
        int c (e.code ().value ());
        if (c == ENOSYS || // Not implemented.
            c == EPERM  || // Not supported by the filesystem(s).
            c == EXDEV)    // On different filesystems.
        {
          auto_rmfile arm (link + ".tmp");
          const path& p (arm.path);

          try
          {
            cpfile (target, p);
          }
          catch (const system_error& e)
          {
            fail << "unable to copy file " << target << " to " << p << ": "
                 << e;
          }

          mv (p, link);
          arm.cancel ();
          return;
        }
      }

      fail << "unable to create hard link for " << target << " at " << link
           << ": " << e;
    }
  }

  dir_path
  change_wd (const dir_path& d)
  {
    try
    {
      dir_path r (dir_path::current_directory ());

      if (verb >= 3)
        text << "cd " << d; // Prints trailing slash.

      dir_path::current_directory (d);
      return r;
    }
    catch (const system_error& e)
    {
      fail << "unable to change current directory to " << d << ": " << e
           << endf;
    }
  }

  fdpipe
  open_pipe ()
  {
    try
    {
      return fdopen_pipe ();
    }
    catch (const io_error& e)
    {
      fail << "unable to open pipe: " << e << endf;
    }
  }

  auto_fd
  open_null ()
  {
    try
    {
      return fdopen_null ();
    }
    catch (const io_error& e)
    {
      fail << "unable to open null device: " << e << endf;
    }
  }

  dir_path exec_dir;

  const char*
  name_b (const common_options& co)
  {
    return co.build_specified ()
      ? co.build ().string ().c_str ()
      : BPKG_EXE_PREFIX "b" BPKG_EXE_SUFFIX;
  }

  process_path
  search_b (const common_options& co)
  {
    const char* b (name_b (co));

    try
    {
      // Use our executable directory as a fallback search since normally the
      // entire toolchain is installed into one directory. This way, for
      // example, if we installed into /opt/build2 and run bpkg with absolute
      // path (and without PATH), then bpkg will be able to find "its" b.
      //
      return process::path_search (b, true /* init */, exec_dir);
    }
    catch (const process_error& e)
    {
      fail << "unable to execute " << b << ": " << e << endf;
    }
  }

  void
  dump_stderr (auto_fd&& fd)
  {
    ifdstream is (move (fd), fdstream_mode::skip, ifdstream::badbit);

    // We could probably write something like this, instead:
    //
    // *diag_stream << is.rdbuf () << flush;
    //
    // However, it would never throw and we could potentially miss the reading
    // failure, unless we decide to additionally mess with the diagnostics
    // stream exception mask.
    //
    for (string l; !eof (getline (is, l)); )
      *diag_stream << l << endl;

    is.close ();
  }

  optional<uint64_t>
  parse_number (const string& s, uint64_t max_num)
  {
    optional<uint64_t> r;

    if (!s.empty ())
    {
      const char* b (s.c_str ());
      char* e (nullptr);
      errno = 0; // We must clear it according to POSIX.
      uint64_t v (strtoull (b, &e, 10)); // Can't throw.

      if (errno != ERANGE && e == b + s.size () && v <= max_num)
        r = v;
    }

    return r;
  }

  optional<sqlite_synchronous>
  to_sqlite_synchronous (const string& v)
  {
    if      (icasecmp (v, "OFF")    == 0) return sqlite_synchronous::off;
    else if (icasecmp (v, "NORMAL") == 0) return sqlite_synchronous::normal;
    else if (icasecmp (v, "FULL")   == 0) return sqlite_synchronous::full;
    else if (icasecmp (v, "EXTRA")  == 0) return sqlite_synchronous::extra;
    else                                  return nullopt;
  }

  string
  to_string (sqlite_synchronous sync)
  {
    switch (sync)
    {
    case sqlite_synchronous::off:    return "OFF";
    case sqlite_synchronous::normal: return "NORMAL";
    case sqlite_synchronous::full:   return "FULL";
    case sqlite_synchronous::extra:  return "EXTRA";
    }

    assert (false); // Can't be here.
    return string ();
  }
}
