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

#include <algorithm>
#include <sstream>
#include <utility>

#ifdef CMAKE_BUILD_WITH_CMAKE
#    include "cmExportInstallAndroidMKGenerator.h"
#endif
#include "cmExportInstallFileGenerator.h"
#include "cmExportSet.h"
#include "cmInstallType.h"
#include "cmLocalGenerator.h"
#include "cmSystemTools.h"
#include "cmake.h"

cmInstallExportGenerator::cmInstallExportGenerator(
    cmExportSet* exportSet, const char* destination,
    const char*                     file_permissions,
    std::vector<std::string> const& configurations, const char* component,
    MessageLevel message, bool exclude_from_all, const char* filename,
    const char* name_space, bool exportOld, bool android)
: cmInstallGenerator(destination, configurations, component, message,
                     exclude_from_all)
, ExportSet(exportSet)
, FilePermissions(file_permissions)
, FileName(filename)
, Namespace(name_space)
, ExportOld(exportOld)
, LocalGenerator(nullptr)
{
    if(android)
    {
#ifdef CMAKE_BUILD_WITH_CMAKE
        this->EFGen = new cmExportInstallAndroidMKGenerator(this);
#endif
    } else
    {
        this->EFGen = new cmExportInstallFileGenerator(this);
    }
    exportSet->AddInstallation(this);
}

cmInstallExportGenerator::~cmInstallExportGenerator() { delete this->EFGen; }

void
cmInstallExportGenerator::Compute(cmLocalGenerator* lg)
{
    this->LocalGenerator = lg;
    this->ExportSet->Compute(lg);
}

void
cmInstallExportGenerator::ComputeTempDir()
{
    // Choose a temporary directory in which to generate the import
    // files to be installed.
    this->TempDir = this->LocalGenerator->GetCurrentBinaryDirectory();
    this->TempDir += cmake::GetCMakeFilesDirectory();
    this->TempDir += "/Export";
    if(this->Destination.empty())
    {
        return;
    }
    this->TempDir += "/";

    // Enforce a maximum length.
    bool useMD5 = false;
#if defined(_WIN32) || defined(__CYGWIN__)
    std::string::size_type const max_total_len = 250;
#else
    std::string::size_type const max_total_len = 1000;
#endif
    // Will generate files of the form "<temp-dir>/<base>-<config>.<ext>".
    std::string::size_type const len = this->TempDir.size() + 1 +
                                       this->FileName.size() + 1 +
                                       this->GetMaxConfigLength();
    if(len < max_total_len)
    {
        // Keep the total path length below the limit.
        std::string::size_type const max_len = max_total_len - len;
        if(this->Destination.size() > max_len)
        {
            useMD5 = true;
        }
    } else
    {
        useMD5 = true;
    }
    if(useMD5)
    {
        // Replace the destination path with a hash to keep it short.
        this->TempDir += cmSystemTools::ComputeStringMD5(this->Destination);
    } else
    {
        std::string dest = this->Destination;
        // Avoid unix full paths.
        if(dest[0] == '/')
        {
            dest[0] = '_';
        }
        // Avoid windows full paths by removing colons.
        std::replace(dest.begin(), dest.end(), ':', '_');
        // Avoid relative paths that go up the tree.
        cmSystemTools::ReplaceString(dest, "../", "__/");
        // Avoid spaces.
        std::replace(dest.begin(), dest.end(), ' ', '_');
        this->TempDir += dest;
    }
}

size_t
cmInstallExportGenerator::GetMaxConfigLength() const
{
    // Always use at least 8 for "noconfig".
    size_t len = 8;
    if(this->ConfigurationTypes->empty())
    {
        if(this->ConfigurationName.size() > 8)
        {
            len = this->ConfigurationName.size();
        }
    } else
    {
        for(std::string const& c : *this->ConfigurationTypes)
        {
            if(c.size() > len)
            {
                len = c.size();
            }
        }
    }
    return len;
}

void
cmInstallExportGenerator::GenerateScript(std::ostream& os)
{
    // Skip empty sets.
    if(ExportSet->GetTargetExports()->empty())
    {
        std::ostringstream e;
        e << "INSTALL(EXPORT) given unknown export \"" << ExportSet->GetName()
          << "\"";
        cmSystemTools::Error(e.str().c_str());
        return;
    }

    // Create the temporary directory in which to store the files.
    this->ComputeTempDir();
    cmSystemTools::MakeDirectory(this->TempDir);

    // Construct a temporary location for the file.
    this->MainImportFile = this->TempDir;
    this->MainImportFile += "/";
    this->MainImportFile += this->FileName;

    // Generate the import file for this export set.
    this->EFGen->SetExportFile(this->MainImportFile.c_str());
    this->EFGen->SetNamespace(this->Namespace);
    this->EFGen->SetExportOld(this->ExportOld);
    if(this->ConfigurationTypes->empty())
    {
        if(!this->ConfigurationName.empty())
        {
            this->EFGen->AddConfiguration(this->ConfigurationName);
        } else
        {
            this->EFGen->AddConfiguration("");
        }
    } else
    {
        for(std::string const& c : *this->ConfigurationTypes)
        {
            this->EFGen->AddConfiguration(c);
        }
    }
    this->EFGen->GenerateImportFile();

    // Perform the main install script generation.
    this->cmInstallGenerator::GenerateScript(os);
}

void
cmInstallExportGenerator::GenerateScriptConfigs(std::ostream& os, Indent indent)
{
    // Create the main install rules first.
    this->cmInstallGenerator::GenerateScriptConfigs(os, indent);

    // Now create a configuration-specific install rule for the import
    // file of each configuration.
    std::vector<std::string> files;
    for(auto const& i : this->EFGen->GetConfigImportFiles())
    {
        files.push_back(i.second);
        std::string config_test = this->CreateConfigTest(i.first);
        os << indent << "if(" << config_test << ")\n";
        this->AddInstallRule(os, this->Destination, cmInstallType_FILES, files,
                             false, this->FilePermissions.c_str(), nullptr,
                             nullptr, nullptr, indent.Next());
        os << indent << "endif()\n";
        files.clear();
    }
}

void
cmInstallExportGenerator::GenerateScriptActions(std::ostream& os, Indent indent)
{
    // Remove old per-configuration export files if the main changes.
    std::string installedDir = "$ENV{DESTDIR}";
    installedDir += this->ConvertToAbsoluteDestination(this->Destination);
    installedDir += "/";
    std::string installedFile = installedDir;
    installedFile += this->FileName;
    os << indent << "if(EXISTS \"" << installedFile << "\")\n";
    Indent indentN   = indent.Next();
    Indent indentNN  = indentN.Next();
    Indent indentNNN = indentNN.Next();
    /* clang-format off */
  os << indentN << "file(DIFFERENT EXPORT_FILE_CHANGED FILES\n"
     << indentN << "     \"" << installedFile << "\"\n"
     << indentN << "     \"" << this->MainImportFile << "\")\n";
  os << indentN << "if(EXPORT_FILE_CHANGED)\n";
  os << indentNN << "file(GLOB OLD_CONFIG_FILES \"" << installedDir
     << this->EFGen->GetConfigImportFileGlob() << "\")\n";
  os << indentNN << "if(OLD_CONFIG_FILES)\n";
  os << indentNNN << "message(STATUS \"Old export file \\\"" << installedFile
     << "\\\" will be replaced.  Removing files [${OLD_CONFIG_FILES}].\")\n";
  os << indentNNN << "file(REMOVE ${OLD_CONFIG_FILES})\n";
  os << indentNN << "endif()\n";
  os << indentN << "endif()\n";
  os << indent << "endif()\n";
    /* clang-format on */

    // Install the main export file.
    std::vector<std::string> files;
    files.push_back(this->MainImportFile);
    this->AddInstallRule(os, this->Destination, cmInstallType_FILES, files,
                         false, this->FilePermissions.c_str(), nullptr, nullptr,
                         nullptr, indent);
}
