/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt 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)
  {
    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)))) {
        Directory = splitFramework.match(1);
        FileName =
          std::string(file.begin() + Directory.size() + 1, file.end());
      }
    }

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

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

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

  void FindConflicts(unsigned int index)
  {
    for (unsigned int i = 0; i < OD->OriginalDirectories.size(); ++i) {
      // Check if this directory conflicts with the entry.
      std::string const& dir = OD->OriginalDirectories[i];
      if (!OD->IsSameDirectory(dir, Directory) && 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(DirectoryIndex, index);
        OD->ConflictGraph[i].push_back(p);
      }
    }
  }

  void FindImplicitConflicts(std::ostringstream& w)
  {
    bool first = true;
    for (std::string const& dir : OD->OriginalDirectories) {
      // Check if this directory conflicts with the entry.
      if (dir != Directory &&
          cmSystemTools::GetRealPath(dir) !=
            cmSystemTools::GetRealPath(Directory) &&
          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 << "  ";
          Report(w);
          w << " in " << 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(FullPath, file);
  }

  // Check if the file will be built by cmake.
  std::set<std::string> const& files =
    (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,
                                     const char* soname)
    : cmOrderDirectoriesConstraint(od, file)
    , SOName(soname ? soname : "")
  {
    if (SOName.empty()) {
      // Try to guess the soname.
      std::string soguess;
      if (cmSystemTools::GuessLibrarySOName(file, soguess)) {
        SOName = soguess;
      }
    }
  }

  void Report(std::ostream& e) override
  {
    e << "runtime library [";
    if (SOName.empty()) {
      e << FileName;
    } else {
      e << 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 (!SOName.empty()) {
    // We have the library soname.  Check if it will be found.
    if (FileMayConflict(dir, 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 =
      (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 = 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 [" << 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 (FileMayConflict(dir, FileName)) {
    return true;
  }

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

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

cmOrderDirectories::~cmOrderDirectories() = default;

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

void cmOrderDirectories::AddRuntimeLibrary(std::string const& fullPath,
                                           const char* soname)
{
  // Add the runtime library at most once.
  if (EmmittedConstraintSOName.insert(fullPath).second) {
    // Implicit link directories need special handling.
    if (!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 (IsImplicitDirectory(dir)) {
        ImplicitDirEntries.push_back(
          cm::make_unique<cmOrderDirectoriesConstraintSOName>(this, fullPath,
                                                              soname));
        return;
      }
    }

    // Construct the runtime information entry for this library.
    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(!LinkExtensions.empty());

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

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

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

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

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

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

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

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.
  AddOriginalDirectories(UserDirectories);

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

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

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

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

    // Add this directory.
    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.
  ConflictGraph.resize(OriginalDirectories.size());
  DirectoryVisited.resize(OriginalDirectories.size(), 0);

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

  // Clean up the conflict graph representation.
  for (ConflictList& cl : 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.
  FindImplicitConflicts();
}

void cmOrderDirectories::FindImplicitConflicts()
{
  // Check for items in implicit link directories that have conflicts
  // in the explicit directories.
  std::ostringstream conflicts;
  for (const auto& entry : 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.
  GlobalGenerator->GetCMakeInstance()->IssueMessage(
    MessageType::WARNING,
    cmStrCat("Cannot generate a safe ", Purpose, " for target ",
             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."),
    Target->GetBacktrace());
}

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

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

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

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

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

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

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

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

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

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

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