/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */
#include "cmLoadCacheCommand.h"

#include <set>

#include "cmsys/FStream.hxx"

#include "cmExecutionStatus.h"
#include "cmMakefile.h"
#include "cmState.h"
#include "cmStateTypes.h"
#include "cmSystemTools.h"
#include "cmake.h"

static bool ReadWithPrefix(std::vector<std::string> const& args,
                           cmExecutionStatus& status);

static void CheckLine(cmMakefile& mf, std::string const& prefix,
                      std::set<std::string> const& variablesToRead,
                      char const* line);

bool cmLoadCacheCommand(std::vector<std::string> const& args,
                        cmExecutionStatus& status)
{
  if (args.empty()) {
    status.SetError("called with wrong number of arguments.");
    return false;
  }

  if (args.size() >= 2 && args[1] == "READ_WITH_PREFIX") {
    return ReadWithPrefix(args, status);
  }

  cmState::Role const role =
    status.GetMakefile().GetCMakeInstance()->GetState()->GetRole();
  if (role != cmState::Role::Project) {
    status.SetError(
      "Only load_cache(READ_WITH_PREFIX) may be used in script mode");
    return false;
  }

  // Cache entries to be excluded from the import list.
  // If this set is empty, all cache entries are brought in
  // and they can not be overridden.
  bool excludeFiles = false;
  std::set<std::string> excludes;

  for (std::string const& arg : args) {
    if (excludeFiles) {
      excludes.insert(arg);
    }
    if (arg == "EXCLUDE") {
      excludeFiles = true;
    }
    if (excludeFiles && (arg == "INCLUDE_INTERNALS")) {
      break;
    }
  }

  // Internal cache entries to be imported.
  // If this set is empty, no internal cache entries are
  // brought in.
  bool includeFiles = false;
  std::set<std::string> includes;

  for (std::string const& arg : args) {
    if (includeFiles) {
      includes.insert(arg);
    }
    if (arg == "INCLUDE_INTERNALS") {
      includeFiles = true;
    }
    if (includeFiles && (arg == "EXCLUDE")) {
      break;
    }
  }

  cmMakefile& mf = status.GetMakefile();

  // Loop over each build directory listed in the arguments.  Each
  // directory has a cache file.
  for (std::string const& arg : args) {
    if ((arg == "EXCLUDE") || (arg == "INCLUDE_INTERNALS")) {
      break;
    }
    mf.GetCMakeInstance()->LoadCache(arg, false, excludes, includes);
  }

  return true;
}

static bool ReadWithPrefix(std::vector<std::string> const& args,
                           cmExecutionStatus& status)
{
  // Make sure we have a prefix.
  if (args.size() < 3) {
    status.SetError("READ_WITH_PREFIX form must specify a prefix.");
    return false;
  }

  // Make sure the cache file exists.
  std::string cacheFile = args[0] + "/CMakeCache.txt";
  if (!cmSystemTools::FileExists(cacheFile)) {
    std::string e = "Cannot load cache file from " + cacheFile;
    status.SetError(e);
    return false;
  }

  // Prepare the table of variables to read.
  std::string const& prefix = args[2];
  std::set<std::string> const variablesToRead(args.begin() + 3, args.end());

  // Read the cache file.
  cmsys::ifstream fin(cacheFile.c_str());

  cmMakefile& mf = status.GetMakefile();

  // This is a big hack read loop to overcome a buggy ifstream
  // implementation on HP-UX.  This should work on all platforms even
  // for small buffer sizes.
  int const bufferSize = 4096;
  char buffer[bufferSize];
  std::string line;
  while (fin) {
    // Read a block of the file.
    fin.read(buffer, bufferSize);
    if (fin.gcount()) {
      // Parse for newlines directly.
      char const* i = buffer;
      char const* end = buffer + fin.gcount();
      while (i != end) {
        char const* begin = i;
        while (i != end && *i != '\n') {
          ++i;
        }
        if (i == begin || *(i - 1) != '\r') {
          // Include this portion of the line.
          line += std::string(begin, i - begin);
        } else {
          // Include this portion of the line.
          // Don't include the \r in a \r\n pair.
          line += std::string(begin, i - 1 - begin);
        }
        if (i != end) {
          // Completed a line.
          CheckLine(mf, prefix, variablesToRead, line.c_str());
          line.clear();

          // Skip the newline character.
          ++i;
        }
      }
    }
  }
  if (!line.empty()) {
    // Partial last line.
    CheckLine(mf, prefix, variablesToRead, line.c_str());
  }

  return true;
}

static void CheckLine(cmMakefile& mf, std::string const& prefix,
                      std::set<std::string> const& variablesToRead,
                      char const* line)
{
  // Check one line of the cache file.
  std::string var;
  std::string value;
  cmStateEnums::CacheEntryType type = cmStateEnums::UNINITIALIZED;
  if (cmake::ParseCacheEntry(line, var, value, type)) {
    // Found a real entry.  See if this one was requested.
    if (variablesToRead.find(var) != variablesToRead.end()) {
      // This was requested.  Set this variable locally with the given
      // prefix.
      var = prefix + var;
      if (!value.empty()) {
        mf.AddDefinition(var, value);
      } else {
        mf.RemoveDefinition(var);
      }
    }
  }
}
