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

#include <sstream>

#include "cmsys/FStream.hxx"

#include "cmGeneratedFileStream.h"
#include "cmGlobCacheEntry.h"
#include "cmListFileCache.h"
#include "cmMessageType.h"
#include "cmMessenger.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmVersion.h"

bool cmGlobVerificationManager::SaveVerificationScript(std::string const& path,
                                                       cmMessenger* messenger)
{
  if (this->Cache.empty()) {
    return true;
  }

  std::string scriptFile = cmStrCat(path, "/CMakeFiles");
  std::string stampFile = scriptFile;
  cmSystemTools::MakeDirectory(scriptFile);
  scriptFile += "/VerifyGlobs.cmake";
  stampFile += "/cmake.verify_globs";
  cmGeneratedFileStream verifyScriptFile(scriptFile);
  verifyScriptFile.SetCopyIfDifferent(true);
  if (!verifyScriptFile) {
    cmSystemTools::Error("Unable to open verification script file for save. " +
                         scriptFile);
    cmSystemTools::ReportLastSystemError("");
    return false;
  }

  verifyScriptFile << std::boolalpha;
  verifyScriptFile << "# CMAKE generated file: DO NOT EDIT!\n"
                   << "# Generated by CMake Version "
                   << cmVersion::GetMajorVersion() << "."
                   << cmVersion::GetMinorVersion() << "\n";

  for (auto const& i : this->Cache) {
    CacheEntryKey k = std::get<0>(i);
    CacheEntryValue v = std::get<1>(i);

    if (!v.Initialized) {
      continue;
    }

    verifyScriptFile << "\n";

    for (auto const& bt : v.Backtraces) {
      verifyScriptFile << "# " << std::get<0>(bt);
      messenger->PrintBacktraceTitle(verifyScriptFile, std::get<1>(bt));
      verifyScriptFile << "\n";
    }

    k.PrintGlobCommand(verifyScriptFile, "NEW_GLOB");
    verifyScriptFile << "\n";

    verifyScriptFile << "set(OLD_GLOB\n";
    for (std::string const& file : v.Files) {
      verifyScriptFile << "  \"" << file << "\"\n";
    }
    verifyScriptFile << "  )\n";

    verifyScriptFile << "if(NOT \"${NEW_GLOB}\" STREQUAL \"${OLD_GLOB}\")\n"
                     << "  message(\"-- GLOB mismatch!\")\n"
                     << "  set(NEW_ONLY ${NEW_GLOB})\n"
                     << "  set(OLD_ONLY ${OLD_GLOB})\n"
                     << "  list(REMOVE_ITEM NEW_ONLY ${OLD_GLOB})\n"
                     << "  list(REMOVE_ITEM OLD_ONLY ${NEW_GLOB})\n"
                     << "  if(NEW_ONLY)\n"
                     << "    message(\"The following files were added:\")\n"
                     << "    foreach(VAR_FILE IN LISTS NEW_ONLY)\n"
                     << "      message(\"  +${VAR_FILE}\")\n"
                     << "    endforeach()\n"
                     << "  endif()\n"
                     << "  if(OLD_ONLY)\n"
                     << "    message(\"The following files were removed:\")\n"
                     << "    foreach(VAR_FILE IN LISTS OLD_ONLY)\n"
                     << "      message(\"  -${VAR_FILE}\")\n"
                     << "    endforeach()\n"
                     << "  endif()\n"
                     << "  file(TOUCH_NOCREATE \"" << stampFile << "\")\n"
                     << "endif()\n";
  }
  verifyScriptFile.Close();

  cmsys::ofstream verifyStampFile(stampFile.c_str());
  if (!verifyStampFile) {
    cmSystemTools::Error("Unable to open verification stamp file for write. " +
                         stampFile);
    return false;
  }
  verifyStampFile << "# This file is generated by CMake for checking of the "
                     "VerifyGlobs.cmake file\n";
  this->VerifyScript = scriptFile;
  this->VerifyStamp = stampFile;
  return true;
}

bool cmGlobVerificationManager::DoWriteVerifyTarget() const
{
  return !this->VerifyScript.empty() && !this->VerifyStamp.empty();
}

bool cmGlobVerificationManager::CacheEntryKey::operator<(
  CacheEntryKey const& r) const
{
  if (this->Recurse < r.Recurse) {
    return true;
  }
  if (this->Recurse > r.Recurse) {
    return false;
  }
  if (this->ListDirectories < r.ListDirectories) {
    return true;
  }
  if (this->ListDirectories > r.ListDirectories) {
    return false;
  }
  if (this->FollowSymlinks < r.FollowSymlinks) {
    return true;
  }
  if (this->FollowSymlinks > r.FollowSymlinks) {
    return false;
  }
  if (this->Relative < r.Relative) {
    return true;
  }
  if (this->Relative > r.Relative) {
    return false;
  }
  if (this->Expression < r.Expression) {
    return true;
  }
  if (this->Expression > r.Expression) {
    return false;
  }
  return false;
}

void cmGlobVerificationManager::CacheEntryKey::PrintGlobCommand(
  std::ostream& out, std::string const& cmdVar)
{
  out << "file(GLOB" << (this->Recurse ? "_RECURSE " : " ");
  out << cmdVar << " ";
  if (this->Recurse && this->FollowSymlinks) {
    out << "FOLLOW_SYMLINKS ";
  }
  out << "LIST_DIRECTORIES " << this->ListDirectories << " ";
  if (!this->Relative.empty()) {
    out << "RELATIVE \"" << this->Relative << "\" ";
  }
  out << "\"" << this->Expression << "\")";
}

void cmGlobVerificationManager::AddCacheEntry(
  cmGlobCacheEntry const& entry, std::string const& variable,
  cmListFileBacktrace const& backtrace, cmMessenger* messenger)
{
  CacheEntryKey key =
    CacheEntryKey(entry.Recurse, entry.ListDirectories, entry.FollowSymlinks,
                  entry.Relative, entry.Expression);
  CacheEntryValue& value = this->Cache[key];
  if (!value.Initialized) {
    value.Files = entry.Files;
    value.Initialized = true;
    value.Backtraces.emplace_back(variable, backtrace);
  } else if (value.Initialized && value.Files != entry.Files) {
    std::ostringstream message;
    message << std::boolalpha;
    message << "The glob expression\n ";
    key.PrintGlobCommand(message, variable);
    message << "\nwas already present in the glob cache but the directory "
               "contents have changed during the configuration run.\n";
    message << "Matching glob expressions:";
    for (auto const& bt : value.Backtraces) {
      message << "\n  " << std::get<0>(bt);
      messenger->PrintBacktraceTitle(message, std::get<1>(bt));
    }
    messenger->IssueMessage(MessageType::FATAL_ERROR, message.str(),
                            backtrace);
  } else {
    value.Backtraces.emplace_back(variable, backtrace);
  }
}

std::vector<cmGlobCacheEntry> cmGlobVerificationManager::GetCacheEntries()
  const
{
  std::vector<cmGlobCacheEntry> entries;
  for (auto const& i : this->Cache) {
    CacheEntryKey k = std::get<0>(i);
    CacheEntryValue v = std::get<1>(i);
    if (v.Initialized) {
      entries.emplace_back(k.Recurse, k.ListDirectories, k.FollowSymlinks,
                           k.Relative, k.Expression, v.Files);
    }
  }
  return entries;
}

void cmGlobVerificationManager::Reset()
{
  this->Cache.clear();
  this->VerifyScript.clear();
  this->VerifyStamp.clear();
}
