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

#include <algorithm>
#include <ostream>
#include <utility>
#include <vector>

#include <fcntl.h>
#include <pkg.h>

#include <sys/stat.h>

#include "cmArchiveWrite.h"
#include "cmCPackArchiveGenerator.h"
#include "cmCPackLog.h"
#include "cmGeneratedFileStream.h"
#include "cmList.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmWorkingDirectory.h"

// Suffix used to tell libpkg what compression to use
static char const FreeBSDPackageCompression[] = "txz";
static char const FreeBSDPackageSuffix_17[] = ".pkg";

cmCPackFreeBSDGenerator::cmCPackFreeBSDGenerator()
  : cmCPackArchiveGenerator(cmArchiveWrite::CompressXZ, "paxr",
                            FreeBSDPackageSuffix_17)
{
}

int cmCPackFreeBSDGenerator::InitializeInternal()
{
  this->SetOptionIfNotSet("CPACK_PACKAGING_INSTALL_PREFIX", "/usr/local");
  this->SetOption("CPACK_INCLUDE_TOPLEVEL_DIRECTORY", "0");
  return this->Superclass::InitializeInternal();
}

cmCPackFreeBSDGenerator::~cmCPackFreeBSDGenerator() = default;

// This is a wrapper for struct pkg_create and pkg_create()
//
// Instantiate this class with suitable parameters, then
// check isValid() to check if it's ok. Afterwards, call
// Create() to do the actual work. This will leave a package
// in the given `output_dir`.
//
// This wrapper cleans up the struct pkg_create.
class PkgCreate
{
public:
  PkgCreate()
    : d(nullptr)
  {
  }
  PkgCreate(std::string const& output_dir, std::string const& toplevel_dir,
            std::string const& manifest_name)
    : d(pkg_create_new())
    , manifest(manifest_name)

  {
    if (d) {
      pkg_create_set_format(d, FreeBSDPackageCompression);
      pkg_create_set_compression_level(d, 0); // Explicitly set default
      pkg_create_set_overwrite(d, false);
      pkg_create_set_rootdir(d, toplevel_dir.c_str());
      pkg_create_set_output_dir(d, output_dir.c_str());
    }
  }
  ~PkgCreate()
  {
    if (d)
      pkg_create_free(d);
  }

  bool isValid() const { return d; }

  bool Create()
  {
    if (!isValid())
      return false;
    // The API in the FreeBSD sources (the header has no documentation),
    // is as follows:
    //
    // int pkg_create(struct pkg_create *pc, const char *metadata, const char
    // *plist, bool hash)
    //
    // We let the plist be determined from what is installed, and all
    // the rest comes from the manifest data.
    int r = pkg_create(d, manifest.c_str(), nullptr, false);
    return r == 0;
  }

private:
  struct pkg_create* d;
  std::string manifest;
};

// This is a wrapper, for use only in stream-based output,
// that will output a string in UCL escaped fashion (in particular,
// quotes and backslashes are escaped). The list of characters
// to escape is taken from https://github.com/vstakhov/libucl
// (which is the reference implementation pkg(8) refers to).
class EscapeQuotes
{
public:
  std::string const& value;

  EscapeQuotes(std::string const& s)
    : value(s)
  {
  }
};

// Output a string as "string" with escaping applied.
cmGeneratedFileStream& operator<<(cmGeneratedFileStream& s,
                                  EscapeQuotes const& v)
{
  s << '"';
  for (char c : v.value) {
    switch (c) {
      case '\n':
        s << "\\n";
        break;
      case '\r':
        s << "\\r";
        break;
      case '\b':
        s << "\\b";
        break;
      case '\t':
        s << "\\t";
        break;
      case '\f':
        s << "\\f";
        break;
      case '\\':
        s << "\\\\";
        break;
      case '"':
        s << "\\\"";
        break;
      default:
        s << c;
        break;
    }
  }
  s << '"';
  return s;
}

// The following classes are all helpers for writing out the UCL
// manifest file (it also looks like JSON). ManifestKey just has
// a (string-valued) key; subclasses add a specific kind of
// value-type to the key, and implement write_value() to output
// the corresponding UCL.
class ManifestKey
{
public:
  std::string key;

  ManifestKey(std::string k)
    : key(std::move(k))
  {
  }

  virtual ~ManifestKey() = default;

  // Output the value associated with this key to the stream @p s.
  // Format is to be decided by subclasses.
  virtual void write_value(cmGeneratedFileStream& s) const = 0;
};

// Basic string-value (e.g. "name": "cmake")
class ManifestKeyValue : public ManifestKey
{
public:
  std::string value;

  ManifestKeyValue(std::string const& k, std::string v)
    : ManifestKey(k)
    , value(std::move(v))
  {
  }

  void write_value(cmGeneratedFileStream& s) const override
  {
    s << EscapeQuotes(value);
  }
};

// List-of-strings values (e.g. "licenses": ["GPLv2", "LGPLv2"])
class ManifestKeyListValue : public ManifestKey
{
public:
  using VList = std::vector<std::string>;
  VList value;

  ManifestKeyListValue(std::string const& k)
    : ManifestKey(k)
  {
  }

  ManifestKeyListValue& operator<<(std::string const& v)
  {
    value.push_back(v);
    return *this;
  }

  ManifestKeyListValue& operator<<(std::vector<std::string> const& v)
  {
    for (std::string const& e : v) {
      (*this) << e;
    }
    return *this;
  }

  void write_value(cmGeneratedFileStream& s) const override
  {
    bool with_comma = false;

    s << '[';
    for (std::string const& elem : value) {
      s << (with_comma ? ',' : ' ');
      s << EscapeQuotes(elem);
      with_comma = true;
    }
    s << " ]";
  }
};

// Deps: actually a dictionary, but we'll treat it as a
// list so we only name the deps, and produce dictionary-
// like output via write_value()
class ManifestKeyDepsValue : public ManifestKeyListValue
{
public:
  ManifestKeyDepsValue(std::string const& k)
    : ManifestKeyListValue(k)
  {
  }

  void write_value(cmGeneratedFileStream& s) const override
  {
    s << "{\n";
    for (std::string const& elem : value) {
      s << "  \"" << elem << R"(": {"origin": ")" << elem << "\"},\n";
    }
    s << '}';
  }
};

// Write one of the key-value classes (above) to the stream @p s
cmGeneratedFileStream& operator<<(cmGeneratedFileStream& s,
                                  ManifestKey const& v)
{
  s << '"' << v.key << "\": ";
  v.write_value(s);
  s << ",\n";
  return s;
}

// Look up variable; if no value is set, returns an empty string;
// basically a wrapper that handles the nullptr return from GetOption().
std::string cmCPackFreeBSDGenerator::var_lookup(char const* var_name)
{
  cmValue pv = this->GetOption(var_name);
  if (!pv) {
    return {};
  }
  return *pv;
}

// Produce UCL in the given @p manifest file for the common
// manifest fields (common to the compact and regular formats),
// by reading the CPACK_FREEBSD_* variables.
void cmCPackFreeBSDGenerator::write_manifest_fields(
  cmGeneratedFileStream& manifest)
{
  manifest << ManifestKeyValue("name",
                               var_lookup("CPACK_FREEBSD_PACKAGE_NAME"));
  manifest << ManifestKeyValue("origin",
                               var_lookup("CPACK_FREEBSD_PACKAGE_ORIGIN"));
  manifest << ManifestKeyValue("version",
                               var_lookup("CPACK_FREEBSD_PACKAGE_VERSION"));
  manifest << ManifestKeyValue("maintainer",
                               var_lookup("CPACK_FREEBSD_PACKAGE_MAINTAINER"));
  manifest << ManifestKeyValue("comment",
                               var_lookup("CPACK_FREEBSD_PACKAGE_COMMENT"));
  manifest << ManifestKeyValue(
    "desc", var_lookup("CPACK_FREEBSD_PACKAGE_DESCRIPTION"));
  manifest << ManifestKeyValue("www", var_lookup("CPACK_FREEBSD_PACKAGE_WWW"));
  cmList licenses{ var_lookup("CPACK_FREEBSD_PACKAGE_LICENSE") };
  std::string licenselogic("single");
  if (licenses.empty()) {
    cmSystemTools::SetFatalErrorOccurred();
  } else if (licenses.size() > 1) {
    licenselogic = var_lookup("CPACK_FREEBSD_PACKAGE_LICENSE_LOGIC");
  }
  manifest << ManifestKeyValue("licenselogic", licenselogic);
  manifest << (ManifestKeyListValue("licenses") << licenses);
  cmList categories{ var_lookup("CPACK_FREEBSD_PACKAGE_CATEGORIES") };
  manifest << (ManifestKeyListValue("categories") << categories);
  manifest << ManifestKeyValue("prefix", var_lookup("CMAKE_INSTALL_PREFIX"));
  cmList deps{ var_lookup("CPACK_FREEBSD_PACKAGE_DEPS") };
  if (!deps.empty()) {
    manifest << (ManifestKeyDepsValue("deps") << deps);
  }
}

// Package only actual files; others are ignored (in particular,
// intermediate subdirectories are ignored).
static bool ignore_file(std::string const& filename)
{
  struct stat statbuf;
  return stat(filename.c_str(), &statbuf) < 0 ||
    (statbuf.st_mode & S_IFMT) != S_IFREG;
}

// Write the given list of @p files to the manifest stream @p s,
// as the UCL field "files" (which is dictionary-valued, to
// associate filenames with hashes). All the files are transformed
// to paths relative to @p toplevel, with a leading / (since the paths
// in FreeBSD package files are supposed to be absolute).
void write_manifest_files(cmGeneratedFileStream& s,
                          std::string const& toplevel,
                          std::vector<std::string> const& files)
{
  s << "\"files\": {\n";
  for (std::string const& file : files) {
    s << "  \"/" << cmSystemTools::RelativePath(toplevel, file) << "\": \""
      << "<sha256>" // this gets replaced by libpkg by the actual SHA256
      << "\",\n";
  }
  s << "  },\n";
}

int cmCPackFreeBSDGenerator::PackageFiles()
{
  if (!this->ReadListFile("Internal/CPack/CPackFreeBSD.cmake")) {
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "Error while executing CPackFreeBSD.cmake" << std::endl);
    return 0;
  }

  cmWorkingDirectory wd(toplevel);

  files.erase(std::remove_if(files.begin(), files.end(), ignore_file),
              files.end());

  std::string manifestname = toplevel + "/+MANIFEST";
  {
    cmGeneratedFileStream manifest(manifestname);
    manifest << "{\n";
    write_manifest_fields(manifest);
    write_manifest_files(manifest, toplevel, files);
    manifest << "}\n";
  }

  cmCPackLogger(cmCPackLog::LOG_DEBUG, "Toplevel: " << toplevel << std::endl);

  if (WantsComponentInstallation()) {
    // CASE 1 : COMPONENT ALL-IN-ONE package
    // If ALL COMPONENTS in ONE package has been requested
    // then the package file is unique and should be open here.
    if (componentPackageMethod == ONE_PACKAGE) {
      return PackageComponentsAllInOne();
    }
    // CASE 2 : COMPONENT CLASSICAL package(s) (i.e. not all-in-one)
    // There will be 1 package for each component group
    // however one may require to ignore component group and
    // in this case you'll get 1 package for each component.
    return PackageComponents(componentPackageMethod ==
                             ONE_PACKAGE_PER_COMPONENT);
  }

  // There should be one name in the packageFileNames (already, see comment
  // in cmCPackGenerator::DoPackage(), which holds what CPack guesses
  // will be the package filename. libpkg does something else, though,
  // so update the single filename to what we know will be right.
  if (this->packageFileNames.size() == 1) {
    std::string currentPackage = this->packageFileNames[0];
    auto lastSlash = currentPackage.rfind('/');

    // If there is a pathname, preserve that; libpkg will write out
    // a file with the package name and version as specified in the
    // manifest, so we look those up (again). lastSlash is the slash
    // itself, we need that as path separator to the calculated package name.
    std::string actualPackage =
      ((lastSlash != std::string::npos)
         ? std::string(currentPackage, 0, lastSlash + 1)
         : std::string()) +
      var_lookup("CPACK_FREEBSD_PACKAGE_NAME") + '-' +
      var_lookup("CPACK_FREEBSD_PACKAGE_VERSION") + FreeBSDPackageSuffix_17;

    this->packageFileNames.clear();
    this->packageFileNames.emplace_back(actualPackage);
  }

  if (!pkg_initialized() && pkg_init(nullptr, nullptr) != EPKG_OK) {
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "Can not initialize FreeBSD libpkg." << std::endl);
    return 0;
  }

  std::string const output_dir = cmSystemTools::GetFilenamePath(toplevel);
  PkgCreate package(output_dir, toplevel, manifestname);
  if (package.isValid()) {
    if (!package.Create()) {
      cmCPackLogger(cmCPackLog::LOG_ERROR,
                    "Error during pkg_create()" << std::endl);
      return 0;
    }
  } else {
    cmCPackLogger(cmCPackLog::LOG_ERROR,
                  "Error before pkg_create()" << std::endl);
    return 0;
  }

  // Specifically looking for packages suffixed with the TAG
  std::string broken_suffix_17 =
    cmStrCat('-', var_lookup("CPACK_TOPLEVEL_TAG"), FreeBSDPackageSuffix_17);
  for (std::string& name : packageFileNames) {
    cmCPackLogger(cmCPackLog::LOG_DEBUG, "Packagefile " << name << std::endl);
    if (cmHasSuffix(name, broken_suffix_17)) {
      name.replace(name.size() - broken_suffix_17.size(), std::string::npos,
                   FreeBSDPackageSuffix_17);
      break;
    }
  }

  std::string const packageFileName =
    var_lookup("CPACK_PACKAGE_FILE_NAME") + FreeBSDPackageSuffix_17;
  if (packageFileNames.size() == 1 && !packageFileName.empty() &&
      packageFileNames[0] != packageFileName) {
    // Since libpkg always writes <name>-<version>.<suffix>,
    // if there is a CPACK_PACKAGE_FILE_NAME set, we need to
    // rename, and then re-set the name.
    std::string const sourceFile = packageFileNames[0];
    std::string const packageSubDirectory =
      cmSystemTools::GetParentDirectory(sourceFile);
    std::string const targetFileName =
      packageSubDirectory + '/' + packageFileName;
    if (cmSystemTools::RenameFile(sourceFile, targetFileName)) {
      this->packageFileNames.clear();
      this->packageFileNames.emplace_back(targetFileName);
    }
  }

  return 1;
}
