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

#include "cmDependsCompiler.h"

#include <algorithm>
#include <map>
#include <string>
#include <unordered_set>
#include <utility>

#include <cm/optional>
#include <cm/string_view>
#include <cm/vector>
#include <cmext/string_view>

#include "cmsys/FStream.hxx"

#include "cmFileTime.h"
#include "cmGccDepfileReader.h"
#include "cmGccDepfileReaderTypes.h"
#include "cmGlobalUnixMakefileGenerator3.h"
#include "cmLocalUnixMakefileGenerator3.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

bool cmDependsCompiler::CheckDependencies(
  std::string const& internalDepFile, std::vector<std::string> const& depFiles,
  cmDepends::DependencyMap& dependencies,
  std::function<bool(std::string const&)> const& isValidPath)
{
  bool status = true;
  bool forceReadDeps = true;

  cmFileTime internalDepFileTime;
  // read cached dependencies stored in internal file
  if (cmSystemTools::FileExists(internalDepFile)) {
    internalDepFileTime.Load(internalDepFile);
    forceReadDeps = false;

    // read current dependencies
    cmsys::ifstream fin(internalDepFile.c_str());
    if (fin) {
      std::string line;
      std::string depender;
      std::vector<std::string>* currentDependencies = nullptr;
      while (std::getline(fin, 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 = std::move(line);
          currentDependencies = &dependencies[depender];
          continue;
        }
        // This is a dependee line
        if (currentDependencies) {
          currentDependencies->emplace_back(line.substr(1));
        }
      }
      fin.close();
    }
  }

  // Now, update dependencies map with all new compiler generated
  // dependencies files
  cmFileTime depFileTime;
  for (auto dep = depFiles.begin(); dep != depFiles.end(); dep++) {
    auto const& source = *dep++;
    auto const& target = *dep++;
    auto const& format = *dep++;
    auto const& depFile = *dep;

    if (!cmSystemTools::FileExists(depFile)) {
      continue;
    }

    if (!forceReadDeps) {
      depFileTime.Load(depFile);
    }
    if (forceReadDeps || depFileTime.Compare(internalDepFileTime) >= 0) {
      status = false;
      if (this->Verbose) {
        cmSystemTools::Stdout(cmStrCat("Dependencies file \"", depFile,
                                       "\" is newer than depends file \"",
                                       internalDepFile, "\".\n"));
      }

      std::vector<std::string> depends;
      if (format == "custom"_s) {
        cm::optional<cmGccDepfileContent> deps = cmReadGccDepfile(
          depFile.c_str(), this->LocalGenerator->GetCurrentBinaryDirectory());
        if (!deps || deps->empty()) {
          continue;
        }

        for (auto& entry : *deps) {
          depends = std::move(entry.paths);
          if (isValidPath) {
            cm::erase_if(depends, isValidPath);
          }
          // copy depends for each target, except first one, which can be
          // moved
          for (auto index = entry.rules.size() - 1; index > 0; --index) {
            dependencies[entry.rules[index]] = depends;
          }
          dependencies[entry.rules.front()] = std::move(depends);
        }
      } else {
        if (format == "msvc"_s) {
          cmsys::ifstream fin(depFile.c_str());
          if (!fin) {
            continue;
          }

          std::string line;
          if (!isValidPath && !source.empty()) {
            // insert source as first dependency
            depends.push_back(source);
          }
          while (cmSystemTools::GetLineFromStream(fin, line)) {
            depends.emplace_back(std::move(line));
          }
        } else if (format == "gcc"_s) {
          cm::optional<cmGccDepfileContent> deps = cmReadGccDepfile(
            depFile.c_str(), this->LocalGenerator->GetCurrentBinaryDirectory(),
            GccDepfilePrependPaths::Deps);
          if (!deps || deps->empty()) {
            continue;
          }

          // dependencies generated by the compiler contains only one target
          depends = std::move(deps->front().paths);
          if (depends.empty()) {
            // unexpectedly empty, ignore it and continue
            continue;
          }

          // depending of the effective format of the dependencies file
          // generated by the compiler, the target can be wrongly identified
          // as a dependency so remove it from the list
          if (depends.front() == target) {
            depends.erase(depends.begin());
          }

          // ensure source file is the first dependency
          if (!source.empty()) {
            if (depends.front() != source) {
              cm::erase(depends, source);
              if (!isValidPath) {
                depends.insert(depends.begin(), source);
              }
            } else if (isValidPath) {
              // remove first dependency because it must not be filtered out
              depends.erase(depends.begin());
            }
          }
        } else {
          // unknown format, ignore it
          continue;
        }

        if (isValidPath) {
          cm::erase_if(depends, isValidPath);
          if (!source.empty()) {
            // insert source as first dependency
            depends.insert(depends.begin(), source);
          }
        }

        dependencies[target] = std::move(depends);
      }
    }
  }

  return status;
}

void cmDependsCompiler::WriteDependencies(
  cmDepends::DependencyMap const& dependencies, std::ostream& makeDepends,
  std::ostream& internalDepends)
{
  // dependencies file consumed by make tool
  auto const& lineContinue = static_cast<cmGlobalUnixMakefileGenerator3*>(
                               this->LocalGenerator->GetGlobalGenerator())
                               ->LineContinueDirective;
  bool supportLongLineDepend = static_cast<cmGlobalUnixMakefileGenerator3*>(
                                 this->LocalGenerator->GetGlobalGenerator())
                                 ->SupportsLongLineDependencies();
  cmDepends::DependencyMap makeDependencies(dependencies);
  std::unordered_set<cm::string_view> phonyTargets;

  // external dependencies file
  for (auto& node : makeDependencies) {
    auto target = this->LocalGenerator->ConvertToMakefilePath(
      this->LocalGenerator->MaybeRelativeToTopBinDir(node.first));
    auto& deps = node.second;
    std::transform(deps.cbegin(), deps.cend(), deps.begin(),
                   [this](std::string const& dep) {
                     return this->LocalGenerator->ConvertToMakefilePath(
                       this->LocalGenerator->MaybeRelativeToTopBinDir(dep));
                   });

    bool first_dep = true;
    if (supportLongLineDepend) {
      makeDepends << target << ": ";
    }
    for (auto const& dep : deps) {
      if (supportLongLineDepend) {
        if (first_dep) {
          first_dep = false;
          makeDepends << dep;
        } else {
          makeDepends << ' ' << lineContinue << "  " << dep;
        }
      } else {
        makeDepends << target << ": " << dep << std::endl;
      }

      phonyTargets.emplace(dep.data(), dep.length());
    }
    makeDepends << std::endl << std::endl;
  }

  // add phony targets
  for (auto const& target : phonyTargets) {
    makeDepends << std::endl << target << ':' << std::endl;
  }

  // internal dependencies file
  for (auto const& node : dependencies) {
    internalDepends << node.first << std::endl;
    for (auto const& dep : node.second) {
      internalDepends << ' ' << dep << std::endl;
    }
    internalDepends << std::endl;
  }
}

void cmDependsCompiler::ClearDependencies(
  std::vector<std::string> const& depFiles)
{
  for (auto dep = depFiles.begin(); dep != depFiles.end(); dep++) {
    dep += 3;
    cmSystemTools::RemoveFile(*dep);
  }
}
