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

#include "cmBinUtilsWindowsPELinker.h"

#include <algorithm>
#include <iterator>
#include <sstream>
#include <utility>
#include <vector>

#include <cm/memory>

#include "cmBinUtilsWindowsPEDumpbinGetRuntimeDependenciesTool.h"
#include "cmBinUtilsWindowsPEObjdumpGetRuntimeDependenciesTool.h"
#include "cmRuntimeDependencyArchive.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

#ifdef _WIN32
#  include <windows.h>

#  include "cmsys/Encoding.hxx"
#endif

#ifdef _WIN32
namespace {

void ReplaceWithActualNameCasing(std::string& path)
{
  WIN32_FIND_DATAW findData;
  HANDLE hFind = ::FindFirstFileW(
    cmsys::Encoding::ToWindowsExtendedPath(path).c_str(), &findData);

  if (hFind != INVALID_HANDLE_VALUE) {
    auto onDiskName = cmsys::Encoding::ToNarrow(findData.cFileName);
    ::FindClose(hFind);
    path.replace(path.end() - onDiskName.size(), path.end(), onDiskName);
  }
}

}
#endif

cmBinUtilsWindowsPELinker::cmBinUtilsWindowsPELinker(
  cmRuntimeDependencyArchive* archive)
  : cmBinUtilsLinker(archive)
{
}

bool cmBinUtilsWindowsPELinker::Prepare()
{
  std::string tool = this->Archive->GetGetRuntimeDependenciesTool();
  if (tool.empty()) {
    std::vector<std::string> command;
    if (this->Archive->GetGetRuntimeDependenciesCommand("dumpbin", command)) {
      tool = "dumpbin";
    } else {
      tool = "objdump";
    }
  }
  if (tool == "dumpbin") {
    this->Tool =
      cm::make_unique<cmBinUtilsWindowsPEDumpbinGetRuntimeDependenciesTool>(
        this->Archive);
  } else if (tool == "objdump") {
    this->Tool =
      cm::make_unique<cmBinUtilsWindowsPEObjdumpGetRuntimeDependenciesTool>(
        this->Archive);
  } else {
    std::ostringstream e;
    e << "Invalid value for CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL: " << tool;
    this->SetError(e.str());
    return false;
  }

  return true;
}

bool cmBinUtilsWindowsPELinker::ScanDependencies(
  std::string const& file, cmStateEnums::TargetType /* unused */)
{
  std::vector<std::string> needed;
  if (!this->Tool->GetFileInfo(file, needed)) {
    return false;
  }

  struct WinPEDependency
  {
    WinPEDependency(std::string o)
      : Original(std::move(o))
      , LowerCase(cmSystemTools::LowerCase(Original))
    {
    }
    std::string const Original;
    std::string const LowerCase;
  };

  std::vector<WinPEDependency> depends;
  depends.reserve(needed.size());
  std::move(needed.begin(), needed.end(), std::back_inserter(depends));
  std::string origin = cmSystemTools::GetFilenamePath(file);

  for (auto const& lib : depends) {
    if (!this->Archive->IsPreExcluded(lib.LowerCase)) {
      std::string path;
      bool resolved = false;
      if (!this->ResolveDependency(lib.LowerCase, origin, path, resolved)) {
        return false;
      }
      if (resolved) {
        if (!this->Archive->IsPostExcluded(path)) {
#ifdef _WIN32
          ReplaceWithActualNameCasing(path);
#else
          path.replace(path.end() - lib.Original.size(), path.end(),
                       lib.Original);
#endif
          bool unique;
          this->Archive->AddResolvedPath(lib.Original, path, unique);
          if (unique &&
              !this->ScanDependencies(path, cmStateEnums::SHARED_LIBRARY)) {
            return false;
          }
        }
      } else {
        this->Archive->AddUnresolvedPath(lib.Original);
      }
    }
  }

  return true;
}

bool cmBinUtilsWindowsPELinker::ResolveDependency(std::string const& name,
                                                  std::string const& origin,
                                                  std::string& path,
                                                  bool& resolved)
{
  auto dirs = this->Archive->GetSearchDirectories();

#ifdef _WIN32
  char buf[MAX_PATH];
  unsigned int len;
  if ((len = GetWindowsDirectoryA(buf, MAX_PATH)) > 0) {
    dirs.insert(dirs.begin(), std::string(buf, len));
  }
  if ((len = GetSystemDirectoryA(buf, MAX_PATH)) > 0) {
    dirs.insert(dirs.begin(), std::string(buf, len));
  }
#endif

  dirs.insert(dirs.begin(), origin);

  for (auto const& searchPath : dirs) {
    path = cmStrCat(searchPath, '/', name);
    if (cmSystemTools::PathExists(path)) {
      this->NormalizePath(path);
      resolved = true;
      return true;
    }
  }

  resolved = false;
  return true;
}
