/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
#include "cmUVisionTargetGenerator.h"
#include "cmComputeLinkInformation.h"
#include "cmCustomCommand.h"
#include "cmGeneratedFileStream.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalUVisionGenerator.h"
#include "cmLinkLineComputer.h"
#include "cmLocalUVisionGenerator.h"
#include "cmMakefile.h"
#include "cmSourceFile.h"
#include "cmSourceGroup.h"
#include "cmTarget.h"
#include <cmXMLParser.h>
#include <cmake.h>

class cmUvProjectVersionParser : public cmXMLParser
{
public:
  cmUvProjectVersionParser(std::string newTargetName, bool markAsLib,
                           std::string preBuildCmdLine,
                           const std::vector<cmSourceFile*>& sourceFiles,
                           const std::vector<std::string>& includes,
                           std::string outputDirectory,
                           std::string outputBaseFilename,
                           std::vector<std::string> libraries,
                           std::vector<std::string> defines)
    : ignore(0)
    , Sources(sourceFiles)
    , Includes(includes)
    , PreBuildCmdLine(preBuildCmdLine)
    , OutputDirectory(outputDirectory)
    , OutputBaseFilename(outputBaseFilename)
    , Libraries(libraries)
    , Defines(defines)
  {
    cmSystemTools::ReplaceString(this->PreBuildCmdLine, "/", "\\");
    cmSystemTools::ReplaceString(this->OutputDirectory, "/", "\\");

    this->TargetName = newTargetName;
    this->CreateLib = markAsLib;
    this->xmlOutput =
      "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>";
  }

  bool isGroups(const std::string& name) { return name == "Groups"; }
  bool isTargetName(const std::string& name) { return name == "TargetName"; }
  bool isCreateLib(const std::string& name) { return name == "CreateLib"; }
  bool isOutputDirectory(const std::string& name)
  {
    return name == "OutputDirectory";
  }
  bool isOutputName(const std::string& name) { return name == "OutputName"; }

  bool isCreateExecutable(const std::string& name)
  {
    return name == "CreateExecutable";
  }
  bool isBeforeCompile(const std::string& name)
  {
    return name == "BeforeMake";
  }
  bool isDefine(const std::string& name) { return name == "Define"; }

  bool isIncludePath(const std::string& name) { return name == "IncludePath"; }
  bool isInIgnoreList(const std::string& name)
  {
    return isGroups(name) || isTargetName(name) || isCreateLib(name) ||
      isCreateExecutable(name) || isIncludePath(name) ||
      isBeforeCompile(name) || isOutputDirectory(name) || isOutputName(name) ||
      isDefine(name);
  }

  void StartElement(const std::string& name, const char** attributes) override
  {
    if (isInIgnoreList(name)) {
      this->ignore++;
    } else {
      if (this->ignore == 0) {
        this->xmlOutput += "<" + name;
        for (int i = 0; attributes[i]; i += 2) {
          this->xmlOutput += " " + std::string(attributes[i]);
          this->xmlOutput += "=\"";
          this->xmlOutput += attributes[i + 1];
          this->xmlOutput += "\" ";
        }
        this->xmlOutput += ">";
      }
    }
  }

  void EndElement(const std::string& name) override
  {
    if (isInIgnoreList(name)) {
      this->ignore--;
      if (isTargetName(name)) {
        this->xmlOutput += std::string("<TargetName>") + this->TargetName +
          std::string("</TargetName>");
      } else if (isBeforeCompile(name) && !this->PreBuildCmdLine.empty()) {

        this->xmlOutput += "<BeforeMake>\n";
        this->xmlOutput += "<RunUserProg1>1</RunUserProg1>\n";
        this->xmlOutput +=
          "<UserProg1Name>" + this->PreBuildCmdLine + "</UserProg1Name>\n";
        this->xmlOutput += "<UserProg1Dos16Mode>0</UserProg1Dos16Mode>\n";
        this->xmlOutput += "</BeforeMake>\n";
      } else if (isOutputDirectory(name)) {
        cmSystemTools::ReplaceString(OutputDirectory, "/", "\\");

        this->xmlOutput +=
          "<OutputDirectory>" + OutputDirectory + "\\" + "</OutputDirectory>";
      } else if (isOutputName(name)) {
        this->xmlOutput +=
          "<OutputName>" + OutputBaseFilename + "</OutputName>";
      } else if (isCreateLib(name)) {
        this->xmlOutput += "<CreateLib>";
        this->xmlOutput += this->CreateLib ? "1" : "0";
        this->xmlOutput += "</CreateLib>";
      } else if (isCreateExecutable(name)) {
        this->xmlOutput += "<CreateExecutable>";
        this->xmlOutput += this->CreateLib ? "0" : "1";
        this->xmlOutput += "</CreateExecutable>";
      } else if (isIncludePath(name)) {
        this->xmlOutput += "<IncludePath>";
        for (std::string id : this->Includes) {
          cmSystemTools::ReplaceString(id, "/", "\\");
          this->xmlOutput += id;
          this->xmlOutput += ";";
        }
        this->xmlOutput += "</IncludePath>";
      } else if (isGroups(name)) {
        this->xmlOutput += "<Groups>";
        this->xmlOutput += "<Group>";
        this->xmlOutput += "<GroupName>Sources</GroupName>";
        this->xmlOutput += "<Files>";

        for (const cmSourceFile* fullname : this->Sources) {
          // fullname: c:/test/abc/test.c
          // --> basename = test.c
          // --> pathname = c:/test/abc/

          // skip unknown or non C/CXX Files
          std::string pathname = fullname->GetFullPath();
          std::string basename = fullname->GetLocation().GetName();

          std::string filetype;
          std::string lang(fullname->GetLanguage());
          if (lang == "C")
            filetype = "1";
          else if (lang == "CXX")
            filetype = "8";
          else if (fullname->GetExtension() == "s")
            filetype = "2";
          else
            filetype = "5";
          cmSystemTools::ReplaceString(pathname, "/", "\\");

          this->xmlOutput += "<File>"
                             "<FileName>" +
            basename +
            "</FileName>"
            "<FileType>" +
            filetype +
            "</FileType>"
            "<FilePath>" +
            pathname + "</FilePath></File>";
        }
        this->xmlOutput += "</Files>";
        this->xmlOutput += "</Group>";

        this->xmlOutput += "<Group>";
        this->xmlOutput += "<GroupName>Libraries</GroupName>";
        this->xmlOutput += "<Files>";
        for (std::string lib : this->Libraries) {
          cmSystemTools::ReplaceString(lib, "/", "\\");

          std::string basename = cmSystemTools::GetFilenameName(lib);
          this->xmlOutput += "<File>"
                             "<FileName>" +
            basename +
            "</FileName>"
            "<FileType>4</FileType>"
            "<FilePath>" +
            lib + "</FilePath></File>";
        }
        this->xmlOutput += "</Files>";
        this->xmlOutput += "</Group>";

        this->xmlOutput += "</Groups>";
      } else if (isDefine(name)) {
        this->xmlOutput += "<Define>";
        while (this->Data[0] == ' ') {
          this->Data.erase(0, 1);
        }
        if (!this->Data.empty()) {
          this->xmlOutput += this->Data + ",";
        }
        this->xmlOutput += "__KEIL__=1,";
        for (std::string def : this->Defines) {
          this->xmlOutput += def + ",";
        }
        //********************************
        this->xmlOutput += "</Define>";
      }
    } else {
      if (this->ignore == 0) {
        this->xmlOutput += "</" + name + "> ";
      }
    }
  }
  void CharacterDataHandler(const char* data, int length) override
  {
    this->Data = std::string(data, length);
    if (this->ignore == 0) {
      this->xmlOutput.append(data, length);
    } else {
      // pass
    }
  }

  std::string Data;
  const std::string& Output() { return this->xmlOutput; }

protected:
  std::string TargetName;
  bool CreateLib;
  unsigned ignore; // if not 0 irgnore whole tag and data
  std::string
    xmlOutput; // this will containt the rebuild xml file after parsing
  std::vector<cmSourceFile*> Sources;
  std::vector<std::string> Libraries;
  std::vector<std::string> Includes;
  std::string PreBuildCmdLine;
  std::string OutputDirectory;
  std::string OutputBaseFilename;
  std::vector<std::string> Defines;
};

cmUVisionTargetGenerator::cmUVisionTargetGenerator(cmGeneratorTarget* target)
  : GeneratorTarget(target)
  , LocalGenerator(
      static_cast<cmLocalUVisionGenerator*>(target->GetLocalGenerator()))
  , Makefile(target->Target->GetMakefile())
  , Name(target->GetName())
{
  // Store the configuration name that is being used
  if (const char* config = this->Makefile->GetDefinition("CMAKE_BUILD_TYPE")) {
    // Use the build type given by the user.
    this->ConfigName = config;
  } else {
    // No configuration type given.
    this->ConfigName.clear();
  }
}

cmUVisionTargetGenerator::~cmUVisionTargetGenerator()
{
}

void cmUVisionTargetGenerator::Generate()
{
  switch (this->GeneratorTarget->GetType()) {
    case cmStateEnums::EXECUTABLE:
    case cmStateEnums::STATIC_LIBRARY:
    case cmStateEnums::OBJECT_LIBRARY:
    case cmStateEnums::UTILITY:
      // pass --> supported types
      break;
    case cmStateEnums::SHARED_LIBRARY: {
      std::string msg = "add_library(<name> SHARED ...) not supported: ";
      msg += this->Name;
      cmSystemTools::Message(msg);
      return;
    }
    case cmStateEnums::MODULE_LIBRARY: {
      std::string msg = "add_library(<name> MODULE ...) not supported: ";
      msg += this->Name;
      cmSystemTools::Message(msg);
      return;
    }
    default: {
      std::string msg =
        "cmUVisionTargetGenerator Type for target not supported: ";
      msg += this->Name;
      cmSystemTools::Message(msg);
    }
      return;
  }

  // Tell the global generator the name of the project file
  this->GeneratorTarget->Target->SetProperty("GENERATOR_FILE_NAME",
                                             this->Name.c_str());
  this->GenerateTarget();
}

std::string cmUVisionTargetGenerator::getTemplateFileContent()
{
  std::string templateFileContent;
  const char* templateProject =
    this->GeneratorTarget->Makefile->GetDefinition("CMAKE_UVISION5_TEMPLATE");

  if (templateProject) {
    const std::string language(
      this->GeneratorTarget->GetLinkerLanguage(this->ConfigName));
    std::ifstream is(templateProject, std::ifstream::binary);
    if (is) {
      std::ifstream t(templateProject);
      std::stringstream buffer;
      buffer << t.rdbuf();
      templateFileContent = buffer.str();
    } else {
      cmSystemTools::Error("Could not open uVision Project Template file\r\n" +
                           std::string(templateProject));
      cmSystemTools::SetFatalErrorOccured();
    }
  } else {
    cmSystemTools::Error("Missing definition of uVision Project Template"
                         "(CMAKE_UVISION5_TEMPLATE)");
    cmSystemTools::SetFatalErrorOccured();
  }
  return templateFileContent;
}

void cmUVisionTargetGenerator::GenerateTarget()
{
  cmStateEnums::TargetType type = this->GeneratorTarget->GetType();
  std::string templateFileContent = getTemplateFileContent();

  std::string fname = this->LocalGenerator->GetCurrentBinaryDirectory();
  if (type == cmStateEnums::UTILITY) {
    //  this->Makefile->Output
    cmSourceFile* sourceFile = this->Makefile->GetSourceFileWithOutput(
      fname + "/CMakeFiles/" + this->Name);
    if (sourceFile) {
      cmCustomCommand* cc = sourceFile->GetCustomCommand();

      fname += "/";
      fname += this->Name;
      fname += ".cmd";

      this->GetGlobalGenerator()->appendUtility(fname);

      cmGeneratedFileStream utilityBuildScript(fname.c_str());
      utilityBuildScript.SetCopyIfDifferent(true);
      utilityBuildScript
        << "@echo CMAKE Generated Utility RunScript for uVision5\r\n";

      utilityBuildScript << "cd /D " + cc->GetWorkingDirectory() << "\r\n";
      for (cmCustomCommandLine cline : cc->GetCommandLines()) {
        for (std::string clineParts : cline) {
          utilityBuildScript << clineParts << " ";
        }
        utilityBuildScript << "\r\n";
      }
      utilityBuildScript.close();
    }
  } else {
    fname += "/";
    fname += this->Name;
    fname += ".uvprojx";
    this->GetGlobalGenerator()->appendProjectFile(
      fname); // add this project into the global generator to get a uVision
              // MultiProjectWorkspace

    std::string cmdLine;
    if (!this->GetGlobalGenerator()->PreBuildSteps().empty()) {
      // uVision only supports two PreBuildCmds --> so we add them into one
      cmdLine = this->LocalGenerator->GetCurrentBinaryDirectory() +
        this->Name + ".prebuild.cmd";
      // Generate the UVision Project for the current Target .. for PREBUILD we
      // generate separate Projects and do not add these to existing
      // PROJECTS-PreBuild-Steps

      cmGeneratedFileStream preBuildScript(cmdLine.c_str());
      preBuildScript.SetCopyIfDifferent(true);
      for (std::string pendingUtility :
           this->GetGlobalGenerator()->PreBuildSteps()) {
        preBuildScript << "CALL " + pendingUtility + "\r\n";
      }
      preBuildScript.close();
      // clear all PreBuildSteps so far...
      this->GetGlobalGenerator()->PreBuildSteps().clear();
    } else {
      // pass, no pending utils
    }

    std::vector<cmSourceFile*> sources;
    this->GeneratorTarget->GetSourceFiles(sources, this->ConfigName);
    const std::string language(
      this->GeneratorTarget->GetLinkerLanguage(this->ConfigName));

    std::vector<std::string> includes;
    this->LocalGenerator->GetIncludeDirectories(
      includes, this->GeneratorTarget, language, this->ConfigName);
    const cmGeneratorTarget::OutputInfo* outputInfo =
      (this->GeneratorTarget->GetOutputInfo(this->ConfigName));

    std::string linkLibraries;
    std::string flags;
    std::string linkFlags;
    std::string frameworkPath;
    std::string linkPath;

    std::unique_ptr<cmLinkLineComputer> linkLineComputer(
      this->GetGlobalGenerator()->CreateLinkLineComputer(
        this->LocalGenerator,
        this->LocalGenerator->GetStateSnapshot().GetDirectory()));
    this->LocalGenerator->GetTargetFlags(
      linkLineComputer.get(), this->ConfigName, linkLibraries, flags,
      linkFlags, frameworkPath, linkPath, this->GeneratorTarget);
    std::string defines = this->GetDefines(language, this->ConfigName);
    std::vector<std::string> definesList =
      cmSystemTools::SplitString(defines, ' ');
    for (std::string& i : definesList) {
      i = i.substr(2);
    }
    // write out link options
    std::vector<std::string> lopts =
      cmSystemTools::ParseArguments(linkLibraries.c_str());
    std::vector<std::string> libraries;
    for (auto& l : lopts) {
      libraries.push_back(l);
    }
    cmUvProjectVersionParser projectFile(
      this->Name, type != cmStateEnums::EXECUTABLE, cmdLine, sources, includes,
      outputInfo->OutDir,
      this->GeneratorTarget->GetOutputName(
        this->ConfigName, cmStateEnums::RuntimeBinaryArtifact),
      libraries, definesList);
    projectFile.Parse(templateFileContent.c_str());
    cmGeneratedFileStream fout(fname.c_str());
    fout.SetCopyIfDifferent(true);
    fout << projectFile.Output();
    fout.Close();
  }
}

cmGlobalUVisionGenerator* cmUVisionTargetGenerator::GetGlobalGenerator() const
{
  return static_cast<cmGlobalUVisionGenerator*>(
    this->LocalGenerator->GetGlobalGenerator());
}

std::string cmUVisionTargetGenerator::GetDefines(const std::string& language,
                                                 std::string const& config)
{
  std::map<std::string, std::string>::iterator i =
    this->DefinesByLanguage.find(language);
  if (i == this->DefinesByLanguage.end()) {
    std::set<std::string> defines;
    const char* lang = language.c_str();
    // Add preprocessor definitions for this target and configuration.
    this->LocalGenerator->GetTargetDefines(this->GeneratorTarget, config,
                                           language, defines);
    std::string definesString;
    this->LocalGenerator->JoinDefines(defines, definesString, lang);

    std::map<std::string, std::string>::value_type entry(language,
                                                         definesString);
    i = this->DefinesByLanguage.insert(entry).first;
  }
  return i->second;
}
