// file      : libbuild2/file-cache.cxx -*- C++ -*-
// license   : MIT; see accompanying LICENSE file

#include <libbuild2/file-cache.hxx>

#include <libbutl/lz4.hxx>

#include <libbuild2/filesystem.hxx>  // exists(), try_rmfile()
#include <libbuild2/diagnostics.hxx>

using namespace butl;

namespace build2
{
  // file_cache::entry
  //
  file_cache::write file_cache::entry::
  init_new ()
  {
    assert (state_ == uninit);

    // Remove stale compressed file if it exists. While not strictly necessary
    // (since the presence of the new uncompressed file will render the
    // compressed one invalid), this makes things cleaner in case we don't get
    // to compressing the new file (for example, if we fail and leave the
    // uncompressed file behind for troubleshooting).
    //
    if (!comp_path_.empty ())
      try_rmfile_ignore_error (comp_path_);

    // Note: state remains uninit until write::close().

    pin ();
    return write (*this);
  }

  void file_cache::entry::
  init_existing ()
  {
    assert (state_ == uninit);

    // Determine the cache state from the filesystem state.
    //
    // First check for the uncompressed file. Its presence means that the
    // compressed file, if exists, is invalid and we clean it up, similar to
    // init_new().
    //
    // Note that if compression is disabled, we omit the check assuming the
    // the uncompressed file exists.
    //
    if (!comp_path_.empty ())
    {
      if (exists (path_))
      {
        try_rmfile_ignore_error (comp_path_);
        state_ = uncomp;
      }
      else if (exists (comp_path_))
      {
        state_ = comp;
      }
      else
        fail << comp_path_ << " (or its uncompressed variant) does not exist" <<
          info << "consider cleaning the build state";
    }
    else
      state_ = uncomp;
  }

  void file_cache::entry::
  preempt ()
  {
    // Note that this function is called from destructors so it's best if it
    // doesn't throw.
    //
    switch (state_)
    {
    case uncomp:
      {
        if (!compress ())
          break;

        state_ = decomp; // We now have both.
      }
      // Fall through.
    case decomp:
      {
        if (try_rmfile_ignore_error (path_))
          state_ = comp;

        break;
      }
    default:
      assert (false);
    }
  }

  bool file_cache::entry::
  compress ()
  {
    tracer trace ("file_cache::entry::compress");

    try
    {
      ifdstream ifs (path_,      fdopen_mode::binary, ifdstream::badbit);
      ofdstream ofs (comp_path_, fdopen_mode::binary);

      uint64_t n (fdstat (ifs.fd ()).size);

      // Experience shows that for the type of content we typically cache
      // using 1MB blocks results in almost the same comression as for 4MB.
      //
      uint64_t cn (lz4::compress (ofs, ifs,
                                  1 /* compression_level (fastest) */,
                                  6 /* block_size_id (1MB) */,
                                  n));

      ofs.close ();

      l6 ([&]{trace << "compressed " << path_ << " to "
                    << (cn * 100 / n) << '%';});
    }
    catch (const std::exception& e)
    {
      l5 ([&]{trace << "unable to compress " << path_ << ": " << e;});
      try_rmfile_ignore_error (comp_path_);
      return false;
    }

    return true;
  }

  void file_cache::entry::
  decompress ()
  {
    try
    {
      ifdstream ifs (comp_path_, fdopen_mode::binary, ifdstream::badbit);
      ofdstream ofs (path_,      fdopen_mode::binary);

      lz4::decompress (ofs, ifs);

      ofs.close ();
    }
    catch (const std::exception& e)
    {
      fail << "unable to decompress " << comp_path_ << ": " << e <<
        info << "consider cleaning the build state";
    }
  }

  void file_cache::entry::
  remove ()
  {
    switch (state_)
    {
    case uninit:
      {
        // In this case we are cleaning the filesystem without having any idea
        // about its state. As a result, if we couldn't remove the compressed
        // file, then we don't attempt to remove the uncompressed file either
        // since it could be an indicator that the compressed file is invalid.
        //
        if (comp_path_.empty () || try_rmfile_ignore_error (comp_path_))
          try_rmfile_ignore_error (path_);
        break;
      }
    case uncomp:
      {
        try_rmfile_ignore_error (path_);
        break;
      }
    case comp:
      {
        try_rmfile_ignore_error (comp_path_);
        break;
      }
    case decomp:
      {
        // Both are valid so we are ok with failing to remove either.
        //
        try_rmfile_ignore_error (comp_path_);
        try_rmfile_ignore_error (path_);
        break;
      }
    case null:
      assert (false);
    }
  }
}