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

#include <sstream>
#include <utility>

#include "cmCPackComponentGroup.h"
#include "cmCPackGenerator.h"
#include "cmCPackIFWCommon.h"
#include "cmCPackIFWInstaller.h"
#include "cmCPackIFWPackage.h"
#include "cmCPackIFWRepository.h"
#include "cmCPackLog.h" // IWYU pragma: keep
#include "cmDuration.h"
#include "cmGeneratedFileStream.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

cmCPackIFWGenerator::cmCPackIFWGenerator()
{
  Generator = this;
}

cmCPackIFWGenerator::~cmCPackIFWGenerator() = default;

int cmCPackIFWGenerator::PackageFiles()
{
  cmCPackIFWLogger(OUTPUT, "- Configuration" << std::endl);

  // Installer configuragion
  Installer.GenerateInstallerFile();

  // Packages configuration
  Installer.GeneratePackageFiles();

  std::string ifwTLD = GetOption("CPACK_TOPLEVEL_DIRECTORY");
  std::string ifwTmpFile = cmStrCat(ifwTLD, "/IFWOutput.log");

  // Run repogen
  if (!Installer.RemoteRepositories.empty()) {
    std::vector<std::string> ifwCmd;
    std::string ifwArg;

    ifwCmd.emplace_back(RepoGen);

    if (IsVersionLess("2.0.0")) {
      ifwCmd.emplace_back("-c");
      ifwCmd.emplace_back(toplevel + "/config/config.xml");
    }

    ifwCmd.emplace_back("-p");
    ifwCmd.emplace_back(toplevel + "/packages");

    if (!PkgsDirsVector.empty()) {
      for (std::string const& it : PkgsDirsVector) {
        ifwCmd.emplace_back("-p");
        ifwCmd.emplace_back(it);
      }
    }

    if (!RepoDirsVector.empty()) {
      if (!IsVersionLess("3.1")) {
        for (std::string const& rd : RepoDirsVector) {
          ifwCmd.emplace_back("--repository");
          ifwCmd.emplace_back(rd);
        }
      } else {
        cmCPackIFWLogger(WARNING,
                         "The \"CPACK_IFW_REPOSITORIES_DIRECTORIES\" "
                           << "variable is set, but content will be skipped, "
                           << "because this feature available only since "
                           << "QtIFW 3.1. Please update your QtIFW instance."
                           << std::endl);
      }
    }

    if (!OnlineOnly && !DownloadedPackages.empty()) {
      ifwCmd.emplace_back("-i");
      auto it = DownloadedPackages.begin();
      ifwArg = (*it)->Name;
      ++it;
      while (it != DownloadedPackages.end()) {
        ifwArg += "," + (*it)->Name;
        ++it;
      }
      ifwCmd.emplace_back(ifwArg);
    }
    ifwCmd.emplace_back(toplevel + "/repository");
    cmCPackIFWLogger(VERBOSE,
                     "Execute: " << cmSystemTools::PrintSingleCommand(ifwCmd)
                                 << std::endl);
    std::string output;
    int retVal = 1;
    cmCPackIFWLogger(OUTPUT, "- Generate repository" << std::endl);
    bool res = cmSystemTools::RunSingleCommand(
      ifwCmd, &output, &output, &retVal, nullptr, GeneratorVerbose,
      cmDuration::zero());
    if (!res || retVal) {
      cmGeneratedFileStream ofs(ifwTmpFile);
      ofs << "# Run command: " << cmSystemTools::PrintSingleCommand(ifwCmd)
          << std::endl
          << "# Output:" << std::endl
          << output << std::endl;
      cmCPackIFWLogger(
        ERROR,
        "Problem running IFW command: "
          << cmSystemTools::PrintSingleCommand(ifwCmd) << std::endl
          << "Please check \"" << ifwTmpFile << "\" for errors" << std::endl);
      return 0;
    }

    if (!Repository.RepositoryUpdate.empty() &&
        !Repository.PatchUpdatesXml()) {
      cmCPackIFWLogger(WARNING,
                       "Problem patch IFW \"Updates\" "
                         << "file: \"" << toplevel
                         << "/repository/Updates.xml\"" << std::endl);
    }

    cmCPackIFWLogger(OUTPUT,
                     "- repository: \""
                       << toplevel << "/repository\" generated" << std::endl);
  }

  // Run binary creator
  {
    std::vector<std::string> ifwCmd;
    std::string ifwArg;

    ifwCmd.emplace_back(BinCreator);

    ifwCmd.emplace_back("-c");
    ifwCmd.emplace_back(toplevel + "/config/config.xml");

    if (!Installer.Resources.empty()) {
      ifwCmd.emplace_back("-r");
      auto it = Installer.Resources.begin();
      std::string path = toplevel + "/resources/";
      ifwArg = path + *it;
      ++it;
      while (it != Installer.Resources.end()) {
        ifwArg += "," + path + *it;
        ++it;
      }
      ifwCmd.emplace_back(ifwArg);
    }

    ifwCmd.emplace_back("-p");
    ifwCmd.emplace_back(toplevel + "/packages");

    if (!PkgsDirsVector.empty()) {
      for (std::string const& it : PkgsDirsVector) {
        ifwCmd.emplace_back("-p");
        ifwCmd.emplace_back(it);
      }
    }

    if (!RepoDirsVector.empty()) {
      if (!IsVersionLess("3.1")) {
        for (std::string const& rd : RepoDirsVector) {
          ifwCmd.emplace_back("--repository");
          ifwCmd.emplace_back(rd);
        }
      } else {
        cmCPackIFWLogger(WARNING,
                         "The \"CPACK_IFW_REPOSITORIES_DIRECTORIES\" "
                           << "variable is set, but content will be skipped, "
                           << "because this feature available only since "
                           << "QtIFW 3.1. Please update your QtIFW instance."
                           << std::endl);
      }
    }

    if (OnlineOnly) {
      ifwCmd.emplace_back("--online-only");
    } else if (!DownloadedPackages.empty() &&
               !Installer.RemoteRepositories.empty()) {
      ifwCmd.emplace_back("-e");
      auto it = DownloadedPackages.begin();
      ifwArg = (*it)->Name;
      ++it;
      while (it != DownloadedPackages.end()) {
        ifwArg += "," + (*it)->Name;
        ++it;
      }
      ifwCmd.emplace_back(ifwArg);
    } else if (!DependentPackages.empty()) {
      ifwCmd.emplace_back("-i");
      ifwArg.clear();
      // Binary
      auto bit = BinaryPackages.begin();
      while (bit != BinaryPackages.end()) {
        ifwArg += (*bit)->Name + ",";
        ++bit;
      }
      // Depend
      auto it = DependentPackages.begin();
      ifwArg += it->second.Name;
      ++it;
      while (it != DependentPackages.end()) {
        ifwArg += "," + it->second.Name;
        ++it;
      }
      ifwCmd.emplace_back(ifwArg);
    }
    // TODO: set correct name for multipackages
    if (!packageFileNames.empty()) {
      ifwCmd.emplace_back(packageFileNames[0]);
    } else {
      ifwCmd.emplace_back("installer" + OutputExtension);
    }
    cmCPackIFWLogger(VERBOSE,
                     "Execute: " << cmSystemTools::PrintSingleCommand(ifwCmd)
                                 << std::endl);
    std::string output;
    int retVal = 1;
    cmCPackIFWLogger(OUTPUT, "- Generate package" << std::endl);
    bool res = cmSystemTools::RunSingleCommand(
      ifwCmd, &output, &output, &retVal, nullptr, GeneratorVerbose,
      cmDuration::zero());
    if (!res || retVal) {
      cmGeneratedFileStream ofs(ifwTmpFile);
      ofs << "# Run command: " << cmSystemTools::PrintSingleCommand(ifwCmd)
          << std::endl
          << "# Output:" << std::endl
          << output << std::endl;
      cmCPackIFWLogger(
        ERROR,
        "Problem running IFW command: "
          << cmSystemTools::PrintSingleCommand(ifwCmd) << std::endl
          << "Please check \"" << ifwTmpFile << "\" for errors" << std::endl);
      return 0;
    }
  }

  return 1;
}

const char* cmCPackIFWGenerator::GetPackagingInstallPrefix()
{
  const char* defPrefix = cmCPackGenerator::GetPackagingInstallPrefix();

  std::string tmpPref = defPrefix ? defPrefix : "";

  if (Components.empty()) {
    tmpPref += "packages/" + GetRootPackageName() + "/data";
  }

  SetOption("CPACK_IFW_PACKAGING_INSTALL_PREFIX", tmpPref.c_str());

  return GetOption("CPACK_IFW_PACKAGING_INSTALL_PREFIX");
}

const char* cmCPackIFWGenerator::GetOutputExtension()
{
  return OutputExtension.c_str();
}

int cmCPackIFWGenerator::InitializeInternal()
{
  // Search Qt Installer Framework tools

  const std::string BinCreatorOpt = "CPACK_IFW_BINARYCREATOR_EXECUTABLE";
  const std::string RepoGenOpt = "CPACK_IFW_REPOGEN_EXECUTABLE";
  const std::string FrameworkVersionOpt = "CPACK_IFW_FRAMEWORK_VERSION";

  if (!IsSet(BinCreatorOpt) || !IsSet(RepoGenOpt) ||
      !IsSet(FrameworkVersionOpt)) {
    ReadListFile("CPackIFW.cmake");
  }

  // Look 'binarycreator' executable (needs)

  const char* BinCreatorStr = GetOption(BinCreatorOpt);
  if (!BinCreatorStr || cmIsNOTFOUND(BinCreatorStr)) {
    BinCreator.clear();
  } else {
    BinCreator = BinCreatorStr;
  }

  if (BinCreator.empty()) {
    cmCPackIFWLogger(ERROR,
                     "Cannot find QtIFW compiler \"binarycreator\": "
                     "likely it is not installed, or not in your PATH"
                       << std::endl);
    return 0;
  }

  // Look 'repogen' executable (optional)

  const char* RepoGenStr = GetOption(RepoGenOpt);
  if (!RepoGenStr || cmIsNOTFOUND(RepoGenStr)) {
    RepoGen.clear();
  } else {
    RepoGen = RepoGenStr;
  }

  // Framework version
  if (const char* FrameworkVersionSrt = GetOption(FrameworkVersionOpt)) {
    FrameworkVersion = FrameworkVersionSrt;
  } else {
    FrameworkVersion = "1.9.9";
  }

  // Variables that Change Behavior

  // Resolve duplicate names
  ResolveDuplicateNames = IsOn("CPACK_IFW_RESOLVE_DUPLICATE_NAMES");

  // Additional packages dirs
  PkgsDirsVector.clear();
  if (const char* dirs = GetOption("CPACK_IFW_PACKAGES_DIRECTORIES")) {
    cmExpandList(dirs, PkgsDirsVector);
  }

  // Additional repositories dirs
  RepoDirsVector.clear();
  if (const char* dirs = GetOption("CPACK_IFW_REPOSITORIES_DIRECTORIES")) {
    cmExpandList(dirs, RepoDirsVector);
  }

  // Installer
  Installer.Generator = this;
  Installer.ConfigureFromOptions();

  // Repository
  Repository.Generator = this;
  Repository.Name = "Unspecified";
  if (const char* site = GetOption("CPACK_DOWNLOAD_SITE")) {
    Repository.Url = site;
    Installer.RemoteRepositories.push_back(&Repository);
  }

  // Repositories
  if (const char* RepoAllStr = GetOption("CPACK_IFW_REPOSITORIES_ALL")) {
    std::vector<std::string> RepoAllVector = cmExpandedList(RepoAllStr);
    for (std::string const& r : RepoAllVector) {
      GetRepository(r);
    }
  }

  if (const char* ifwDownloadAll = GetOption("CPACK_IFW_DOWNLOAD_ALL")) {
    OnlineOnly = cmIsOn(ifwDownloadAll);
  } else if (const char* cpackDownloadAll = GetOption("CPACK_DOWNLOAD_ALL")) {
    OnlineOnly = cmIsOn(cpackDownloadAll);
  } else {
    OnlineOnly = false;
  }

  if (!Installer.RemoteRepositories.empty() && RepoGen.empty()) {
    cmCPackIFWLogger(ERROR,
                     "Cannot find QtIFW repository generator \"repogen\": "
                     "likely it is not installed, or not in your PATH"
                       << std::endl);
    return 0;
  }

  // Executable suffix
  std::string exeSuffix(GetOption("CMAKE_EXECUTABLE_SUFFIX"));
  std::string sysName(GetOption("CMAKE_SYSTEM_NAME"));
  if (sysName == "Linux") {
    ExecutableSuffix = ".run";
  } else if (sysName == "Windows") {
    ExecutableSuffix = ".exe";
  } else if (sysName == "Darwin") {
    ExecutableSuffix = ".app";
  } else {
    ExecutableSuffix = exeSuffix;
  }

  // Output extension
  if (const char* optOutExt = GetOption("CPACK_IFW_PACKAGE_FILE_EXTENSION")) {
    OutputExtension = optOutExt;
  } else if (sysName == "Darwin") {
    OutputExtension = ".dmg";
  } else {
    OutputExtension = ExecutableSuffix;
  }
  if (OutputExtension.empty()) {
    OutputExtension = cmCPackGenerator::GetOutputExtension();
  }

  return Superclass::InitializeInternal();
}

std::string cmCPackIFWGenerator::GetComponentInstallDirNameSuffix(
  const std::string& componentName)
{
  const std::string prefix = "packages/";
  const std::string suffix = "/data";

  if (componentPackageMethod == ONE_PACKAGE) {
    return std::string(prefix + GetRootPackageName() + suffix);
  }

  return prefix + GetComponentPackageName(&Components[componentName]) + suffix;
}

cmCPackComponent* cmCPackIFWGenerator::GetComponent(
  const std::string& projectName, const std::string& componentName)
{
  auto cit = Components.find(componentName);
  if (cit != Components.end()) {
    return &(cit->second);
  }

  cmCPackComponent* component =
    cmCPackGenerator::GetComponent(projectName, componentName);
  if (!component) {
    return component;
  }

  std::string name = GetComponentPackageName(component);
  auto pit = Packages.find(name);
  if (pit != Packages.end()) {
    return component;
  }

  cmCPackIFWPackage* package = &Packages[name];
  package->Name = name;
  package->Generator = this;
  if (package->ConfigureFromComponent(component)) {
    package->Installer = &Installer;
    Installer.Packages.insert(
      std::pair<std::string, cmCPackIFWPackage*>(name, package));
    ComponentPackages.insert(
      std::pair<cmCPackComponent*, cmCPackIFWPackage*>(component, package));
    if (component->IsDownloaded) {
      DownloadedPackages.insert(package);
    } else {
      BinaryPackages.insert(package);
    }
  } else {
    Packages.erase(name);
    cmCPackIFWLogger(ERROR,
                     "Cannot configure package \""
                       << name << "\" for component \"" << component->Name
                       << "\"" << std::endl);
  }

  return component;
}

cmCPackComponentGroup* cmCPackIFWGenerator::GetComponentGroup(
  const std::string& projectName, const std::string& groupName)
{
  cmCPackComponentGroup* group =
    cmCPackGenerator::GetComponentGroup(projectName, groupName);
  if (!group) {
    return group;
  }

  std::string name = GetGroupPackageName(group);
  auto pit = Packages.find(name);
  if (pit != Packages.end()) {
    return group;
  }

  cmCPackIFWPackage* package = &Packages[name];
  package->Name = name;
  package->Generator = this;
  if (package->ConfigureFromGroup(group)) {
    package->Installer = &Installer;
    Installer.Packages.insert(
      std::pair<std::string, cmCPackIFWPackage*>(name, package));
    GroupPackages.insert(
      std::pair<cmCPackComponentGroup*, cmCPackIFWPackage*>(group, package));
    BinaryPackages.insert(package);
  } else {
    Packages.erase(name);
    cmCPackIFWLogger(ERROR,
                     "Cannot configure package \""
                       << name << "\" for component group \"" << group->Name
                       << "\"" << std::endl);
  }
  return group;
}

enum cmCPackGenerator::CPackSetDestdirSupport
cmCPackIFWGenerator::SupportsSetDestdir() const
{
  return cmCPackGenerator::SETDESTDIR_SHOULD_NOT_BE_USED;
}

bool cmCPackIFWGenerator::SupportsAbsoluteDestination() const
{
  return false;
}

bool cmCPackIFWGenerator::SupportsComponentInstallation() const
{
  return true;
}

bool cmCPackIFWGenerator::IsOnePackage() const
{
  return componentPackageMethod == cmCPackGenerator::ONE_PACKAGE;
}

std::string cmCPackIFWGenerator::GetRootPackageName()
{
  // Default value
  std::string name = "root";
  if (const char* optIFW_PACKAGE_GROUP =
        GetOption("CPACK_IFW_PACKAGE_GROUP")) {
    // Configure from root group
    cmCPackIFWPackage package;
    package.Generator = this;
    package.ConfigureFromGroup(optIFW_PACKAGE_GROUP);
    name = package.Name;
  } else if (const char* optIFW_PACKAGE_NAME =
               GetOption("CPACK_IFW_PACKAGE_NAME")) {
    // Configure from root package name
    name = optIFW_PACKAGE_NAME;
  } else if (const char* optPACKAGE_NAME = GetOption("CPACK_PACKAGE_NAME")) {
    // Configure from package name
    name = optPACKAGE_NAME;
  }
  return name;
}

std::string cmCPackIFWGenerator::GetGroupPackageName(
  cmCPackComponentGroup* group) const
{
  std::string name;
  if (!group) {
    return name;
  }
  if (cmCPackIFWPackage* package = GetGroupPackage(group)) {
    return package->Name;
  }
  const char* option =
    GetOption("CPACK_IFW_COMPONENT_GROUP_" +
              cmsys::SystemTools::UpperCase(group->Name) + "_NAME");
  name = option ? option : group->Name;
  if (group->ParentGroup) {
    cmCPackIFWPackage* package = GetGroupPackage(group->ParentGroup);
    bool dot = !ResolveDuplicateNames;
    if (dot && !cmHasPrefix(name, package->Name)) {
      name = package->Name + "." + name;
    }
  }
  return name;
}

std::string cmCPackIFWGenerator::GetComponentPackageName(
  cmCPackComponent* component) const
{
  std::string name;
  if (!component) {
    return name;
  }
  if (cmCPackIFWPackage* package = GetComponentPackage(component)) {
    return package->Name;
  }
  std::string prefix = "CPACK_IFW_COMPONENT_" +
    cmsys::SystemTools::UpperCase(component->Name) + "_";
  const char* option = GetOption(prefix + "NAME");
  name = option ? option : component->Name;
  if (component->Group) {
    cmCPackIFWPackage* package = GetGroupPackage(component->Group);
    if ((componentPackageMethod == cmCPackGenerator::ONE_PACKAGE_PER_GROUP) ||
        IsOn(prefix + "COMMON")) {
      return package->Name;
    }
    bool dot = !ResolveDuplicateNames;
    if (dot && !cmHasPrefix(name, package->Name)) {
      name = package->Name + "." + name;
    }
  }
  return name;
}

cmCPackIFWPackage* cmCPackIFWGenerator::GetGroupPackage(
  cmCPackComponentGroup* group) const
{
  auto pit = GroupPackages.find(group);
  return pit != GroupPackages.end() ? pit->second : nullptr;
}

cmCPackIFWPackage* cmCPackIFWGenerator::GetComponentPackage(
  cmCPackComponent* component) const
{
  auto pit = ComponentPackages.find(component);
  return pit != ComponentPackages.end() ? pit->second : nullptr;
}

cmCPackIFWRepository* cmCPackIFWGenerator::GetRepository(
  const std::string& repositoryName)
{
  auto rit = Repositories.find(repositoryName);
  if (rit != Repositories.end()) {
    return &(rit->second);
  }

  cmCPackIFWRepository* repository = &Repositories[repositoryName];
  repository->Name = repositoryName;
  repository->Generator = this;
  if (repository->ConfigureFromOptions()) {
    if (repository->Update == cmCPackIFWRepository::None) {
      Installer.RemoteRepositories.push_back(repository);
    } else {
      Repository.RepositoryUpdate.push_back(repository);
    }
  } else {
    Repositories.erase(repositoryName);
    repository = nullptr;
    cmCPackIFWLogger(WARNING,
                     "Invalid repository \""
                       << repositoryName << "\""
                       << " configuration. Repository will be skipped."
                       << std::endl);
  }
  return repository;
}
