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

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

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

#include "cmExportSet.h"
#include "cmFileSetMetadata.h"
#include "cmFindPackageStack.h"
#include "cmGeneratedFileStream.h"
#include "cmGeneratorFileSet.h"
#include "cmGeneratorFileSets.h"
#include "cmGeneratorTarget.h"
#include "cmLocalGenerator.h"
#include "cmMessageType.h"
#include "cmOutputConverter.h"
#include "cmScriptGenerator.h"
#include "cmStateTypes.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmTarget.h"
#include "cmValue.h"
#include "cmVersionMacros.h"

struct cmLinkInterface;

static std::string cmExportFileGeneratorEscape(std::string const& str)
{
  // Escape a property value for writing into a .cmake file.
  std::string result = cmOutputConverter::EscapeForCMake(str);
  // Un-escape variable references generated by our own export code.
  cmSystemTools::ReplaceString(result, "\\${_IMPORT_PREFIX}",
                               "${_IMPORT_PREFIX}");
  cmSystemTools::ReplaceString(result, "\\${CMAKE_IMPORT_LIBRARY_SUFFIX}",
                               "${CMAKE_IMPORT_LIBRARY_SUFFIX}");
  return result;
}

cmExportCMakeConfigGenerator::cmExportCMakeConfigGenerator() = default;

cm::string_view cmExportCMakeConfigGenerator::GetImportPrefixWithSlash() const
{
  return "${_IMPORT_PREFIX}/"_s;
}

bool cmExportCMakeConfigGenerator::GenerateImportFile(std::ostream& os)
{
  std::stringstream mainFileWithHeadersAndFootersBuffer;

  // Start with the import file header.
  this->GenerateImportHeaderCode(mainFileWithHeadersAndFootersBuffer);

  // Create all the imported targets.
  std::stringstream mainFileBuffer;
  bool result = this->GenerateMainFile(mainFileBuffer);

  // Export find_dependency() calls. Must be done after GenerateMainFile(),
  // because that's when target dependencies are gathered, which we need for
  // the find_dependency() calls.
  if (!this->AppendMode && this->GetExportSet() &&
      this->ExportPackageDependencies) {
    this->SetRequiredCMakeVersion(3, 9, 0);
    this->GenerateFindDependencyCalls(mainFileWithHeadersAndFootersBuffer);
  }

  // Write cached import code.
  mainFileWithHeadersAndFootersBuffer << mainFileBuffer.rdbuf();

  // End with the import file footer.
  this->GenerateImportFooterCode(mainFileWithHeadersAndFootersBuffer);
  this->GeneratePolicyFooterCode(mainFileWithHeadersAndFootersBuffer);

  // This has to be done last, after the minimum CMake version has been
  // determined.
  this->GeneratePolicyHeaderCode(os);
  os << mainFileWithHeadersAndFootersBuffer.rdbuf();

  return result;
}

void cmExportCMakeConfigGenerator::GenerateInterfaceProperties(
  cmGeneratorTarget const* target, std::ostream& os,
  ImportPropertyMap const& properties)
{
  if (!properties.empty()) {
    std::string targetName =
      cmStrCat(this->Namespace, target->GetExportName());
    os << "set_target_properties(" << targetName << " PROPERTIES\n";
    for (auto const& property : properties) {
      os << "  " << property.first << ' '
         << cmExportFileGeneratorEscape(property.second) << '\n';
    }
    os << ")\n\n";
  }
}

void cmExportCMakeConfigGenerator::SetImportLinkInterface(
  std::string const& config, std::string const& suffix,
  cmGeneratorExpression::PreprocessContext preprocessRule,
  cmGeneratorTarget const* target, ImportPropertyMap& properties)
{
  // Add the transitive link dependencies for this configuration.
  cmLinkInterface const* iface = target->GetLinkInterface(config, target);
  if (!iface) {
    return;
  }

  cmValue propContent;

  if (cmValue prop_suffixed =
        target->GetProperty("LINK_INTERFACE_LIBRARIES" + suffix)) {
    propContent = prop_suffixed;
  } else if (cmValue prop = target->GetProperty("LINK_INTERFACE_LIBRARIES")) {
    propContent = prop;
  } else {
    return;
  }

  if (!this->ExportOld) {
    cmLocalGenerator* lg = target->GetLocalGenerator();
    std::ostringstream e;
    e << "Target \"" << target->GetName()
      << "\" has policy CMP0022 enabled, "
         "but also has old-style LINK_INTERFACE_LIBRARIES properties "
         "populated, but it was exported without the "
         "EXPORT_LINK_INTERFACE_LIBRARIES to export the old-style properties";
    lg->IssueMessage(MessageType::FATAL_ERROR, e.str());
    return;
  }

  if (propContent->empty()) {
    properties["IMPORTED_LINK_INTERFACE_LIBRARIES" + suffix].clear();
    return;
  }

  std::string prepro =
    cmGeneratorExpression::Preprocess(*propContent, preprocessRule);
  if (!prepro.empty()) {
    this->ResolveTargetsInGeneratorExpressions(prepro, target,
                                               ReplaceFreeTargets);
    properties["IMPORTED_LINK_INTERFACE_LIBRARIES" + suffix] = prepro;
  }
}

void cmExportCMakeConfigGenerator::GeneratePolicyHeaderCode(std::ostream& os)
{
  // Protect that file against use with older CMake versions.
  /* clang-format off */
  os << "# Generated by CMake\n\n"
        "if(\"${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}\" LESS 2.8)\n"
        "   message(FATAL_ERROR \"CMake >= "
     << this->RequiredCMakeVersionMajor << '.'
     << this->RequiredCMakeVersionMinor << '.'
     << this->RequiredCMakeVersionPatch << " required\")\n"
        "endif()\n"
        "if(CMAKE_VERSION VERSION_LESS \""
     << this->RequiredCMakeVersionMajor << '.'
     << this->RequiredCMakeVersionMinor << '.'
     << this->RequiredCMakeVersionPatch << "\")\n"
        "   message(FATAL_ERROR \"CMake >= "
     << this->RequiredCMakeVersionMajor << '.'
     << this->RequiredCMakeVersionMinor << '.'
     << this->RequiredCMakeVersionPatch << " required\")\n"
        "endif()\n";
  /* clang-format on */

  // Isolate the file policy level.
  // Support CMake versions as far back as the
  // RequiredCMakeVersion{Major,Minor,Patch}, but also support using NEW
  // policy settings for up to CMake 4.2 (this upper limit may be reviewed
  // and increased from time to time). This reduces the opportunity for CMake
  // warnings when an older export file is later used with newer CMake
  // versions.
  /* clang-format off */
  os << "cmake_policy(PUSH)\n"
        "cmake_policy(VERSION "
     << this->RequiredCMakeVersionMajor << '.'
     << this->RequiredCMakeVersionMinor << '.'
     << this->RequiredCMakeVersionPatch << "...4.2)\n";
  /* clang-format on */
}

void cmExportCMakeConfigGenerator::GeneratePolicyFooterCode(std::ostream& os)
{
  os << "cmake_policy(POP)\n";
}

void cmExportCMakeConfigGenerator::GenerateImportHeaderCode(
  std::ostream& os, std::string const& config)
{
  os << "#----------------------------------------------------------------\n"
        "# Generated CMake target import file";
  if (!config.empty()) {
    os << " for configuration \"" << config << "\".\n";
  } else {
    os << ".\n";
  }
  os << "#----------------------------------------------------------------\n"
        "\n";
  this->GenerateImportVersionCode(os);
}

void cmExportCMakeConfigGenerator::GenerateImportFooterCode(std::ostream& os)
{
  os << "# Commands beyond this point should not need to know the version.\n"
        "set(CMAKE_IMPORT_FILE_VERSION)\n";
}

void cmExportCMakeConfigGenerator::GenerateImportVersionCode(std::ostream& os)
{
  // Store an import file format version.  This will let us change the
  // format later while still allowing old import files to work.
  os << "# Commands may need to know the format version.\n"
        "set(CMAKE_IMPORT_FILE_VERSION 1)\n"
        "\n";
}

void cmExportCMakeConfigGenerator::GenerateExpectedTargetsCode(
  std::ostream& os, std::string const& expectedTargets)
{
  /* clang-format off */
  os << "# Protect against multiple inclusion, which would fail when already "
        "imported targets are added once more.\n"
        "set(_cmake_targets_defined \"\")\n"
        "set(_cmake_targets_not_defined \"\")\n"
        "set(_cmake_expected_targets \"\")\n"
        "foreach(_cmake_expected_target IN ITEMS " << expectedTargets << ")\n"
        "  list(APPEND _cmake_expected_targets \"${_cmake_expected_target}\")\n"
        "  if(TARGET \"${_cmake_expected_target}\")\n"
        "    list(APPEND _cmake_targets_defined \"${_cmake_expected_target}\")\n"
        "  else()\n"
        "    list(APPEND _cmake_targets_not_defined \"${_cmake_expected_target}\")\n"
        "  endif()\n"
        "endforeach()\n"
        "unset(_cmake_expected_target)\n"
        "if(_cmake_targets_defined STREQUAL _cmake_expected_targets)\n"
        "  unset(_cmake_targets_defined)\n"
        "  unset(_cmake_targets_not_defined)\n"
        "  unset(_cmake_expected_targets)\n"
        "  unset(CMAKE_IMPORT_FILE_VERSION)\n"
        "  cmake_policy(POP)\n"
        "  return()\n"
        "endif()\n"
        "if(NOT _cmake_targets_defined STREQUAL \"\")\n"
        "  string(REPLACE \";\" \", \" _cmake_targets_defined_text \"${_cmake_targets_defined}\")\n"
        "  string(REPLACE \";\" \", \" _cmake_targets_not_defined_text \"${_cmake_targets_not_defined}\")\n"
        "  message(FATAL_ERROR \"Some (but not all) targets in this export "
        "set were already defined.\\nTargets Defined: ${_cmake_targets_defined_text}\\n"
        "Targets not yet defined: ${_cmake_targets_not_defined_text}\\n\")\n"
        "endif()\n"
        "unset(_cmake_targets_defined)\n"
        "unset(_cmake_targets_not_defined)\n"
        "unset(_cmake_expected_targets)\n"
        "\n\n";
  /* clang-format on */
}

void cmExportCMakeConfigGenerator::GenerateImportTargetCode(
  std::ostream& os, cmGeneratorTarget const* target,
  cmStateEnums::TargetType targetType)
{
  // Construct the imported target name.
  std::string targetName = this->Namespace;

  targetName += target->GetExportName();

  // Create the imported target.
  os << "# Create imported target " << targetName << "\n";
  switch (targetType) {
    case cmStateEnums::EXECUTABLE:
      os << "add_executable(" << targetName << " IMPORTED)\n";
      break;
    case cmStateEnums::STATIC_LIBRARY:
      os << "add_library(" << targetName << " STATIC IMPORTED)\n";
      break;
    case cmStateEnums::SHARED_LIBRARY:
      os << "add_library(" << targetName << " SHARED IMPORTED)\n";
      break;
    case cmStateEnums::MODULE_LIBRARY:
      os << "add_library(" << targetName << " MODULE IMPORTED)\n";
      break;
    case cmStateEnums::UNKNOWN_LIBRARY:
      os << "add_library(" << targetName << " UNKNOWN IMPORTED)\n";
      break;
    case cmStateEnums::OBJECT_LIBRARY:
      os << "add_library(" << targetName << " OBJECT IMPORTED)\n";
      break;
    case cmStateEnums::INTERFACE_LIBRARY:
      if (target->IsSymbolic()) {
        os << "if(CMAKE_VERSION VERSION_GREATER_EQUAL \"4.2.0\")\n"
              "  add_library("
           << targetName << " INTERFACE SYMBOLIC IMPORTED)\n"
           << "elseif(CMAKE_VERSION VERSION_GREATER_EQUAL 3.2.0)\n"
           << "  add_library(" << targetName << " INTERFACE IMPORTED)\n"
           << "  set_target_properties(" << targetName
           << " PROPERTIES SYMBOLIC TRUE)\n"
           << "endif()\n";
      } else {
        os << "add_library(" << targetName << " INTERFACE IMPORTED)\n";
      }
      break;
    default: // should never happen
      break;
  }

  // Mark the imported executable if it has exports.
  if (target->IsExecutableWithExports() ||
      (target->IsSharedLibraryWithExports() && target->HasImportLibrary(""))) {
    os << "set_property(TARGET " << targetName
       << " PROPERTY ENABLE_EXPORTS 1)\n";
  }

  // Mark the imported library if it is a framework.
  if (target->IsFrameworkOnApple()) {
    os << "set_property(TARGET " << targetName << " PROPERTY FRAMEWORK 1)\n";
  }

  // Mark the imported executable if it is an application bundle.
  if (target->IsAppBundleOnApple()) {
    os << "set_property(TARGET " << targetName
       << " PROPERTY MACOSX_BUNDLE 1)\n";
  }

  if (target->IsCFBundleOnApple()) {
    os << "set_property(TARGET " << targetName << " PROPERTY BUNDLE 1)\n";
  }

  // Mark the imported library if it is an AIX shared library archive.
  if (target->IsArchivedAIXSharedLibrary()) {
    os << "set_property(TARGET " << targetName
       << " PROPERTY AIX_SHARED_LIBRARY_ARCHIVE 1)\n";
  }

  // generate DEPRECATION
  if (target->IsDeprecated()) {
    os << "set_property(TARGET " << targetName << " PROPERTY DEPRECATION "
       << cmExportFileGeneratorEscape(target->GetDeprecation()) << ")\n";
  }

  if (target->GetPropertyAsBool("IMPORTED_NO_SYSTEM")) {
    os << "set_property(TARGET " << targetName
       << " PROPERTY IMPORTED_NO_SYSTEM 1)\n";
  }

  if (target->GetPropertyAsBool("EXPORT_NO_SYSTEM")) {
    os << "set_property(TARGET " << targetName << " PROPERTY SYSTEM 0)\n";
  }

  os << '\n';
}

void cmExportCMakeConfigGenerator::GenerateImportPropertyCode(
  std::ostream& os, std::string const& config, std::string const& suffix,
  cmGeneratorTarget const* target, ImportPropertyMap const& properties,
  std::string const& importedXcFrameworkLocation)
{
  // Construct the imported target name.
  std::string targetName = this->Namespace;

  targetName += target->GetExportName();

  // Set the import properties.
  os << "# Import target \"" << targetName << "\" for configuration \""
     << config
     << "\"\n"
        "set_property(TARGET "
     << targetName << " APPEND PROPERTY IMPORTED_CONFIGURATIONS ";
  if (!config.empty()) {
    os << cmSystemTools::UpperCase(config);
  } else {
    os << "NOCONFIG";
  }
  os << ")\n"
        "set_target_properties("
     << targetName << " PROPERTIES\n";
  std::string importedLocationProp = cmStrCat("IMPORTED_LOCATION", suffix);
  for (auto const& property : properties) {
    if (importedXcFrameworkLocation.empty() ||
        property.first != importedLocationProp) {
      os << "  " << property.first << ' '
         << cmExportFileGeneratorEscape(property.second) << '\n';
    }
  }
  os << "  )\n";
  if (!importedXcFrameworkLocation.empty()) {
    auto importedLocationIt = properties.find(importedLocationProp);
    if (importedLocationIt != properties.end()) {
      os << "if(NOT CMAKE_VERSION VERSION_LESS \"3.28\" AND IS_DIRECTORY "
         << cmExportFileGeneratorEscape(importedXcFrameworkLocation)
         << ")\n"
            "  set_property(TARGET "
         << targetName << " PROPERTY " << importedLocationProp << ' '
         << cmExportFileGeneratorEscape(importedXcFrameworkLocation)
         << ")\nelse()\n  set_property(TARGET " << targetName << " PROPERTY "
         << importedLocationProp << ' '
         << cmExportFileGeneratorEscape(importedLocationIt->second)
         << ")\nendif()\n";
    }
  }
  os << '\n';
}

void cmExportCMakeConfigGenerator::GenerateFindDependencyCalls(
  std::ostream& os)
{
  os << "include(CMakeFindDependencyMacro)\n";
  std::map<std::string, cmExportSet::PackageDependency> packageDependencies;
  auto* exportSet = this->GetExportSet();
  if (exportSet) {
    packageDependencies = exportSet->GetPackageDependencies();
  }

  for (cmGeneratorTarget const* gt : this->ExternalTargets) {
    std::string findPackageName;
    auto exportFindPackageName = gt->GetProperty("EXPORT_FIND_PACKAGE_NAME");
    cmFindPackageStack pkgStack = gt->Target->GetFindPackageStack();
    if (!exportFindPackageName.IsEmpty()) {
      findPackageName = *exportFindPackageName;
    } else {
      if (!pkgStack.Empty()) {
        cmFindPackageCall const& fpc = pkgStack.Top();
        findPackageName = fpc.Name;
      }
    }
    if (!findPackageName.empty()) {
      auto& dep = packageDependencies[findPackageName];
      if (!pkgStack.Empty()) {
        dep.FindPackageIndex = pkgStack.Top().Index;
      }
      if (dep.Enabled == cmExportSet::PackageDependencyExportEnabled::Auto) {
        dep.Enabled = cmExportSet::PackageDependencyExportEnabled::On;
      }
    }
  }

  std::vector<std::pair<std::string, cmExportSet::PackageDependency>>
    packageDependenciesSorted(packageDependencies.begin(),
                              packageDependencies.end());
  std::sort(
    packageDependenciesSorted.begin(), packageDependenciesSorted.end(),
    [](std::pair<std::string, cmExportSet::PackageDependency> const& lhs,
       std::pair<std::string, cmExportSet::PackageDependency> const& rhs)
      -> bool {
      if (lhs.second.SpecifiedIndex) {
        if (rhs.second.SpecifiedIndex) {
          return lhs.second.SpecifiedIndex < rhs.second.SpecifiedIndex;
        }
        assert(rhs.second.FindPackageIndex);
        return true;
      }
      assert(lhs.second.FindPackageIndex);
      if (rhs.second.SpecifiedIndex) {
        return false;
      }
      assert(rhs.second.FindPackageIndex);
      return lhs.second.FindPackageIndex < rhs.second.FindPackageIndex;
    });

  // Unwinding is only valid in a find_package() context
  os << "if(DEFINED CMAKE_FIND_PACKAGE_NAME)\n"
     << "  set(_cmake_unwind_arg UNWIND_INCLUDE)\n"
     << "endif()\n\n";

  for (auto const& it : packageDependenciesSorted) {
    if (it.second.Enabled == cmExportSet::PackageDependencyExportEnabled::On) {
      os << "__find_dependency_no_return(" << it.first;
      for (auto const& arg : it.second.ExtraArguments) {
        os << ' ' << cmScriptGenerator::Quote(arg);
      }
      os << " ${_cmake_unwind_arg})\n";
      os << "if(NOT " << it.first << "_FOUND)\n"
         << "  unset(_cmake_unwind_arg)\n"
         << "  cmake_policy(POP)\n"
         << "  return()\n"
         << "endif()\n\n";
    }
  }

  os << "unset(_cmake_unwind_arg)\n\n\n";
}

void cmExportCMakeConfigGenerator::GenerateMissingTargetsCheckCode(
  std::ostream& os)
{
  if (this->MissingTargets.empty()) {
    os << "# This file does not depend on other imported targets which have\n"
          "# been exported from the same project but in a separate "
          "export set.\n\n";
    return;
  }
  os << "# Make sure the targets which have been exported in some other\n"
        "# export set exist.\n"
        "unset(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets)\n"
        "foreach(_target ";
  std::set<std::string> emitted;
  for (std::string const& missingTarget : this->MissingTargets) {
    if (emitted.insert(missingTarget).second) {
      os << '"' << missingTarget << "\" ";
    }
  }
  os << ")\n"
        "  if(NOT TARGET \"${_target}\" )\n"
        "    set(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets \""
        "${${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets} ${_target}\")"
        "\n"
        "  endif()\n"
        "endforeach()\n"
        "\n"
        "if(DEFINED ${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets)\n"
        "  if(CMAKE_FIND_PACKAGE_NAME)\n"
        "    set( ${CMAKE_FIND_PACKAGE_NAME}_FOUND FALSE)\n"
        "    set( ${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE "
        "\"The following imported targets are "
        "referenced, but are missing: "
        "${${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets}\")\n"
        "  else()\n"
        "    message(FATAL_ERROR \"The following imported targets are "
        "referenced, but are missing: "
        "${${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets}\")\n"
        "  endif()\n"
        "endif()\n"
        "unset(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets)\n"
        "\n";
}

void cmExportCMakeConfigGenerator::GenerateImportedFileCheckLoop(
  std::ostream& os)
{
  // Add code which verifies at cmake time that the file which is being
  // imported actually exists on disk. This should in theory always be theory
  // case, but still when packages are split into normal and development
  // packages this might get broken (e.g. the Config.cmake could be part of
  // the non-development package, something similar happened to me without
  // on SUSE with a mysql pkg-config file, which claimed everything is fine,
  // but the development package was not installed.).
  os << "# Loop over all imported files and verify that they actually exist\n"
        "foreach(_cmake_target IN LISTS _cmake_import_check_targets)\n"
        "  if(CMAKE_VERSION VERSION_LESS \"3.28\"\n"
        "      OR NOT DEFINED "
        "_cmake_import_check_xcframework_for_${_cmake_target}\n"
        "      OR NOT IS_DIRECTORY "
        "\"${_cmake_import_check_xcframework_for_${_cmake_target}}\")\n"
        "    foreach(_cmake_file IN LISTS "
        "\"_cmake_import_check_files_for_${_cmake_target}\")\n"
        "      if(NOT EXISTS \"${_cmake_file}\")\n"
        "        message(FATAL_ERROR \"The imported target "
        "\\\"${_cmake_target}\\\" references the file\n"
        "   \\\"${_cmake_file}\\\"\n"
        "but this file does not exist.  Possible reasons include:\n"
        "* The file was deleted, renamed, or moved to another location.\n"
        "* An install or uninstall procedure did not complete successfully.\n"
        "* The installation package was faulty and contained\n"
        "   \\\"${CMAKE_CURRENT_LIST_FILE}\\\"\n"
        "but not all the files it references.\n"
        "\")\n"
        "      endif()\n"
        "    endforeach()\n"
        "  endif()\n"
        "  unset(_cmake_file)\n"
        "  unset(\"_cmake_import_check_files_for_${_cmake_target}\")\n"
        "endforeach()\n"
        "unset(_cmake_target)\n"
        "unset(_cmake_import_check_targets)\n"
        "\n";
}

void cmExportCMakeConfigGenerator::GenerateImportedFileChecksCode(
  std::ostream& os, cmGeneratorTarget const* target,
  ImportPropertyMap const& properties,
  std::set<std::string> const& importedLocations,
  std::string const& importedXcFrameworkLocation)
{
  // Construct the imported target name.
  std::string targetName = cmStrCat(this->Namespace, target->GetExportName());

  os << "list(APPEND _cmake_import_check_targets " << targetName << " )\n";
  if (!importedXcFrameworkLocation.empty()) {
    os << "set(_cmake_import_check_xcframework_for_" << targetName << ' '
       << cmExportFileGeneratorEscape(importedXcFrameworkLocation) << ")\n";
  }
  os << "list(APPEND _cmake_import_check_files_for_" << targetName << ' ';

  for (std::string const& li : importedLocations) {
    auto pi = properties.find(li);
    if (pi != properties.end()) {
      os << cmExportFileGeneratorEscape(pi->second) << ' ';
    }
  }

  os << ")\n\n";
}

namespace {
struct FileSetInformation
{
  cm::string_view Type;
  cm::string_view CMakeVersion;
};

cm::optional<FileSetInformation> GetFileSetInformation(cm::string_view type)
{
  static std::unordered_map<cm::string_view, FileSetInformation>
    fileSetsInformation{
      { cm::FileSetMetadata::HEADERS,
        { cm::FileSetMetadata::HEADERS, "3.23.0"_s } },
      { cm::FileSetMetadata::SOURCES,
        { cm::FileSetMetadata::SOURCES, "4.4.0"_s } },
      { cm::FileSetMetadata::CXX_MODULES,
        { cm::FileSetMetadata::CXX_MODULES, "3.28.0"_s } },
    };

  auto fsInfo = fileSetsInformation.find(type);
  if (fsInfo != fileSetsInformation.end()) {
    return fsInfo->second;
  }
  return {};
}
}

void cmExportCMakeConfigGenerator::GenerateTargetFileSets(
  cmGeneratorTarget* gte, std::ostream& os,
  ImportFileSetPropertyMap const& properties, cmTargetExport const* te)
{
  cmGeneratorFileSets const* gfs = gte->GetGeneratorFileSets();
  auto const& types = gfs->GetInterfaceFileSetTypes();

  if (!types.empty()) {
    std::string targetName = cmStrCat(this->Namespace, gte->GetExportName());
    for (auto const& type : types) {
      auto fsInfo = GetFileSetInformation(type);
      if (fsInfo) {
        os << "if(NOT CMAKE_VERSION VERSION_LESS \"" << fsInfo->CMakeVersion
           << "\")\n  ";
      }
      os << "target_sources(" << targetName << '\n';

      for (auto const* fileSet : gfs->GetInterfaceFileSets(type)) {
        os << "    INTERFACE"
           << "\n      FILE_SET "
           << cmScriptGenerator::Quote(fileSet->GetName()) << "\n      TYPE "
           << cmScriptGenerator::Quote(type) << "\n      BASE_DIRS "
           << this->GetFileSetDirectories(gte, fileSet, te) << "\n      FILES "
           << this->GetFileSetFiles(gte, fileSet, te) << '\n';
      }
      os << "  )\n";
      for (auto const* fileSet : gfs->GetInterfaceFileSets(type)) {
        auto fsProperties = properties.find(fileSet->GetName());
        if (fsProperties != properties.end()) {
          for (auto const& property : fsProperties->second) {
            os << "\n  set_property(FILE_SET "
               << cmScriptGenerator::Quote(fileSet->GetName()) << " TARGET "
               << targetName << "\n    PROPERTY " << property.first << ' '
               << cmExportFileGeneratorEscape(property.second) << "\n  )";
          }
        }
      }

      if (fsInfo) {
        if (type == cm::FileSetMetadata::HEADERS) {
          os << "\nelse()\n  set_property(TARGET " << targetName
             << "\n    APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES";
          for (auto const* fileSet : gfs->GetInterfaceFileSets(type)) {
            os << "\n      " << this->GetFileSetDirectories(gte, fileSet, te);
          }
          os << "\n  )";
        } else if (type == cm::FileSetMetadata::SOURCES) {
          os << "\nelse()\n  message(FATAL_ERROR \"The target '" << targetName
             << "' cannot be imported because it relies on 'FILE_SET' of type "
                "'SOURCES' which is not supported by this CMake version.\")";
        } else if (type == cm::FileSetMetadata::CXX_MODULES) {
          os << "\nelse()\n  message(AUTHOR_WARNING \"The target '"
             << targetName
             << "' cannot be imported properly because it relies on C++ "
                "modules which are not supported by this CMake version.\")";
        }
        os << "\nendif()\n\n";
      } else {
        os << ")\n\n";
      }
    }
  }
}

std::string cmExportCMakeConfigGenerator::GetCxxModuleFile(
  std::string const& name) const
{
  auto const& cxxModuleDirname = this->GetCxxModulesDirectory();
  if (cxxModuleDirname.empty()) {
    return {};
  }

  return cmStrCat(cmSystemTools::GetFilenamePath(this->MainImportFile), '/',
                  cxxModuleDirname, "/cxx-modules-", name, ".cmake");
}

void cmExportCMakeConfigGenerator::GenerateCxxModuleInformation(
  std::string const& name, std::ostream& os)
{
  auto const cxx_module_dirname = this->GetCxxModulesDirectory();
  if (cxx_module_dirname.empty()) {
    return;
  }

  // Write the include.
  os << "# Include C++ module properties\n"
        "include(\"${CMAKE_CURRENT_LIST_DIR}/"
     << cxx_module_dirname << "/cxx-modules-" << name << ".cmake\")\n\n";

  // Include all configuration-specific include files.
  cmGeneratedFileStream ap(this->GetCxxModuleFile(name), true);
  ap.SetCopyIfDifferent(true);

  this->GenerateCxxModuleConfigInformation(name, ap);
}

void cmExportCMakeConfigGenerator::SetRequiredCMakeVersion(unsigned int major,
                                                           unsigned int minor,
                                                           unsigned int patch)
{
  if (CMake_VERSION_ENCODE(major, minor, patch) >
      CMake_VERSION_ENCODE(this->RequiredCMakeVersionMajor,
                           this->RequiredCMakeVersionMinor,
                           this->RequiredCMakeVersionPatch)) {
    this->RequiredCMakeVersionMajor = major;
    this->RequiredCMakeVersionMinor = minor;
    this->RequiredCMakeVersionPatch = patch;
  }
}
