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

#include "cmCPackInnoSetupGenerator.h"

#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <ostream>
#include <utility>

#include "cmsys/RegularExpression.hxx"

#include "cmCPackComponentGroup.h"
#include "cmCPackLog.h"
#include "cmDuration.h"
#include "cmGeneratedFileStream.h"
#include "cmList.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

cmCPackInnoSetupGenerator::cmCPackInnoSetupGenerator() = default;
cmCPackInnoSetupGenerator::~cmCPackInnoSetupGenerator() = default;

bool cmCPackInnoSetupGenerator::CanGenerate()
{
  return true;
}

int cmCPackInnoSetupGenerator::InitializeInternal()
{
  if (GetOption("CPACK_INCLUDE_TOPLEVEL_DIRECTORY").IsOn()) {
    cmCPackLogger(cmCPackLog::LOG_WARNING,
                  "Inno Setup Generator cannot work with "
                  "CPACK_INCLUDE_TOPLEVEL_DIRECTORY set. "
                  "This option will be reset to 0 (for this generator only)."
                    << std::endl);
    SetOption("CPACK_INCLUDE_TOPLEVEL_DIRECTORY", nullptr);
  }

  std::vector<std::string> path;

#ifdef _WIN32
  path.push_back("C:\\Program Files (x86)\\Inno Setup 5");
  path.push_back("C:\\Program Files (x86)\\Inno Setup 6");
#endif

  SetOptionIfNotSet("CPACK_INNOSETUP_EXECUTABLE", "ISCC");
  const std::string& isccPath = cmSystemTools::FindProgram(
    GetOption("CPACK_INNOSETUP_EXECUTABLE"), path, false);

  if (isccPath.empty()) {
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "Cannot find Inno Setup compiler ISCC: "
                  "likely it is not installed, or not in your PATH"
                    << std::endl);

    return 0;
  }

  const std::string isccCmd =
    cmStrCat(QuotePath(isccPath, PathType::Native), "/?");
  cmCPackLogger(cmCPackLog::LOG_VERBOSE,
                "Test Inno Setup version: " << isccCmd << std::endl);
  std::string output;
  cmSystemTools::RunSingleCommand(isccCmd, &output, &output, nullptr, nullptr,
                                  this->GeneratorVerbose, cmDuration::zero());
  cmsys::RegularExpression vRex("Inno Setup ([0-9]+)");
  if (!vRex.find(output)) {
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "Problem checking Inno Setup version with command: "
                    << isccCmd << std::endl
                    << "Have you downloaded Inno Setup from "
                       "https://jrsoftware.org/isinfo.php?"
                    << std::endl);
    return 0;
  }

  const int isccVersion = atoi(vRex.match(1).c_str());
  const int minIsccVersion = 6;
  cmCPackLogger(cmCPackLog::LOG_DEBUG,
                "Inno Setup Version: " << isccVersion << std::endl);

  if (isccVersion < minIsccVersion) {
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "CPack requires Inno Setup Version 6 or greater. "
                  "Inno Setup found on the system was: "
                    << isccVersion << std::endl);
    return 0;
  }

  SetOption("CPACK_INSTALLER_PROGRAM", isccPath);

  return this->Superclass::InitializeInternal();
}

int cmCPackInnoSetupGenerator::PackageFiles()
{
  // Includes
  if (IsSet("CPACK_INNOSETUP_EXTRA_SCRIPTS")) {
    const cmList extraScripts(GetOption("CPACK_INNOSETUP_EXTRA_SCRIPTS"));

    for (const std::string& i : extraScripts) {
      includeDirectives.emplace_back(cmStrCat(
        "#include ", QuotePath(cmSystemTools::CollapseFullPath(i, toplevel))));
    }
  }

  // [Languages] section
  SetOptionIfNotSet("CPACK_INNOSETUP_LANGUAGES", "english");
  const cmList languages(GetOption("CPACK_INNOSETUP_LANGUAGES"));
  for (std::string i : languages) {
    cmCPackInnoSetupKeyValuePairs params;

    params["Name"] = Quote(i);

    if (cmSystemTools::LowerCase(i) == "english") {
      params["MessagesFile"] = "\"compiler:Default.isl\"";
    } else {
      i[0] = static_cast<char>(std::toupper(i[0]));
      params["MessagesFile"] = cmStrCat("\"compiler:Languages\\", i, ".isl\"");
    }

    languageInstructions.push_back(ISKeyValueLine(params));
  }

  if (!Components.empty() && !ProcessComponents()) {
    return false;
  }

  if (!(ProcessSetupSection() && ProcessFiles())) {
    return false;
  }

  // [Code] section
  if (IsSet("CPACK_INNOSETUP_CODE_FILES")) {
    const cmList codeFiles(GetOption("CPACK_INNOSETUP_CODE_FILES"));

    for (const std::string& i : codeFiles) {
      codeIncludes.emplace_back(cmStrCat(
        "#include ", QuotePath(cmSystemTools::CollapseFullPath(i, toplevel))));
    }
  }

  return ConfigureISScript() && Compile();
}

bool cmCPackInnoSetupGenerator::ProcessSetupSection()
{
  if (!RequireOption("CPACK_PACKAGE_INSTALL_REGISTRY_KEY")) {
    return false;
  }
  setupDirectives["AppId"] = GetOption("CPACK_PACKAGE_INSTALL_REGISTRY_KEY");

  if (!RequireOption("CPACK_PACKAGE_NAME")) {
    return false;
  }
  setupDirectives["AppName"] = GetOption("CPACK_PACKAGE_NAME");
  setupDirectives["UninstallDisplayName"] = GetOption("CPACK_PACKAGE_NAME");

  if (!RequireOption("CPACK_PACKAGE_VERSION")) {
    return false;
  }
  setupDirectives["AppVersion"] = GetOption("CPACK_PACKAGE_VERSION");

  if (!RequireOption("CPACK_PACKAGE_VENDOR")) {
    return false;
  }
  setupDirectives["AppPublisher"] = GetOption("CPACK_PACKAGE_VENDOR");

  if (IsSet("CPACK_PACKAGE_HOMEPAGE_URL")) {
    setupDirectives["AppPublisherURL"] =
      GetOption("CPACK_PACKAGE_HOMEPAGE_URL");
    setupDirectives["AppSupportURL"] = GetOption("CPACK_PACKAGE_HOMEPAGE_URL");
    setupDirectives["AppUpdatesURL"] = GetOption("CPACK_PACKAGE_HOMEPAGE_URL");
  }

  SetOptionIfNotSet("CPACK_INNOSETUP_IGNORE_LICENSE_PAGE", "OFF");
  if (IsSet("CPACK_RESOURCE_FILE_LICENSE") &&
      !GetOption("CPACK_INNOSETUP_IGNORE_LICENSE_PAGE").IsOn()) {
    setupDirectives["LicenseFile"] = cmSystemTools::ConvertToWindowsOutputPath(
      GetOption("CPACK_RESOURCE_FILE_LICENSE"));
  }

  SetOptionIfNotSet("CPACK_INNOSETUP_IGNORE_README_PAGE", "ON");
  if (IsSet("CPACK_RESOURCE_FILE_README") &&
      !GetOption("CPACK_INNOSETUP_IGNORE_README_PAGE").IsOn()) {
    setupDirectives["InfoBeforeFile"] =
      cmSystemTools::ConvertToWindowsOutputPath(
        GetOption("CPACK_RESOURCE_FILE_README"));
  }

  SetOptionIfNotSet("CPACK_INNOSETUP_USE_MODERN_WIZARD", "OFF");
  if (GetOption("CPACK_INNOSETUP_USE_MODERN_WIZARD").IsOn()) {
    setupDirectives["WizardStyle"] = "modern";
  } else {
    setupDirectives["WizardStyle"] = "classic";
    setupDirectives["WizardSmallImageFile"] =
      "compiler:WizClassicSmallImage.bmp";
    setupDirectives["WizardImageFile"] = "compiler:WizClassicImage.bmp";
    setupDirectives["SetupIconFile"] = "compiler:SetupClassicIcon.ico";
  }

  if (IsSet("CPACK_INNOSETUP_ICON_FILE")) {
    setupDirectives["SetupIconFile"] =
      cmSystemTools::ConvertToWindowsOutputPath(
        GetOption("CPACK_INNOSETUP_ICON_FILE"));
  }

  if (IsSet("CPACK_PACKAGE_ICON")) {
    setupDirectives["WizardSmallImageFile"] =
      cmSystemTools::ConvertToWindowsOutputPath(
        GetOption("CPACK_PACKAGE_ICON"));
  }

  if (!RequireOption("CPACK_PACKAGE_INSTALL_DIRECTORY")) {
    return false;
  }
  SetOptionIfNotSet("CPACK_INNOSETUP_INSTALL_ROOT", "{autopf}");
  setupDirectives["DefaultDirName"] =
    cmSystemTools::ConvertToWindowsOutputPath(
      cmStrCat(GetOption("CPACK_INNOSETUP_INSTALL_ROOT"), '\\',
               GetOption("CPACK_PACKAGE_INSTALL_DIRECTORY")));

  SetOptionIfNotSet("CPACK_INNOSETUP_ALLOW_CUSTOM_DIRECTORY", "ON");
  if (GetOption("CPACK_INNOSETUP_ALLOW_CUSTOM_DIRECTORY").IsOff()) {
    setupDirectives["DisableDirPage"] = "yes";
  }

  SetOptionIfNotSet("CPACK_INNOSETUP_PROGRAM_MENU_FOLDER",
                    GetOption("CPACK_PACKAGE_NAME"));
  if (GetOption("CPACK_INNOSETUP_PROGRAM_MENU_FOLDER") == ".") {
    setupDirectives["DisableProgramGroupPage"] = "yes";
    toplevelProgramFolder = true;
  } else {
    setupDirectives["DefaultGroupName"] =
      GetOption("CPACK_INNOSETUP_PROGRAM_MENU_FOLDER");
    toplevelProgramFolder = false;
  }

  if (IsSet("CPACK_INNOSETUP_PASSWORD")) {
    setupDirectives["Password"] = GetOption("CPACK_INNOSETUP_PASSWORD");
    setupDirectives["Encryption"] = "yes";
  }

  /*
   * These directives can only be modified using the
   * CPACK_INNOSETUP_SETUP_<directive> variables
   */
  setupDirectives["ShowLanguageDialog"] = "auto";
  setupDirectives["AllowNoIcons"] = "yes";
  setupDirectives["Compression"] = "lzma";
  setupDirectives["SolidCompression"] = "yes";

  // Output file and directory
  if (!RequireOption("CPACK_PACKAGE_FILE_NAME")) {
    return false;
  }
  setupDirectives["OutputBaseFilename"] = GetOption("CPACK_PACKAGE_FILE_NAME");

  if (!RequireOption("CPACK_TOPLEVEL_DIRECTORY")) {
    return false;
  }
  setupDirectives["OutputDir"] = cmSystemTools::ConvertToWindowsOutputPath(
    GetOption("CPACK_TOPLEVEL_DIRECTORY"));

  setupDirectives["SourceDir"] =
    cmSystemTools::ConvertToWindowsOutputPath(toplevel);

  // Target architecture
  if (!RequireOption("CPACK_INNOSETUP_ARCHITECTURE")) {
    return false;
  }

  cmValue const architecture = GetOption("CPACK_INNOSETUP_ARCHITECTURE");
  if (architecture != "x86" && architecture != "x64" &&
      architecture != "arm64" && architecture != "ia64") {
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "CPACK_INNOSETUP_ARCHITECTURE must be either x86, x64, "
                  "arm64 or ia64"
                    << std::endl);
    return false;
  }

  // The following directives must not be set to target x86
  if (architecture != "x86") {
    setupDirectives["ArchitecturesAllowed"] = architecture;
    setupDirectives["ArchitecturesInstallIn64BitMode"] = architecture;
  }

  /*
   * Handle custom directives (they have higher priority than other variables,
   * so they have to be processed after all other variables)
   */
  for (const std::string& i : GetOptions()) {
    if (cmHasPrefix(i, "CPACK_INNOSETUP_SETUP_")) {
      const std::string& directive =
        i.substr(cmStrLen("CPACK_INNOSETUP_SETUP_"));
      setupDirectives[directive] = GetOption(i);
    }
  }

  return true;
}

bool cmCPackInnoSetupGenerator::ProcessFiles()
{
  std::map<std::string, std::string> customFileInstructions;
  if (IsSet("CPACK_INNOSETUP_CUSTOM_INSTALL_INSTRUCTIONS")) {
    const cmList instructions(
      GetOption("CPACK_INNOSETUP_CUSTOM_INSTALL_INSTRUCTIONS"));
    if (instructions.size() % 2 != 0) {
      cmCPackLogger(cmCPackLog::LOG_ERROR,
                    "CPACK_INNOSETUP_CUSTOM_INSTALL_INSTRUCTIONS should "
                    "contain pairs of <path> and <instruction>"
                      << std::endl);
      return false;
    }

    for (auto it = instructions.begin(); it != instructions.end(); ++it) {
      const std::string& key =
        QuotePath(cmSystemTools::CollapseFullPath(*it, toplevel));
      customFileInstructions[key] = *(++it);
    }
  }

  const std::string& iconsPrefix =
    toplevelProgramFolder ? "{autoprograms}\\" : "{group}\\";

  std::map<std::string, std::string> icons;
  if (IsSet("CPACK_PACKAGE_EXECUTABLES")) {
    const cmList executables(GetOption("CPACK_PACKAGE_EXECUTABLES"));
    if (executables.size() % 2 != 0) {
      cmCPackLogger(cmCPackLog::LOG_ERROR,
                    "CPACK_PACKAGE_EXECUTABLES should should contain pairs of "
                    "<executable> and <text label>"
                      << std::endl);
      return false;
    }

    for (auto it = executables.begin(); it != executables.end(); ++it) {
      const std::string& key = *it;
      icons[key] = *(++it);
    }
  }

  std::vector<std::string> desktopIcons;
  if (IsSet("CPACK_CREATE_DESKTOP_LINKS")) {
    cmExpandList(GetOption("CPACK_CREATE_DESKTOP_LINKS"), desktopIcons);
  }

  std::vector<std::string> runExecutables;
  if (IsSet("CPACK_INNOSETUP_RUN_EXECUTABLES")) {
    cmExpandList(GetOption("CPACK_INNOSETUP_RUN_EXECUTABLES"), runExecutables);
  }

  for (const std::string& i : files) {
    cmCPackInnoSetupKeyValuePairs params;

    std::string toplevelDirectory;
    std::string outputDir;
    cmCPackComponent* component = nullptr;
    std::string componentParam;
    if (!Components.empty()) {
      const std::string& fileName = cmSystemTools::RelativePath(toplevel, i);
      const std::string::size_type pos = fileName.find('/');

      // Use the custom component install directory if we have one
      if (pos != std::string::npos) {
        const std::string& componentName = fileName.substr(0, pos);
        component = &Components[componentName];

        toplevelDirectory =
          cmSystemTools::CollapseFullPath(componentName, toplevel);
        outputDir = CustomComponentInstallDirectory(component);
        componentParam =
          CreateRecursiveComponentPath(component->Group, component->Name);

        if (component->IsHidden && component->IsDisabledByDefault) {
          continue;
        }

        if (component->IsHidden) {
          componentParam.clear();
        }
      } else {
        // Don't install component directories
        continue;
      }
    } else {
      toplevelDirectory = toplevel;
      outputDir = "{app}";
    }

    if (!componentParam.empty()) {
      params["Components"] = componentParam;
    }

    if (cmSystemTools::FileIsDirectory(i)) {
      // Custom instructions replace the automatic generated instructions
      if (customFileInstructions.count(QuotePath(i))) {
        dirInstructions.push_back(customFileInstructions[QuotePath(i)]);
      } else {
        std::string destDir = cmSystemTools::ConvertToWindowsOutputPath(
          cmStrCat(outputDir, '\\',
                   cmSystemTools::RelativePath(toplevelDirectory, i)));
        cmStripSuffixIfExists(destDir, '\\');

        params["Name"] = QuotePath(destDir);

        dirInstructions.push_back(ISKeyValueLine(params));
      }
    } else {
      // Custom instructions replace the automatic generated instructions
      if (customFileInstructions.count(QuotePath(i))) {
        fileInstructions.push_back(customFileInstructions[QuotePath(i)]);
      } else {
        std::string destDir = cmSystemTools::ConvertToWindowsOutputPath(
          cmStrCat(outputDir, '\\',
                   cmSystemTools::GetParentDirectory(
                     cmSystemTools::RelativePath(toplevelDirectory, i))));
        cmStripSuffixIfExists(destDir, '\\');

        params["DestDir"] = QuotePath(destDir);

        if (component != nullptr && component->IsDownloaded) {
          const std::string& archiveName =
            cmSystemTools::GetFilenameWithoutLastExtension(
              component->ArchiveFile);
          const std::string& relativePath =
            cmSystemTools::RelativePath(toplevelDirectory, i);

          params["Source"] =
            QuotePath(cmStrCat("{tmp}\\", archiveName, '\\', relativePath));
          params["ExternalSize"] =
            std::to_string(cmSystemTools::FileLength(i));
          params["Flags"] = "external ignoreversion";
          params["BeforeInstall"] =
            cmStrCat("CPackExtractFile('", archiveName, "', '",
                     cmRemoveQuotes(cmSystemTools::ConvertToWindowsOutputPath(
                       relativePath)),
                     "')");
        } else {
          params["Source"] = QuotePath(i);
          params["Flags"] = "ignoreversion";
        }

        fileInstructions.push_back(ISKeyValueLine(params));

        // Icon
        const std::string& name =
          cmSystemTools::GetFilenameWithoutLastExtension(i);
        const std::string& extension =
          cmSystemTools::GetFilenameLastExtension(i);
        if ((extension == ".exe" || extension == ".com") && // only .exe, .com
            icons.count(name)) {
          cmCPackInnoSetupKeyValuePairs iconParams;

          iconParams["Name"] = QuotePath(cmStrCat(iconsPrefix, icons[name]));
          iconParams["Filename"] =
            QuotePath(cmStrCat(destDir, '\\', name, extension));

          if (!componentParam.empty()) {
            iconParams["Components"] = componentParam;
          }

          iconInstructions.push_back(ISKeyValueLine(iconParams));

          // Desktop icon
          if (std::find(desktopIcons.begin(), desktopIcons.end(), name) !=
              desktopIcons.end()) {
            iconParams["Name"] =
              QuotePath(cmStrCat("{autodesktop}\\", icons[name]));
            iconParams["Tasks"] = "desktopicon";

            if (!componentParam.empty() &&
                std::find(desktopIconComponents.begin(),
                          desktopIconComponents.end(),
                          componentParam) == desktopIconComponents.end()) {
              desktopIconComponents.push_back(componentParam);
            }
            iconInstructions.push_back(ISKeyValueLine(iconParams));
          }

          // [Run] section
          if (std::find(runExecutables.begin(), runExecutables.end(), name) !=
              runExecutables.end()) {
            cmCPackInnoSetupKeyValuePairs runParams;

            runParams["Filename"] = iconParams["Filename"];
            runParams["Description"] = cmStrCat(
              "\"{cm:LaunchProgram,", PrepareForConstant(icons[name]), "}\"");
            runParams["Flags"] = "nowait postinstall skipifsilent";

            if (!componentParam.empty()) {
              runParams["Components"] = componentParam;
            }

            runInstructions.push_back(ISKeyValueLine(runParams));
          }
        }
      }
    }
  }

  // Additional icons
  static cmsys::RegularExpression urlRegex(
    "^(mailto:|(ftps?|https?|news)://).*$");

  if (IsSet("CPACK_INNOSETUP_MENU_LINKS")) {
    const cmList menuIcons(GetOption("CPACK_INNOSETUP_MENU_LINKS"));
    if (menuIcons.size() % 2 != 0) {
      cmCPackLogger(cmCPackLog::LOG_ERROR,
                    "CPACK_INNOSETUP_MENU_LINKS should "
                    "contain pairs of <shortcut target> and <shortcut label>"
                      << std::endl);
      return false;
    }

    for (auto it = menuIcons.begin(); it != menuIcons.end(); ++it) {
      const std::string& target = *it;
      const std::string& label = *(++it);
      cmCPackInnoSetupKeyValuePairs params;

      params["Name"] = QuotePath(cmStrCat(iconsPrefix, label));
      if (urlRegex.find(target)) {
        params["Filename"] = Quote(target);
      } else {
        std::string dir = "{app}";
        std::string componentName;
        for (const auto& i : Components) {
          if (cmSystemTools::FileExists(cmSystemTools::CollapseFullPath(
                cmStrCat(i.second.Name, '\\', target), toplevel))) {
            dir = CustomComponentInstallDirectory(&i.second);
            componentName =
              CreateRecursiveComponentPath(i.second.Group, i.second.Name);

            if (i.second.IsHidden && i.second.IsDisabledByDefault) {
              goto continueOuterLoop;
            } else if (i.second.IsHidden) {
              componentName.clear();
            }

            break;
          }
        }

        params["Filename"] = QuotePath(cmStrCat(dir, '\\', target));

        if (!componentName.empty()) {
          params["Components"] = componentName;
        }
      }

      iconInstructions.push_back(ISKeyValueLine(params));
    continueOuterLoop:;
    }
  }

  SetOptionIfNotSet("CPACK_INNOSETUP_CREATE_UNINSTALL_LINK", "OFF");
  if (GetOption("CPACK_INNOSETUP_CREATE_UNINSTALL_LINK").IsOn()) {
    cmCPackInnoSetupKeyValuePairs params;

    params["Name"] = QuotePath(
      cmStrCat(iconsPrefix, "{cm:UninstallProgram,",
               PrepareForConstant(GetOption("CPACK_PACKAGE_NAME")), '}'));
    params["Filename"] = "\"{uninstallexe}\"";

    iconInstructions.push_back(ISKeyValueLine(params));
  }

  return true;
}

bool cmCPackInnoSetupGenerator::ProcessComponents()
{
  codeIncludes.emplace_back(
    "{ The following lines are required by CPack because "
    "this script uses components }");

  // Installation types
  std::vector<cmCPackInstallationType*> types(InstallationTypes.size());
  for (auto& i : InstallationTypes) {
    types[i.second.Index - 1] = &i.second;
  }

  std::vector<std::string> allTypes; // For required components
  for (cmCPackInstallationType* i : types) {
    cmCPackInnoSetupKeyValuePairs params;

    params["Name"] = Quote(i->Name);
    params["Description"] = Quote(i->DisplayName);

    allTypes.push_back(i->Name);
    typeInstructions.push_back(ISKeyValueLine(params));
  }

  // Inno Setup requires the additional "custom" type
  cmCPackInnoSetupKeyValuePairs customTypeParams;

  customTypeParams["Name"] = "\"custom\"";
  customTypeParams["Description"] =
    "\"{code:CPackGetCustomInstallationMessage}\"";
  customTypeParams["Flags"] = "iscustom";

  allTypes.emplace_back("custom");
  typeInstructions.push_back(ISKeyValueLine(customTypeParams));

  // Components
  std::vector<cmCPackComponent*> downloadedComponents;
  for (auto& i : Components) {
    cmCPackInnoSetupKeyValuePairs params;
    cmCPackComponent* component = &i.second;

    if (component->IsHidden) {
      continue;
    }

    CreateRecursiveComponentGroups(component->Group);

    params["Name"] =
      Quote(CreateRecursiveComponentPath(component->Group, component->Name));
    params["Description"] = Quote(component->DisplayName);

    if (component->IsRequired) {
      params["Types"] = cmJoin(allTypes, " ");
      params["Flags"] = "fixed";
    } else if (!component->InstallationTypes.empty()) {
      std::vector<std::string> installationTypes;

      installationTypes.reserve(component->InstallationTypes.size());
      for (cmCPackInstallationType* j : component->InstallationTypes) {
        installationTypes.push_back(j->Name);
      }

      params["Types"] = cmJoin(installationTypes, " ");
    }

    componentInstructions.push_back(ISKeyValueLine(params));

    if (component->IsDownloaded) {
      downloadedComponents.push_back(component);

      if (component->ArchiveFile.empty()) {
        // Compute the name of the archive.
        if (!RequireOption("CPACK_TEMPORARY_DIRECTORY")) {
          return false;
        }

        std::string packagesDir =
          cmStrCat(GetOption("CPACK_TEMPORARY_DIRECTORY"), ".dummy");
        component->ArchiveFile =
          cmStrCat(cmSystemTools::GetFilenameWithoutLastExtension(packagesDir),
                   '-', component->Name, ".zip");
      } else if (!cmHasSuffix(component->ArchiveFile, ".zip")) {
        component->ArchiveFile = cmStrCat(component->ArchiveFile, ".zip");
      }
    }
  }

  // Downloaded components
  if (!downloadedComponents.empty()) {
    // Create the directory for the upload area
    cmValue userUploadDirectory = GetOption("CPACK_UPLOAD_DIRECTORY");
    std::string uploadDirectory;
    if (cmNonempty(userUploadDirectory)) {
      uploadDirectory = *userUploadDirectory;
    } else {
      if (!RequireOption("CPACK_PACKAGE_DIRECTORY")) {
        return false;
      }

      uploadDirectory =
        cmStrCat(GetOption("CPACK_PACKAGE_DIRECTORY"), "/CPackUploads");
    }

    if (!cmSystemTools::FileExists(uploadDirectory)) {
      if (!cmSystemTools::MakeDirectory(uploadDirectory)) {
        cmCPackLogger(cmCPackLog::LOG_ERROR,
                      "Unable to create Inno Setup upload directory "
                        << uploadDirectory << std::endl);
        return false;
      }
    }

    if (!RequireOption("CPACK_DOWNLOAD_SITE")) {
      return false;
    }

    SetOptionIfNotSet("CPACK_INNOSETUP_VERIFY_DOWNLOADS", "ON");
    const bool verifyDownloads =
      GetOption("CPACK_INNOSETUP_VERIFY_DOWNLOADS").IsOn();

    const std::string& urlPrefix =
      cmHasSuffix(GetOption("CPACK_DOWNLOAD_SITE").GetCStr(), '/')
      ? GetOption("CPACK_DOWNLOAD_SITE")
      : cmStrCat(GetOption("CPACK_DOWNLOAD_SITE"), '/');

    std::vector<std::string> archiveUrls;
    std::vector<std::string> archiveFiles;
    std::vector<std::string> archiveHashes;
    std::vector<std::string> archiveComponents;
    for (cmCPackComponent* i : downloadedComponents) {
      std::string hash;
      if (!BuildDownloadedComponentArchive(
            i, uploadDirectory, (verifyDownloads ? &hash : nullptr))) {
        return false;
      }

      archiveUrls.push_back(Quote(cmStrCat(urlPrefix, i->ArchiveFile)));
      archiveFiles.push_back(
        Quote(cmSystemTools::GetFilenameWithoutLastExtension(i->ArchiveFile)));
      archiveHashes.push_back(Quote(hash));
      archiveComponents.push_back(
        Quote(CreateRecursiveComponentPath(i->Group, i->Name)));
    }

    SetOption("CPACK_INNOSETUP_DOWNLOAD_COUNT_INTERNAL",
              std::to_string(archiveFiles.size()));
    SetOption("CPACK_INNOSETUP_DOWNLOAD_URLS_INTERNAL",
              cmJoin(archiveUrls, ", "));
    SetOption("CPACK_INNOSETUP_DOWNLOAD_ARCHIVES_INTERNAL",
              cmJoin(archiveFiles, ", "));
    SetOption("CPACK_INNOSETUP_DOWNLOAD_HASHES_INTERNAL",
              cmJoin(archiveHashes, ", "));
    SetOption("CPACK_INNOSETUP_DOWNLOAD_COMPONENTS_INTERNAL",
              cmJoin(archiveComponents, ", "));

    static const std::string& downloadLines =
      "#define protected CPackDownloadCount "
      "@CPACK_INNOSETUP_DOWNLOAD_COUNT_INTERNAL@\n"
      "#dim protected CPackDownloadUrls[CPackDownloadCount] "
      "{@CPACK_INNOSETUP_DOWNLOAD_URLS_INTERNAL@}\n"
      "#dim protected CPackDownloadArchives[CPackDownloadCount] "
      "{@CPACK_INNOSETUP_DOWNLOAD_ARCHIVES_INTERNAL@}\n"
      "#dim protected CPackDownloadHashes[CPackDownloadCount] "
      "{@CPACK_INNOSETUP_DOWNLOAD_HASHES_INTERNAL@}\n"
      "#dim protected CPackDownloadComponents[CPackDownloadCount] "
      "{@CPACK_INNOSETUP_DOWNLOAD_COMPONENTS_INTERNAL@}";

    std::string output;
    if (!ConfigureString(downloadLines, output)) {
      return false;
    }
    codeIncludes.push_back(output);
  }

  // Add the required script
  const std::string& componentsScriptTemplate =
    FindTemplate("ISComponents.pas");
  if (componentsScriptTemplate.empty()) {
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "Could not find additional Inno Setup script file."
                    << std::endl);
    return false;
  }

  codeIncludes.push_back("#include " + QuotePath(componentsScriptTemplate) +
                         "\n");

  return true;
}

bool cmCPackInnoSetupGenerator::ConfigureISScript()
{
  const std::string& isScriptTemplate = FindTemplate("ISScript.template.in");
  const std::string& isScriptFile =
    cmStrCat(GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/ISScript.iss");

  if (isScriptTemplate.empty()) {
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "Could not find Inno Setup installer template file."
                    << std::endl);
    return false;
  }

  // Create internal variables
  std::vector<std::string> setupSection;
  for (const auto& i : setupDirectives) {
    setupSection.emplace_back(cmStrCat(i.first, '=', TranslateBool(i.second)));
  }

  // Also create comments if the sections are empty
  const std::string& defaultMessage =
    "; CPack didn't find any entries for this section";

  if (IsSet("CPACK_CREATE_DESKTOP_LINKS") &&
      !GetOption("CPACK_CREATE_DESKTOP_LINKS").Get()->empty()) {
    cmCPackInnoSetupKeyValuePairs tasks;
    tasks["Name"] = "\"desktopicon\"";
    tasks["Description"] = "\"{cm:CreateDesktopIcon}\"";
    tasks["GroupDescription"] = "\"{cm:AdditionalIcons}\"";
    tasks["Flags"] = "unchecked";

    if (!desktopIconComponents.empty()) {
      tasks["Components"] = cmJoin(desktopIconComponents, " ");
    }

    SetOption("CPACK_INNOSETUP_TASKS_INTERNAL", ISKeyValueLine(tasks));
  } else {
    SetOption("CPACK_INNOSETUP_TASKS_INTERNAL", defaultMessage);
  }

  SetOption("CPACK_INNOSETUP_INCLUDES_INTERNAL",
            includeDirectives.empty() ? "; No extra script files specified"
                                      : cmJoin(includeDirectives, "\n"));
  SetOption("CPACK_INNOSETUP_SETUP_INTERNAL",
            setupSection.empty() ? defaultMessage
                                 : cmJoin(setupSection, "\n"));
  SetOption("CPACK_INNOSETUP_LANGUAGES_INTERNAL",
            languageInstructions.empty() ? defaultMessage
                                         : cmJoin(languageInstructions, "\n"));
  SetOption("CPACK_INNOSETUP_DIRS_INTERNAL",
            dirInstructions.empty() ? defaultMessage
                                    : cmJoin(dirInstructions, "\n"));
  SetOption("CPACK_INNOSETUP_FILES_INTERNAL",
            fileInstructions.empty() ? defaultMessage
                                     : cmJoin(fileInstructions, "\n"));
  SetOption("CPACK_INNOSETUP_TYPES_INTERNAL",
            typeInstructions.empty() ? defaultMessage
                                     : cmJoin(typeInstructions, "\n"));
  SetOption("CPACK_INNOSETUP_COMPONENTS_INTERNAL",
            componentInstructions.empty()
              ? defaultMessage
              : cmJoin(componentInstructions, "\n"));
  SetOption("CPACK_INNOSETUP_ICONS_INTERNAL",
            iconInstructions.empty() ? defaultMessage
                                     : cmJoin(iconInstructions, "\n"));
  SetOption("CPACK_INNOSETUP_RUN_INTERNAL",
            runInstructions.empty() ? defaultMessage
                                    : cmJoin(runInstructions, "\n"));
  SetOption("CPACK_INNOSETUP_CODE_INTERNAL",
            codeIncludes.empty() ? "{ No extra code files specified }"
                                 : cmJoin(codeIncludes, "\n"));

  cmCPackLogger(cmCPackLog::LOG_VERBOSE,
                "Configure file: " << isScriptTemplate << " to "
                                   << isScriptFile << std::endl);

  return ConfigureFile(isScriptTemplate, isScriptFile);
}

bool cmCPackInnoSetupGenerator::Compile()
{
  const std::string& isScriptFile =
    cmStrCat(GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/ISScript.iss");
  const std::string& isccLogFile =
    cmStrCat(GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/ISCCOutput.log");

  std::vector<std::string> isccArgs;

  // Custom defines
  for (const std::string& i : GetOptions()) {
    if (cmHasPrefix(i, "CPACK_INNOSETUP_DEFINE_")) {
      const std::string& name = i.substr(cmStrLen("CPACK_INNOSETUP_DEFINE_"));
      isccArgs.push_back(
        cmStrCat("\"/D", name, '=', TranslateBool(GetOption(i)), '"'));
    }
  }

  if (IsSet("CPACK_INNOSETUP_EXECUTABLE_ARGUMENTS")) {
    const cmList args(GetOption("CPACK_INNOSETUP_EXECUTABLE_ARGUMENTS"));

    isccArgs.insert(isccArgs.end(), args.begin(), args.end());
  }

  const std::string& isccCmd =
    cmStrCat(QuotePath(GetOption("CPACK_INSTALLER_PROGRAM"), PathType::Native),
             ' ', cmJoin(isccArgs, " "), ' ', QuotePath(isScriptFile));

  cmCPackLogger(cmCPackLog::LOG_VERBOSE, "Execute: " << isccCmd << std::endl);

  std::string output;
  int retVal = 1;
  const bool res = cmSystemTools::RunSingleCommand(
    isccCmd, &output, &output, &retVal, nullptr, this->GeneratorVerbose,
    cmDuration::zero());

  if (!res || retVal) {
    cmGeneratedFileStream ofs(isccLogFile);
    ofs << "# Run command: " << isccCmd << std::endl
        << "# Output:" << std::endl
        << output << std::endl;
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "Problem running ISCC. Please check "
                    << isccLogFile << " for errors." << std::endl);
    return false;
  }

  return true;
}

bool cmCPackInnoSetupGenerator::BuildDownloadedComponentArchive(
  cmCPackComponent* component, const std::string& uploadDirectory,
  std::string* hash)
{
  // Remove the old archive, if one exists
  const std::string& archiveFile =
    uploadDirectory + '/' + component->ArchiveFile;
  cmCPackLogger(cmCPackLog::LOG_OUTPUT,
                "-   Building downloaded component archive: " << archiveFile
                                                              << std::endl);
  if (cmSystemTools::FileExists(archiveFile, true)) {
    if (!cmSystemTools::RemoveFile(archiveFile)) {
      cmCPackLogger(cmCPackLog::LOG_ERROR,
                    "Unable to remove archive file " << archiveFile
                                                     << std::endl);
      return false;
    }
  }

  // Find a ZIP program
  if (!IsSet("ZIP_EXECUTABLE")) {
    ReadListFile("Internal/CPack/CPackZIP.cmake");

    if (!IsSet("ZIP_EXECUTABLE")) {
      cmCPackLogger(cmCPackLog::LOG_ERROR,
                    "Unable to find ZIP program" << std::endl);
      return false;
    }
  }

  if (!RequireOption("CPACK_TOPLEVEL_DIRECTORY") ||
      !RequireOption("CPACK_TEMPORARY_DIRECTORY") ||
      !RequireOption("CPACK_ZIP_NEED_QUOTES") ||
      !RequireOption("CPACK_ZIP_COMMAND")) {
    return false;
  }

  // The directory where this component's files reside
  const std::string& dirName =
    cmStrCat(GetOption("CPACK_TEMPORARY_DIRECTORY"), '/', component->Name);

  // Build the list of files to go into this archive
  const std::string& zipListFileName =
    cmStrCat(GetOption("CPACK_TEMPORARY_DIRECTORY"), "/winZip.filelist");
  const bool needQuotesInFile = GetOption("CPACK_ZIP_NEED_QUOTES").IsOn();
  { // the scope is needed for cmGeneratedFileStream
    cmGeneratedFileStream out(zipListFileName);
    for (const std::string& i : component->Files) {
      out << (needQuotesInFile ? Quote(i) : i) << std::endl;
    }
  }

  // Build the archive in the upload area
  std::string cmd = GetOption("CPACK_ZIP_COMMAND");
  cmsys::SystemTools::ReplaceString(cmd, "<ARCHIVE>", archiveFile.c_str());
  cmsys::SystemTools::ReplaceString(cmd, "<FILELIST>",
                                    zipListFileName.c_str());
  std::string output;
  int retVal = -1;
  const bool res = cmSystemTools::RunSingleCommand(
    cmd, &output, &output, &retVal, dirName.c_str(), this->GeneratorVerbose,
    cmDuration::zero());
  if (!res || retVal) {
    std::string tmpFile =
      cmStrCat(GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/CompressZip.log");
    cmGeneratedFileStream ofs(tmpFile);
    ofs << "# Run command: " << cmd << std::endl
        << "# Output:" << std::endl
        << output << std::endl;
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "Problem running zip command: " << cmd << std::endl
                                                  << "Please check " << tmpFile
                                                  << " for errors"
                                                  << std::endl);
    return false;
  }

  // Try to get the SHA256 hash of the archive file
  if (hash == nullptr) {
    return true;
  }

#ifdef _WIN32
  const std::string& hashCmd =
    cmStrCat("certutil -hashfile ", QuotePath(archiveFile), " SHA256");

  std::string hashOutput;
  int hashRetVal = -1;
  const bool hashRes = cmSystemTools::RunSingleCommand(
    hashCmd, &hashOutput, &hashOutput, &hashRetVal, nullptr,
    this->GeneratorVerbose, cmDuration::zero());
  if (!hashRes || hashRetVal) {
    cmCPackLogger(cmCPackLog::LOG_WARNING,
                  "Problem running certutil command: " << hashCmd
                                                       << std::endl);
  }
  *hash = cmTrimWhitespace(cmTokenize(hashOutput, "\n").at(1));

  if (hash->length() != 64) {
    cmCPackLogger(cmCPackLog::LOG_WARNING,
                  "Problem parsing certutil output of command: " << hashCmd
                                                                 << std::endl);
    hash->clear();
  }
#endif

  return true;
}

cmValue cmCPackInnoSetupGenerator::RequireOption(const std::string& key)
{
  cmValue value = GetOption(key);

  if (!value) {
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "Required variable " << key << " not set" << std::endl);
  }

  return value;
}

std::string cmCPackInnoSetupGenerator::CustomComponentInstallDirectory(
  const cmCPackComponent* component)
{
  cmValue outputDir = GetOption(
    cmStrCat("CPACK_INNOSETUP_", component->Name, "_INSTALL_DIRECTORY"));
  if (outputDir) {
    std::string destDir = cmSystemTools::ConvertToWindowsOutputPath(outputDir);
    cmStripSuffixIfExists(destDir, '\\');

    /*
     * Add a dir instruction for the custom directory
     * (only once and not for Inno Setup constants ending with '}')
     */
    static std::vector<std::string> customDirectories;
    if (!cmHasSuffix(destDir, '}') &&
        std::find(customDirectories.begin(), customDirectories.end(),
                  component->Name) == customDirectories.end()) {
      cmCPackInnoSetupKeyValuePairs params;
      params["Name"] = QuotePath(destDir);
      params["Components"] =
        CreateRecursiveComponentPath(component->Group, component->Name);

      dirInstructions.push_back(ISKeyValueLine(params));
      customDirectories.push_back(component->Name);
    }
    return destDir;
  }

  return "{app}";
}

std::string cmCPackInnoSetupGenerator::TranslateBool(const std::string& value)
{
  if (value.empty()) {
    return value;
  }

  SetOptionIfNotSet("CPACK_INNOSETUP_USE_CMAKE_BOOL_FORMAT", "ON");
  if (GetOption("CPACK_INNOSETUP_USE_CMAKE_BOOL_FORMAT").IsOn()) {
    if (cmIsOn(value)) {
      return "yes";
    }
    if (cmIsOff(value)) {
      return "no";
    }
  }

  return value;
}

std::string cmCPackInnoSetupGenerator::ISKeyValueLine(
  const cmCPackInnoSetupKeyValuePairs& params)
{
  /*
   * To simplify readability of the generated code, the keys are sorted.
   * Unknown keys are ignored to avoid errors during compilation.
   */
  static const char* const availableKeys[] = {
    "Source",       "DestDir",          "Name",         "Filename",
    "Description",  "GroupDescription", "MessagesFile", "Types",
    "ExternalSize", "BeforeInstall",    "Flags",        "Components",
    "Tasks"
  };

  std::vector<std::string> keys;
  for (const char* i : availableKeys) {
    if (params.count(i)) {
      keys.emplace_back(cmStrCat(i, ": ", params.at(i)));
    }
  }

  return cmJoin(keys, "; ");
}

std::string cmCPackInnoSetupGenerator::CreateRecursiveComponentPath(
  cmCPackComponentGroup* group, const std::string& path)
{
  if (group == nullptr) {
    return path;
  }

  const std::string& newPath =
    path.empty() ? group->Name : cmStrCat(group->Name, '\\', path);
  return CreateRecursiveComponentPath(group->ParentGroup, newPath);
}

void cmCPackInnoSetupGenerator::CreateRecursiveComponentGroups(
  cmCPackComponentGroup* group)
{
  if (group == nullptr) {
    return;
  }

  CreateRecursiveComponentGroups(group->ParentGroup);

  static std::vector<cmCPackComponentGroup*> processedGroups;
  if (std::find(processedGroups.begin(), processedGroups.end(), group) ==
      processedGroups.end()) {
    processedGroups.push_back(group);

    cmCPackInnoSetupKeyValuePairs params;

    params["Name"] = Quote(CreateRecursiveComponentPath(group));
    params["Description"] = Quote(group->DisplayName);

    componentInstructions.push_back(ISKeyValueLine(params));
  }
}

std::string cmCPackInnoSetupGenerator::Quote(const std::string& string)
{
  if (cmHasPrefix(string, '"') && cmHasSuffix(string, '"')) {
    return Quote(string.substr(1, string.length() - 2));
  }

  // Double quote syntax
  std::string nString = string;
  cmSystemTools::ReplaceString(nString, "\"", "\"\"");
  return cmStrCat('"', nString, '"');
}

std::string cmCPackInnoSetupGenerator::QuotePath(const std::string& path,
                                                 PathType type)
{
#ifdef _WIN32
  static_cast<void>(type);
#else
  if (type == PathType::Native) {
    return Quote(cmSystemTools::ConvertToUnixOutputPath(path));
  }
#endif
  return Quote(cmSystemTools::ConvertToWindowsOutputPath(path));
}

std::string cmCPackInnoSetupGenerator::PrepareForConstant(
  const std::string& string)
{
  std::string nString = string;

  cmSystemTools::ReplaceString(nString, "%", "%25"); // First replacement!
  cmSystemTools::ReplaceString(nString, "\"", "%22");
  cmSystemTools::ReplaceString(nString, ",", "%2c");
  cmSystemTools::ReplaceString(nString, "|", "%7c");
  cmSystemTools::ReplaceString(nString, "}", "%7d");

  return nString;
}
