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

#include <memory>
#include <ostream>
#include <set>
#include <string>
#include <utility>
#include <vector>

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

#include "cmGeneratorExpression.h"
#include "cmInstallRuntimeDependencySet.h"
#include "cmListFileCache.h"
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmOutputConverter.h"
#include "cmPolicies.h"
#include "cmScriptGenerator.h"
#include "cmStringAlgorithms.h"

namespace {
template <typename T, typename F>
void WriteMultiArgument(std::ostream& os, cm::string_view keyword,
                        std::vector<T> const& list,
                        cmScriptGeneratorIndent indent, F transform)
{
  bool first = true;
  for (auto const& item : list) {
    cm::optional<std::string> result = transform(item);
    if (result) {
      if (first) {
        os << indent << "  " << keyword << "\n";
        first = false;
      }
      os << indent << "    " << *result << "\n";
    }
  }
}

void WriteFilesArgument(
  std::ostream& os, cm::string_view keyword,
  std::vector<std::unique_ptr<cmInstallRuntimeDependencySet::Item>> const&
    items,
  std::string const& config, cmScriptGeneratorIndent indent)
{
  WriteMultiArgument(
    os, keyword, items, indent,
    [config](std::unique_ptr<cmInstallRuntimeDependencySet::Item> const& i)
      -> std::string { return cmStrCat('"', i->GetItemPath(config), '"'); });
}

void WriteGenexEvaluatorArgument(std::ostream& os, cm::string_view keyword,
                                 std::vector<std::string> const& genexes,
                                 std::string const& config,
                                 cmLocalGenerator* lg,
                                 cmScriptGeneratorIndent indent)
{
  WriteMultiArgument(
    os, keyword, genexes, indent,
    [config, lg](std::string const& genex) -> cm::optional<std::string> {
      std::string result = cmGeneratorExpression::Evaluate(genex, lg, config);
      if (result.empty()) {
        return cm::nullopt;
      }
      return cmOutputConverter::EscapeForCMake(result);
    });
}
}

cmInstallGetRuntimeDependenciesGenerator::
  cmInstallGetRuntimeDependenciesGenerator(
    cmInstallRuntimeDependencySet* runtimeDependencySet,
    std::vector<std::string> directories,
    std::vector<std::string> preIncludeRegexes,
    std::vector<std::string> preExcludeRegexes,
    std::vector<std::string> postIncludeRegexes,
    std::vector<std::string> postExcludeRegexes,
    std::vector<std::string> postIncludeFiles,
    std::vector<std::string> postExcludeFiles, std::string libraryComponent,
    std::string frameworkComponent, bool noInstallRPath, char const* depsVar,
    char const* rpathPrefix, std::vector<std::string> const& configurations,
    MessageLevel message, bool exclude_from_all, cmListFileBacktrace backtrace,
    cmPolicies::PolicyStatus policyStatusCMP0207)
  : cmInstallGenerator("", configurations, "", message, exclude_from_all,
                       false, std::move(backtrace))
  , RuntimeDependencySet(runtimeDependencySet)
  , Directories(std::move(directories))
  , PreIncludeRegexes(std::move(preIncludeRegexes))
  , PreExcludeRegexes(std::move(preExcludeRegexes))
  , PostIncludeRegexes(std::move(postIncludeRegexes))
  , PostExcludeRegexes(std::move(postExcludeRegexes))
  , PostIncludeFiles(std::move(postIncludeFiles))
  , PostExcludeFiles(std::move(postExcludeFiles))
  , LibraryComponent(std::move(libraryComponent))
  , FrameworkComponent(std::move(frameworkComponent))
  , PolicyStatusCMP0207(policyStatusCMP0207)
  , NoInstallRPath(noInstallRPath)
  , DepsVar(depsVar)
  , RPathPrefix(rpathPrefix)
{
  this->ActionsPerConfig = true;
}

bool cmInstallGetRuntimeDependenciesGenerator::Compute(cmLocalGenerator* lg)
{
  this->LocalGenerator = lg;
  return true;
}

void cmInstallGetRuntimeDependenciesGenerator::GenerateScript(std::ostream& os)
{
  // Track indentation.
  Indent indent;

  // Begin this block of installation.
  os << indent << "if(";
  if (this->FrameworkComponent.empty() ||
      this->FrameworkComponent == this->LibraryComponent) {
    os << this->CreateComponentTest(this->LibraryComponent,
                                    this->ExcludeFromAll);
  } else {
    os << this->CreateComponentTest(this->LibraryComponent, true) << " OR "
       << this->CreateComponentTest(this->FrameworkComponent,
                                    this->ExcludeFromAll);
  }
  os << ")\n";

  // Generate the script possibly with per-configuration code.
  this->GenerateScriptConfigs(os, indent.Next());

  // End this block of installation.
  os << indent << "endif()\n\n";
}

void cmInstallGetRuntimeDependenciesGenerator::GenerateScriptForConfig(
  std::ostream& os, std::string const& config, Indent indent)
{
  std::string installNameTool =
    this->LocalGenerator->GetMakefile()->GetSafeDefinition(
      "CMAKE_INSTALL_NAME_TOOL");

  Indent inputIndent = indent;
  if (this->PolicyStatusCMP0207 != cmPolicies::WARN) {
    indent = indent.Next();
    os << inputIndent << "block(SCOPE_FOR POLICIES)\n"
       << indent << "cmake_policy(SET CMP0207 "
       << (this->PolicyStatusCMP0207 == cmPolicies::NEW ? "NEW" : "OLD")
       << ")\n";
  }
  os << indent << "file(GET_RUNTIME_DEPENDENCIES\n"
     << indent << "  RESOLVED_DEPENDENCIES_VAR " << this->DepsVar << '\n';
  WriteFilesArgument(os, "EXECUTABLES"_s,
                     this->RuntimeDependencySet->GetExecutables(), config,
                     indent);
  WriteFilesArgument(os, "LIBRARIES"_s,
                     this->RuntimeDependencySet->GetLibraries(), config,
                     indent);
  WriteFilesArgument(os, "MODULES"_s, this->RuntimeDependencySet->GetModules(),
                     config, indent);
  if (this->RuntimeDependencySet->GetBundleExecutable()) {
    os << indent << "  BUNDLE_EXECUTABLE \""
       << this->RuntimeDependencySet->GetBundleExecutable()->GetItemPath(
            config)
       << "\"\n";
  }
  WriteGenexEvaluatorArgument(os, "DIRECTORIES"_s, this->Directories, config,
                              this->LocalGenerator, indent);
  WriteGenexEvaluatorArgument(os, "PRE_INCLUDE_REGEXES"_s,
                              this->PreIncludeRegexes, config,
                              this->LocalGenerator, indent);
  WriteGenexEvaluatorArgument(os, "PRE_EXCLUDE_REGEXES"_s,
                              this->PreExcludeRegexes, config,
                              this->LocalGenerator, indent);
  WriteGenexEvaluatorArgument(os, "POST_INCLUDE_REGEXES"_s,
                              this->PostIncludeRegexes, config,
                              this->LocalGenerator, indent);
  WriteGenexEvaluatorArgument(os, "POST_EXCLUDE_REGEXES"_s,
                              this->PostExcludeRegexes, config,
                              this->LocalGenerator, indent);
  WriteGenexEvaluatorArgument(os, "POST_INCLUDE_FILES"_s,
                              this->PostIncludeFiles, config,
                              this->LocalGenerator, indent);
  WriteGenexEvaluatorArgument(os, "POST_EXCLUDE_FILES"_s,
                              this->PostExcludeFiles, config,
                              this->LocalGenerator, indent);

  std::set<std::string> postExcludeFiles;
  auto const addPostExclude =
    [config, &postExcludeFiles, this](
      std::vector<std::unique_ptr<cmInstallRuntimeDependencySet::Item>> const&
        tgts) {
      for (auto const& item : tgts) {
        item->AddPostExcludeFiles(config, postExcludeFiles,
                                  this->RuntimeDependencySet);
      }
    };
  addPostExclude(this->RuntimeDependencySet->GetExecutables());
  addPostExclude(this->RuntimeDependencySet->GetLibraries());
  addPostExclude(this->RuntimeDependencySet->GetModules());
  bool first = true;
  for (auto const& file : postExcludeFiles) {
    if (first) {
      os << indent << "  POST_EXCLUDE_FILES_STRICT\n";
      first = false;
    }
    os << indent << "    \"" << file << "\"\n";
  }

  if (!installNameTool.empty() && !this->NoInstallRPath) {
    os << indent << "  RPATH_PREFIX " << this->RPathPrefix << '\n';
  }
  os << indent << "  )\n";

  if (this->PolicyStatusCMP0207 != cmPolicies::WARN) {
    os << inputIndent << "endblock()\n";
  }
}
