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

#include <algorithm>
#include <sstream>
#include <utility>

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

#include "cmComputeLinkInformation.h"
#include "cmGenExContext.h"
#include "cmGeneratorExpression.h"
#include "cmGeneratorExpressionDAGChecker.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalCommonGenerator.h"
#include "cmGlobalGenerator.h"
#include "cmList.h"
#include "cmLocalCommonGenerator.h"
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmOutputConverter.h"
#include "cmRange.h"
#include "cmSourceFile.h"
#include "cmState.h"
#include "cmStateTypes.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmValue.h"

cmCommonTargetGenerator::cmCommonTargetGenerator(cmGeneratorTarget* gt)
  : GeneratorTarget(gt)
  , Makefile(gt->Makefile)
  , LocalCommonGenerator(
      static_cast<cmLocalCommonGenerator*>(gt->LocalGenerator))
  , GlobalCommonGenerator(static_cast<cmGlobalCommonGenerator*>(
      gt->LocalGenerator->GetGlobalGenerator()))
  , ConfigNames(this->LocalCommonGenerator->GetConfigNames())
{
}

cmCommonTargetGenerator::~cmCommonTargetGenerator() = default;

std::vector<std::string> const& cmCommonTargetGenerator::GetConfigNames() const
{
  return this->ConfigNames;
}

cmValue cmCommonTargetGenerator::GetFeature(std::string const& feature,
                                            std::string const& config)
{
  return this->GeneratorTarget->GetFeature(feature, config);
}

void cmCommonTargetGenerator::AppendFortranFormatFlags(
  std::string& flags, cmSourceFile const& source)
{
  std::string const srcfmt = source.GetSafeProperty("Fortran_FORMAT");
  cmOutputConverter::FortranFormat format =
    cmOutputConverter::GetFortranFormat(srcfmt);
  if (format == cmOutputConverter::FortranFormatNone) {
    std::string const& tgtfmt =
      this->GeneratorTarget->GetSafeProperty("Fortran_FORMAT");
    format = cmOutputConverter::GetFortranFormat(tgtfmt);
  }
  char const* var = nullptr;
  switch (format) {
    case cmOutputConverter::FortranFormatFixed:
      var = "CMAKE_Fortran_FORMAT_FIXED_FLAG";
      break;
    case cmOutputConverter::FortranFormatFree:
      var = "CMAKE_Fortran_FORMAT_FREE_FLAG";
      break;
    default:
      break;
  }
  if (var) {
    this->LocalCommonGenerator->AppendFlags(
      flags, this->Makefile->GetSafeDefinition(var));
  }
}

void cmCommonTargetGenerator::AppendFortranPreprocessFlags(
  std::string& flags, cmSourceFile const& source,
  PreprocessFlagsRequired requires_pp)
{
  std::string const srcpp = source.GetSafeProperty("Fortran_PREPROCESS");
  cmOutputConverter::FortranPreprocess preprocess =
    cmOutputConverter::GetFortranPreprocess(srcpp);
  if (preprocess == cmOutputConverter::FortranPreprocess::Unset) {
    std::string const& tgtpp =
      this->GeneratorTarget->GetSafeProperty("Fortran_PREPROCESS");
    preprocess = cmOutputConverter::GetFortranPreprocess(tgtpp);
  }
  char const* var = nullptr;
  switch (preprocess) {
    case cmOutputConverter::FortranPreprocess::Needed:
      if (requires_pp == PreprocessFlagsRequired::YES) {
        var = "CMAKE_Fortran_COMPILE_OPTIONS_PREPROCESS_ON";
      }
      break;
    case cmOutputConverter::FortranPreprocess::NotNeeded:
      var = "CMAKE_Fortran_COMPILE_OPTIONS_PREPROCESS_OFF";
      break;
    default:
      break;
  }
  if (var) {
    this->LocalCommonGenerator->AppendCompileOptions(
      flags, this->Makefile->GetSafeDefinition(var));
  }
}

std::string cmCommonTargetGenerator::GetFlags(std::string const& l,
                                              std::string const& config,
                                              std::string const& arch)
{
  std::string const key = config + arch;

  auto i = this->Configs[key].FlagsByLanguage.find(l);
  if (i == this->Configs[key].FlagsByLanguage.end()) {
    std::string flags;

    this->LocalCommonGenerator->GetTargetCompileFlags(this->GeneratorTarget,
                                                      config, l, flags, arch);

    ByLanguageMap::value_type entry(l, flags);
    i = this->Configs[key].FlagsByLanguage.insert(entry).first;
  }
  return i->second;
}

std::string cmCommonTargetGenerator::GetDefines(std::string const& l,
                                                std::string const& config)
{
  auto i = this->Configs[config].DefinesByLanguage.find(l);
  if (i == this->Configs[config].DefinesByLanguage.end()) {
    std::set<std::string> defines;
    this->LocalCommonGenerator->GetTargetDefines(this->GeneratorTarget, config,
                                                 l, defines);

    std::string definesString;
    this->LocalCommonGenerator->JoinDefines(defines, definesString, l);

    ByLanguageMap::value_type entry(l, definesString);
    i = this->Configs[config].DefinesByLanguage.insert(entry).first;
  }
  return i->second;
}

std::string cmCommonTargetGenerator::GetIncludes(std::string const& l,
                                                 std::string const& config)
{
  auto i = this->Configs[config].IncludesByLanguage.find(l);
  if (i == this->Configs[config].IncludesByLanguage.end()) {
    std::string includes;
    this->AddIncludeFlags(includes, l, config);
    ByLanguageMap::value_type entry(l, includes);
    i = this->Configs[config].IncludesByLanguage.insert(entry).first;
  }
  return i->second;
}

cmCommonTargetGenerator::LinkedTargetDirs
cmCommonTargetGenerator::GetLinkedTargetDirectories(
  std::string const& lang, std::string const& config) const
{
  LinkedTargetDirs dirs;
  std::set<cmGeneratorTarget const*> forward_emitted;
  std::set<cmGeneratorTarget const*> direct_emitted;
  cmGlobalCommonGenerator* const gg = this->GlobalCommonGenerator;

  enum class Forwarding
  {
    Yes,
    No
  };

  if (cmComputeLinkInformation* cli =
        this->GeneratorTarget->GetLinkInformation(config)) {
    auto addLinkedTarget =
      [this, &lang, &config, &dirs, &direct_emitted, &forward_emitted,
       gg](cmGeneratorTarget const* linkee, Forwarding forward) {
        if (linkee &&
            !linkee->IsImported()
            // Skip targets that build after this one in a static lib cycle.
            && gg->TargetOrderIndexLess(linkee, this->GeneratorTarget)
            // We can ignore the INTERFACE_LIBRARY items because
            // Target->GetLinkInformation already processed their
            // link interface and they don't have any output themselves.
            && (linkee->GetType() != cmStateEnums::INTERFACE_LIBRARY
                // Synthesized targets may have relevant rules.
                || linkee->IsSynthetic()) &&
            ((lang == "CXX"_s && linkee->HaveCxx20ModuleSources()) ||
             (lang == "Fortran"_s && linkee->HaveFortranSources(config)))) {
          cmLocalGenerator* lg = linkee->GetLocalGenerator();
          std::string di = linkee->GetSupportDirectory();
          if (lg->GetGlobalGenerator()->IsMultiConfig()) {
            di = cmStrCat(di, '/', config);
          }
          if (forward == Forwarding::Yes &&
              forward_emitted.insert(linkee).second) {
            dirs.Forward.push_back(di);
          }
          if (direct_emitted.insert(linkee).second) {
            dirs.Direct.emplace_back(di);
          }
        }
      };
    for (auto const& item : cli->GetItems()) {
      if (item.Target) {
        addLinkedTarget(item.Target, Forwarding::No);
      } else if (item.ObjectSource && lang == "Fortran"_s
                 /* Object source files do not have a language associated with
                    them. */
                 /* && item.ObjectSource->GetLanguage() == "Fortran"_s*/) {
        // Fortran modules provided by `$<TARGET_OBJECTS>` as linked items
        // should be collated for use in this target.
        addLinkedTarget(this->LocalCommonGenerator->FindGeneratorTargetToUse(
                          item.ObjectSource->GetObjectLibrary()),
                        Forwarding::Yes);
      }
    }
    for (cmGeneratorTarget const* target : cli->GetExternalObjectTargets()) {
      addLinkedTarget(target, Forwarding::No);
    }
    if (lang == "Fortran"_s) {
      // Fortran modules provided by `$<TARGET_OBJECTS>` as sources should be
      // collated for use in this target.
      for (cmGeneratorTarget const* target :
           this->GeneratorTarget->GetSourceObjectLibraries(config)) {
        addLinkedTarget(target, Forwarding::Yes);
      }
    }
  }
  return dirs;
}

std::string cmCommonTargetGenerator::ComputeTargetCompilePDB(
  std::string const& config) const
{
  std::string compilePdbPath;
  if (this->GeneratorTarget->GetType() > cmStateEnums::OBJECT_LIBRARY) {
    return compilePdbPath;
  }

  compilePdbPath = this->GeneratorTarget->GetCompilePDBPath(config);
  if (compilePdbPath.empty()) {
    // Match VS default: `$(IntDir)vc$(PlatformToolsetVersion).pdb`.
    // A trailing slash tells the toolchain to add its default file name.
    compilePdbPath = this->GeneratorTarget->GetSupportDirectory();
    if (this->GlobalCommonGenerator->IsMultiConfig()) {
      compilePdbPath += "/";
      compilePdbPath += config;
    }
    compilePdbPath += "/";
    if (this->GeneratorTarget->GetType() == cmStateEnums::STATIC_LIBRARY) {
      // Match VS default for static libs: `$(IntDir)$(ProjectName).pdb`.
      compilePdbPath += this->GeneratorTarget->GetName();
      compilePdbPath += ".pdb";
    }
  }

  return compilePdbPath;
}

std::string cmCommonTargetGenerator::GetManifests(std::string const& config)
{
  std::vector<cmSourceFile const*> manifest_srcs;
  this->GeneratorTarget->GetManifests(manifest_srcs, config);

  std::vector<std::string> manifests;
  manifests.reserve(manifest_srcs.size());

  std::string lang = this->GeneratorTarget->GetLinkerLanguage(config);
  std::string manifestFlag =
    this->Makefile->GetDefinition("CMAKE_" + lang + "_LINKER_MANIFEST_FLAG");
  for (cmSourceFile const* manifest_src : manifest_srcs) {
    manifests.push_back(manifestFlag +
                        this->LocalCommonGenerator->ConvertToOutputFormat(
                          this->LocalCommonGenerator->MaybeRelativeToWorkDir(
                            manifest_src->GetFullPath()),
                          cmOutputConverter::SHELL));
  }

  return cmJoin(manifests, " ");
}

std::string cmCommonTargetGenerator::GetAIXExports(std::string const&)
{
  std::string aixExports;
  if (this->GeneratorTarget->IsAIX()) {
    if (cmValue exportAll =
          this->GeneratorTarget->GetProperty("AIX_EXPORT_ALL_SYMBOLS")) {
      if (exportAll.IsOff()) {
        aixExports = "-n";
      }
    }
  }
  return aixExports;
}

void cmCommonTargetGenerator::AppendOSXVerFlag(std::string& flags,
                                               std::string const& lang,
                                               char const* name, bool so)
{
  // Lookup the flag to specify the version.
  std::string fvar = cmStrCat("CMAKE_", lang, "_OSX_", name, "_VERSION_FLAG");
  cmValue flag = this->Makefile->GetDefinition(fvar);

  // Skip if no such flag.
  if (!flag) {
    return;
  }

  // Lookup the target version information.
  int major;
  int minor;
  int patch;
  std::string prop = cmStrCat("MACHO_", name, "_VERSION");
  std::string fallback_prop = so ? "SOVERSION" : "VERSION";
  this->GeneratorTarget->GetTargetVersionFallback(prop, fallback_prop, major,
                                                  minor, patch);
  if (major > 0 || minor > 0 || patch > 0) {
    // Append the flag since a non-zero version is specified.
    std::ostringstream vflag;
    vflag << *flag << major << "." << minor << "." << patch;
    this->LocalCommonGenerator->AppendFlags(flags, vflag.str());
  }
}

std::string cmCommonTargetGenerator::GetCompilerLauncher(
  std::string const& lang, std::string const& config)
{
  std::string compilerLauncher;
  if (lang == "C" || lang == "CXX" || lang == "Fortran" || lang == "CUDA" ||
      lang == "HIP" || lang == "ISPC" || lang == "OBJC" || lang == "OBJCXX") {
    std::string const clauncher_prop = cmStrCat(lang, "_COMPILER_LAUNCHER");
    cmValue clauncher = this->GeneratorTarget->GetProperty(clauncher_prop);
    std::string const evaluatedClauncher = cmGeneratorExpression::Evaluate(
      *clauncher, this->GeneratorTarget->GetLocalGenerator(), config,
      this->GeneratorTarget, nullptr, this->GeneratorTarget, lang);
    if (!evaluatedClauncher.empty()) {
      compilerLauncher = evaluatedClauncher;
    }
  }
  return compilerLauncher;
}

std::string cmCommonTargetGenerator::GenerateCodeCheckRules(
  cmSourceFile const& source, std::string& compilerLauncher,
  std::string const& cmakeCmd, std::string const& config,
  std::function<std::string(std::string const&)> const& pathConverter)
{
  auto const lang = source.GetLanguage();
  std::string tidy;
  std::string iwyu;
  std::string cpplint;
  std::string cppcheck;
  std::string icstat;

  auto evaluateProp = [&](std::string const& prop) -> std::string {
    auto const value = this->GeneratorTarget->GetProperty(prop);
    if (!value) {
      return std::string{};
    }
    auto evaluatedProp = cmGeneratorExpression::Evaluate(
      *value, this->GeneratorTarget->GetLocalGenerator(), config,
      this->GeneratorTarget, nullptr, this->GeneratorTarget, lang);
    return evaluatedProp;
  };
  std::string const tidy_prop = cmStrCat(lang, "_CLANG_TIDY");
  tidy = evaluateProp(tidy_prop);

  if (lang == "C" || lang == "CXX") {
    std::string const iwyu_prop = cmStrCat(lang, "_INCLUDE_WHAT_YOU_USE");
    iwyu = evaluateProp(iwyu_prop);

    std::string const cpplint_prop = cmStrCat(lang, "_CPPLINT");
    cpplint = evaluateProp(cpplint_prop);

    std::string const cppcheck_prop = cmStrCat(lang, "_CPPCHECK");
    cppcheck = evaluateProp(cppcheck_prop);

    std::string const icstat_prop = cmStrCat(lang, "_ICSTAT");
    icstat = evaluateProp(icstat_prop);
  }
  if (cmNonempty(iwyu) || cmNonempty(tidy) || cmNonempty(cpplint) ||
      cmNonempty(cppcheck) || cmNonempty(icstat)) {
    std::string code_check = cmakeCmd + " -E __run_co_compile";
    if (!compilerLauncher.empty()) {
      // In __run_co_compile case the launcher command is supplied
      // via --launcher=<maybe-list> and consumed
      code_check += " --launcher=";
      code_check += this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(
        compilerLauncher);
      compilerLauncher.clear();
    }
    if (cmNonempty(iwyu)) {
      code_check += " --iwyu=";

      // Only add --driver-mode if it is not already specified, as adding
      // it unconditionally might override a user-specified driver-mode
      if (iwyu.find("--driver-mode=") == std::string::npos) {
        cmValue const p = this->Makefile->GetDefinition(
          cmStrCat("CMAKE_", lang, "_INCLUDE_WHAT_YOU_USE_DRIVER_MODE"));
        std::string driverMode;

        if (cmNonempty(p)) {
          driverMode = *p;
        } else {
          driverMode = lang == "C" ? "gcc" : "g++";
        }

        code_check +=
          this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(
            cmStrCat(iwyu, ";--driver-mode=", driverMode));
      } else {
        code_check +=
          this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(iwyu);
      }
    }
    if (cmNonempty(tidy)) {
      code_check += " --tidy=";
      cmValue const p = this->Makefile->GetDefinition(
        "CMAKE_" + lang + "_CLANG_TIDY_DRIVER_MODE");
      std::string driverMode;
      if (cmNonempty(p)) {
        driverMode = *p;
      } else {
        driverMode = lang == "C" ? "gcc" : "g++";
      }

      auto const generatorName = this->GeneratorTarget->GetLocalGenerator()
                                   ->GetGlobalGenerator()
                                   ->GetName();
      auto const clangTidyExportFixedDir =
        this->GeneratorTarget->GetClangTidyExportFixesDirectory(lang);
      auto fixesFile = this->GetClangTidyReplacementsFilePath(
        clangTidyExportFixedDir, source, config);
      std::string exportFixes;
      if (!clangTidyExportFixedDir.empty()) {
        this->GlobalCommonGenerator->AddClangTidyExportFixesDir(
          clangTidyExportFixedDir);
      }
      if (generatorName.find("Make") != std::string::npos) {
        if (!clangTidyExportFixedDir.empty()) {
          this->GlobalCommonGenerator->AddClangTidyExportFixesFile(fixesFile);
          cmSystemTools::MakeDirectory(
            cmSystemTools::GetFilenamePath(fixesFile));
          fixesFile = this->GeneratorTarget->GetLocalGenerator()
                        ->MaybeRelativeToCurBinDir(fixesFile);
          exportFixes = cmStrCat(";--export-fixes=", fixesFile);
        }
        code_check +=
          this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(
            cmStrCat(tidy, ";--extra-arg-before=--driver-mode=", driverMode,
                     exportFixes));
      } else if (generatorName.find("Ninja") != std::string::npos ||
                 generatorName.find("FASTBuild") != std::string::npos) {
        if (!clangTidyExportFixedDir.empty()) {
          this->GlobalCommonGenerator->AddClangTidyExportFixesFile(fixesFile);
          cmSystemTools::MakeDirectory(
            cmSystemTools::GetFilenamePath(fixesFile));
          if (pathConverter) {
            fixesFile = pathConverter(fixesFile);
          }
          exportFixes = cmStrCat(";--export-fixes=", fixesFile);
        }
        code_check +=
          this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(
            cmStrCat(tidy, ";--extra-arg-before=--driver-mode=", driverMode,
                     exportFixes));
      }
    }
    if (cmNonempty(cpplint)) {
      code_check += " --cpplint=";
      code_check +=
        this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(cpplint);
    }
    if (cmNonempty(cppcheck)) {
      code_check += " --cppcheck=";
      code_check +=
        this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(cppcheck);
    }
    if (cmNonempty(icstat)) {
      code_check += " --icstat=";
      std::string checksParam{};
      std::string dbParam{};
      // Set default values for mandatory parameters
      std::string checksFile{ "cstat_sel_checks.txt" };
      std::string dbFile{ "cstat.db" };
      // Populate the command line with C-STAT
      // mandatory parameters unless specified
      if (icstat.find("--checks=") == std::string::npos) {
        checksParam = cmStrCat(";--checks=", checksFile);
      }
      if (icstat.find("--db=") == std::string::npos) {
        dbParam = cmStrCat(";--db=", dbFile);
      }
      code_check += this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(
        cmStrCat(icstat, checksParam, dbParam));
    }
    if (cmNonempty(tidy) || (cmNonempty(cpplint)) || (cmNonempty(cppcheck))) {
      code_check += " --source=";
      code_check +=
        this->GeneratorTarget->GetLocalGenerator()->ConvertToOutputFormat(
          source.GetFullPath(), cmOutputConverter::SHELL);
    }
    code_check += " -- ";
    return code_check;
  }
  return "";
}

std::string cmCommonTargetGenerator::GetLinkerLauncher(
  std::string const& config)
{
  std::string lang = this->GeneratorTarget->GetLinkerLanguage(config);
  std::string propName = lang + "_LINKER_LAUNCHER";
  cmValue launcherProp = this->GeneratorTarget->GetProperty(propName);
  if (cmNonempty(launcherProp)) {
    cm::GenEx::Context context(this->LocalCommonGenerator, config, lang);
    cmGeneratorExpressionDAGChecker dagChecker{ this->GeneratorTarget,
                                                propName, nullptr, nullptr,
                                                context };
    std::string evaluatedLinklauncher = cmGeneratorExpression::Evaluate(
      *launcherProp, context.LG, context.Config, this->GeneratorTarget,
      &dagChecker, this->GeneratorTarget, context.Language);
    // Convert ;-delimited list to single string
    cmList args{ evaluatedLinklauncher, cmList::EmptyElements::Yes };
    if (!args.empty()) {
      args[0] = this->LocalCommonGenerator->ConvertToOutputFormat(
        args[0], cmOutputConverter::SHELL);
      for (std::string& i : cmMakeRange(args.begin() + 1, args.end())) {
        i = this->LocalCommonGenerator->EscapeForShell(i);
      }
      return cmJoin(args, " ");
    }
  }
  return std::string();
}

bool cmCommonTargetGenerator::HaveRequiredLanguages(
  std::vector<cmSourceFile const*> const& sources,
  std::set<std::string>& languagesNeeded) const
{
  for (cmSourceFile const* sf : sources) {
    languagesNeeded.insert(sf->GetLanguage());
  }

  auto* makefile = this->Makefile;
  auto* state = makefile->GetState();
  auto unary = [&state, &makefile](std::string const& lang) -> bool {
    bool const valid = state->GetLanguageEnabled(lang);
    if (!valid) {
      makefile->IssueMessage(
        MessageType::FATAL_ERROR,
        cmStrCat("The language ", lang,
                 " was requested for compilation but was not enabled."
                 " To enable a language it needs to be specified in a"
                 " 'project' or 'enable_language' command in the root"
                 " CMakeLists.txt"));
    }
    return valid;
  };
  return std::all_of(languagesNeeded.cbegin(), languagesNeeded.cend(), unary);
}
