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

#include "cmPkgConfigResolver.h"

#include <algorithm>
#include <cstring>
#include <iterator>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

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

#include "cmsys/String.h"

#include "cmPkgConfigParser.h"
#include "cmStringAlgorithms.h"

namespace {

void TrimBack(std::string& str)
{
  if (!str.empty()) {
    auto it = str.end() - 1;
    for (; cmsysString_isspace(*it); --it) {
      if (it == str.begin()) {
        str.clear();
        return;
      }
    }
    str.erase(++it, str.end());
  }
}

std::string AppendAndTrim(std::string& str, cm::string_view sv)
{
  auto size = str.length();
  str += sv;
  if (str.empty()) {
    return {};
  }

  auto begin = str.begin() + size;
  auto cur = str.end() - 1;

  while (cur != begin && cmsysString_isspace(*cur)) {
    --cur;
  }

  if (cmsysString_isspace(*cur)) {
    return {};
  }

  return { &*begin, static_cast<std::size_t>(cur - begin) + 1 };
}

cm::string_view TrimFlag(cm::string_view flag)
{
  std::size_t trim_size = 2;
  for (auto c = flag.rbegin(); c != flag.rend() && cmsysString_isspace(*c);
       ++c) {
    ++trim_size;
  }
  return { flag.data() + 2, flag.size() - trim_size };
}

} // namespace

std::string cmPkgConfigVersionReq::string() const
{
  switch (Operation) {
    case ANY:
      return "";
    case LT:
      return cmStrCat('<', Version);
    case LT_EQ:
      return cmStrCat("<=", Version);
    case EQ:
      return cmStrCat('=', Version);
    case NEQ:
      return cmStrCat("!=", Version);
    case GT_EQ:
      return cmStrCat(">=", Version);
    case GT:
      return cmStrCat('>', Version);
  }
  return "";
}

std::string cmPkgConfigResult::StrOrDefault(std::string const& key,
                                            cm::string_view def)
{
  auto it = Keywords.find(key);
  return it == Keywords.end() ? std::string{ def } : it->second;
};

std::string cmPkgConfigResult::Name()
{
  return StrOrDefault("Name");
}

std::string cmPkgConfigResult::Description()
{
  return StrOrDefault("Description");
}

std::string cmPkgConfigResult::Version()
{
  return StrOrDefault("Version");
}

std::vector<cmPkgConfigDependency> cmPkgConfigResult::Conflicts()
{
  auto it = Keywords.find("Conflicts");
  if (it == Keywords.end()) {
    return {};
  }

  return cmPkgConfigResolver::ParseDependencies(it->second);
}

std::vector<cmPkgConfigDependency> cmPkgConfigResult::Provides()
{
  auto it = Keywords.find("Provides");
  if (it == Keywords.end()) {
    return {};
  }

  return cmPkgConfigResolver::ParseDependencies(it->second);
}

std::vector<cmPkgConfigDependency> cmPkgConfigResult::Requires(bool priv)
{
  auto it = Keywords.find(priv ? "Requires.private" : "Requires");
  if (it == Keywords.end()) {
    return {};
  }

  return cmPkgConfigResolver::ParseDependencies(it->second);
}

cmPkgConfigCflagsResult cmPkgConfigResult::Cflags(bool priv)
{
  std::string cflags;
  auto it = Keywords.find(priv ? "Cflags.private" : "Cflags");
  if (it != Keywords.end()) {
    cflags += it->second;
  }

  it = Keywords.find(priv ? "CFlags.private" : "CFlags");
  if (it != Keywords.end()) {
    if (!cflags.empty()) {
      cflags += " ";
    }
    cflags += it->second;
  }

  auto tokens = cmPkgConfigResolver::TokenizeFlags(cflags);

  if (env->AllowSysCflags) {
    if (env->SysrootDir) {
      return cmPkgConfigResolver::MangleCflags(tokens, *env->SysrootDir);
    }
    return cmPkgConfigResolver::MangleCflags(tokens);
  }

  if (env->SysCflags) {
    if (env->SysrootDir) {
      return cmPkgConfigResolver::MangleCflags(tokens, *env->SysrootDir,
                                               *env->SysCflags);
    }
    return cmPkgConfigResolver::MangleCflags(tokens, *env->SysCflags);
  }

  if (env->SysrootDir) {
    return cmPkgConfigResolver::MangleCflags(
      tokens, *env->SysrootDir, std::vector<std::string>{ "/usr/include" });
  }

  return cmPkgConfigResolver::MangleCflags(
    tokens, std::vector<std::string>{ "/usr/include" });
}

cmPkgConfigLibsResult cmPkgConfigResult::Libs(bool priv)
{
  auto it = Keywords.find(priv ? "Libs.private" : "Libs");
  if (it == Keywords.end()) {
    return cmPkgConfigLibsResult();
  }

  auto tokens = cmPkgConfigResolver::TokenizeFlags(it->second);

  if (env->AllowSysLibs) {
    if (env->SysrootDir) {
      return cmPkgConfigResolver::MangleLibs(tokens, *env->SysrootDir);
    }
    return cmPkgConfigResolver::MangleLibs(tokens);
  }

  if (env->SysLibs) {
    if (env->SysrootDir) {
      return cmPkgConfigResolver::MangleLibs(tokens, *env->SysrootDir,
                                             *env->SysLibs);
    }
    return cmPkgConfigResolver::MangleLibs(tokens, *env->SysLibs);
  }

  if (env->SysrootDir) {
    return cmPkgConfigResolver::MangleLibs(
      tokens, *env->SysrootDir, std::vector<std::string>{ "/usr/lib" });
  }

  return cmPkgConfigResolver::MangleLibs(
    tokens, std::vector<std::string>{ "/usr/lib" });
}

void cmPkgConfigResolver::ReplaceSep(std::string& list)
{
#ifndef _WIN32
  std::replace(list.begin(), list.end(), ':', ';');
#else
  static_cast<void>(list); // Unused parameter
#endif
}

cm::optional<cmPkgConfigResult> cmPkgConfigResolver::ResolveStrict(
  std::vector<cmPkgConfigEntry> const& entries, cmPkgConfigEnv& env)
{
  cm::optional<cmPkgConfigResult> result;
  cmPkgConfigResult config;
  auto& keys = config.Keywords;

  if (env.SysrootDir) {
    config.Variables["pc_sysrootdir"] = *env.SysrootDir;
  } else {
    config.Variables["pc_sysrootdir"] = "/";
  }

  if (env.TopBuildDir) {
    config.Variables["pc_top_builddir"] = *env.TopBuildDir;
  }

  config.env = &env;

  for (auto const& entry : entries) {
    std::string key(entry.Key);
    if (entry.IsVariable) {
      if (config.Variables.find(key) != config.Variables.end()) {
        return result;
      }
      auto var = HandleVariableStrict(entry, config.Variables);
      if (!var) {
        return result;
      }
      config.Variables[key] = *var;
    } else {
      if (key == "Cflags" && keys.find("CFlags") != keys.end()) {
        return result;
      }
      if (key == "CFlags" && keys.find("Cflags") != keys.end()) {
        return result;
      }
      if (key == "Cflags.private" &&
          keys.find("CFlags.private") != keys.end()) {
        return result;
      }
      if (key == "CFlags.private" &&
          keys.find("Cflags.private") != keys.end()) {
        return result;
      }
      if (keys.find(key) != keys.end()) {
        return result;
      }
      keys[key] = HandleKeyword(entry, config.Variables);
    }
  }

  if (keys.find("Name") == keys.end() ||
      keys.find("Description") == keys.end() ||
      keys.find("Version") == keys.end()) {
    return result;
  }

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

cm::optional<cmPkgConfigResult> cmPkgConfigResolver::ResolvePermissive(
  std::vector<cmPkgConfigEntry> const& entries, cmPkgConfigEnv& env)
{
  cm::optional<cmPkgConfigResult> result;

  cmPkgConfigResult config = ResolveBestEffort(entries, env);
  auto const& keys = config.Keywords;

  if (keys.find("Name") == keys.end() ||
      keys.find("Description") == keys.end() ||
      keys.find("Version") == keys.end()) {
    return result;
  }

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

cmPkgConfigResult cmPkgConfigResolver::ResolveBestEffort(
  std::vector<cmPkgConfigEntry> const& entries, cmPkgConfigEnv& env)
{
  cmPkgConfigResult result;

  if (env.SysrootDir) {
    result.Variables["pc_sysrootdir"] = *env.SysrootDir;
  } else {
    result.Variables["pc_sysrootdir"] = "/";
  }

  if (env.TopBuildDir) {
    result.Variables["pc_top_builddir"] = *env.TopBuildDir;
  }

  result.env = &env;

  for (auto const& entry : entries) {
    std::string key(entry.Key);
    if (entry.IsVariable) {
      result.Variables[key] =
        HandleVariablePermissive(entry, result.Variables);
    } else {
      result.Keywords[key] += HandleKeyword(entry, result.Variables);
    }
  }
  return result;
}

std::string cmPkgConfigResolver::HandleVariablePermissive(
  cmPkgConfigEntry const& entry,
  std::unordered_map<std::string, std::string> const& variables)
{
  std::string result;
  for (auto const& segment : entry.Val) {
    if (!segment.IsVariable) {
      result += segment.Data;
    } else if (entry.Key != segment.Data) {
      auto it = variables.find(std::string{ segment.Data });
      if (it != variables.end()) {
        result += it->second;
      }
    }
  }

  TrimBack(result);
  return result;
}

cm::optional<std::string> cmPkgConfigResolver::HandleVariableStrict(
  cmPkgConfigEntry const& entry,
  std::unordered_map<std::string, std::string> const& variables)
{
  cm::optional<std::string> result;

  std::string value;
  for (auto const& segment : entry.Val) {
    if (!segment.IsVariable) {
      value += segment.Data;
    } else if (entry.Key == segment.Data) {
      return result;
    } else {
      auto it = variables.find(std::string{ segment.Data });
      if (it != variables.end()) {
        value += it->second;
      } else {
        return result;
      }
    }
  }

  TrimBack(value);
  result = std::move(value);
  return result;
}

std::string cmPkgConfigResolver::HandleKeyword(
  cmPkgConfigEntry const& entry,
  std::unordered_map<std::string, std::string> const& variables)
{
  std::string result;
  for (auto const& segment : entry.Val) {
    if (!segment.IsVariable) {
      result += segment.Data;
    } else {
      auto it = variables.find(std::string{ segment.Data });
      if (it != variables.end()) {
        result += it->second;
      }
    }
  }

  TrimBack(result);
  return result;
}

std::vector<cm::string_view> cmPkgConfigResolver::TokenizeFlags(
  std::string const& flagline)
{
  std::vector<cm::string_view> result;

  auto it = flagline.begin();
  while (it != flagline.end() && cmsysString_isspace(*it)) {
    ++it;
  }

  while (it != flagline.end()) {
    char const* start = &(*it);
    std::size_t len = 0;

    for (; it != flagline.end() && !cmsysString_isspace(*it); ++it) {
      ++len;
    }

    for (; it != flagline.end() && cmsysString_isspace(*it); ++it) {
      ++len;
    }

    result.emplace_back(start, len);
  }

  return result;
}

cmPkgConfigCflagsResult cmPkgConfigResolver::MangleCflags(
  std::vector<cm::string_view> const& flags)
{
  cmPkgConfigCflagsResult result;

  for (auto flag : flags) {
    if (flag.rfind("-I", 0) == 0) {
      result.Includes.emplace_back(AppendAndTrim(result.Flagline, flag));
    } else {
      result.CompileOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
    }
  }

  return result;
}

cmPkgConfigCflagsResult cmPkgConfigResolver::MangleCflags(
  std::vector<cm::string_view> const& flags, std::string const& sysroot)
{
  cmPkgConfigCflagsResult result;

  for (auto flag : flags) {
    if (flag.rfind("-I", 0) == 0) {
      std::string reroot = Reroot(flag, "-I", sysroot);
      result.Includes.emplace_back(AppendAndTrim(result.Flagline, reroot));
    } else {
      result.CompileOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
    }
  }

  return result;
}

cmPkgConfigCflagsResult cmPkgConfigResolver::MangleCflags(
  std::vector<cm::string_view> const& flags,
  std::vector<std::string> const& syspaths)
{
  cmPkgConfigCflagsResult result;

  for (auto flag : flags) {
    if (flag.rfind("-I", 0) == 0) {
      cm::string_view trimmed = TrimFlag(flag);
      if (std::all_of(
            syspaths.begin(), syspaths.end(),
            [&](std::string const& path) { return path != trimmed; })) {
        result.Includes.emplace_back(AppendAndTrim(result.Flagline, flag));
      }

    } else {
      result.CompileOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
    }
  }

  return result;
}

cmPkgConfigCflagsResult cmPkgConfigResolver::MangleCflags(
  std::vector<cm::string_view> const& flags, std::string const& sysroot,
  std::vector<std::string> const& syspaths)
{
  cmPkgConfigCflagsResult result;

  for (auto flag : flags) {
    if (flag.rfind("-I", 0) == 0) {
      std::string reroot = Reroot(flag, "-I", sysroot);
      cm::string_view trimmed = TrimFlag(reroot);
      if (std::all_of(
            syspaths.begin(), syspaths.end(),
            [&](std::string const& path) { return path != trimmed; })) {
        result.Includes.emplace_back(AppendAndTrim(result.Flagline, reroot));
      }

    } else {
      result.CompileOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
    }
  }

  return result;
}

cmPkgConfigLibsResult cmPkgConfigResolver::MangleLibs(
  std::vector<cm::string_view> const& flags)
{
  cmPkgConfigLibsResult result;

  for (auto flag : flags) {
    if (flag.rfind("-L", 0) == 0) {
      result.LibDirs.emplace_back(AppendAndTrim(result.Flagline, flag));
    } else if (flag.rfind("-l", 0) == 0) {
      result.LibNames.emplace_back(AppendAndTrim(result.Flagline, flag));
    } else {
      result.LinkOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
    }
  }

  return result;
}

cmPkgConfigLibsResult cmPkgConfigResolver::MangleLibs(
  std::vector<cm::string_view> const& flags, std::string const& sysroot)
{
  cmPkgConfigLibsResult result;

  for (auto flag : flags) {
    if (flag.rfind("-L", 0) == 0) {
      std::string reroot = Reroot(flag, "-L", sysroot);
      result.LibDirs.emplace_back(AppendAndTrim(result.Flagline, reroot));
    } else if (flag.rfind("-l", 0) == 0) {
      result.LibNames.emplace_back(AppendAndTrim(result.Flagline, flag));
    } else {
      result.LinkOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
    }
  }

  return result;
}

cmPkgConfigLibsResult cmPkgConfigResolver::MangleLibs(
  std::vector<cm::string_view> const& flags,
  std::vector<std::string> const& syspaths)
{
  cmPkgConfigLibsResult result;

  for (auto flag : flags) {
    if (flag.rfind("-L", 0) == 0) {
      cm::string_view trimmed = TrimFlag(flag);
      if (std::all_of(
            syspaths.begin(), syspaths.end(),
            [&](std::string const& path) { return path != trimmed; })) {
        result.LibDirs.emplace_back(AppendAndTrim(result.Flagline, flag));
      }

    } else if (flag.rfind("-l", 0) == 0) {
      result.LibNames.emplace_back(AppendAndTrim(result.Flagline, flag));
    } else {
      result.LinkOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
    }
  }

  return result;
}

cmPkgConfigLibsResult cmPkgConfigResolver::MangleLibs(
  std::vector<cm::string_view> const& flags, std::string const& sysroot,
  std::vector<std::string> const& syspaths)
{
  cmPkgConfigLibsResult result;

  for (auto flag : flags) {
    if (flag.rfind("-L", 0) == 0) {
      std::string reroot = Reroot(flag, "-L", sysroot);
      cm::string_view trimmed = TrimFlag(reroot);
      if (std::all_of(
            syspaths.begin(), syspaths.end(),
            [&](std::string const& path) { return path != trimmed; })) {
        result.LibDirs.emplace_back(AppendAndTrim(result.Flagline, reroot));
      }

    } else if (flag.rfind("-l", 0) == 0) {
      result.LibNames.emplace_back(AppendAndTrim(result.Flagline, flag));
    } else {
      result.LinkOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
    }
  }

  return result;
}

std::string cmPkgConfigResolver::Reroot(cm::string_view flag,
                                        cm::string_view prefix,
                                        std::string const& sysroot)
{
  std::string result = std::string{ prefix };
  result += sysroot;
  result += cm::string_view{ flag.data() + prefix.length(),
                             flag.size() - prefix.length() };
  return result;
}

cmPkgConfigVersionReq cmPkgConfigResolver::ParseVersion(
  std::string::const_iterator& cur, std::string::const_iterator end)
{
  cmPkgConfigVersionReq result;
  if (*cur == '=') {
    result.Operation = result.EQ;
    ++cur;
  } else if (*cur == '>') {
    ++cur;

    if (cur == end) {
      result.Operation = result.GT;
      return result;
    }

    if (*cur == '=') {
      result.Operation = result.GT_EQ;
      ++cur;
    } else {
      result.Operation = result.GT;
    }

  } else if (*cur == '<') {
    ++cur;

    if (cur == end) {
      result.Operation = result.LT;
      return result;
    }

    if (*cur == '=') {
      result.Operation = result.LT_EQ;
      ++cur;
    } else {
      result.Operation = result.LT;
    }

  } else if (*cur == '!') {
    ++cur;

    if (cur == end) {
      result.Operation = result.ANY;
      return result;
    }

    if (*cur == '=') {
      result.Operation = result.NEQ;
      ++cur;
    } else {
      result.Operation = result.ANY;
    }
  }

  for (;; ++cur) {
    if (cur == end) {
      return result;
    }

    if (!cmsysString_isspace(*cur)) {
      break;
    }
  }

  for (; cur != end && !cmsysString_isspace(*cur) && *cur != ','; ++cur) {
    result.Version += *cur;
  }

  return result;
}

std::vector<cmPkgConfigDependency> cmPkgConfigResolver::ParseDependencies(
  std::string const& deps)
{

  std::vector<cmPkgConfigDependency> result;

  auto cur = deps.begin();
  auto end = deps.end();

  while (cur != end) {
    while ((cmsysString_isspace(*cur) || *cur == ',')) {
      if (++cur == end) {
        return result;
      }
    }

    result.emplace_back();
    auto& dep = result.back();

    while (!cmsysString_isspace(*cur) && *cur != ',') {
      dep.Name += *cur;
      if (++cur == end) {
        return result;
      }
    }

    auto in_operator = [&]() -> bool {
      for (;; ++cur) {
        if (cur == end) {
          return false;
        }

        if (*cur == '>' || *cur == '=' || *cur == '<' || *cur == '!') {
          return true;
        }

        if (!cmsysString_isspace(*cur)) {
          return false;
        }
      }
    };

    if (!in_operator()) {
      continue;
    }

    dep.VerReq = ParseVersion(cur, end);
  }

  return result;
}

bool cmPkgConfigResolver::CheckVersion(cmPkgConfigVersionReq const& desired,
                                       std::string const& provided)
{

  if (desired.Operation == cmPkgConfigVersionReq::ANY) {
    return true;
  }

  // https://blog.jasonantman.com/2014/07/how-yum-and-rpm-compare-versions/

  auto check_with_op = [&](int comp) -> bool {
    switch (desired.Operation) {
      case cmPkgConfigVersionReq::EQ:
        return comp == 0;
      case cmPkgConfigVersionReq::NEQ:
        return comp != 0;
      case cmPkgConfigVersionReq::GT:
        return comp < 0;
      case cmPkgConfigVersionReq::GT_EQ:
        return comp <= 0;
      case cmPkgConfigVersionReq::LT:
        return comp > 0;
      case cmPkgConfigVersionReq::LT_EQ:
        return comp >= 0;
      default:
        return true;
    }
  };

  if (desired.Version == provided) {
    return check_with_op(0);
  }

  auto a_cur = desired.Version.begin();
  auto a_end = desired.Version.end();

  auto b_cur = provided.begin();
  auto b_end = provided.end();

  while (a_cur != a_end && b_cur != b_end) {
    while (a_cur != a_end && !cmsysString_isalnum(*a_cur) && *a_cur != '~') {
      ++a_cur;
    }

    while (b_cur != b_end && !cmsysString_isalnum(*b_cur) && *b_cur != '~') {
      ++b_cur;
    }

    if (a_cur == a_end || b_cur == b_end) {
      break;
    }

    if (*a_cur == '~' || *b_cur == '~') {
      if (*a_cur != '~') {
        return check_with_op(1);
      }

      if (*b_cur != '~') {
        return check_with_op(-1);
      }

      ++a_cur;
      ++b_cur;
      continue;
    }

    auto a_seg = a_cur;
    auto b_seg = b_cur;
    bool is_num;

    if (cmsysString_isdigit(*a_cur)) {
      is_num = true;
      while (a_cur != a_end && cmsysString_isdigit(*a_cur)) {
        ++a_cur;
      }

      while (b_cur != b_end && cmsysString_isdigit(*b_cur)) {
        ++b_cur;
      }

    } else {
      is_num = false;
      while (a_cur != a_end && cmsysString_isalpha(*a_cur)) {
        ++a_cur;
      }

      while (b_cur != b_end && cmsysString_isalpha(*b_cur)) {
        ++b_cur;
      }
    }

    auto a_len = std::distance(a_seg, a_cur);
    auto b_len = std::distance(b_seg, b_cur);

    if (!b_len) {
      return check_with_op(is_num ? 1 : -1);
    }

    if (is_num) {
      while (a_seg != a_cur && *a_seg == '0') {
        ++a_seg;
      }

      while (b_seg != b_cur && *b_seg == '0') {
        ++b_seg;
      }

      a_len = std::distance(a_seg, a_cur);
      b_len = std::distance(b_seg, b_cur);

      if (a_len != b_len) {
        return check_with_op(a_len > b_len ? 1 : -1);
      }

      auto cmp = std::memcmp(&*a_seg, &*b_seg, a_len);
      if (cmp) {
        return check_with_op(cmp);
      }
    } else {
      auto cmp = std::memcmp(&*a_seg, &*b_seg, std::min(a_len, b_len));
      if (cmp) {
        return check_with_op(cmp);
      }

      if (a_len != b_len) {
        return check_with_op(a_len > b_len ? 1 : -1);
      }
    }
  }

  if (a_cur == a_end) {
    if (b_cur == b_end) {
      return check_with_op(0);
    }
    return check_with_op(-1);
  }

  return check_with_op(1);
}

cmPkgConfigVersionReq cmPkgConfigResolver::ParseVersion(
  std::string const& version)
{
  cmPkgConfigVersionReq result;

  auto cur = version.begin();
  auto end = version.end();

  if (cur == end) {
    result.Operation = cmPkgConfigVersionReq::EQ;
    return result;
  }

  result = ParseVersion(cur, end);
  cur = version.begin();

  if (*cur != '=' && *cur != '!' && *cur != '<' && *cur != '>') {
    result.Operation = cmPkgConfigVersionReq::EQ;
  }

  return result;
}
