#include "cmFASTBuildTargetGenerator.h"

#include "cmCustomCommand.h"
#include "cmCustomCommandGenerator.h"
#include "cmFASTBuildNormalTargetGenerator.h"
#include "cmFASTBuildUtilityTargetGenerator.h"
#include "cmGeneratedFileStream.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalFASTBuildGenerator.h"
#include "cmLocalFASTBuildGenerator.h"
#include "cmMakefile.h"
#include "cmOSXBundleGenerator.h"
#include "cmRulePlaceholderExpander.h"
#include "cmSourceFile.h"

#define FASTBUILD_DOLLAR_TAG "FASTBUILD_DOLLAR_TAG"

cmFASTBuildTargetGenerator::CustomCommandAliasMap
  cmFASTBuildTargetGenerator::s_customCommandAliases;

cmFASTBuildTargetGenerator* cmFASTBuildTargetGenerator::New(
  cmGeneratorTarget* target)
{
  switch (target->GetType()) {
    case cmStateEnums::EXECUTABLE:
    case cmStateEnums::SHARED_LIBRARY:
    case cmStateEnums::STATIC_LIBRARY:
    case cmStateEnums::MODULE_LIBRARY:
    case cmStateEnums::OBJECT_LIBRARY:
      return new cmFASTBuildNormalTargetGenerator(target);

    case cmStateEnums::UTILITY:
    case cmStateEnums::GLOBAL_TARGET:
      return new cmFASTBuildUtilityTargetGenerator(target);

    default:
      return nullptr;
  }
}

cmFASTBuildTargetGenerator::cmFASTBuildTargetGenerator(
  cmGeneratorTarget* target)
  : cmCommonTargetGenerator(target)
  , LocalGenerator(
      static_cast<cmLocalFASTBuildGenerator*>(target->GetLocalGenerator()))
  , MacOSXContentGenerator(nullptr)
  , OSXBundleGenerator(nullptr)
{
  MacOSXContentGenerator = new MacOSXContentGeneratorType(this);
}

cmFASTBuildTargetGenerator::~cmFASTBuildTargetGenerator()
{
  delete this->MacOSXContentGenerator;
}

std::string cmFASTBuildTargetGenerator::GetConfigName()
{
  auto const& configNames = this->LocalGenerator->GetConfigNames();
  assert(configNames.size() == 1);
  return configNames.front();
}

void cmFASTBuildTargetGenerator::MacOSXContentGeneratorType::operator()(
  cmSourceFile const& source, const char* pkgloc, const std::string& config)
{
  // Skip OS X content when not building a Framework or Bundle.
  if (!this->Generator->GetGeneratorTarget()->IsBundleOnApple()) {
    return;
  }

  std::string macdir =
    this->Generator->OSXBundleGenerator->InitMacOSXContentDirectory(pkgloc,
                                                                    config);

  // Get the input file location.
  std::string input = source.GetFullPath();
  input = this->Generator->GetGlobalGenerator()->ConvertToFASTBuildPath(input);

  // Get the output file location.
  std::string output = macdir;
  output += "/";
  output += cmSystemTools::GetFilenameName(input);
  output =
    this->Generator->GetGlobalGenerator()->ConvertToFASTBuildPath(output);

  // Write a build statement to copy the content into the bundle.
  cmGlobalFASTBuildGenerator::WriteCommand(
    this->Generator->GetBuildFileStream(), "Copy",
    cmGlobalFASTBuildGenerator::Quote(output), 1);
  cmGlobalFASTBuildGenerator::Indent(this->Generator->GetBuildFileStream(), 1);
  this->Generator->GetBuildFileStream() << "{\n";
  cmGlobalFASTBuildGenerator::WriteVariable(
    this->Generator->GetBuildFileStream(), "Source",
    cmGlobalFASTBuildGenerator::Quote(input), 2);
  cmGlobalFASTBuildGenerator::WriteVariable(
    this->Generator->GetBuildFileStream(), "Dest",
    cmGlobalFASTBuildGenerator::Quote(output), 2);
  cmGlobalFASTBuildGenerator::Indent(this->Generator->GetBuildFileStream(), 1);
  this->Generator->GetBuildFileStream() << "}\n";

  this->Generator->ExtraFiles.push_back(output);
}

void cmFASTBuildTargetGenerator::UnescapeFASTBuildVariables(
  std::string& string)
{
  // Unescape the FASTBuild configName symbol with $
  cmSystemTools::ReplaceString(string, "^", "^^");
  cmSystemTools::ReplaceString(string, "$$", "^$");
  cmSystemTools::ReplaceString(string, FASTBUILD_DOLLAR_TAG, "$");
}

void cmFASTBuildTargetGenerator::UnescapeFASTBuildDefines(std::string& string)
{
  std::string sep = "  ";
  std::vector<std::string> chunks;
  size_t pos = 0;
  for (std::string token; (pos = string.find(sep)) != std::string::npos;) {
    token = string.substr(0, pos);
    chunks.push_back(token);
    string.erase(0, pos + sep.length());
  }
  chunks.push_back(string);

  string = "";
  sep = "";
  for (const auto& chunk : chunks) {
    if (!sep.empty() && !chunk.empty())
      sep += "\\\"";
    string += sep + chunk;
    sep = "\\\" \\\"\\\" \\\"";
    if (!chunk.empty())
      sep = "\\\"" + sep;
  }
}

std::string cmFASTBuildTargetGenerator::GetCustomCommandTargetName(
  const cmCustomCommand& cc, const std::string& extra) const
{
  std::string targetName = "cc";

  const std::vector<std::string>& outputs = cc.GetOutputs();
  const std::vector<std::string>& byproducts = cc.GetByproducts();
  std::vector<std::string> mergedOutputs;
  mergedOutputs.insert(mergedOutputs.end(), outputs.begin(), outputs.end());
  mergedOutputs.insert(mergedOutputs.end(), byproducts.begin(),
                       byproducts.end());

  std::string extras = extra;
  // If this exec node always generates outputs,
  // then we need to make sure we don't define outputs multiple times.
  // but if the command should always run (i.e. post builds etc)
  // then we will output a new one.
  // when generating output file, makes realpath as part of targetName
  // to make it unique
  for (const auto& output : mergedOutputs) {
    std::string relPath = this->ConvertToFASTBuildPath(output);
    extras += "-" + relPath;
  }

  cmCryptoHash hash(cmCryptoHash::AlgoSHA256);
  targetName += "-" + hash.HashString(extras).substr(0, 7);

  return targetName;
}

bool cmFASTBuildTargetGenerator::isConfigDependant(
  const cmCustomCommandGenerator* ccg)
{
  using StringVector = std::vector<std::string>;
  StringVector outputs = ccg->GetOutputs();
  StringVector byproducts = ccg->GetByproducts();

  std::for_each(outputs.begin(), outputs.end(), &UnescapeFASTBuildVariables);
  std::for_each(byproducts.begin(), byproducts.end(),
                &UnescapeFASTBuildVariables);

  // Make sure that the outputs don't depend on the config name
  for (StringVector::const_iterator iter = outputs.begin();
       iter != outputs.end(); ++iter) {
    const std::string& str = *iter;
    if (str.find("$ConfigName$") != std::string::npos) {
      return true;
    }
  }
  for (StringVector::const_iterator iter = byproducts.begin();
       iter != byproducts.end(); ++iter) {
    const std::string& str = *iter;
    if (str.find("$ConfigName$") != std::string::npos) {
      return true;
    }
  }

  return false;
}

void cmFASTBuildTargetGenerator::ResolveFASTBuildVariables(
  std::string& string, const std::string& configName)
{ // Replace FASTBuild configName with the config name
  cmSystemTools::ReplaceString(string, "$ConfigName$", configName);
}

std::vector<cmGlobalFASTBuildGenerator::FASTBuildExecNode>
cmFASTBuildTargetGenerator::GenerateCommands(const std::string& buildStep)
{
  std::vector<cmGlobalFASTBuildGenerator::FASTBuildExecNode> nodes;

  const std::string& configName = this->GetConfigName();
  const std::string& hostTargetName = GeneratorTarget->GetName();

  std::vector<cmCustomCommand> commands;
  if (buildStep == "PreBuild")
    commands = GeneratorTarget->GetPreBuildCommands();
  else if (buildStep == "PreLink")
    commands = GeneratorTarget->GetPreLinkCommands();
  else if (buildStep == "PostBuild")
    commands = GeneratorTarget->GetPostBuildCommands();
  else {
    std::vector<cmSourceFile const*> customCommands;
    GeneratorTarget->GetCustomCommands(customCommands, configName);
    std::unordered_multimap<cmSourceFile const*, cmSourceFile const*>
      dependencies;
    for (cmSourceFile const* source : customCommands) {
      cmCustomCommandGenerator ccg(*source->GetCustomCommand(), configName,
                                   LocalCommonGenerator);
      for (const std::string& dep : ccg.GetDepends()) {
        // Check if we know how to generate this file.
        cmSourcesWithOutput sources =
          this->LocalGenerator->GetSourcesWithOutput(dep);
        // If we failed to find a target or source and we have a relative path,
        // it might be a valid source if made relative to the current binary
        // directory.
        if (!sources.Target && !sources.Source &&
            !cmSystemTools::FileIsFullPath(dep)) {
          auto fullname =
            cmStrCat(this->Makefile->GetCurrentBinaryDirectory(), '/', dep);
          fullname = cmSystemTools::CollapseFullPath(
            fullname, this->Makefile->GetHomeOutputDirectory());
          sources = this->LocalGenerator->GetSourcesWithOutput(fullname);
        }

        // If this dependency comes from a custom command, add that command to
        // the dependencies list
        if (sources.Source) {
          auto command =
            std::find_if(customCommands.begin(), customCommands.end(),
                         [src = sources.Source](cmSourceFile const* source) {
                           return src == source;
                         });
          // Found and not self
          if (command != customCommands.end() && source != *command) {
            dependencies.emplace(source, *command);
          }
        }
      }
    }

    cmGlobalFASTBuildGenerator::SortByDependencies(customCommands,
                                                   dependencies);

    for (cmSourceFile const* source : customCommands)
      commands.emplace_back(*source->GetCustomCommand());
  }

  int i = 0;
  for (const cmCustomCommand& cc : commands) {
    // We need to generate the command for execution.
    cmCustomCommandGenerator ccg(cc, configName, LocalCommonGenerator);

    std::string targetName;
    if (!buildStep.empty()) {
      targetName = Makefile->GetCurrentBinaryDirectory();
      targetName += "/";
      targetName += GeneratorTarget->GetName();
      targetName = this->ConvertToFASTBuildPath(targetName);
      targetName += "_" + buildStep + "_" + std::to_string(++i);
    }
    targetName = GetCustomCommandTargetName(cc, targetName);
    std::vector<std::string> inputTargets;
    std::vector<std::string> inputFiles;
    // Take the dependencies listed and split into targets and files.
    for (const std::string& dep : ccg.GetDepends()) {
      if (GlobalCommonGenerator->FindTarget(dep)) {
        // Keep as target name for now, we'll need to search for a target later
        inputTargets.push_back(dep);
      } else {
        // Try to convert file name to full path
        std::string realDep;
        GetLocalGenerator()->GetRealDependency(dep, this->GetConfigName(),
                                               realDep);
        inputFiles.push_back(realDep);
      }
    }

    std::vector<std::string> cmdLines;
    if (ccg.GetNumberOfCommands() > 0) {
      std::string wd = ccg.GetWorkingDirectory();
      if (wd.empty()) {
        wd = this->LocalGenerator->GetCurrentBinaryDirectory();
      }

      std::ostringstream cdCmd;
#ifdef _WIN32
      std::string cdStr = "cd /D ";
#else
      std::string cdStr = "cd ";
#endif
      cdCmd << cdStr
            << this->LocalGenerator->ConvertToOutputFormat(
                 wd, cmOutputConverter::SHELL);
      cmdLines.push_back(cdCmd.str());
    }

    std::string launcher;

    const auto property_value =
      this->Makefile->GetProperty("RULE_LAUNCH_CUSTOM");

    if (property_value && !property_value->empty()) {
      // Expand rule variables referenced in the given launcher command.
      cmRulePlaceholderExpander::RuleVariables vars;

      std::string output;
      const std::vector<std::string>& outputs = ccg.GetOutputs();
      if (!outputs.empty()) {
        output = outputs[0];
        if (ccg.GetWorkingDirectory().empty()) {
          output = this->LocalGenerator->MaybeRelativeToCurBinDir(output);
        }
        output = this->LocalGenerator->ConvertToOutputFormat(
          output, cmOutputConverter::SHELL);
      }
      vars.Output = output.c_str();

      std::unique_ptr<cmRulePlaceholderExpander> rulePlaceholderExpander(
        this->LocalGenerator->CreateRulePlaceholderExpander());

      std::string launcher = *property_value;
      rulePlaceholderExpander->ExpandRuleVariables(this->LocalGenerator,
                                                   launcher, vars);
      if (!launcher.empty()) {
        launcher += " ";
      }
    }

    for (unsigned i = 0; i != ccg.GetNumberOfCommands(); ++i) {
      cmdLines.push_back(launcher +
                         this->LocalGenerator->ConvertToOutputFormat(
                           ccg.GetCommand(i), cmOutputConverter::SHELL));

      std::string& cmd = cmdLines.back();
      ccg.AppendArguments(i, cmd);
    }

    std::for_each(inputFiles.begin(), inputFiles.end(),
                  &UnescapeFASTBuildVariables);

    cmGlobalFASTBuildGenerator::FASTBuildExecNode execNode;
    execNode.Name = targetName;
    execNode.IsNoop = cmdLines.empty();

    std::vector<std::string> outputs;
    for (std::string const& output : ccg.GetOutputs()) {
      if (cmSourceFile* sf = this->Makefile->GetSource(output)) {
        if (!sf->GetPropertyAsBool("SYMBOLIC")) {
          outputs.push_back(output);
        }
      }
    }
    execNode.ExecAlways = inputFiles.empty() && inputTargets.empty();
    for (std::string const& output : cc.GetByproducts()) {
      if (cmSourceFile* sf = this->Makefile->GetSource(output)) {
        if (!sf->GetPropertyAsBool("SYMBOLIC")) {
          outputs.push_back(output);
        }
      }
    }

    if (!execNode.IsNoop) {
      std::string scriptFileName = Makefile->GetCurrentBinaryDirectory();
      scriptFileName += "/CMakeFiles";
      scriptFileName += "/";
      scriptFileName += targetName;

#ifdef _WIN32
      scriptFileName += ".bat";
#else
      scriptFileName += ".sh";
#endif

      cmsys::ofstream scriptFile(scriptFileName.c_str());

#ifdef _WIN32
      scriptFile << "@echo off\n";
      int line = 1;
#else
      scriptFile << "set -e\n\n";
#endif
      std::string output;
      if (outputs.size() == 1) {
        output = outputs[0];
      } else {
        // Currently fastbuild doesn't support more than 1
        // output for a custom command (soon to change hopefully).
        std::string outputDir =
          LocalCommonGenerator->GetMakefile()->GetHomeOutputDirectory();
        output = outputDir + "/dummy-out-" + targetName + ".txt";
        std::string cmakeCommand =
          this->GetLocalGenerator()->ConvertToOutputFormat(
            cmSystemTools::GetCMakeCommand(), cmOutputConverter::SHELL);
        cmdLines.push_back(cmakeCommand + " -E touch " + output);
        // Forward command output to the file
        execNode.ExecUseStdOutAsOutput = true;
      }
      execNode.ExecOutput = ConvertToFASTBuildPath(output);

      for (auto cmd : cmdLines) {
        cmSystemTools::ReplaceString(cmd, "$$", "$");
        cmSystemTools::ReplaceString(cmd, FASTBUILD_DOLLAR_TAG, "$");
#ifdef _WIN32
        // in windows batch, '%' is a special character that needs to be
        // doubled to be escaped
        cmSystemTools::ReplaceString(cmd, "%", "%%");
#endif
        ResolveFASTBuildVariables(cmd, configName);
#ifdef _WIN32
        scriptFile << cmd << " || (set FAIL_LINE=" << ++line
                   << "& goto :ABORT)" << '\n';
#else
        scriptFile << cmd << '\n';
#endif
      }

#ifdef _WIN32
      scriptFile << "goto :EOF\n\n"
                    ":ABORT\n"
                    "set ERROR_CODE=%ERRORLEVEL%\n"
                    "echo Batch file failed at line %FAIL_LINE% "
                    "with errorcode %ERRORLEVEL%\n"
                    "exit /b %ERROR_CODE%";
#endif

#ifdef _WIN32
      execNode.ExecExecutable = cmSystemTools::FindProgram("cmd.exe");
      execNode.ExecArguments = "/C " + scriptFileName;
#else
      execNode.ExecExecutable = cmSystemTools::FindProgram("sh");
      execNode.ExecArguments = scriptFileName;
#endif
      std::string workingDirectory = ccg.GetWorkingDirectory();
      if (workingDirectory.empty()) {
        workingDirectory =
          this->LocalCommonGenerator->GetCurrentBinaryDirectory();
      }
      if (!workingDirectory.empty()) {
        execNode.ExecWorkingDir = workingDirectory;
      }
    }

    // Remove empty file names
    inputFiles.erase(std::remove_if(inputFiles.begin(), inputFiles.end(),
                                    [](const auto& s) { return s.empty(); }),
                     inputFiles.end());
    // Add files to input dependencies, after being converted to local path
    execNode.ExecInput =
      GetGlobalGenerator()->ConvertToFASTBuildPath(inputFiles);
    // Also add targets to input dependencies
    for (auto const& target : inputTargets) {
      execNode.ExecInput.push_back(target);
    }

    // Make sure we execute in order if it's a buildStep
    if (!buildStep.empty() && !nodes.empty())
      execNode.PreBuildDependencies.insert(nodes.back().Name);

    for (const std::string& dep : ccg.GetDepends()) {
      auto depFilePath = dep;
      if (!cmSystemTools::FileIsFullPath(depFilePath)) {
        depFilePath = cmSystemTools::CollapseFullPath(
          cmStrCat(this->Makefile->GetCurrentBinaryDirectory(), '/', dep));
      }
      // If this dependency comes from a custom command, add that command to
      // the dependencies list
      auto command = std::find_if(
        commands.begin(), commands.end(),
        [currentCc = &cc, &depFilePath](cmCustomCommand const& cc) {
          // Ignore self
          if (&cc != currentCc) {
            const std::vector<std::string>& outputs = cc.GetOutputs();
            return std::find(outputs.begin(), outputs.end(), depFilePath) !=
              outputs.end();
          }
          return false;
        });

      if (command != commands.end()) {
        execNode.PreBuildDependencies.insert(
          GetCustomCommandTargetName(*command));
      }
    }

    // Make sure we depend from 'noop' if we have a full noop command
    if (execNode.IsNoop && execNode.PreBuildDependencies.empty()) {
      execNode.PreBuildDependencies.insert("noop");
    }

    nodes.push_back(execNode);

    if (outputs.size() > 1) {
      for (const auto& output : outputs) {
        cmGlobalFASTBuildGenerator::FASTBuildExecNode noop;
        noop.Name = execNode.Name;
        cmCryptoHash hash(cmCryptoHash::AlgoSHA256);
        noop.Name += "-" + hash.HashString(output).substr(0, 7);
        noop.PreBuildDependencies.insert(execNode.Name);
        noop.ExecInput.push_back(execNode.ExecOutput);
        noop.ExecOutput = ConvertToFASTBuildPath(output);
        std::string cmakeCommand =
          this->GetLocalGenerator()->ConvertToOutputFormat(
            cmSystemTools::GetCMakeCommand(), cmOutputConverter::SHELL);
        noop.ExecExecutable = cmakeCommand;
        noop.ExecArguments = " -E touch " + output;
        noop.ExecWorkingDir = execNode.ExecWorkingDir;
      }
    }
  }

  return nodes;
}

std::string cmFASTBuildTargetGenerator::GetTargetName() const
{
  return this->GeneratorTarget->GetName();
}

void cmFASTBuildTargetGenerator::AddIncludeFlags(std::string&,
                                                 std::string const&,
                                                 const std::string&)
{
}

std::string cmFASTBuildTargetGenerator::GetClangTidyReplacementsFilePath(
  std::string const& directory, cmSourceFile const& source,
  std::string const& config) const
{
  auto const& objectName = this->GeneratorTarget->GetObjectName(&source);
  auto fixesFile = cmSystemTools::CollapseFullPath(cmStrCat(
    directory, '/',
    this->GeneratorTarget->GetLocalGenerator()->MaybeRelativeToTopBinDir(
      cmStrCat(this->GeneratorTarget->GetLocalGenerator()
                 ->GetCurrentBinaryDirectory(),
               '/',
               this->GeneratorTarget->GetLocalGenerator()->GetTargetDirectory(
                 this->GeneratorTarget),
               '/',
               this->GetGlobalGenerator()->ConfigDirectory(config),
               '/', objectName, ".yaml"))));
  return fixesFile;
}


std::string cmFASTBuildTargetGenerator::GetName()
{
  return GeneratorTarget->GetName();
}

std::string cmFASTBuildTargetGenerator::ConvertToFASTBuildPath(
  const std::string& path) const
{
  return GetGlobalGenerator()->ConvertToFASTBuildPath(path);
}

cmGlobalFASTBuildGenerator* cmFASTBuildTargetGenerator::GetGlobalGenerator()
  const
{
  return this->LocalGenerator->GetGlobalFASTBuildGenerator();
}
