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

#include "cmCMakePkgConfigCommand.h"

#include <cstdio>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

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

#include "cmsys/FStream.hxx"

#include "cmArgumentParser.h"
#include "cmArgumentParserTypes.h"
#include "cmExecutionStatus.h"
#include "cmList.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmPkgConfigParser.h"
#include "cmPkgConfigResolver.h"
#include "cmStateTypes.h"
#include "cmStringAlgorithms.h"
#include "cmSubcommandTable.h"
#include "cmSystemTools.h"
#include "cmTarget.h"
#include "cmValue.h"
#include <cmllpkgc/llpkgc.h>

// IWYU wants this
namespace {
struct ExtractArguments;
struct PopulateArguments;
}

namespace {

cm::optional<std::string> GetPkgConfigBin(cmMakefile& mf)
{
  cm::optional<std::string> result;

  auto pkgcfg = mf.GetDefinition("CMAKE_PKG_CONFIG_BIN");
  if (pkgcfg.IsNOTFOUND()) {
    return result;
  }

  if (pkgcfg) {
    result = *pkgcfg;
    return result;
  }

  std::string path = cmSystemTools::FindProgram("pkgconf");
  if (path.empty()) {
    path = cmSystemTools::FindProgram("pkg-config");
    if (path.empty()) {
      mf.AddCacheDefinition("CMAKE_PKG_CONFIG_BIN", "pkg-config-NOTFOUND",
                            "Location of pkg-config or pkgconf binary",
                            cmStateEnums::FILEPATH);
      return result;
    }
  }

  mf.AddCacheDefinition("CMAKE_PKG_CONFIG_BIN", path,
                        "Location of pkg-config or pkgconf binary",
                        cmStateEnums::FILEPATH);

  result = std::move(path);
  return result;
}

std::vector<std::string> GetLocations(cmMakefile& mf, char const* cachevar,
                                      char const* envvar, char const* desc,
                                      char const* pcvar, bool need_pkgconf,
                                      std::vector<std::string> default_locs)
{
  auto def = mf.GetDefinition(cachevar);
  if (def) {
    return cmList(def);
  }

  std::string paths;
  if (cmSystemTools::GetEnv(envvar, paths)) {
    cmPkgConfigResolver::ReplaceSep(paths);
    mf.AddCacheDefinition(cachevar, paths, desc, cmStateEnums::STRING);
    return cmList(paths);
  }

  auto pkgcfg = GetPkgConfigBin(mf);
  if (!pkgcfg || (need_pkgconf && (pkgcfg->find("pkgconf") == pkgcfg->npos))) {
    mf.AddCacheDefinition(cachevar, cmList::to_string(default_locs), desc,
                          cmStateEnums::STRING);
    return default_locs;
  }

  std::string out;
  cmSystemTools::RunSingleCommand({ *pkgcfg, pcvar, "pkg-config" }, &out,
                                  nullptr, nullptr, nullptr,
                                  cmSystemTools::OUTPUT_NONE);

  cmPkgConfigResolver::ReplaceSep(out);
  out = cmTrimWhitespace(out);
  mf.AddCacheDefinition(cachevar, out, desc, cmStateEnums::STRING);
  return cmList(out);
}

std::vector<std::string> GetPcLibDirs(cmMakefile& mf)
{
  std::vector<std::string> default_locs = {
#ifndef _WIN32
    "/usr/lib/pkgconfig", "/usr/share/pkgconfig"
#endif
  };
  return GetLocations(mf, "CMAKE_PKG_CONFIG_PC_LIB_DIRS", "PKG_CONFIG_LIBDIR",
                      "Default search locations for package files",
                      "--variable=pc_path", false, std::move(default_locs));
}

std::vector<std::string> GetSysLibDirs(cmMakefile& mf)
{
  std::vector<std::string> default_locs = {
#ifndef _WIN32
    "/lib", "/usr/lib"
#endif
  };
  return GetLocations(
    mf, "CMAKE_PKG_CONFIG_SYS_LIB_DIRS", "PKG_CONFIG_SYSTEM_LIBRARY_PATH",
    "System library directories filtered by flag mangling",
    "--variable=pc_system_libdirs", true, std::move(default_locs));
}

std::vector<std::string> GetSysCflags(cmMakefile& mf)
{
  std::vector<std::string> default_locs = {
#ifndef _WIN32
    "/usr/include"
#endif
  };
  return GetLocations(
    mf, "CMAKE_PKG_CONFIG_SYS_INCLUDE_DIRS", "PKG_CONFIG_SYSTEM_INCLUDE_PATH",
    "System include directories filtered by flag mangling",
    "--variable=pc_system_includedirs", true, std::move(default_locs));
}

std::vector<std::string> GetPkgConfSysLibs(cmMakefile& mf)
{
  auto def = mf.GetDefinition("CMAKE_PKG_CONFIG_PKGCONF_LIB_DIRS");
  if (def) {
    return cmList(def);
  }

  std::string paths;
  if (!cmSystemTools::GetEnv("LIBRARY_PATH", paths)) {
    return {};
  }

  cmPkgConfigResolver::ReplaceSep(paths);
  mf.AddCacheDefinition("CMAKE_PKG_CONFIG_PKGCONF_LIB_DIRS", paths,
                        "Additional system library directories filtered by "
                        "flag mangling in PKGCONF mode",
                        cmStateEnums::STRING);
  return cmList(paths);
}

std::vector<std::string> GetPkgConfSysCflags(cmMakefile& mf)
{
  auto def = mf.GetDefinition("CMAKE_PKG_CONFIG_PKGCONF_INCLUDES");
  if (def) {
    return cmList(def);
  }

  std::string paths;
  auto get_and_append = [&](char const* var) {
    if (paths.empty()) {
      cmSystemTools::GetEnv(var, paths);
    } else {
      std::string tmp;
      cmSystemTools::GetEnv(var, tmp);
      if (!tmp.empty()) {
        paths += ";" + tmp;
      }
    }
  };

  get_and_append("CPATH");
  get_and_append("C_INCLUDE_PATH");
  get_and_append("CPLUS_INCLUDE_PATH");
  get_and_append("OBJC_INCLUDE_PATH");

#ifdef _WIN32
  get_and_append("INCLUDE");
#endif

  cmPkgConfigResolver::ReplaceSep(paths);
  mf.AddCacheDefinition("CMAKE_PKG_CONFIG_PKGCONF_INCLUDES", paths,
                        "Additional system include directories filtered by "
                        "flag mangling in PKGCONF mode",
                        cmStateEnums::STRING);
  return cmList(paths);
}

std::vector<std::string> GetPcPath(cmMakefile& mf)
{
  auto def = mf.GetDefinition("CMAKE_PKG_CONFIG_PC_PATH");
  if (def) {
    return cmList(def);
  }

  std::string pcpath;
  if (cmSystemTools::GetEnv("PKG_CONFIG_PATH", pcpath)) {
    auto result = cmSystemTools::SplitString(pcpath, cmPkgConfigResolver::Sep);
    mf.AddCacheDefinition(
      "CMAKE_PKG_CONFIG_PC_PATH", cmList::to_string(result),
      "Additional search locations for package files", cmStateEnums::STRING);
    return result;
  }

  mf.AddCacheDefinition("CMAKE_PKG_CONFIG_PC_PATH", "",
                        "Additional search locations for package files",
                        cmStateEnums::STRING);
  return {};
}

cm::optional<std::string> GetPath(cmMakefile& mf, char const* cachevar,
                                  char const* envvar, char const* desc)
{
  cm::optional<std::string> result;

  auto def = mf.GetDefinition(cachevar);
  if (def) {
    result = *def;
    return result;
  }

  std::string path;
  if (cmSystemTools::GetEnv(envvar, path)) {
    mf.AddCacheDefinition(cachevar, path, desc, cmStateEnums::FILEPATH);
    result = std::move(path);
    return result;
  }

  return result;
}

cm::optional<std::string> GetSysrootDir(cmMakefile& mf)
{
  return GetPath(mf, "CMAKE_PKG_CONFIG_SYSROOT_DIR", "PKG_CONFIG_SYSROOT_DIR",
                 "System root used for re-rooting package includes and "
                 "library directories");
}

cm::optional<std::string> GetTopBuildDir(cmMakefile& mf)
{
  return GetPath(mf, "CMAKE_PKG_CONFIG_TOP_BUILD_DIR",
                 "PKG_CONFIG_TOP_BUILD_DIR",
                 "Package file top_build_dir variable default value");
}

bool GetBool(cmMakefile& mf, char const* cachevar, char const* envvar,
             char const* desc)
{
  auto def = mf.GetDefinition(cachevar);
  if (def) {
    return def.IsOn();
  }

  if (cmSystemTools::HasEnv(envvar)) {
    mf.AddCacheDefinition(cachevar, "ON", desc, cmStateEnums::BOOL);
    return true;
  }

  return false;
}

bool GetDisableUninstalled(cmMakefile& mf)
{
  return GetBool(mf, "CMAKE_PKG_CONFIG_DISABLE_UNINSTALLED",
                 "PKG_CONFIG_DISABLE_UNINSTALLED",
                 "Disable search for `-uninstalled` (build tree) packages");
}

bool GetAllowSysLibs(cmMakefile& mf)
{
  return GetBool(mf, "CMAKE_PKG_CONFIG_ALLOW_SYS_LIBS",
                 "PKG_CONFIG_ALLOW_SYSTEM_LIBS",
                 "Allow system library directories during flag mangling");
}

bool GetAllowSysInclude(cmMakefile& mf)
{
  return GetBool(mf, "CMAKE_PKG_CONFIG_ALLOW_SYS_INCLUDES",
                 "PKG_CONFIG_ALLOW_SYSTEM_CFLAGS",
                 "Allow system include paths during flag manglging");
}

struct CommonArguments : ArgumentParser::ParseResult
{
  bool Required = false;
  bool Exact = false;
  bool Quiet = false;

  enum StrictnessType
  {
    STRICTNESS_STRICT,
    STRICTNESS_PERMISSIVE,
    STRICTNESS_BEST_EFFORT,
  };

  StrictnessType Strictness = STRICTNESS_PERMISSIVE;
  std::string StrictnessError;

  ArgumentParser::Continue SetStrictness(cm::string_view strictness)
  {
    if (strictness == "STRICT"_s) {
      Strictness = STRICTNESS_STRICT;
    } else if (strictness == "PERMISSIVE"_s) {
      Strictness = STRICTNESS_PERMISSIVE;
    } else if (strictness == "BEST_EFFORT"_s) {
      Strictness = STRICTNESS_BEST_EFFORT;
    } else {
      StrictnessError =
        cmStrCat("Invalid 'STRICTNESS' '", strictness,
                 "'; must be one of 'STRICT', 'PERMISSIVE', or 'BEST_EFFORT'");
    }
    return ArgumentParser::Continue::Yes;
  }

  enum EnvModeType
  {
    ENVMODE_FDO,
    ENVMODE_PKGCONF,
    ENVMODE_IGNORE,
  };

  EnvModeType EnvMode = ENVMODE_PKGCONF;
  std::string EnvModeError;

  ArgumentParser::Continue SetEnvMode(cm::string_view envMode)
  {
    if (envMode == "FDO"_s) {
      EnvMode = ENVMODE_FDO;
    } else if (envMode == "PKGCONF"_s) {
      EnvMode = ENVMODE_PKGCONF;
    } else if (envMode == "IGNORE"_s) {
      EnvMode = ENVMODE_IGNORE;
    } else {
      EnvModeError =
        cmStrCat("Invalid 'ENV_MODE' '", envMode,
                 "'; must be one of 'FDO', 'PKGCONF', or 'IGNORE'");
    }
    return ArgumentParser::Continue::Yes;
  }

  cm::optional<std::string> Package;
  cm::optional<std::string> Version;
  cm::optional<std::string> SysrootDir;
  cm::optional<std::string> TopBuildDir;

  cm::optional<bool> DisableUninstalled;

  cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>> PcPath;
  cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>> PcLibdir;

  bool CheckArgs(cmExecutionStatus& status) const
  {

    if (!Package) {
      status.SetError("A package name or absolute path must be specified");
      return false;
    }

    if (!StrictnessError.empty()) {
      status.SetError(StrictnessError);
      return false;
    }

    if (!EnvModeError.empty()) {
      status.SetError(EnvModeError);
      return false;
    }

    return true;
  }
};

#define BIND_COMMON(argtype)                                                  \
  (cmArgumentParser<argtype>{})                                               \
    .Bind(1, &argtype::Package)                                               \
    .Bind(2, &argtype::Version)                                               \
    .Bind("REQUIRED"_s, &argtype::Required)                                   \
    .Bind("EXACT"_s, &argtype::Exact)                                         \
    .Bind("QUIET"_s, &argtype::Quiet)                                         \
    .Bind("STRICTNESS"_s, &argtype::SetStrictness)                            \
    .Bind("ENV_MODE"_s, &argtype::SetEnvMode)                                 \
    .Bind("PC_SYSROOT_DIR"_s, &argtype::SysrootDir)                           \
    .Bind("TOP_BUILD_DIR"_s, &argtype::TopBuildDir)                           \
    .Bind("DISABLE_UNINSTALLED"_s, &argtype::DisableUninstalled)              \
    .Bind("PC_LIBDIR"_s, &argtype::PcLibdir)                                  \
    .Bind("PC_PATH"_s, &argtype::PcPath)

void CollectEnv(cmMakefile& mf, cmPkgConfigEnv& env,
                CommonArguments::EnvModeType mode)
{
  if (mode == CommonArguments::EnvModeType::ENVMODE_IGNORE) {
    return;
  }

  if (!env.Path) {
    env.Path = GetPcPath(mf);
  }

  if (!env.LibDirs) {
    env.LibDirs = GetPcLibDirs(mf);
  }

  if (!env.DisableUninstalled) {
    env.DisableUninstalled = GetDisableUninstalled(mf);
  }

  if (!env.SysrootDir) {
    env.SysrootDir = GetSysrootDir(mf);
  }

  if (!env.TopBuildDir) {
    env.TopBuildDir = GetTopBuildDir(mf);
  }

  env.AllowSysCflags = GetAllowSysInclude(mf);
  env.SysCflags = GetSysCflags(mf);

  env.AllowSysLibs = GetAllowSysLibs(mf);
  env.SysLibs = GetSysLibDirs(mf);

  if (mode == CommonArguments::EnvModeType::ENVMODE_FDO) {
    return;
  }

  *env.SysCflags += GetPkgConfSysCflags(mf);
  *env.SysLibs += GetPkgConfSysLibs(mf);
}

struct ImportEnv
{
  bool required;
  bool quiet;
  bool exact;
  bool err;
  CommonArguments::StrictnessType strictness;
  cmExecutionStatus& status;
};

void warn_or_error(std::string const& err, ImportEnv& imEnv)
{
  if (imEnv.required) {
    imEnv.status.SetError(err);
    cmSystemTools::SetFatalErrorOccurred();
  } else if (!imEnv.quiet) {
    imEnv.status.GetMakefile().IssueMessage(MessageType::WARNING, err);
  }
  imEnv.err = true;
}

cm::optional<cmPkgConfigResult> ReadPackage(std::string const& package,
                                            ImportEnv& imEnv,
                                            cmPkgConfigEnv& pcEnv)
{
  cm::optional<cmPkgConfigResult> result;
  cm::filesystem::path path{ package };

  if (path.extension() == ".pc") {
    if (!cmSystemTools::FileExists(path.string())) {
      return result;
    }
  } else {

    if (pcEnv.DisableUninstalled && !*pcEnv.DisableUninstalled) {
      auto uninstalled = path;
      uninstalled.concat("-uninstalled.pc");
      uninstalled =
        cmSystemTools::FindFile(uninstalled.string(), pcEnv.search, true);
      if (uninstalled.empty()) {
        path = cmSystemTools::FindFile(path.concat(".pc").string(),
                                       pcEnv.search, true);
        if (path.empty()) {
          return result;
        }
      } else {
        path = uninstalled;
      }
    } else {
      path = cmSystemTools::FindFile(path.concat(".pc").string(), pcEnv.search,
                                     true);
      if (path.empty()) {
        return result;
      }
    }
  }

  auto len = cmSystemTools::FileLength(path.string());

  // Windows requires this weird string -> c_str dance
  cmsys::ifstream ifs(path.string().c_str(), std::ios::binary);

  if (!ifs) {
    warn_or_error(cmStrCat("Could not open file '", path.string(), "'"),
                  imEnv);
    return result;
  }

  std::unique_ptr<char[]> buf(new char[len]);
  ifs.read(buf.get(), len);

  // Shouldn't have hit eof on previous read, should hit eof now
  if (ifs.fail() || ifs.eof() || ifs.get() != EOF) {
    warn_or_error(cmStrCat("Error while reading file '", path.string(), "'"),
                  imEnv);
    return result;
  }

  using StrictnessType = CommonArguments::StrictnessType;

  cmPkgConfigParser parser;
  auto err = parser.Finish(buf.get(), len);

  if (imEnv.strictness != StrictnessType::STRICTNESS_BEST_EFFORT &&
      err != PCE_OK) {
    warn_or_error(cmStrCat("Parsing failed for file '", path.string(), "'"),
                  imEnv);
    return result;
  }

  if (imEnv.strictness == StrictnessType::STRICTNESS_STRICT) {
    result = cmPkgConfigResolver::ResolveStrict(parser.Data(), pcEnv);
  } else if (imEnv.strictness == StrictnessType::STRICTNESS_PERMISSIVE) {
    result = cmPkgConfigResolver::ResolvePermissive(parser.Data(), pcEnv);
  } else {
    result = cmPkgConfigResolver::ResolveBestEffort(parser.Data(), pcEnv);
  }

  if (!result) {
    warn_or_error(cmStrCat("Resolution failed for file '", path.string(), "'"),
                  imEnv);
  }

  return result;
}

cm::optional<cmPkgConfigResult> ImportPackage(
  std::string const& package, cm::optional<std::string> version,
  ImportEnv& imEnv, cmPkgConfigEnv& pcEnv)
{
  auto result = ReadPackage(package, imEnv, pcEnv);

  if (!result) {
    if (!imEnv.err) {
      warn_or_error(cmStrCat("Could not find pkg-config: '", package, "'"),
                    imEnv);
    }
    return result;
  }

  if (imEnv.exact) {
    std::string ver;

    if (version) {
      ver = cmPkgConfigResolver::ParseVersion(*version).Version;
    }

    if (ver != result->Version()) {
      warn_or_error(
        cmStrCat("Package '", package, "' version '", result->Version(),
                 "' does not meet exact version requirement '", ver, "'"),
        imEnv);
      return {};
    }

  } else if (version) {
    auto rv = cmPkgConfigResolver::ParseVersion(*version);
    if (!cmPkgConfigResolver::CheckVersion(rv, result->Version())) {
      warn_or_error(
        cmStrCat("Package '", package, "' version '", result->Version(),
                 "' does not meet version requirement '", *version, "'"),
        imEnv);
      return {};
    }
  }

  result->env = &pcEnv;
  return result;
}

struct pkgStackEntry
{
  cmPkgConfigVersionReq ver;
  std::string parent;
};

cm::optional<cmPkgConfigResult> ImportPackage(
  std::string const& package, std::vector<pkgStackEntry> const& reqs,
  ImportEnv& imEnv, cmPkgConfigEnv& pcEnv)
{
  auto result = ReadPackage(package, imEnv, pcEnv);

  if (!result) {
    if (!imEnv.err) {
      std::string req_str = cmStrCat("'", reqs.begin()->parent, "'");
      for (auto it = reqs.begin() + 1; it != reqs.end(); ++it) {
        req_str = cmStrCat(req_str, ", '", it->parent, "'");
      }
      warn_or_error(cmStrCat("Could not find pkg-config: '", package,
                             "' required by: ", req_str),
                    imEnv);
    }
    return result;
  }

  auto ver = result->Version();
  for (auto const& req : reqs) {

    if (!cmPkgConfigResolver::CheckVersion(req.ver, ver)) {
      warn_or_error(cmStrCat("Package '", package, "' version '", ver,
                             "' does not meet version requirement '",
                             req.ver.string(), "' ", "of '", req.parent, "'"),
                    imEnv);
      return {};
    }
  }

  result->env = &pcEnv;
  return result;
}

cm::optional<std::pair<cmPkgConfigEnv, ImportEnv>> HandleCommon(
  CommonArguments& args, cmExecutionStatus& status)
{

  auto& mf = status.GetMakefile();

  if (!args.CheckArgs(status)) {
    return {};
  }

  cmPkgConfigEnv pcEnv;

  if (args.PcLibdir) {
    pcEnv.LibDirs = std::move(*args.PcLibdir);
  }

  if (args.PcPath) {
    pcEnv.Path = std::move(*args.PcPath);
  }

  pcEnv.DisableUninstalled = args.DisableUninstalled;

  if (args.SysrootDir) {
    pcEnv.SysrootDir = std::move(*args.SysrootDir);
  }

  if (args.TopBuildDir) {
    pcEnv.TopBuildDir = std::move(*args.TopBuildDir);
  }

  CollectEnv(mf, pcEnv, args.EnvMode);

  if (pcEnv.Path) {
    pcEnv.search = *pcEnv.Path;
    if (pcEnv.LibDirs) {
      pcEnv.search += *pcEnv.LibDirs;
    }
  } else if (pcEnv.LibDirs) {
    pcEnv.search = *pcEnv.LibDirs;
  }

  return std::pair<cmPkgConfigEnv, ImportEnv>{
    pcEnv,
    { args.Required, args.Quiet, args.Exact, false, args.Strictness, status }
  };
}

struct ExtractArguments : CommonArguments
{
  cm::optional<bool> AllowSystemIncludes;
  cm::optional<bool> AllowSystemLibs;

  cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>>
    SystemIncludeDirs;
  cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>>
    SystemLibraryDirs;
};

auto const ExtractParser =
  BIND_COMMON(ExtractArguments)
    .Bind("ALLOW_SYSTEM_INCLUDES"_s, &ExtractArguments::AllowSystemIncludes)
    .Bind("ALLOW_SYSTEM_LIBS"_s, &ExtractArguments::AllowSystemLibs)
    .Bind("SYSTEM_INCLUDE_DIRS"_s, &ExtractArguments::SystemIncludeDirs)
    .Bind("SYSTEM_LIBRARY_DIRS"_s, &ExtractArguments::SystemLibraryDirs);

bool HandleExtractCommand(std::vector<std::string> const& args,
                          cmExecutionStatus& status)
{

  std::vector<std::string> unparsed;
  auto parsedArgs = ExtractParser.Parse(args, &unparsed);
  auto maybeEnv = HandleCommon(parsedArgs, status);

  if (!maybeEnv) {
    return !parsedArgs.Required;
  }
  auto& pcEnv = maybeEnv->first;
  auto& imEnv = maybeEnv->second;

  auto maybePackage =
    ImportPackage(*parsedArgs.Package, parsedArgs.Version, imEnv, pcEnv);
  if (!maybePackage) {
    return !parsedArgs.Required;
  }
  auto& package = *maybePackage;

  if (parsedArgs.AllowSystemIncludes) {
    pcEnv.AllowSysCflags = *parsedArgs.AllowSystemIncludes;
  }

  if (parsedArgs.AllowSystemLibs) {
    pcEnv.AllowSysLibs = *parsedArgs.AllowSystemLibs;
  }

  if (parsedArgs.SystemIncludeDirs) {
    pcEnv.SysCflags = *parsedArgs.SystemIncludeDirs;
  }

  if (parsedArgs.SystemLibraryDirs) {
    pcEnv.SysLibs = *parsedArgs.SystemLibraryDirs;
  }

  auto& mf = status.GetMakefile();
  mf.AddDefinition("CMAKE_PKG_CONFIG_NAME", package.Name());
  mf.AddDefinition("CMAKE_PKG_CONFIG_DESCRIPTION", package.Description());
  mf.AddDefinition("CMAKE_PKG_CONFIG_VERSION", package.Version());

  auto make_list = [&](char const* def,
                       std::vector<cmPkgConfigDependency> const& deps) {
    std::vector<cm::string_view> vec;
    vec.reserve(deps.size());

    for (auto const& dep : deps) {
      vec.emplace_back(dep.Name);
    }

    mf.AddDefinition(def, cmList::to_string(vec));
  };

  make_list("CMAKE_PKG_CONFIG_CONFLICTS", package.Conflicts());
  make_list("CMAKE_PKG_CONFIG_PROVIDES", package.Provides());
  make_list("CMAKE_PKG_CONFIG_REQUIRES", package.Requires());
  make_list("CMAKE_PKG_CONFIG_REQUIRES_PRIVATE", package.Requires(true));

  auto cflags = package.Cflags();
  mf.AddDefinition("CMAKE_PKG_CONFIG_CFLAGS", cflags.Flagline);
  mf.AddDefinition("CMAKE_PKG_CONFIG_INCLUDES",
                   cmList::to_string(cflags.Includes));
  mf.AddDefinition("CMAKE_PKG_CONFIG_COMPILE_OPTIONS",
                   cmList::to_string(cflags.CompileOptions));

  cflags = package.Cflags(true);
  mf.AddDefinition("CMAKE_PKG_CONFIG_CFLAGS_PRIVATE", cflags.Flagline);
  mf.AddDefinition("CMAKE_PKG_CONFIG_INCLUDES_PRIVATE",
                   cmList::to_string(cflags.Includes));
  mf.AddDefinition("CMAKE_PKG_CONFIG_COMPILE_OPTIONS_PRIVATE",
                   cmList::to_string(cflags.CompileOptions));

  auto libs = package.Libs();
  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBS", libs.Flagline);
  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBDIRS",
                   cmList::to_string(libs.LibDirs));
  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBNAMES",
                   cmList::to_string(libs.LibNames));
  mf.AddDefinition("CMAKE_PKG_CONFIG_LINK_OPTIONS",
                   cmList::to_string(libs.LinkOptions));

  libs = package.Libs(true);
  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBS_PRIVATE", libs.Flagline);
  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBDIRS_PRIVATE",
                   cmList::to_string(libs.LibDirs));
  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBNAMES_PRIVATE",
                   cmList::to_string(libs.LibNames));
  mf.AddDefinition("CMAKE_PKG_CONFIG_LINK_OPTIONS_PRIVATE",
                   cmList::to_string(libs.LinkOptions));

  return true;
}

using pkgStack = std::unordered_map<std::string, std::vector<pkgStackEntry>>;
using pkgProviders = std::unordered_map<std::string, std::string>;

cmTarget* CreateCMakeTarget(std::string const& name, cmPkgConfigResult& pkg,
                            pkgProviders& providers, cmMakefile& mf)
{
  auto* tgt = mf.AddForeignTarget("pkgcfg", name);

  tgt->AppendProperty("VERSION", pkg.Version());

  auto libs = pkg.Libs();
  for (auto const& flag : libs.LibNames) {
    tgt->AppendProperty("INTERFACE_LINK_LIBRARIES", flag.substr(2));
  }
  for (auto const& flag : libs.LibDirs) {
    tgt->AppendProperty("INTERFACE_LINK_DIRECTORIES", flag.substr(2));
  }
  tgt->AppendProperty("INTERFACE_LINK_OPTIONS",
                      cmList::to_string(libs.LinkOptions));

  auto cflags = pkg.Cflags();
  for (auto const& flag : cflags.Includes) {
    tgt->AppendProperty("INTERFACE_INCLUDE_DIRECTORIES", flag.substr(2));
  }
  tgt->AppendProperty("INTERFACE_COMPILE_OPTIONS",
                      cmList::to_string(cflags.CompileOptions));

  for (auto& dep : pkg.Requires()) {
    auto it = providers.find(dep.Name);
    if (it != providers.end()) {
      tgt->AppendProperty("INTERFACE_LINK_LIBRARIES", it->second);
      continue;
    }

    tgt->AppendProperty("INTERFACE_LINK_LIBRARIES",
                        cmStrCat("@foreign_pkgcfg::", dep.Name));
  }
  return tgt;
}

bool CheckPackageDependencies(
  std::string const& name, cmPkgConfigResult& pkg, pkgStack& inStack,
  std::unordered_map<std::string, cmPkgConfigResult>& outStack,
  pkgProviders& providers, ImportEnv& imEnv)
{
  for (auto& dep : pkg.Requires()) {
    auto prov_it = providers.find(dep.Name);
    if (prov_it != providers.end()) {
      continue;
    }

    auto* tgt = imEnv.status.GetMakefile().FindTargetToUse(
      cmStrCat("@foreign_pkgcfg::", dep.Name),
      cmStateEnums::TargetDomain::FOREIGN);
    if (tgt) {
      auto ver = tgt->GetProperty("VERSION");
      if (!cmPkgConfigResolver::CheckVersion(dep.VerReq, *ver)) {
        warn_or_error(cmStrCat("Package '", dep.Name, "' version '", *ver,
                               "' does not meet version requirement '",
                               dep.VerReq.string(), "' ", "of '", name, "'"),
                      imEnv);
        return false;
      }
      continue;
    }

    auto it = outStack.find(dep.Name);
    if (it != outStack.end()) {
      auto ver = it->second.Version();
      if (!cmPkgConfigResolver::CheckVersion(dep.VerReq, ver)) {
        warn_or_error(cmStrCat("Package '", dep.Name, "' version '", ver,
                               "' does not meet version requirement '",
                               dep.VerReq.string(), "' ", "of '", name, "'"),
                      imEnv);
        return false;
      }
      continue;
    }

    inStack[dep.Name].emplace_back(
      pkgStackEntry{ std::move(dep.VerReq), name });
  }

  return true;
}

struct PopulateArguments : CommonArguments
{
  cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>> providers;
};

auto const PopulateParser =
  BIND_COMMON(PopulateArguments)
    .Bind("BIND_PC_REQUIRES"_s, &PopulateArguments::providers);

std::pair<bool, bool> PopulatePCTarget(PopulateArguments& args,
                                       cmExecutionStatus& status)
{

  auto& mf = status.GetMakefile();
  auto maybeEnv = HandleCommon(args, status);

  if (!maybeEnv) {
    return { !args.Required, false };
  }
  auto& pcEnv = maybeEnv->first;
  auto& imEnv = maybeEnv->second;

  pkgProviders providers;
  if (args.providers) {
    for (auto const& provider_str : *args.providers) {
      auto assignment = provider_str.find('=');
      if (assignment != std::string::npos) {
        providers.emplace(provider_str.substr(0, assignment),
                          provider_str.substr(assignment + 1));
      } else {
        imEnv.status.SetError(cmStrCat(
          "No '=' found in BIND_PC_REQUIRES argument '", provider_str, "'"));
        cmSystemTools::SetFatalErrorOccurred();
        return { false, false };
      }
    }
  }

  pkgStack inStack;
  std::unordered_map<std::string, cmPkgConfigResult> outStack;

  auto maybePackage = ImportPackage(*args.Package, args.Version, imEnv, pcEnv);
  if (!maybePackage) {
    return { !args.Required, false };
  }
  imEnv.exact = false;

  if (!CheckPackageDependencies(*args.Package, *maybePackage, inStack,
                                outStack, providers, imEnv)) {
    return { !args.Required, false };
  }
  outStack[*args.Package] = std::move(*maybePackage);

  while (!inStack.empty()) {
    auto name = inStack.begin()->first;
    auto reqs = inStack.begin()->second;
    maybePackage = ImportPackage(name, reqs, imEnv, pcEnv);
    if (!maybePackage) {
      return { !args.Required, false };
    }
    if (!CheckPackageDependencies(name, *maybePackage, inStack, outStack,
                                  providers, imEnv)) {
      return { !args.Required, false };
    }
    inStack.erase(name);
    outStack[std::move(name)] = std::move(*maybePackage);
  }

  for (auto& entry : outStack) {
    CreateCMakeTarget(entry.first, entry.second, providers, mf);
  }

  return { true, true };
}

bool HandlePopulateCommand(std::vector<std::string> const& args,
                           cmExecutionStatus& status)
{
  std::vector<std::string> unparsed;
  auto parsedArgs = PopulateParser.Parse(args, &unparsed);

  auto foreign_name = cmStrCat("@foreign_pkgcfg::", *parsedArgs.Package);
  auto found_var = cmStrCat("PKGCONFIG_", *parsedArgs.Package, "_FOUND");

  auto& mf = status.GetMakefile();

  if (mf.FindTargetToUse(foreign_name, cmStateEnums::TargetDomain::FOREIGN)) {
    mf.AddDefinition(found_var, "TRUE");
    return true;
  }

  auto result = PopulatePCTarget(parsedArgs, status);
  mf.AddDefinition(found_var, result.second ? "TRUE" : "FALSE");
  return result.first;
}

bool HandleImportCommand(std::vector<std::string> const& args,
                         cmExecutionStatus& status)
{
  std::vector<std::string> unparsed;
  auto parsedArgs = PopulateParser.Parse(args, &unparsed);
  auto foreign_name = cmStrCat("@foreign_pkgcfg::", *parsedArgs.Package);
  auto local_name = cmStrCat("PkgConfig::", *parsedArgs.Package);
  auto found_var = cmStrCat("PKGCONFIG_", *parsedArgs.Package, "_FOUND");

  auto& mf = status.GetMakefile();

  if (mf.FindTargetToUse(local_name)) {
    mf.AddDefinition(found_var, "TRUE");
    return true;
  }

  if (!mf.FindTargetToUse(foreign_name, cmStateEnums::TargetDomain::FOREIGN)) {
    auto result = PopulatePCTarget(parsedArgs, status);
    if (!result.second) {
      mf.AddDefinition(found_var, "FALSE");
      return result.first;
    }
  }

  mf.AddDefinition(found_var, "TRUE");
  auto* tgt = mf.AddImportedTarget(
    local_name, cmStateEnums::TargetType::INTERFACE_LIBRARY, false);
  tgt->AppendProperty("INTERFACE_LINK_LIBRARIES", foreign_name);
  return true;
}

} // namespace

bool cmCMakePkgConfigCommand(std::vector<std::string> const& args,
                             cmExecutionStatus& status)
{
  if (args.size() < 2) {
    status.SetError("must be called with at least two arguments.");
    return false;
  }

  static cmSubcommandTable const subcommand{
    { "EXTRACT"_s, HandleExtractCommand },
    { "POPULATE"_s, HandlePopulateCommand },
    { "IMPORT"_s, HandleImportCommand },
  };

  return subcommand(args[0], args, status);
}
