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

#include <utility>

#include "cmsys/FStream.hxx"

#include "cmFileTime.h"
#include "cmFileTimeCache.h"
#include "cmGeneratedFileStream.h"
#include "cmLocalUnixMakefileGenerator3.h"
#include "cmMakefile.h"
#include "cmProperty.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

cmDepends::cmDepends(cmLocalUnixMakefileGenerator3* lg, std::string targetDir)
  : LocalGenerator(lg)
  , TargetDirectory(std::move(targetDir))
{
}

cmDepends::~cmDepends() = default;

bool cmDepends::Write(std::ostream& makeDepends, std::ostream& internalDepends)
{
  std::map<std::string, std::set<std::string>> dependencies;
  {
    // Lookup the set of sources to scan.
    std::vector<std::string> pairs;
    {
      std::string const srcLang = "CMAKE_DEPENDS_CHECK_" + this->Language;
      cmMakefile* mf = this->LocalGenerator->GetMakefile();
      cmExpandList(mf->GetSafeDefinition(srcLang), pairs);
    }
    for (auto si = pairs.begin(); si != pairs.end();) {
      // Get the source and object file.
      std::string const& src = *si++;
      if (si == pairs.end()) {
        break;
      }
      std::string const& obj = *si++;
      dependencies[obj].insert(src);
    }
  }
  for (auto const& d : dependencies) {
    // Write the dependencies for this pair.
    if (!this->WriteDependencies(d.second, d.first, makeDepends,
                                 internalDepends)) {
      return false;
    }
  }

  return this->Finalize(makeDepends, internalDepends);
}

bool cmDepends::Finalize(std::ostream& /*unused*/, std::ostream& /*unused*/)
{
  return true;
}

bool cmDepends::Check(const std::string& makeFile,
                      const std::string& internalFile,
                      DependencyMap& validDeps)
{
  // Check whether dependencies must be regenerated.
  bool okay = true;
  cmsys::ifstream fin(internalFile.c_str());
  if (!(fin && this->CheckDependencies(fin, internalFile, validDeps))) {
    // Clear all dependencies so they will be regenerated.
    this->Clear(makeFile);
    cmSystemTools::RemoveFile(internalFile);
    this->FileTimeCache->Remove(internalFile);
    okay = false;
  }
  return okay;
}

void cmDepends::Clear(const std::string& file) const
{
  // Print verbose output.
  if (this->Verbose) {
    cmSystemTools::Stdout(
      cmStrCat("Clearing dependencies in \"", file, "\".\n"));
  }

  // Write an empty dependency file.
  cmGeneratedFileStream depFileStream(file);
  depFileStream << "# Empty dependencies file\n"
                   "# This may be replaced when dependencies are built.\n";
}

bool cmDepends::WriteDependencies(const std::set<std::string>& /*unused*/,
                                  const std::string& /*unused*/,
                                  std::ostream& /*unused*/,
                                  std::ostream& /*unused*/)
{
  // This should be implemented by the subclass.
  return false;
}

bool cmDepends::CheckDependencies(std::istream& internalDepends,
                                  const std::string& internalDependsFileName,
                                  DependencyMap& validDeps)
{
  // Read internal depends file time
  cmFileTime internalDependsTime;
  if (!this->FileTimeCache->Load(internalDependsFileName,
                                 internalDependsTime)) {
    return false;
  }

  // Parse dependencies from the stream.  If any dependee is missing
  // or newer than the depender then dependencies should be
  // regenerated.
  bool okay = true;
  bool dependerExists = false;

  std::string line;
  line.reserve(1024);
  std::string depender;
  std::string dependee;
  cmFileTime dependerTime;
  cmFileTime dependeeTime;
  std::vector<std::string>* currentDependencies = nullptr;

  while (std::getline(internalDepends, line)) {
    // Check if this an empty or a comment line
    if (line.empty() || line.front() == '#') {
      continue;
    }
    // Drop carriage return character at the end
    if (line.back() == '\r') {
      line.pop_back();
      if (line.empty()) {
        continue;
      }
    }
    // Check if this a depender line
    if (line.front() != ' ') {
      depender = line;
      dependerExists = this->FileTimeCache->Load(depender, dependerTime);
      // If we erase validDeps[this->Depender] by overwriting it with an empty
      // vector, we lose dependencies for dependers that have multiple
      // entries. No need to initialize the entry, std::map will do so on first
      // access.
      currentDependencies = &validDeps[depender];
      continue;
    }

    // This is a dependee line
    dependee = line.substr(1);

    // Add dependee to depender's list
    if (currentDependencies != nullptr) {
      currentDependencies->push_back(dependee);
    }

    // Dependencies must be regenerated
    // * if the dependee does not exist
    // * if the depender exists and is older than the dependee.
    // * if the depender does not exist, but the dependee is newer than the
    //   depends file
    bool regenerate = false;
    bool dependeeExists = this->FileTimeCache->Load(dependee, dependeeTime);
    if (!dependeeExists) {
      // The dependee does not exist.
      regenerate = true;

      // Print verbose output.
      if (this->Verbose) {
        cmSystemTools::Stdout(cmStrCat("Dependee \"", dependee,
                                       "\" does not exist for depender \"",
                                       depender, "\".\n"));
      }
    } else if (dependerExists) {
      // The dependee and depender both exist.  Compare file times.
      if (dependerTime.Older(dependeeTime)) {
        // The depender is older than the dependee.
        regenerate = true;

        // Print verbose output.
        if (this->Verbose) {
          cmSystemTools::Stdout(cmStrCat("Dependee \"", dependee,
                                         "\" is newer than depender \"",
                                         depender, "\".\n"));
        }
      }
    } else {
      // The dependee exists, but the depender doesn't. Regenerate if the
      // internalDepends file is older than the dependee.
      if (internalDependsTime.Older(dependeeTime)) {
        // The depends-file is older than the dependee.
        regenerate = true;

        // Print verbose output.
        if (this->Verbose) {
          cmSystemTools::Stdout(cmStrCat("Dependee \"", dependee,
                                         "\" is newer than depends file \"",
                                         internalDependsFileName, "\".\n"));
        }
      }
    }

    if (regenerate) {
      // Dependencies must be regenerated.
      okay = false;

      // Remove the information of this depender from the map, it needs
      // to be rescanned
      if (currentDependencies != nullptr) {
        validDeps.erase(depender);
        currentDependencies = nullptr;
      }

      // Remove the depender to be sure it is rebuilt.
      if (dependerExists) {
        cmSystemTools::RemoveFile(depender);
        this->FileTimeCache->Remove(depender);
        dependerExists = false;
      }
    }
  }

  return okay;
}

void cmDepends::SetIncludePathFromLanguage(const std::string& lang)
{
  // Look for the new per "TARGET_" variant first:
  std::string includePathVar =
    cmStrCat("CMAKE_", lang, "_TARGET_INCLUDE_PATH");
  cmMakefile* mf = this->LocalGenerator->GetMakefile();
  cmProp includePath = mf->GetDefinition(includePathVar);
  if (includePath) {
    cmExpandList(*includePath, this->IncludePath);
  } else {
    // Fallback to the old directory level variable if no per-target var:
    includePathVar = cmStrCat("CMAKE_", lang, "_INCLUDE_PATH");
    includePath = mf->GetDefinition(includePathVar);
    if (includePath) {
      cmExpandList(*includePath, this->IncludePath);
    }
  }
}
