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

#include "cmStandardLevelResolver.h"

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <sstream>
#include <stdexcept>
#include <unordered_map>
#include <utility>
#include <vector>

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

#include "cmGeneratorExpression.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalGenerator.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmPolicies.h"
#include "cmStringAlgorithms.h"
#include "cmTarget.h"
#include "cmValue.h"
#include "cmake.h"

namespace {

#define FEATURE_STRING(F) , #F
const char* const C_FEATURES[] = { nullptr FOR_EACH_C_FEATURE(
  FEATURE_STRING) };

const char* const CXX_FEATURES[] = { nullptr FOR_EACH_CXX_FEATURE(
  FEATURE_STRING) };

const char* const CUDA_FEATURES[] = { nullptr FOR_EACH_CUDA_FEATURE(
  FEATURE_STRING) };

const char* const HIP_FEATURES[] = { nullptr FOR_EACH_HIP_FEATURE(
  FEATURE_STRING) };
#undef FEATURE_STRING

struct StandardNeeded
{
  int index;
  int value;
};

int ParseStd(std::string const& level)
{
  try {
    return std::stoi(level);
  } catch (std::invalid_argument&) {
    // Fall through to use an invalid value.
  }
  return -1;
}

struct StandardLevelComputer
{
  explicit StandardLevelComputer(std::string lang, std::vector<int> levels,
                                 std::vector<std::string> levelsStr)
    : Language(std::move(lang))
    , Levels(std::move(levels))
    , LevelsAsStrings(std::move(levelsStr))
  {
    assert(this->Levels.size() == this->LevelsAsStrings.size());
  }

  std::string GetCompileOptionDef(cmMakefile* makefile,
                                  cmGeneratorTarget const* target,
                                  std::string const& config) const
  {

    const auto& stds = this->Levels;
    const auto& stdsStrings = this->LevelsAsStrings;

    cmValue defaultStd = makefile->GetDefinition(
      cmStrCat("CMAKE_", this->Language, "_STANDARD_DEFAULT"));
    if (!cmNonempty(defaultStd)) {
      // this compiler has no notion of language standard levels
      return std::string{};
    }

    cmPolicies::PolicyStatus const cmp0128{ makefile->GetPolicyStatus(
      cmPolicies::CMP0128) };
    bool const defaultExt{ cmIsOn(*makefile->GetDefinition(
      cmStrCat("CMAKE_", this->Language, "_EXTENSIONS_DEFAULT"))) };
    bool ext = true;

    if (cmp0128 == cmPolicies::NEW) {
      ext = defaultExt;
    }

    if (cmValue extPropValue = target->GetLanguageExtensions(this->Language)) {
      ext = cmIsOn(*extPropValue);
    }

    std::string const type{ ext ? "EXTENSION" : "STANDARD" };

    cmValue standardProp = target->GetLanguageStandard(this->Language, config);
    if (!standardProp) {
      if (cmp0128 == cmPolicies::NEW) {
        // Add extension flag if compiler's default doesn't match.
        if (ext != defaultExt) {
          return cmStrCat("CMAKE_", this->Language, *defaultStd, "_", type,
                          "_COMPILE_OPTION");
        }
      } else {
        if (cmp0128 == cmPolicies::WARN &&
            makefile->PolicyOptionalWarningEnabled(
              "CMAKE_POLICY_WARNING_CMP0128") &&
            ext != defaultExt) {
          const char* state{};
          if (ext) {
            if (!makefile->GetDefinition(cmStrCat(
                  "CMAKE_", this->Language, "_EXTENSION_COMPILE_OPTION"))) {
              state = "enabled";
            }
          } else {
            state = "disabled";
          }
          if (state) {
            makefile->IssueMessage(
              MessageType::AUTHOR_WARNING,
              cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0128),
                       "\nFor compatibility with older versions of CMake, "
                       "compiler extensions won't be ",
                       state, "."));
          }
        }

        if (ext) {
          return cmStrCat("CMAKE_", this->Language,
                          "_EXTENSION_COMPILE_OPTION");
        }
      }
      return std::string{};
    }

    if (target->GetLanguageStandardRequired(this->Language)) {
      std::string option_flag = cmStrCat(
        "CMAKE_", this->Language, *standardProp, "_", type, "_COMPILE_OPTION");

      cmValue opt = target->Target->GetMakefile()->GetDefinition(option_flag);
      if (!opt) {
        std::ostringstream e;
        e << "Target \"" << target->GetName()
          << "\" requires the language "
             "dialect \""
          << this->Language << *standardProp << "\" "
          << (ext ? "(with compiler extensions)" : "")
          << ", but CMake "
             "does not know the compile flags to use to enable it.";
        makefile->IssueMessage(MessageType::FATAL_ERROR, e.str());
      }
      return option_flag;
    }

    // If the request matches the compiler's defaults we don't need to add
    // anything.
    if (*standardProp == *defaultStd && ext == defaultExt) {
      if (cmp0128 == cmPolicies::NEW) {
        return std::string{};
      }

      if (cmp0128 == cmPolicies::WARN &&
          makefile->PolicyOptionalWarningEnabled(
            "CMAKE_POLICY_WARNING_CMP0128")) {
        makefile->IssueMessage(
          MessageType::AUTHOR_WARNING,
          cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0128),
                   "\nFor compatibility with older versions of CMake, "
                   "unnecessary flags for language standard or compiler "
                   "extensions may be added."));
      }
    }

    std::string standardStr(*standardProp);
    if (this->Language == "CUDA" && standardStr == "98") {
      standardStr = "03";
    }

    auto stdIt =
      std::find(cm::cbegin(stds), cm::cend(stds), ParseStd(standardStr));
    if (stdIt == cm::cend(stds)) {
      std::string e =
        cmStrCat(this->Language, "_STANDARD is set to invalid value '",
                 standardStr, "'");
      makefile->GetCMakeInstance()->IssueMessage(MessageType::FATAL_ERROR, e,
                                                 target->GetBacktrace());
      return std::string{};
    }

    auto defaultStdIt =
      std::find(cm::cbegin(stds), cm::cend(stds), ParseStd(*defaultStd));
    if (defaultStdIt == cm::cend(stds)) {
      std::string e = cmStrCat("CMAKE_", this->Language,
                               "_STANDARD_DEFAULT is set to invalid value '",
                               *defaultStd, "'");
      makefile->IssueMessage(MessageType::INTERNAL_ERROR, e);
      return std::string{};
    }

    // If the standard requested is older than the compiler's default or the
    // extension mode doesn't match then we need to use a flag.
    if ((cmp0128 != cmPolicies::NEW && stdIt <= defaultStdIt) ||
        (cmp0128 == cmPolicies::NEW &&
         (stdIt < defaultStdIt || ext != defaultExt))) {
      auto offset = std::distance(cm::cbegin(stds), stdIt);
      return cmStrCat("CMAKE_", this->Language, stdsStrings[offset], "_", type,
                      "_COMPILE_OPTION");
    }

    // The compiler's default is at least as new as the requested standard,
    // and the requested standard is not required.  Decay to the newest
    // standard for which a flag is defined.
    for (; defaultStdIt < stdIt; --stdIt) {
      auto offset = std::distance(cm::cbegin(stds), stdIt);
      std::string option_flag =
        cmStrCat("CMAKE_", this->Language, stdsStrings[offset], "_", type,
                 "_COMPILE_OPTION");
      if (target->Target->GetMakefile()->GetDefinition(option_flag)) {
        return option_flag;
      }
    }

    return std::string{};
  }

  bool GetNewRequiredStandard(cmMakefile* makefile,
                              std::string const& targetName,
                              const std::string& feature,
                              cmValue currentLangStandardValue,
                              std::string& newRequiredStandard,
                              std::string* error) const
  {
    if (currentLangStandardValue) {
      newRequiredStandard = *currentLangStandardValue;
    } else {
      newRequiredStandard.clear();
    }

    auto needed = this->HighestStandardNeeded(makefile, feature);

    cmValue existingStandard = currentLangStandardValue;
    if (!existingStandard) {
      cmValue defaultStandard = makefile->GetDefinition(
        cmStrCat("CMAKE_", this->Language, "_STANDARD_DEFAULT"));
      if (cmNonempty(defaultStandard)) {
        existingStandard = defaultStandard;
      }
    }

    auto existingLevelIter = cm::cend(this->Levels);
    if (existingStandard) {
      existingLevelIter =
        std::find(cm::cbegin(this->Levels), cm::cend(this->Levels),
                  ParseStd(*existingStandard));
      if (existingLevelIter == cm::cend(this->Levels)) {
        const std::string e =
          cmStrCat("The ", this->Language, "_STANDARD property on target \"",
                   targetName, "\" contained an invalid value: \"",
                   *existingStandard, "\".");
        if (error) {
          *error = e;
        } else {
          makefile->IssueMessage(MessageType::FATAL_ERROR, e);
        }
        return false;
      }
    }

    if (needed.index != -1) {
      // Ensure the C++ language level is high enough to support
      // the needed C++ features.
      if (existingLevelIter == cm::cend(this->Levels) ||
          existingLevelIter < this->Levels.begin() + needed.index) {
        newRequiredStandard = this->LevelsAsStrings[needed.index];
      }
    }

    return true;
  }

  bool HaveStandardAvailable(cmMakefile* makefile,
                             cmGeneratorTarget const* target,
                             std::string const& config,
                             std::string const& feature) const
  {
    cmValue defaultStandard = makefile->GetDefinition(
      cmStrCat("CMAKE_", this->Language, "_STANDARD_DEFAULT"));
    if (!defaultStandard) {
      makefile->IssueMessage(
        MessageType::INTERNAL_ERROR,
        cmStrCat("CMAKE_", this->Language,
                 "_STANDARD_DEFAULT is not set.  COMPILE_FEATURES support "
                 "not fully configured for this compiler."));
      // Return true so the caller does not try to lookup the default standard.
      return true;
    }
    // convert defaultStandard to an integer
    if (std::find(cm::cbegin(this->Levels), cm::cend(this->Levels),
                  ParseStd(*defaultStandard)) == cm::cend(this->Levels)) {
      const std::string e = cmStrCat("The CMAKE_", this->Language,
                                     "_STANDARD_DEFAULT variable contains an "
                                     "invalid value: \"",
                                     *defaultStandard, "\".");
      makefile->IssueMessage(MessageType::INTERNAL_ERROR, e);
      return false;
    }

    cmValue existingStandard =
      target->GetLanguageStandard(this->Language, config);
    if (!existingStandard) {
      existingStandard = defaultStandard;
    }

    auto existingLevelIter =
      std::find(cm::cbegin(this->Levels), cm::cend(this->Levels),
                ParseStd(*existingStandard));
    if (existingLevelIter == cm::cend(this->Levels)) {
      const std::string e =
        cmStrCat("The ", this->Language, "_STANDARD property on target \"",
                 target->GetName(), "\" contained an invalid value: \"",
                 *existingStandard, "\".");
      makefile->IssueMessage(MessageType::FATAL_ERROR, e);
      return false;
    }

    auto needed = this->HighestStandardNeeded(makefile, feature);

    return (needed.index == -1) ||
      (this->Levels.begin() + needed.index) <= existingLevelIter;
  }

  StandardNeeded HighestStandardNeeded(cmMakefile* makefile,
                                       std::string const& feature) const
  {
    std::string prefix = cmStrCat("CMAKE_", this->Language);
    StandardNeeded maxLevel = { -1, -1 };
    for (size_t i = 0; i < this->Levels.size(); ++i) {
      if (cmValue prop = makefile->GetDefinition(
            cmStrCat(prefix, this->LevelsAsStrings[i], "_COMPILE_FEATURES"))) {
        std::vector<std::string> props = cmExpandedList(*prop);
        if (cm::contains(props, feature)) {
          maxLevel = { static_cast<int>(i), this->Levels[i] };
        }
      }
    }
    return maxLevel;
  }

  bool IsLaterStandard(int lhs, int rhs) const
  {
    auto rhsIt =
      std::find(cm::cbegin(this->Levels), cm::cend(this->Levels), rhs);

    return std::find(rhsIt, cm::cend(this->Levels), lhs) !=
      cm::cend(this->Levels);
  }

  std::string Language;
  std::vector<int> Levels;
  std::vector<std::string> LevelsAsStrings;
};

std::unordered_map<std::string, StandardLevelComputer>
  StandardComputerMapping = {
    { "C",
      StandardLevelComputer{
        "C", std::vector<int>{ 90, 99, 11, 17, 23 },
        std::vector<std::string>{ "90", "99", "11", "17", "23" } } },
    { "CXX",
      StandardLevelComputer{
        "CXX", std::vector<int>{ 98, 11, 14, 17, 20, 23 },
        std::vector<std::string>{ "98", "11", "14", "17", "20", "23" } } },
    { "CUDA",
      StandardLevelComputer{
        "CUDA", std::vector<int>{ 03, 11, 14, 17, 20, 23 },
        std::vector<std::string>{ "03", "11", "14", "17", "20", "23" } } },
    { "OBJC",
      StandardLevelComputer{
        "OBJC", std::vector<int>{ 90, 99, 11, 17, 23 },
        std::vector<std::string>{ "90", "99", "11", "17", "23" } } },
    { "OBJCXX",
      StandardLevelComputer{
        "OBJCXX", std::vector<int>{ 98, 11, 14, 17, 20, 23 },
        std::vector<std::string>{ "98", "11", "14", "17", "20", "23" } } },
    { "HIP",
      StandardLevelComputer{
        "HIP", std::vector<int>{ 98, 11, 14, 17, 20, 23 },
        std::vector<std::string>{ "98", "11", "14", "17", "20", "23" } } }
  };
}

std::string cmStandardLevelResolver::GetCompileOptionDef(
  cmGeneratorTarget const* target, std::string const& lang,
  std::string const& config) const
{
  const auto& mapping = StandardComputerMapping.find(lang);
  if (mapping == cm::cend(StandardComputerMapping)) {
    return std::string{};
  }

  return mapping->second.GetCompileOptionDef(this->Makefile, target, config);
}

bool cmStandardLevelResolver::AddRequiredTargetFeature(
  cmTarget* target, const std::string& feature, std::string* error) const
{
  if (cmGeneratorExpression::Find(feature) != std::string::npos) {
    target->AppendProperty("COMPILE_FEATURES", feature);
    return true;
  }

  std::string lang;
  if (!this->CheckCompileFeaturesAvailable(target->GetName(), feature, lang,
                                           error)) {
    return false;
  }

  target->AppendProperty("COMPILE_FEATURES", feature);

  // FIXME: Add a policy to avoid updating the <LANG>_STANDARD target
  // property due to COMPILE_FEATURES.  The language standard selection
  // should be done purely at generate time based on whatever the project
  // code put in these properties explicitly.  That is mostly true now,
  // but for compatibility we need to continue updating the property here.
  std::string newRequiredStandard;
  bool newRequired = this->GetNewRequiredStandard(
    target->GetName(), feature,
    target->GetProperty(cmStrCat(lang, "_STANDARD")), newRequiredStandard,
    error);
  if (!newRequiredStandard.empty()) {
    target->SetProperty(cmStrCat(lang, "_STANDARD"), newRequiredStandard);
  }
  return newRequired;
}

bool cmStandardLevelResolver::CheckCompileFeaturesAvailable(
  const std::string& targetName, const std::string& feature, std::string& lang,
  std::string* error) const
{
  if (!this->CompileFeatureKnown(targetName, feature, lang, error)) {
    return false;
  }

  if (!this->Makefile->GetGlobalGenerator()->GetLanguageEnabled(lang)) {
    return true;
  }

  cmValue features = this->CompileFeaturesAvailable(lang, error);
  if (!features) {
    return false;
  }

  std::vector<std::string> availableFeatures = cmExpandedList(features);
  if (!cm::contains(availableFeatures, feature)) {
    std::ostringstream e;
    e << "The compiler feature \"" << feature << "\" is not known to " << lang
      << " compiler\n\""
      << this->Makefile->GetSafeDefinition("CMAKE_" + lang + "_COMPILER_ID")
      << "\"\nversion "
      << this->Makefile->GetSafeDefinition("CMAKE_" + lang +
                                           "_COMPILER_VERSION")
      << ".";
    if (error) {
      *error = e.str();
    } else {
      this->Makefile->IssueMessage(MessageType::FATAL_ERROR, e.str());
    }
    return false;
  }

  return true;
}

bool cmStandardLevelResolver::CompileFeatureKnown(
  const std::string& targetName, const std::string& feature, std::string& lang,
  std::string* error) const
{
  assert(cmGeneratorExpression::Find(feature) == std::string::npos);

  bool isCFeature =
    std::find_if(cm::cbegin(C_FEATURES) + 1, cm::cend(C_FEATURES),
                 cmStrCmp(feature)) != cm::cend(C_FEATURES);
  if (isCFeature) {
    lang = "C";
    return true;
  }
  bool isCxxFeature =
    std::find_if(cm::cbegin(CXX_FEATURES) + 1, cm::cend(CXX_FEATURES),
                 cmStrCmp(feature)) != cm::cend(CXX_FEATURES);
  if (isCxxFeature) {
    lang = "CXX";
    return true;
  }
  bool isCudaFeature =
    std::find_if(cm::cbegin(CUDA_FEATURES) + 1, cm::cend(CUDA_FEATURES),
                 cmStrCmp(feature)) != cm::cend(CUDA_FEATURES);
  if (isCudaFeature) {
    lang = "CUDA";
    return true;
  }
  bool isHIPFeature =
    std::find_if(cm::cbegin(HIP_FEATURES) + 1, cm::cend(HIP_FEATURES),
                 cmStrCmp(feature)) != cm::cend(HIP_FEATURES);
  if (isHIPFeature) {
    lang = "HIP";
    return true;
  }
  std::ostringstream e;
  if (error) {
    e << "specified";
  } else {
    e << "Specified";
  }
  e << " unknown feature \"" << feature
    << "\" for "
       "target \""
    << targetName << "\".";
  if (error) {
    *error = e.str();
  } else {
    this->Makefile->IssueMessage(MessageType::FATAL_ERROR, e.str());
  }
  return false;
}

cmValue cmStandardLevelResolver::CompileFeaturesAvailable(
  const std::string& lang, std::string* error) const
{
  if (!this->Makefile->GetGlobalGenerator()->GetLanguageEnabled(lang)) {
    std::ostringstream e;
    if (error) {
      e << "cannot";
    } else {
      e << "Cannot";
    }
    e << " use features from non-enabled language " << lang;
    if (error) {
      *error = e.str();
    } else {
      this->Makefile->IssueMessage(MessageType::FATAL_ERROR, e.str());
    }
    return nullptr;
  }

  cmValue featuresKnown =
    this->Makefile->GetDefinition("CMAKE_" + lang + "_COMPILE_FEATURES");

  if (!cmNonempty(featuresKnown)) {
    std::ostringstream e;
    if (error) {
      e << "no";
    } else {
      e << "No";
    }
    e << " known features for " << lang << " compiler\n\""
      << this->Makefile->GetSafeDefinition("CMAKE_" + lang + "_COMPILER_ID")
      << "\"\nversion "
      << this->Makefile->GetSafeDefinition("CMAKE_" + lang +
                                           "_COMPILER_VERSION")
      << ".";
    if (error) {
      *error = e.str();
    } else {
      this->Makefile->IssueMessage(MessageType::FATAL_ERROR, e.str());
    }
    return nullptr;
  }
  return featuresKnown;
}

bool cmStandardLevelResolver::GetNewRequiredStandard(
  const std::string& targetName, const std::string& feature,
  cmValue currentLangStandardValue, std::string& newRequiredStandard,
  std::string* error) const
{
  std::string lang;
  if (!this->CheckCompileFeaturesAvailable(targetName, feature, lang, error)) {
    return false;
  }

  auto mapping = StandardComputerMapping.find(lang);
  if (mapping != cm::cend(StandardComputerMapping)) {
    return mapping->second.GetNewRequiredStandard(
      this->Makefile, targetName, feature, currentLangStandardValue,
      newRequiredStandard, error);
  }
  return false;
}

bool cmStandardLevelResolver::HaveStandardAvailable(
  cmGeneratorTarget const* target, std::string const& lang,
  std::string const& config, const std::string& feature) const
{
  auto mapping = StandardComputerMapping.find(lang);
  if (mapping != cm::cend(StandardComputerMapping)) {
    return mapping->second.HaveStandardAvailable(this->Makefile, target,
                                                 config, feature);
  }
  return false;
}

bool cmStandardLevelResolver::IsLaterStandard(std::string const& lang,
                                              std::string const& lhs,
                                              std::string const& rhs) const
{
  auto mapping = StandardComputerMapping.find(lang);
  if (mapping != cm::cend(StandardComputerMapping)) {
    return mapping->second.IsLaterStandard(std::stoi(lhs), std::stoi(rhs));
  }
  return false;
}
