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

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <memory>
#include <set>
#include <sstream>
#include <utility>

#include "cmExportSet.h"
#include "cmGeneratedFileStream.h"
#include "cmGeneratorFileSet.h"
#include "cmGeneratorFileSets.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalGenerator.h"
#include "cmInstallTargetGenerator.h"
#include "cmList.h"
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmTarget.h"
#include "cmTargetExport.h"
#include "cmValue.h"

cmExportInstallFileGenerator::cmExportInstallFileGenerator(
  cmInstallExportGenerator* iegen)
  : IEGen(iegen)
{
}

void cmExportInstallFileGenerator::ReplaceInstallPrefix(
  std::string& input) const
{
  cmGeneratorExpression::ReplaceInstallPrefix(input, this->GetInstallPrefix());
}

void cmExportInstallFileGenerator::PopulateImportProperties(
  std::string const& config, std::string const& suffix,
  cmTargetExport const* targetExport, ImportPropertyMap& properties,
  std::set<std::string>& importedLocations)
{
  this->SetImportLocationProperty(config, suffix,
                                  targetExport->ArchiveGenerator, properties,
                                  importedLocations);
  this->SetImportLocationProperty(config, suffix,
                                  targetExport->LibraryGenerator, properties,
                                  importedLocations);
  this->SetImportLocationProperty(config, suffix,
                                  targetExport->RuntimeGenerator, properties,
                                  importedLocations);
  this->SetImportLocationProperty(config, suffix,
                                  targetExport->ObjectsGenerator, properties,
                                  importedLocations);
  this->SetImportLocationProperty(config, suffix,
                                  targetExport->FrameworkGenerator, properties,
                                  importedLocations);
  this->SetImportLocationProperty(config, suffix,
                                  targetExport->BundleGenerator, properties,
                                  importedLocations);

  // If any file location was set for the target add it to the
  // import file.
  if (!properties.empty()) {
    // Get the rest of the target details.
    cmGeneratorTarget const* const gtgt = targetExport->Target;
    this->SetImportDetailProperties(config, suffix, gtgt, properties);

    // TODO: PUBLIC_HEADER_LOCATION
    // This should wait until the build feature propagation stuff is done.
    // Then this can be a propagated include directory.
    // this->GenerateImportProperty(config, te->HeaderGenerator, properties);
  }
}

std::string cmExportInstallFileGenerator::GetImportXcFrameworkLocation(
  std::string const& config, cmTargetExport const* targetExport) const
{
  std::string importedXcFrameworkLocation = targetExport->XcFrameworkLocation;
  if (!importedXcFrameworkLocation.empty()) {
    importedXcFrameworkLocation = cmGeneratorExpression::Preprocess(
      importedXcFrameworkLocation,
      cmGeneratorExpression::PreprocessContext::InstallInterface,
      this->GetImportPrefixWithSlash());
    importedXcFrameworkLocation = cmGeneratorExpression::Evaluate(
      importedXcFrameworkLocation, targetExport->Target->GetLocalGenerator(),
      config, targetExport->Target, nullptr, targetExport->Target);
    if (!importedXcFrameworkLocation.empty() &&
        !cmSystemTools::FileIsFullPath(importedXcFrameworkLocation) &&
        !cmHasPrefix(importedXcFrameworkLocation,
                     this->GetImportPrefixWithSlash())) {
      return cmStrCat(this->GetImportPrefixWithSlash(),
                      importedXcFrameworkLocation);
    }
  }

  return importedXcFrameworkLocation;
}

bool cmExportInstallFileGenerator::GenerateImportFileConfig(
  std::string const& config)
{
  // Skip configurations not enabled for this export.
  if (!this->IEGen->InstallsForConfig(config)) {
    return true;
  }

  // Construct the name of the file to generate.
  std::string fileName = cmStrCat(this->FileDir, '/', this->FileBase,
                                  this->GetConfigFileNameSeparator());
  if (!config.empty()) {
    fileName += cmSystemTools::LowerCase(config);
  } else {
    fileName += "noconfig";
  }
  fileName += this->FileExt;

  // Open the output file to generate it.
  cmGeneratedFileStream exportFileStream(fileName, true);
  if (!exportFileStream) {
    std::string se = cmSystemTools::GetLastSystemError();
    std::ostringstream e;
    e << "cannot write to file \"" << fileName << "\": " << se;
    cmSystemTools::Error(e.str());
    return false;
  }
  exportFileStream.SetCopyIfDifferent(true);
  std::ostream& os = exportFileStream;

  // Generate the per-config target information.
  this->GenerateImportConfig(os, config);

  // Record this per-config import file.
  this->ConfigImportFiles[config] = fileName;

  return true;
}

void cmExportInstallFileGenerator::SetImportLocationProperty(
  std::string const& config, std::string const& suffix,
  cmInstallTargetGenerator* itgen, ImportPropertyMap& properties,
  std::set<std::string>& importedLocations)
{
  // Skip rules that do not match this configuration.
  if (!(itgen && itgen->InstallsForConfig(config))) {
    return;
  }

  // Get the target to be installed.
  cmGeneratorTarget* target = itgen->GetTarget();

  // Construct the installed location of the target.
  std::string dest = itgen->GetDestination(config);
  std::string value;
  if (!cmSystemTools::FileIsFullPath(dest)) {
    // The target is installed relative to the installation prefix.
    value = std::string{ this->GetImportPrefixWithSlash() };
  }
  value += dest;
  value += "/";

  if (itgen->IsImportLibrary()) {
    // Construct the property name.
    std::string prop = cmStrCat("IMPORTED_IMPLIB", suffix);

    // Append the installed file name.
    value += cmInstallTargetGenerator::GetInstallFilename(
      target, config, cmInstallTargetGenerator::NameImplibReal);

    // Store the property.
    properties[prop] = value;
    importedLocations.insert(prop);
  } else if (itgen->GetTarget()->GetType() == cmStateEnums::OBJECT_LIBRARY) {
    // Construct the property name.
    std::string prop = cmStrCat("IMPORTED_OBJECTS", suffix);

    // Compute all the object files inside this target and setup
    // IMPORTED_OBJECTS as a list of object files
    std::vector<std::string> objects;
    itgen->GetInstallObjectNames(config, objects);
    for (std::string& obj : objects) {
      obj = cmStrCat(value, obj);
    }

    // Store the property.
    properties[prop] = cmList::to_string(objects);
    importedLocations.insert(prop);
  } else {
    if (target->IsFrameworkOnApple() && target->HasImportLibrary(config)) {
      // store as well IMPLIB value
      auto importProp = cmStrCat("IMPORTED_IMPLIB", suffix);
      auto importValue =
        cmStrCat(value,
                 cmInstallTargetGenerator::GetInstallFilename(
                   target, config, cmInstallTargetGenerator::NameImplibReal));

      // Store the property.
      properties[importProp] = importValue;
      importedLocations.insert(importProp);
    }

    // Construct the property name.
    std::string prop = cmStrCat("IMPORTED_LOCATION", suffix);

    // Append the installed file name.
    if (target->IsAppBundleOnApple()) {
      value += cmInstallTargetGenerator::GetInstallFilename(target, config);
      value += ".app/";
      if (!target->Makefile->PlatformIsAppleEmbedded()) {
        value += "Contents/MacOS/";
      }
      value += cmInstallTargetGenerator::GetInstallFilename(target, config);
    } else {
      value += cmInstallTargetGenerator::GetInstallFilename(
        target, config, cmInstallTargetGenerator::NameReal);
    }

    // Store the property.
    properties[prop] = value;
    importedLocations.insert(prop);
  }
}

cmStateEnums::TargetType cmExportInstallFileGenerator::GetExportTargetType(
  cmTargetExport const* targetExport) const
{
  cmStateEnums::TargetType targetType = targetExport->Target->GetType();
  // An OBJECT library installed with no OBJECTS DESTINATION
  // is transformed to an INTERFACE library.
  if (targetType == cmStateEnums::OBJECT_LIBRARY &&
      !targetExport->ObjectsGenerator) {
    targetType = cmStateEnums::INTERFACE_LIBRARY;
  }
  return targetType;
}

std::string const& cmExportInstallFileGenerator::GetExportName() const
{
  return this->GetExportSet()->GetName();
}

void cmExportInstallFileGenerator::HandleMissingTarget(
  std::string& link_libs, cmGeneratorTarget const* depender,
  cmGeneratorTarget* dependee)
{
  auto const& exportInfo = this->FindExportInfo(dependee);

  if (exportInfo.Namespaces.size() == 1 && exportInfo.Sets.size() == 1) {
    std::string missingTarget = *exportInfo.Namespaces.begin();

    missingTarget += dependee->GetExportName();
    link_libs += missingTarget;
    this->MissingTargets.emplace_back(std::move(missingTarget));
  } else {
    // All exported targets should be known here and should be unique.
    // This is probably user-error.
    this->ComplainAboutMissingTarget(depender, dependee, exportInfo);
  }
}

cmExportFileGenerator::ExportInfo cmExportInstallFileGenerator::FindExportInfo(
  cmGeneratorTarget const* target) const
{
  return target->GetLocalGenerator()
    ->GetGlobalGenerator()
    ->FindInstallExportInfo(target);
}

void cmExportInstallFileGenerator::ComplainAboutMissingTarget(
  cmGeneratorTarget const* depender, cmGeneratorTarget const* dependee,
  ExportInfo const& exportInfo) const
{
  std::ostringstream e;
  e << "install(" << this->IEGen->InstallSubcommand() << " \""
    << this->GetExportName() << "\" ...) " << "includes target \""
    << depender->GetName() << "\" which requires target \""
    << dependee->GetName() << "\" ";
  if (exportInfo.Sets.empty()) {
    e << "that is not in any export set.";
  } else {
    if (exportInfo.Sets.size() == 1) {
      e << "that is not in this export set, but in another export set which "
           "is "
           "exported multiple times with different namespaces: ";
    } else {
      e << "that is not in this export set, but in multiple other export "
           "sets: ";
    }
    e << cmJoin(exportInfo.Files, ", ") << ".\n"
      << "An exported target cannot depend upon another target which is "
         "exported in more than one export set or with more than one "
         "namespace. "
         "Consider consolidating the exports of the \""
      << dependee->GetName() << "\" target to a single export.";
  }
  this->ReportError(e.str());
}

void cmExportInstallFileGenerator::ComplainAboutDuplicateTarget(
  std::string const& targetName) const
{
  std::ostringstream e;
  e << "install(" << this->IEGen->InstallSubcommand() << " \""
    << this->GetExportName() << "\" ...) " << "includes target \""
    << targetName << "\" more than once in the export set.";
  this->ReportError(e.str());
}

void cmExportInstallFileGenerator::IssueMessage(
  MessageType type, std::string const& message) const
{
  cmLocalGenerator const* const lg = this->IEGen->GetLocalGenerator();
  lg->GetMakefile()->IssueMessage(type, message);
}

void cmExportInstallFileGenerator::IssueDiagnostic(
  cmDiagnosticCategory category, std::string const& message) const
{
  cmLocalGenerator const* const lg = this->IEGen->GetLocalGenerator();
  lg->GetMakefile()->IssueDiagnostic(category, message);
}

std::string cmExportInstallFileGenerator::InstallNameDir(
  cmGeneratorTarget const* target, std::string const& config)
{
  std::string install_name_dir;

  cmMakefile* mf = target->Target->GetMakefile();
  if (mf->IsOn("CMAKE_PLATFORM_HAS_INSTALLNAME")) {
    auto const& prefix = this->GetInstallPrefix();
    install_name_dir = target->GetInstallNameDirForInstallTree(config, prefix);
  }

  return install_name_dir;
}

std::string cmExportInstallFileGenerator::GetCxxModuleFile() const
{
  return this->GetCxxModuleFile(this->GetExportSet()->GetName());
}

bool cmExportInstallFileGenerator::CollectExports(
  std::function<void(cmTargetExport const*)> const& visitor)
{
  auto pred = [&](std::unique_ptr<cmTargetExport> const& te) -> bool {
    if (te->NamelinkOnly) {
      return true;
    }
    if (this->ExportedTargets.insert(te->Target).second) {
      visitor(te.get());
      return true;
    }

    this->ComplainAboutDuplicateTarget(te->Target->GetName());
    return false;
  };

  auto const& targets = this->GetExportSet()->GetTargetExports();
  return std::all_of(targets.begin(), targets.end(), pred);
}

bool cmExportInstallFileGenerator::PopulateInterfaceProperties(
  cmTargetExport const* targetExport, ImportPropertyMap& properties)
{
  cmGeneratorTarget const* const gt = targetExport->Target;

  std::string includesDestinationDirs;
  this->PopulateSystemIncludeDirectoriesInterface(
    gt, cmGeneratorExpression::InstallInterface, properties);
  this->PopulateIncludeDirectoriesInterface(
    gt, cmGeneratorExpression::InstallInterface, properties, *targetExport,
    includesDestinationDirs);
  this->PopulateLinkDirectoriesInterface(
    gt, cmGeneratorExpression::InstallInterface, properties);
  this->PopulateLinkDependsInterface(
    gt, cmGeneratorExpression::InstallInterface, properties);
  this->PopulateSourcesInterface(gt, cmGeneratorExpression::InstallInterface,
                                 properties);

  return this->PopulateInterfaceProperties(
    gt, includesDestinationDirs, cmGeneratorExpression::InstallInterface,
    properties);
}

bool cmExportInstallFileGenerator::PopulateFileSetInterfaceProperties(
  cmTargetExport const* targetExport, ImportFileSetPropertyMap& properties)
{
  cmGeneratorTarget const* const gt = targetExport->Target;
  cmGeneratorFileSets const* const gfs = gt->GetGeneratorFileSets();

  bool result = true;

  for (auto const& type : gfs->GetInterfaceFileSetTypes()) {
    for (auto const* fileSet : gfs->GetInterfaceFileSets(type)) {
      ImportPropertyMap& fsProperties = properties[fileSet->GetName()];
      this->PopulateFileSetIncludeDirectoriesInterface(
        gt, fileSet, cmGeneratorExpression::InstallInterface, fsProperties);
      result = result &&
        this->PopulateFileSetInterfaceProperties(
          gt, fileSet, cmGeneratorExpression::InstallInterface, fsProperties);
    }
  }
  return result;
}

namespace {
bool isSubDirectory(std::string const& a, std::string const& b)
{
  return (cmSystemTools::ComparePath(a, b) ||
          cmSystemTools::IsSubDirectory(a, b));
}
}

bool cmExportInstallFileGenerator::CheckInterfaceDirs(
  std::string const& prepro, cmGeneratorTarget const* target,
  std::string const& prop) const
{
  std::string const& installDir =
    target->Makefile->GetSafeDefinition("CMAKE_INSTALL_PREFIX");
  std::string const& topSourceDir =
    target->GetLocalGenerator()->GetSourceDirectory();
  std::string const& topBinaryDir =
    target->GetLocalGenerator()->GetBinaryDirectory();

  std::vector<std::string> parts;
  cmGeneratorExpression::Split(prepro, parts);

  bool const inSourceBuild = topSourceDir == topBinaryDir;

  bool hadFatalError = false;

  for (std::string const& li : parts) {
    size_t genexPos = cmGeneratorExpression::Find(li);
    if (genexPos == 0) {
      continue;
    }
    if (cmHasPrefix(li, this->GetImportPrefixWithSlash())) {
      continue;
    }
    if (genexPos != std::string::npos) {
      hadFatalError = true;
    }
    if (!cmSystemTools::FileIsFullPath(li)) {
      std::ostringstream e;
      /* clang-format off */
      e << "Target \"" << target->GetName() << "\" " << prop <<
           " property contains relative path:\n"
           "  \"" << li << "\"";
      /* clang-format on */
      target->GetLocalGenerator()->IssueMessage(MessageType::FATAL_ERROR,
                                                e.str());
    }
    bool inBinary = isSubDirectory(li, topBinaryDir);
    bool inSource = isSubDirectory(li, topSourceDir);
    if (isSubDirectory(li, installDir)) {
      // The include directory is inside the install tree.  If the
      // install tree is inside the source tree or build tree then do not
      // fall through to the checks below that the include directory is not
      // also inside the source tree or build tree.
      if ((!inBinary || isSubDirectory(installDir, topBinaryDir)) &&
          (!inSource || isSubDirectory(installDir, topSourceDir))) {
        continue;
      }
    }
    if (inBinary) {
      std::ostringstream e;
      /* clang-format off */
      e << "Target \"" << target->GetName() << "\" " << prop <<
           " property contains path:\n"
           "  \"" << li << "\"\nwhich is prefixed in the build directory.";
      /* clang-format on */
      target->GetLocalGenerator()->IssueMessage(MessageType::FATAL_ERROR,
                                                e.str());
    }
    if (!inSourceBuild) {
      if (inSource) {
        std::ostringstream e;
        e << "Target \"" << target->GetName() << "\" " << prop
          << " property contains path:\n"
             "  \""
          << li << "\"\nwhich is prefixed in the source directory.";
        target->GetLocalGenerator()->IssueMessage(MessageType::FATAL_ERROR,
                                                  e.str());
      }
    }
  }
  return !hadFatalError;
}

void cmExportInstallFileGenerator::PopulateSourcesInterface(
  cmGeneratorTarget const* gt,
  cmGeneratorExpression::PreprocessContext preprocessRule,
  ImportPropertyMap& properties)
{
  assert(preprocessRule == cmGeneratorExpression::InstallInterface);

  char const* const propName = "INTERFACE_SOURCES";
  cmValue input = gt->GetProperty(propName);

  if (!input) {
    return;
  }

  if (input->empty()) {
    properties[propName].clear();
    return;
  }

  std::string prepro = cmGeneratorExpression::Preprocess(
    *input, preprocessRule, this->GetImportPrefixWithSlash());
  if (!prepro.empty()) {
    this->ResolveTargetsInGeneratorExpressions(prepro, gt);

    if (!this->CheckInterfaceDirs(prepro, gt, propName)) {
      return;
    }
    properties[propName] = prepro;
  }
}

void cmExportInstallFileGenerator::PopulateSystemIncludeDirectoriesInterface(
  cmGeneratorTarget const* target,
  cmGeneratorExpression::PreprocessContext preprocessRule,
  ImportPropertyMap& properties)
{
  assert(preprocessRule == cmGeneratorExpression::InstallInterface);

  char const* const propName = "INTERFACE_SYSTEM_INCLUDE_DIRECTORIES";
  cmValue input = target->GetProperty(propName);

  if (!input) {
    return;
  }
  if (input->empty()) {
    // Set to empty
    properties[propName].clear();
    return;
  }

  std::string includes = (input ? *input : "");
  std::string prepro = cmGeneratorExpression::Preprocess(
    includes, preprocessRule, this->GetImportPrefixWithSlash());
  if (!prepro.empty()) {
    this->ResolveTargetsInGeneratorExpressions(prepro, target);

    if (!this->CheckInterfaceDirs(prepro, target, propName)) {
      return;
    }
    properties[propName] = prepro;
  }
}

void cmExportInstallFileGenerator::PopulateIncludeDirectoriesInterface(
  cmGeneratorTarget const* target,
  cmGeneratorExpression::PreprocessContext preprocessRule,
  ImportPropertyMap& properties, cmTargetExport const& te,
  std::string& includesDestinationDirs)
{
  assert(preprocessRule == cmGeneratorExpression::InstallInterface);

  includesDestinationDirs.clear();

  char const* const propName = "INTERFACE_INCLUDE_DIRECTORIES";
  cmValue input = target->GetProperty(propName);

  cmGeneratorExpression ge(*target->Makefile->GetCMakeInstance());

  std::string dirs = cmGeneratorExpression::Preprocess(
    cmList::to_string(target->Target->GetInstallIncludeDirectoriesEntries(te)),
    preprocessRule, this->GetImportPrefixWithSlash());
  this->ReplaceInstallPrefix(dirs);
  std::unique_ptr<cmCompiledGeneratorExpression> cge = ge.Parse(dirs);
  std::string exportDirs =
    cge->Evaluate(target->GetLocalGenerator(), "", target);

  if (cge->GetHadContextSensitiveCondition()) {
    cmLocalGenerator* lg = target->GetLocalGenerator();
    std::ostringstream e;
    e << "Target \"" << target->GetName()
      << "\" is installed with "
         "INCLUDES DESTINATION set to a context sensitive path.  Paths which "
         "depend on the configuration, policy values or the link interface "
         "are "
         "not supported.  Consider using target_include_directories instead.";
    lg->IssueMessage(MessageType::FATAL_ERROR, e.str());
    return;
  }

  if (!input && exportDirs.empty()) {
    return;
  }
  if ((input && input->empty()) && exportDirs.empty()) {
    // Set to empty
    properties[propName].clear();
    return;
  }

  this->AddImportPrefix(exportDirs);
  includesDestinationDirs = exportDirs;

  std::string includes = (input ? *input : "");
  char const* const sep = input ? ";" : "";
  includes += sep + exportDirs;
  std::string prepro = cmGeneratorExpression::Preprocess(
    includes, preprocessRule, this->GetImportPrefixWithSlash());
  if (!prepro.empty()) {
    this->ResolveTargetsInGeneratorExpressions(prepro, target);

    if (!this->CheckInterfaceDirs(prepro, target, propName)) {
      return;
    }
    properties[propName] = prepro;
  }
}

void cmExportInstallFileGenerator::PopulateFileSetIncludeDirectoriesInterface(
  cmGeneratorTarget const* target, cmGeneratorFileSet const* fileSet,
  cmGeneratorExpression::PreprocessContext preprocessRule,
  ImportPropertyMap& properties)
{
  assert(preprocessRule == cmGeneratorExpression::InstallInterface);

  char const* const propName = "INTERFACE_INCLUDE_DIRECTORIES";
  cmValue includes = fileSet->GetProperty(propName);

  if (!includes) {
    return;
  }
  if (includes && includes->empty()) {
    // Set to empty
    properties[propName].clear();
    return;
  }

  std::string prepro = cmGeneratorExpression::Preprocess(
    *includes, preprocessRule, this->GetImportPrefixWithSlash());
  if (!prepro.empty()) {
    this->ResolveTargetsInGeneratorExpressions(prepro, target);

    if (!this->CheckInterfaceDirs(prepro, target, propName)) {
      return;
    }
    properties[propName] = prepro;
  }
}

void cmExportInstallFileGenerator::PopulateLinkDependsInterface(
  cmGeneratorTarget const* gt,
  cmGeneratorExpression::PreprocessContext preprocessRule,
  ImportPropertyMap& properties)
{
  assert(preprocessRule == cmGeneratorExpression::InstallInterface);

  char const* const propName = "INTERFACE_LINK_DEPENDS";
  cmValue input = gt->GetProperty(propName);

  if (!input) {
    return;
  }

  if (input->empty()) {
    properties[propName].clear();
    return;
  }

  std::string prepro = cmGeneratorExpression::Preprocess(
    *input, preprocessRule, this->GetImportPrefixWithSlash());
  if (!prepro.empty()) {
    this->ResolveTargetsInGeneratorExpressions(prepro, gt);

    if (!this->CheckInterfaceDirs(prepro, gt, propName)) {
      return;
    }
    properties[propName] = prepro;
  }
}

void cmExportInstallFileGenerator::PopulateLinkDirectoriesInterface(
  cmGeneratorTarget const* gt,
  cmGeneratorExpression::PreprocessContext preprocessRule,
  ImportPropertyMap& properties)
{
  assert(preprocessRule == cmGeneratorExpression::InstallInterface);

  char const* const propName = "INTERFACE_LINK_DIRECTORIES";
  cmValue input = gt->GetProperty(propName);

  if (!input) {
    return;
  }

  if (input->empty()) {
    properties[propName].clear();
    return;
  }

  std::string prepro = cmGeneratorExpression::Preprocess(
    *input, preprocessRule, this->GetImportPrefixWithSlash());
  if (!prepro.empty()) {
    this->ResolveTargetsInGeneratorExpressions(prepro, gt);

    if (!this->CheckInterfaceDirs(prepro, gt, propName)) {
      return;
    }
    properties[propName] = prepro;
  }
}
