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

#include <algorithm>
#include <cassert>
#include <functional>
#include <sstream>
#include <vector>

#include <cm/memory>
#include <cmext/algorithm>

#include "cmGeneratorTarget.h"
#include "cmGlobalGenerator.h"
#include "cmMessageType.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmake.h"

/*
Directory ordering computation.
  - Useful to compute a safe runtime library path order
  - Need runtime path for supporting INSTALL_RPATH_USE_LINK_PATH
  - Need runtime path at link time to pickup transitive link dependencies
    for shared libraries.
*/

class cmOrderDirectoriesConstraint
{
public:
  cmOrderDirectoriesConstraint(cmOrderDirectories* od, std::string const& file)
    : OD(od)
    , GlobalGenerator(od->GlobalGenerator)
  {
    this->FullPath = file;

    if (file.rfind(".framework") != std::string::npos) {
      static cmsys::RegularExpression splitFramework(
        "^(.*)/(.*).framework/(.*)$");
      if (splitFramework.find(file) &&
          (std::string::npos !=
           splitFramework.match(3).find(splitFramework.match(2)))) {
        this->Directory = splitFramework.match(1);
        this->FileName =
          std::string(file.begin() + this->Directory.size() + 1, file.end());
      }
    }

    if (this->FileName.empty()) {
      this->Directory = cmSystemTools::GetFilenamePath(file);
      this->FileName = cmSystemTools::GetFilenameName(file);
    }
  }
  virtual ~cmOrderDirectoriesConstraint() = default;

  void AddDirectory()
  {
    this->DirectoryIndex = this->OD->AddOriginalDirectory(this->Directory);
  }

  virtual void Report(std::ostream& e) = 0;

  void FindConflicts(unsigned int index)
  {
    for (unsigned int i = 0; i < this->OD->OriginalDirectories.size(); ++i) {
      // Check if this directory conflicts with the entry.
      std::string const& dir = this->OD->OriginalDirectories[i];
      if (!this->OD->IsSameDirectory(dir, this->Directory) &&
          this->FindConflict(dir)) {
        // The library will be found in this directory but this is not
        // the directory named for it.  Add an entry to make sure the
        // desired directory comes before this one.
        cmOrderDirectories::ConflictPair p(this->DirectoryIndex, index);
        this->OD->ConflictGraph[i].push_back(p);
      }
    }
  }

  void FindImplicitConflicts(std::ostringstream& w)
  {
    bool first = true;
    for (std::string const& dir : this->OD->OriginalDirectories) {
      // Check if this directory conflicts with the entry.
      if (dir != this->Directory &&
          cmSystemTools::GetRealPath(dir) !=
            cmSystemTools::GetRealPath(this->Directory) &&
          this->FindConflict(dir)) {
        // The library will be found in this directory but it is
        // supposed to be found in an implicit search directory.
        if (first) {
          first = false;
          w << "  ";
          this->Report(w);
          w << " in " << this->Directory << " may be hidden by files in:\n";
        }
        w << "    " << dir << "\n";
      }
    }
  }

protected:
  virtual bool FindConflict(std::string const& dir) = 0;

  bool FileMayConflict(std::string const& dir, std::string const& name);

  cmOrderDirectories* OD;
  cmGlobalGenerator* GlobalGenerator;

  // The location in which the item is supposed to be found.
  std::string FullPath;
  std::string Directory;
  std::string FileName;

  // The index assigned to the directory.
  int DirectoryIndex;
};

bool cmOrderDirectoriesConstraint::FileMayConflict(std::string const& dir,
                                                   std::string const& name)
{
  // Check if the file exists on disk.
  std::string file = cmStrCat(dir, '/', name);
  if (cmSystemTools::FileExists(file, true)) {
    // The file conflicts only if it is not the same as the original
    // file due to a symlink or hardlink.
    return !cmSystemTools::SameFile(this->FullPath, file);
  }

  // Check if the file will be built by cmake.
  std::set<std::string> const& files =
    (this->GlobalGenerator->GetDirectoryContent(dir, false));
  auto fi = files.find(name);
  return fi != files.end();
}

class cmOrderDirectoriesConstraintSOName : public cmOrderDirectoriesConstraint
{
public:
  cmOrderDirectoriesConstraintSOName(cmOrderDirectories* od,
                                     std::string const& file,
                                     char const* soname)
    : cmOrderDirectoriesConstraint(od, file)
    , SOName(soname ? soname : "")
  {
    if (this->SOName.empty()) {
      // Try to guess the soname.
      std::string soguess;
      if (cmSystemTools::GuessLibrarySOName(file, soguess)) {
        this->SOName = soguess;
      }
    }
  }

  void Report(std::ostream& e) override
  {
    e << "runtime library [";
    if (this->SOName.empty()) {
      e << this->FileName;
    } else {
      e << this->SOName;
    }
    e << "]";
  }

  bool FindConflict(std::string const& dir) override;

private:
  // The soname of the shared library if it is known.
  std::string SOName;
};

bool cmOrderDirectoriesConstraintSOName::FindConflict(std::string const& dir)
{
  // Determine which type of check to do.
  if (!this->SOName.empty()) {
    // We have the library soname.  Check if it will be found.
    if (this->FileMayConflict(dir, this->SOName)) {
      return true;
    }
  } else {
    // We do not have the soname.  Look for files in the directory
    // that may conflict.
    std::set<std::string> const& files =
      (this->GlobalGenerator->GetDirectoryContent(dir, true));

    // Get the set of files that might conflict.  Since we do not
    // know the soname just look at all files that start with the
    // file name.  Usually the soname starts with the library name.
    std::string base = this->FileName;
    auto first = files.lower_bound(base);
    ++base.back();
    auto last = files.upper_bound(base);
    if (first != last) {
      return true;
    }
  }
  return false;
}

class cmOrderDirectoriesConstraintLibrary : public cmOrderDirectoriesConstraint
{
public:
  cmOrderDirectoriesConstraintLibrary(cmOrderDirectories* od,
                                      std::string const& file)
    : cmOrderDirectoriesConstraint(od, file)
  {
  }

  void Report(std::ostream& e) override
  {
    e << "link library [" << this->FileName << "]";
  }

  bool FindConflict(std::string const& dir) override;
};

bool cmOrderDirectoriesConstraintLibrary::FindConflict(std::string const& dir)
{
  // We have the library file name.  Check if it will be found.
  if (this->FileMayConflict(dir, this->FileName)) {
    return true;
  }

  // Now check if the file exists with other extensions the linker
  // might consider.
  if (!this->OD->LinkExtensions.empty() &&
      this->OD->RemoveLibraryExtension.find(this->FileName)) {
    std::string lib = this->OD->RemoveLibraryExtension.match(1);
    std::string ext = this->OD->RemoveLibraryExtension.match(2);
    for (std::string const& LinkExtension : this->OD->LinkExtensions) {
      if (LinkExtension != ext) {
        std::string fname = cmStrCat(lib, LinkExtension);
        if (this->FileMayConflict(dir, fname)) {
          return true;
        }
      }
    }
  }
  return false;
}

cmOrderDirectories::cmOrderDirectories(cmGlobalGenerator* gg,
                                       cmGeneratorTarget const* target,
                                       char const* purpose)
{
  this->GlobalGenerator = gg;
  this->Target = target;
  this->Purpose = purpose;
  this->Computed = false;
}

cmOrderDirectories::~cmOrderDirectories() = default;

std::vector<std::string> const& cmOrderDirectories::GetOrderedDirectories()
{
  if (!this->Computed) {
    this->Computed = true;
    this->CollectOriginalDirectories();
    this->FindConflicts();
    this->OrderDirectories();
  }
  return this->OrderedDirectories;
}

void cmOrderDirectories::AddRuntimeLibrary(std::string const& fullPath,
                                           char const* soname)
{
  // Add the runtime library at most once.
  if (this->EmittedConstraintSOName.insert(fullPath).second) {
    // Implicit link directories need special handling.
    if (!this->ImplicitDirectories.empty()) {
      std::string dir = cmSystemTools::GetFilenamePath(fullPath);

      if (fullPath.rfind(".framework") != std::string::npos) {
        static cmsys::RegularExpression splitFramework(
          "^(.*)/(.*).framework/(.*)$");
        if (splitFramework.find(fullPath) &&
            (std::string::npos !=
             splitFramework.match(3).find(splitFramework.match(2)))) {
          dir = splitFramework.match(1);
        }
      }

      if (this->IsImplicitDirectory(dir)) {
        this->ImplicitDirEntries.push_back(
          cm::make_unique<cmOrderDirectoriesConstraintSOName>(this, fullPath,
                                                              soname));
        return;
      }
    }

    // Construct the runtime information entry for this library.
    this->ConstraintEntries.push_back(
      cm::make_unique<cmOrderDirectoriesConstraintSOName>(this, fullPath,
                                                          soname));
  } else {
    // This can happen if the same library is linked multiple times.
    // In that case the runtime information check need be done only
    // once anyway.  For shared libs we could add a check in AddItem
    // to not repeat them.
  }
}

void cmOrderDirectories::AddLinkLibrary(std::string const& fullPath)
{
  // Link extension info is required for library constraints.
  assert(!this->LinkExtensions.empty());

  // Add the link library at most once.
  if (this->EmittedConstraintLibrary.insert(fullPath).second) {
    // Implicit link directories need special handling.
    if (!this->ImplicitDirectories.empty()) {
      std::string dir = cmSystemTools::GetFilenamePath(fullPath);
      if (this->IsImplicitDirectory(dir)) {
        this->ImplicitDirEntries.push_back(
          cm::make_unique<cmOrderDirectoriesConstraintLibrary>(this,
                                                               fullPath));
        return;
      }
    }

    // Construct the link library entry.
    this->ConstraintEntries.push_back(
      cm::make_unique<cmOrderDirectoriesConstraintLibrary>(this, fullPath));
  }
}

void cmOrderDirectories::AddUserDirectories(
  std::vector<std::string> const& extra)
{
  cm::append(this->UserDirectories, extra);
}

void cmOrderDirectories::AddLanguageDirectories(
  std::vector<std::string> const& dirs)
{
  cm::append(this->LanguageDirectories, dirs);
}

void cmOrderDirectories::SetImplicitDirectories(
  std::set<std::string> const& implicitDirs)
{
  this->ImplicitDirectories.clear();
  for (std::string const& implicitDir : implicitDirs) {
    this->ImplicitDirectories.insert(this->GetRealPath(implicitDir));
  }
}

bool cmOrderDirectories::IsImplicitDirectory(std::string const& dir)
{
  std::string const& real = this->GetRealPath(dir);
  return this->ImplicitDirectories.find(real) !=
    this->ImplicitDirectories.end();
}

void cmOrderDirectories::SetLinkExtensionInfo(
  std::vector<std::string> const& linkExtensions,
  std::string const& removeExtRegex)
{
  this->LinkExtensions = linkExtensions;
  this->RemoveLibraryExtension.compile(removeExtRegex);
}

void cmOrderDirectories::CollectOriginalDirectories()
{
  // Add user directories specified for inclusion.  These should be
  // indexed first so their original order is preserved as much as
  // possible subject to the constraints.
  this->AddOriginalDirectories(this->UserDirectories);

  // Add directories containing constraints.
  for (auto const& entry : this->ConstraintEntries) {
    entry->AddDirectory();
  }

  // Add language runtime directories last.
  this->AddOriginalDirectories(this->LanguageDirectories);
}

int cmOrderDirectories::AddOriginalDirectory(std::string const& dir)
{
  // Add the runtime directory with a unique index.
  auto i = this->DirectoryIndex.find(dir);
  if (i == this->DirectoryIndex.end()) {
    std::map<std::string, int>::value_type entry(
      dir, static_cast<int>(this->OriginalDirectories.size()));
    i = this->DirectoryIndex.insert(entry).first;
    this->OriginalDirectories.push_back(dir);
  }

  return i->second;
}

void cmOrderDirectories::AddOriginalDirectories(
  std::vector<std::string> const& dirs)
{
  for (std::string const& dir : dirs) {
    // We never explicitly specify implicit link directories.
    if (this->IsImplicitDirectory(dir)) {
      continue;
    }

    // Skip the empty string.
    if (dir.empty()) {
      continue;
    }

    // Add this directory.
    this->AddOriginalDirectory(dir);
  }
}

struct cmOrderDirectoriesCompare
{
  using ConflictPair = std::pair<int, int>;

  // The conflict pair is unique based on just the directory
  // (first).  The second element is only used for displaying
  // information about why the entry is present.
  bool operator()(ConflictPair l, ConflictPair r)
  {
    return l.first == r.first;
  }
};

void cmOrderDirectories::FindConflicts()
{
  // Allocate the conflict graph.
  this->ConflictGraph.resize(this->OriginalDirectories.size());
  this->DirectoryVisited.resize(this->OriginalDirectories.size(), 0);

  // Find directories conflicting with each entry.
  for (unsigned int i = 0; i < this->ConstraintEntries.size(); ++i) {
    this->ConstraintEntries[i]->FindConflicts(i);
  }

  // Clean up the conflict graph representation.
  for (ConflictList& cl : this->ConflictGraph) {
    // Sort the outgoing edges for each graph node so that the
    // original order will be preserved as much as possible.
    std::sort(cl.begin(), cl.end());

    // Make the edge list unique so cycle detection will be reliable.
    auto last = std::unique(cl.begin(), cl.end(), cmOrderDirectoriesCompare());
    cl.erase(last, cl.end());
  }

  // Check items in implicit link directories.
  this->FindImplicitConflicts();
}

void cmOrderDirectories::FindImplicitConflicts()
{
  // Check for items in implicit link directories that have conflicts
  // in the explicit directories.
  std::ostringstream conflicts;
  for (auto const& entry : this->ImplicitDirEntries) {
    entry->FindImplicitConflicts(conflicts);
  }

  // Skip warning if there were no conflicts.
  std::string const text = conflicts.str();
  if (text.empty()) {
    return;
  }

  // Warn about the conflicts.
  this->GlobalGenerator->GetCMakeInstance()->IssueMessage(
    MessageType::WARNING,
    cmStrCat("Cannot generate a safe ", this->Purpose, " for target ",
             this->Target->GetName(),
             " because files in some directories may "
             "conflict with  libraries in implicit directories:\n",
             text, "Some of these libraries may not be found correctly."),
    this->Target->GetBacktrace());
}

void cmOrderDirectories::OrderDirectories()
{
  // Allow a cycle to be diagnosed once.
  this->CycleDiagnosed = false;
  this->WalkId = 0;

  // Iterate through the directories in the original order.
  for (unsigned int i = 0; i < this->OriginalDirectories.size(); ++i) {
    // Start a new DFS from this node.
    ++this->WalkId;
    this->VisitDirectory(i);
  }
}

void cmOrderDirectories::VisitDirectory(unsigned int i)
{
  // Skip nodes already visited.
  if (this->DirectoryVisited[i]) {
    if (this->DirectoryVisited[i] == this->WalkId) {
      // We have reached a node previously visited on this DFS.
      // There is a cycle.
      this->DiagnoseCycle();
    }
    return;
  }

  // We are now visiting this node so mark it.
  this->DirectoryVisited[i] = this->WalkId;

  // Visit the neighbors of the node first.
  ConflictList const& clist = this->ConflictGraph[i];
  for (ConflictPair const& j : clist) {
    this->VisitDirectory(j.first);
  }

  // Now that all directories required to come before this one have
  // been emitted, emit this directory.
  this->OrderedDirectories.push_back(this->OriginalDirectories[i]);
}

void cmOrderDirectories::DiagnoseCycle()
{
  // Report the cycle at most once.
  if (this->CycleDiagnosed) {
    return;
  }
  this->CycleDiagnosed = true;

  // Construct the message.
  std::ostringstream e;
  e << "Cannot generate a safe " << this->Purpose << " for target "
    << this->Target->GetName()
    << " because there is a cycle in the constraint graph:\n";

  // Display the conflict graph.
  for (unsigned int i = 0; i < this->ConflictGraph.size(); ++i) {
    ConflictList const& clist = this->ConflictGraph[i];
    e << "  dir " << i << " is [" << this->OriginalDirectories[i] << "]\n";
    for (ConflictPair const& j : clist) {
      e << "    dir " << j.first << " must precede it due to ";
      this->ConstraintEntries[j.second]->Report(e);
      e << "\n";
    }
  }
  e << "Some of these libraries may not be found correctly.";
  this->GlobalGenerator->GetCMakeInstance()->IssueMessage(
    MessageType::WARNING, e.str(), this->Target->GetBacktrace());
}

bool cmOrderDirectories::IsSameDirectory(std::string const& l,
                                         std::string const& r)
{
  return this->GetRealPath(l) == this->GetRealPath(r);
}

std::string const& cmOrderDirectories::GetRealPath(std::string const& dir)
{
  auto i = this->RealPaths.lower_bound(dir);
  if (i == this->RealPaths.end() ||
      this->RealPaths.key_comp()(dir, i->first)) {
    using value_type = std::map<std::string, std::string>::value_type;
    i = this->RealPaths.insert(
      i, value_type(dir, cmSystemTools::GetRealPath(dir)));
  }
  return i->second;
}
