/* 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()
{
  this->Generator = this;
}

cmCPackIFWGenerator::~cmCPackIFWGenerator() = default;

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

  // Installer configuragion
  this->Installer.GenerateInstallerFile();

  // Packages configuration
  this->Installer.GeneratePackageFiles();

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

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

    ifwCmd.emplace_back(this->RepoGen);

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

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

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

    if (!this->RepoDirsVector.empty()) {
      if (!this->IsVersionLess("3.1")) {
        for (std::string const& rd : this->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 (!this->OnlineOnly && !this->DownloadedPackages.empty()) {
      ifwCmd.emplace_back("-i");
      auto it = this->DownloadedPackages.begin();
      ifwArg = (*it)->Name;
      ++it;
      while (it != this->DownloadedPackages.end()) {
        ifwArg += "," + (*it)->Name;
        ++it;
      }
      ifwCmd.emplace_back(ifwArg);
    }
    ifwCmd.emplace_back(this->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, this->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 (!this->Repository.RepositoryUpdate.empty() &&
        !this->Repository.PatchUpdatesXml()) {
      cmCPackIFWLogger(WARNING,
                       "Problem patch IFW \"Updates\" "
                         << "file: \"" << this->toplevel
                         << "/repository/Updates.xml\"" << std::endl);
    }

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

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

    ifwCmd.emplace_back(this->BinCreator);

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

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

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

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

    if (!this->RepoDirsVector.empty()) {
      if (!this->IsVersionLess("3.1")) {
        for (std::string const& rd : this->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 (this->OnlineOnly) {
      ifwCmd.emplace_back("--online-only");
    } else if (!this->DownloadedPackages.empty() &&
               !this->Installer.RemoteRepositories.empty()) {
      ifwCmd.emplace_back("-e");
      auto it = this->DownloadedPackages.begin();
      ifwArg = (*it)->Name;
      ++it;
      while (it != this->DownloadedPackages.end()) {
        ifwArg += "," + (*it)->Name;
        ++it;
      }
      ifwCmd.emplace_back(ifwArg);
    } else if (!this->DependentPackages.empty()) {
      ifwCmd.emplace_back("-i");
      ifwArg.clear();
      // Binary
      auto bit = this->BinaryPackages.begin();
      while (bit != this->BinaryPackages.end()) {
        ifwArg += (*bit)->Name + ",";
        ++bit;
      }
      // Depend
      auto it = this->DependentPackages.begin();
      ifwArg += it->second.Name;
      ++it;
      while (it != this->DependentPackages.end()) {
        ifwArg += "," + it->second.Name;
        ++it;
      }
      ifwCmd.emplace_back(ifwArg);
    }
    // TODO: set correct name for multipackages
    if (!this->packageFileNames.empty()) {
      ifwCmd.emplace_back(this->packageFileNames[0]);
    } else {
      ifwCmd.emplace_back("installer" + this->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, this->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 = this->cmCPackGenerator::GetPackagingInstallPrefix();

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

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

  this->SetOption("CPACK_IFW_PACKAGING_INSTALL_PREFIX", tmpPref.c_str());

  return this->GetOption("CPACK_IFW_PACKAGING_INSTALL_PREFIX");
}

const char* cmCPackIFWGenerator::GetOutputExtension()
{
  return this->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 (!this->IsSet(BinCreatorOpt) || !this->IsSet(RepoGenOpt) ||
      !this->IsSet(FrameworkVersionOpt)) {
    this->ReadListFile("CPackIFW.cmake");
  }

  // Look 'binarycreator' executable (needs)

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

  if (this->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 = this->GetOption(RepoGenOpt);
  if (!RepoGenStr || cmIsNOTFOUND(RepoGenStr)) {
    this->RepoGen.clear();
  } else {
    this->RepoGen = RepoGenStr;
  }

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

  // Variables that Change Behavior

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

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

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

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

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

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

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

  if (!this->Installer.RemoteRepositories.empty() && this->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(this->GetOption("CMAKE_EXECUTABLE_SUFFIX"));
  std::string sysName(this->GetOption("CMAKE_SYSTEM_NAME"));
  if (sysName == "Linux") {
    this->ExecutableSuffix = ".run";
  } else if (sysName == "Windows") {
    this->ExecutableSuffix = ".exe";
  } else if (sysName == "Darwin") {
    this->ExecutableSuffix = ".app";
  } else {
    this->ExecutableSuffix = exeSuffix;
  }

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

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

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

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

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

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

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

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

  cmCPackIFWPackage* package = &this->Packages[name];
  package->Name = name;
  package->Generator = this;
  if (package->ConfigureFromComponent(component)) {
    package->Installer = &this->Installer;
    this->Installer.Packages.insert(
      std::pair<std::string, cmCPackIFWPackage*>(name, package));
    this->ComponentPackages.insert(
      std::pair<cmCPackComponent*, cmCPackIFWPackage*>(component, package));
    if (component->IsDownloaded) {
      this->DownloadedPackages.insert(package);
    } else {
      this->BinaryPackages.insert(package);
    }
  } else {
    this->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 =
    this->cmCPackGenerator::GetComponentGroup(projectName, groupName);
  if (!group) {
    return group;
  }

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

  cmCPackIFWPackage* package = &this->Packages[name];
  package->Name = name;
  package->Generator = this;
  if (package->ConfigureFromGroup(group)) {
    package->Installer = &this->Installer;
    this->Installer.Packages.insert(
      std::pair<std::string, cmCPackIFWPackage*>(name, package));
    this->GroupPackages.insert(
      std::pair<cmCPackComponentGroup*, cmCPackIFWPackage*>(group, package));
    this->BinaryPackages.insert(package);
  } else {
    this->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 this->componentPackageMethod == cmCPackGenerator::ONE_PACKAGE;
}

std::string cmCPackIFWGenerator::GetRootPackageName()
{
  // Default value
  std::string name = "root";
  if (const char* optIFW_PACKAGE_GROUP =
        this->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 =
               this->GetOption("CPACK_IFW_PACKAGE_NAME")) {
    // Configure from root package name
    name = optIFW_PACKAGE_NAME;
  } else if (const char* optPACKAGE_NAME =
               this->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 = this->GetGroupPackage(group)) {
    return package->Name;
  }
  const char* option =
    this->GetOption("CPACK_IFW_COMPONENT_GROUP_" +
                    cmsys::SystemTools::UpperCase(group->Name) + "_NAME");
  name = option ? option : group->Name;
  if (group->ParentGroup) {
    cmCPackIFWPackage* package = this->GetGroupPackage(group->ParentGroup);
    bool dot = !this->ResolveDuplicateNames;
    if (dot && name.substr(0, package->Name.size()) == package->Name) {
      dot = false;
    }
    if (dot) {
      name = package->Name + "." + name;
    }
  }
  return name;
}

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

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

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

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

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