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

#include <utility>

#include <cm/string_view>

#include "cmsys/FStream.hxx"
#include "cmsys/RegularExpression.hxx"

#include "cmCTest.h"
#include "cmProcessTools.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmXMLWriter.h"

cmCTestCVS::cmCTestCVS(cmCTest* ct, std::ostream& log)
  : cmCTestVC(ct, log)
{
}

cmCTestCVS::~cmCTestCVS() = default;

class cmCTestCVS::UpdateParser : public cmCTestVC::LineParser
{
public:
  UpdateParser(cmCTestCVS* cvs, const char* prefix)
    : CVS(cvs)
  {
    SetLog(&cvs->Log, prefix);
    // See "man cvs", section "update output".
    RegexFileUpdated.compile("^([UP])  *(.*)");
    RegexFileModified.compile("^([MRA])  *(.*)");
    RegexFileConflicting.compile("^([C])  *(.*)");
    RegexFileRemoved1.compile(
      "cvs[^ ]* update: `?([^']*)'? is no longer in the repository");
    RegexFileRemoved2.compile(
      "cvs[^ ]* update: "
      "warning: `?([^']*)'? is not \\(any longer\\) pertinent");
  }

private:
  cmCTestCVS* CVS;
  cmsys::RegularExpression RegexFileUpdated;
  cmsys::RegularExpression RegexFileModified;
  cmsys::RegularExpression RegexFileConflicting;
  cmsys::RegularExpression RegexFileRemoved1;
  cmsys::RegularExpression RegexFileRemoved2;

  bool ProcessLine() override
  {
    if (RegexFileUpdated.find(Line)) {
      DoFile(PathUpdated, RegexFileUpdated.match(2));
    } else if (RegexFileModified.find(Line)) {
      DoFile(PathModified, RegexFileModified.match(2));
    } else if (RegexFileConflicting.find(Line)) {
      DoFile(PathConflicting, RegexFileConflicting.match(2));
    } else if (RegexFileRemoved1.find(Line)) {
      DoFile(PathUpdated, RegexFileRemoved1.match(1));
    } else if (RegexFileRemoved2.find(Line)) {
      DoFile(PathUpdated, RegexFileRemoved2.match(1));
    }
    return true;
  }

  void DoFile(PathStatus status, std::string const& file)
  {
    std::string dir = cmSystemTools::GetFilenamePath(file);
    std::string name = cmSystemTools::GetFilenameName(file);
    CVS->Dirs[dir][name] = status;
  }
};

bool cmCTestCVS::UpdateImpl()
{
  // Get user-specified update options.
  std::string opts = CTest->GetCTestConfiguration("UpdateOptions");
  if (opts.empty()) {
    opts = CTest->GetCTestConfiguration("CVSUpdateOptions");
    if (opts.empty()) {
      opts = "-dP";
    }
  }
  std::vector<std::string> args = cmSystemTools::ParseArguments(opts);

  // Specify the start time for nightly testing.
  if (CTest->GetTestModel() == cmCTest::NIGHTLY) {
    args.push_back("-D" + GetNightlyTime() + " UTC");
  }

  // Run "cvs update" to update the work tree.
  std::vector<char const*> cvs_update;
  cvs_update.push_back(CommandLineTool.c_str());
  cvs_update.push_back("-z3");
  cvs_update.push_back("update");
  for (std::string const& arg : args) {
    cvs_update.push_back(arg.c_str());
  }
  cvs_update.push_back(nullptr);

  UpdateParser out(this, "up-out> ");
  UpdateParser err(this, "up-err> ");
  return RunUpdateCommand(&cvs_update[0], &out, &err);
}

class cmCTestCVS::LogParser : public cmCTestVC::LineParser
{
public:
  using Revision = cmCTestCVS::Revision;
  LogParser(cmCTestCVS* cvs, const char* prefix, std::vector<Revision>& revs)
    : CVS(cvs)
    , Revisions(revs)
    , Section(SectionHeader)
  {
    SetLog(&cvs->Log, prefix);
    RegexRevision.compile("^revision +([^ ]*) *$");
    RegexBranches.compile("^branches: .*$");
    RegexPerson.compile("^date: +([^;]+); +author: +([^;]+);");
  }

private:
  cmCTestCVS* CVS;
  std::vector<Revision>& Revisions;
  cmsys::RegularExpression RegexRevision;
  cmsys::RegularExpression RegexBranches;
  cmsys::RegularExpression RegexPerson;
  enum SectionType
  {
    SectionHeader,
    SectionRevisions,
    SectionEnd
  };
  SectionType Section;
  Revision Rev;

  bool ProcessLine() override
  {
    if (Line ==
        ("======================================="
         "======================================")) {
      // This line ends the revision list.
      if (Section == SectionRevisions) {
        FinishRevision();
      }
      Section = SectionEnd;
    } else if (Line == "----------------------------") {
      // This line divides revisions from the header and each other.
      if (Section == SectionHeader) {
        Section = SectionRevisions;
      } else if (Section == SectionRevisions) {
        FinishRevision();
      }
    } else if (Section == SectionRevisions) {
      // XXX(clang-tidy): https://bugs.llvm.org/show_bug.cgi?id=44165
      // NOLINTNEXTLINE(bugprone-branch-clone)
      if (!Rev.Log.empty()) {
        // Continue the existing log.
        Rev.Log += Line;
        Rev.Log += '\n';
      } else if (Rev.Rev.empty() && RegexRevision.find(Line)) {
        Rev.Rev = RegexRevision.match(1);
      } else if (Rev.Date.empty() && RegexPerson.find(Line)) {
        Rev.Date = RegexPerson.match(1);
        Rev.Author = RegexPerson.match(2);
      } else if (!RegexBranches.find(Line)) {
        // Start the log.
        Rev.Log += Line;
        Rev.Log += '\n';
      }
    }
    return Section != SectionEnd;
  }

  void FinishRevision()
  {
    if (!Rev.Rev.empty()) {
      // Record this revision.
      /* clang-format off */
      CVS->Log << "Found revision " << Rev.Rev << "\n"
                     << "  author = " << Rev.Author << "\n"
                     << "  date = " << Rev.Date << "\n";
      /* clang-format on */
      Revisions.push_back(Rev);

      // We only need two revisions.
      if (Revisions.size() >= 2) {
        Section = SectionEnd;
      }
    }
    Rev = Revision();
  }
};

std::string cmCTestCVS::ComputeBranchFlag(std::string const& dir)
{
  // Compute the tag file location for this directory.
  std::string tagFile = SourceDirectory;
  if (!dir.empty()) {
    tagFile += "/";
    tagFile += dir;
  }
  tagFile += "/CVS/Tag";

  // Lookup the branch in the tag file, if any.
  std::string tagLine;
  cmsys::ifstream tagStream(tagFile.c_str());
  if (tagStream && cmSystemTools::GetLineFromStream(tagStream, tagLine) &&
      tagLine.size() > 1 && tagLine[0] == 'T') {
    // Use the branch specified in the tag file.
    std::string flag = cmStrCat("-r", cm::string_view(tagLine).substr(1));
    return flag;
  }
  // Use the default branch.
  return "-b";
}

void cmCTestCVS::LoadRevisions(std::string const& file, const char* branchFlag,
                               std::vector<Revision>& revisions)
{
  cmCTestLog(CTest, HANDLER_OUTPUT, "." << std::flush);

  // Run "cvs log" to get revisions of this file on this branch.
  const char* cvs = CommandLineTool.c_str();
  const char* cvs_log[] = {
    cvs, "log", "-N", branchFlag, file.c_str(), nullptr
  };

  LogParser out(this, "log-out> ", revisions);
  OutputLogger err(Log, "log-err> ");
  RunChild(cvs_log, &out, &err);
}

void cmCTestCVS::WriteXMLDirectory(cmXMLWriter& xml, std::string const& path,
                                   Directory const& dir)
{
  const char* slash = path.empty() ? "" : "/";
  xml.StartElement("Directory");
  xml.Element("Name", path);

  // Lookup the branch checked out in the working tree.
  std::string branchFlag = ComputeBranchFlag(path);

  // Load revisions and write an entry for each file in this directory.
  std::vector<Revision> revisions;
  for (auto const& fi : dir) {
    std::string full = path + slash + fi.first;

    // Load two real or unknown revisions.
    revisions.clear();
    if (fi.second != PathUpdated) {
      // For local modifications the current rev is unknown and the
      // prior rev is the latest from cvs.
      revisions.push_back(Unknown);
    }
    LoadRevisions(full, branchFlag.c_str(), revisions);
    revisions.resize(2, Unknown);

    // Write the entry for this file with these revisions.
    File f(fi.second, &revisions[0], &revisions[1]);
    WriteXMLEntry(xml, path, fi.first, full, f);
  }
  xml.EndElement(); // Directory
}

bool cmCTestCVS::WriteXMLUpdates(cmXMLWriter& xml)
{
  cmCTestLog(CTest, HANDLER_OUTPUT,
             "   Gathering version information (one . per updated file):\n"
             "    "
               << std::flush);

  for (auto const& d : Dirs) {
    WriteXMLDirectory(xml, d.first, d.second);
  }

  cmCTestLog(CTest, HANDLER_OUTPUT, std::endl);

  return true;
}
