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

#include <ostream>
#include <vector>

#include <cmext/algorithm>

#include "cmsys/RegularExpression.hxx"

#include "cmCTest.h"
#include "cmCTestVC.h"
#include "cmProcessTools.h"
#include "cmSystemTools.h"
#include "cmXMLParser.h"

cmCTestHG::cmCTestHG(cmCTest* ct, std::ostream& log)
  : cmCTestGlobalVC(ct, log)
{
  PriorRev = Unknown;
}

cmCTestHG::~cmCTestHG() = default;

class cmCTestHG::IdentifyParser : public cmCTestVC::LineParser
{
public:
  IdentifyParser(cmCTestHG* hg, const char* prefix, std::string& rev)
    : Rev(rev)
  {
    SetLog(&hg->Log, prefix);
    RegexIdentify.compile("^([0-9a-f]+)");
  }

private:
  std::string& Rev;
  cmsys::RegularExpression RegexIdentify;

  bool ProcessLine() override
  {
    if (RegexIdentify.find(Line)) {
      Rev = RegexIdentify.match(1);
      return false;
    }
    return true;
  }
};

class cmCTestHG::StatusParser : public cmCTestVC::LineParser
{
public:
  StatusParser(cmCTestHG* hg, const char* prefix)
    : HG(hg)
  {
    SetLog(&hg->Log, prefix);
    RegexStatus.compile("([MARC!?I]) (.*)");
  }

private:
  cmCTestHG* HG;
  cmsys::RegularExpression RegexStatus;

  bool ProcessLine() override
  {
    if (RegexStatus.find(Line)) {
      DoPath(RegexStatus.match(1)[0], RegexStatus.match(2));
    }
    return true;
  }

  void DoPath(char status, std::string const& path)
  {
    if (path.empty()) {
      return;
    }

    // See "hg help status".  Note that there is no 'conflict' status.
    switch (status) {
      case 'M':
      case 'A':
      case '!':
      case 'R':
        HG->DoModification(PathModified, path);
        break;
      case 'I':
      case '?':
      case 'C':
      case ' ':
      default:
        break;
    }
  }
};

std::string cmCTestHG::GetWorkingRevision()
{
  // Run plumbing "hg identify" to get work tree revision.
  const char* hg = CommandLineTool.c_str();
  const char* hg_identify[] = { hg, "identify", "-i", nullptr };
  std::string rev;
  IdentifyParser out(this, "rev-out> ", rev);
  OutputLogger err(Log, "rev-err> ");
  RunChild(hg_identify, &out, &err);
  return rev;
}

bool cmCTestHG::NoteOldRevision()
{
  OldRevision = GetWorkingRevision();
  cmCTestLog(CTest, HANDLER_OUTPUT,
             "   Old revision of repository is: " << OldRevision << "\n");
  PriorRev.Rev = OldRevision;
  return true;
}

bool cmCTestHG::NoteNewRevision()
{
  NewRevision = GetWorkingRevision();
  cmCTestLog(CTest, HANDLER_OUTPUT,
             "   New revision of repository is: " << NewRevision << "\n");
  return true;
}

bool cmCTestHG::UpdateImpl()
{
  // Use "hg pull" followed by "hg update" to update the working tree.
  {
    const char* hg = CommandLineTool.c_str();
    const char* hg_pull[] = { hg, "pull", "-v", nullptr };
    OutputLogger out(Log, "pull-out> ");
    OutputLogger err(Log, "pull-err> ");
    RunChild(&hg_pull[0], &out, &err);
  }

  // TODO: if(this->CTest->GetTestModel() == cmCTest::NIGHTLY)

  std::vector<char const*> hg_update;
  hg_update.push_back(CommandLineTool.c_str());
  hg_update.push_back("update");
  hg_update.push_back("-v");

  // Add user-specified update options.
  std::string opts = CTest->GetCTestConfiguration("UpdateOptions");
  if (opts.empty()) {
    opts = CTest->GetCTestConfiguration("HGUpdateOptions");
  }
  std::vector<std::string> args = cmSystemTools::ParseArguments(opts);
  for (std::string const& arg : args) {
    hg_update.push_back(arg.c_str());
  }

  // Sentinel argument.
  hg_update.push_back(nullptr);

  OutputLogger out(Log, "update-out> ");
  OutputLogger err(Log, "update-err> ");
  return RunUpdateCommand(&hg_update[0], &out, &err);
}

class cmCTestHG::LogParser
  : public cmCTestVC::OutputLogger
  , private cmXMLParser
{
public:
  LogParser(cmCTestHG* hg, const char* prefix)
    : OutputLogger(hg->Log, prefix)
    , HG(hg)
  {
    InitializeParser();
  }
  ~LogParser() override { CleanupParser(); }

private:
  cmCTestHG* HG;

  using Revision = cmCTestHG::Revision;
  using Change = cmCTestHG::Change;
  Revision Rev;
  std::vector<Change> Changes;
  Change CurChange;
  std::vector<char> CData;

  bool ProcessChunk(const char* data, int length) override
  {
    OutputLogger::ProcessChunk(data, length);
    ParseChunk(data, length);
    return true;
  }

  void StartElement(const std::string& name, const char** atts) override
  {
    CData.clear();
    if (name == "logentry") {
      Rev = Revision();
      if (const char* rev =
            cmCTestHG::LogParser::FindAttribute(atts, "revision")) {
        Rev.Rev = rev;
      }
      Changes.clear();
    }
  }

  void CharacterDataHandler(const char* data, int length) override
  {
    cm::append(CData, data, data + length);
  }

  void EndElement(const std::string& name) override
  {
    if (name == "logentry") {
      HG->DoRevision(Rev, Changes);
    } else if (!CData.empty() && name == "author") {
      Rev.Author.assign(&CData[0], CData.size());
    } else if (!CData.empty() && name == "email") {
      Rev.EMail.assign(&CData[0], CData.size());
    } else if (!CData.empty() && name == "date") {
      Rev.Date.assign(&CData[0], CData.size());
    } else if (!CData.empty() && name == "msg") {
      Rev.Log.assign(&CData[0], CData.size());
    } else if (!CData.empty() && name == "files") {
      std::vector<std::string> paths = SplitCData();
      for (std::string const& path : paths) {
        // Updated by default, will be modified using file_adds and
        // file_dels.
        CurChange = Change('U');
        CurChange.Path = path;
        Changes.push_back(CurChange);
      }
    } else if (!CData.empty() && name == "file_adds") {
      std::string added_paths(CData.begin(), CData.end());
      for (Change& change : Changes) {
        if (added_paths.find(change.Path) != std::string::npos) {
          change.Action = 'A';
        }
      }
    } else if (!CData.empty() && name == "file_dels") {
      std::string added_paths(CData.begin(), CData.end());
      for (Change& change : Changes) {
        if (added_paths.find(change.Path) != std::string::npos) {
          change.Action = 'D';
        }
      }
    }
    CData.clear();
  }

  std::vector<std::string> SplitCData()
  {
    std::vector<std::string> output;
    std::string currPath;
    for (char i : CData) {
      if (i != ' ') {
        currPath += i;
      } else {
        output.push_back(currPath);
        currPath.clear();
      }
    }
    output.push_back(currPath);
    return output;
  }

  void ReportError(int /*line*/, int /*column*/, const char* msg) override
  {
    HG->Log << "Error parsing hg log xml: " << msg << "\n";
  }
};

bool cmCTestHG::LoadRevisions()
{
  // Use 'hg log' to get revisions in a xml format.
  //
  // TODO: This should use plumbing or python code to be more precise.
  // The "list of strings" templates like {files} will not work when
  // the project has spaces in the path.  Also, they may not have
  // proper XML escapes.
  std::string range = OldRevision + ":" + NewRevision;
  const char* hg = CommandLineTool.c_str();
  const char* hgXMLTemplate = "<logentry\n"
                              "   revision=\"{node|short}\">\n"
                              "  <author>{author|person}</author>\n"
                              "  <email>{author|email}</email>\n"
                              "  <date>{date|isodate}</date>\n"
                              "  <msg>{desc}</msg>\n"
                              "  <files>{files}</files>\n"
                              "  <file_adds>{file_adds}</file_adds>\n"
                              "  <file_dels>{file_dels}</file_dels>\n"
                              "</logentry>\n";
  const char* hg_log[] = {
    hg,           "log",         "--removed", "-r", range.c_str(),
    "--template", hgXMLTemplate, nullptr
  };

  LogParser out(this, "log-out> ");
  out.Process("<?xml version=\"1.0\"?>\n"
              "<log>\n");
  OutputLogger err(Log, "log-err> ");
  RunChild(hg_log, &out, &err);
  out.Process("</log>\n");
  return true;
}

bool cmCTestHG::LoadModifications()
{
  // Use 'hg status' to get modified files.
  const char* hg = CommandLineTool.c_str();
  const char* hg_status[] = { hg, "status", nullptr };
  StatusParser out(this, "status-out> ");
  OutputLogger err(Log, "status-err> ");
  RunChild(hg_status, &out, &err);
  return true;
}
