/* 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 "cmFileTimeComparison.h"
#include "cmGeneratedFileStream.h"
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmSystemTools.h"

#include "cmsys/FStream.hxx"
#include <sstream>
#include <string.h>
#include <utility>

cmDepends::cmDepends(cmLocalGenerator* lg, const char* targetDir)
  : LocalGenerator(lg)
  , Verbose(false)
  , FileComparison(nullptr)
  , TargetDirectory(targetDir)
  , MaxPath(16384)
  , Dependee(new char[MaxPath])
  , Depender(new char[MaxPath])
{
}

cmDepends::~cmDepends()
{
  delete[] this->Dependee;
  delete[] this->Depender;
}

bool cmDepends::Write(std::ostream& makeDepends, std::ostream& internalDepends)
{
  // Lookup the set of sources to scan.
  std::string srcLang = "CMAKE_DEPENDS_CHECK_";
  srcLang += this->Language;
  cmMakefile* mf = this->LocalGenerator->GetMakefile();
  std::string const& srcStr = mf->GetSafeDefinition(srcLang);
  std::vector<std::string> pairs;
  cmSystemTools::ExpandListArgument(srcStr, pairs);

  std::map<std::string, std::set<std::string>> dependencies;
  for (std::vector<std::string>::iterator 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 char* makeFile, const char* internalFile,
                      std::map<std::string, DependencyVector>& validDeps)
{
  // Check whether dependencies must be regenerated.
  bool okay = true;
  cmsys::ifstream fin(internalFile);
  if (!(fin && this->CheckDependencies(fin, internalFile, validDeps))) {
    // Clear all dependencies so they will be regenerated.
    this->Clear(makeFile);
    cmSystemTools::RemoveFile(internalFile);
    okay = false;
  }

  return okay;
}

void cmDepends::Clear(const char* file)
{
  // Print verbose output.
  if (this->Verbose) {
    std::ostringstream msg;
    msg << "Clearing dependencies in \"" << file << "\"." << std::endl;
    cmSystemTools::Stdout(msg.str().c_str());
  }

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

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 char* internalDependsFileName,
  std::map<std::string, DependencyVector>& validDeps)
{
  // 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;
  DependencyVector* currentDependencies = nullptr;

  while (internalDepends.getline(this->Dependee, this->MaxPath)) {
    if (this->Dependee[0] == 0 || this->Dependee[0] == '#' ||
        this->Dependee[0] == '\r') {
      continue;
    }
    size_t len = internalDepends.gcount() - 1;
    if (this->Dependee[len - 1] == '\r') {
      len--;
      this->Dependee[len] = 0;
    }
    if (this->Dependee[0] != ' ') {
      memcpy(this->Depender, this->Dependee, len + 1);
      // Calling FileExists() for the depender here saves in many cases 50%
      // of the calls to FileExists() further down in the loop. E.g. for
      // kdelibs/khtml this reduces the number of calls from 184k down to 92k,
      // or the time for cmake -E cmake_depends from 0.3 s down to 0.21 s.
      dependerExists = cmSystemTools::FileExists(this->Depender);
      // 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[this->Depender];
      continue;
    }
    /*
    // Parse the dependency line.
    if(!this->ParseDependency(line.c_str()))
      {
      continue;
      }
      */

    // 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;
    const char* dependee = this->Dependee + 1;
    const char* depender = this->Depender;
    if (currentDependencies != nullptr) {
      currentDependencies->push_back(dependee);
    }

    if (!cmSystemTools::FileExists(dependee)) {
      // The dependee does not exist.
      regenerate = true;

      // Print verbose output.
      if (this->Verbose) {
        std::ostringstream msg;
        msg << "Dependee \"" << dependee << "\" does not exist for depender \""
            << depender << "\"." << std::endl;
        cmSystemTools::Stdout(msg.str().c_str());
      }
    } else {
      if (dependerExists) {
        // The dependee and depender both exist.  Compare file times.
        int result = 0;
        if ((!this->FileComparison->FileTimeCompare(depender, dependee,
                                                    &result) ||
             result < 0)) {
          // The depender is older than the dependee.
          regenerate = true;

          // Print verbose output.
          if (this->Verbose) {
            std::ostringstream msg;
            msg << "Dependee \"" << dependee << "\" is newer than depender \""
                << depender << "\"." << std::endl;
            cmSystemTools::Stdout(msg.str().c_str());
          }
        }
      } else {
        // The dependee exists, but the depender doesn't. Regenerate if the
        // internalDepends file is older than the dependee.
        int result = 0;
        if ((!this->FileComparison->FileTimeCompare(internalDependsFileName,
                                                    dependee, &result) ||
             result < 0)) {
          // The depends-file is older than the dependee.
          regenerate = true;

          // Print verbose output.
          if (this->Verbose) {
            std::ostringstream msg;
            msg << "Dependee \"" << dependee
                << "\" is newer than depends file \""
                << internalDependsFileName << "\"." << std::endl;
            cmSystemTools::Stdout(msg.str().c_str());
          }
        }
      }
    }
    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(this->Depender);
        currentDependencies = nullptr;
      }

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

  return okay;
}

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