#include <cassert>
#include <chrono>
#include <cstddef>
#include <cstdlib>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <thread>
#include <utility>
#include <vector>

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

#include "cmCTestHardwareAllocator.h"
#include "cmCTestHardwareSpec.h"
#include "cmCTestMultiProcessHandler.h"
#include "cmCTestTestHandler.h"
#include "cmFileLock.h"
#include "cmFileLockResult.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

/*
 * This helper program is used to verify that the CTest hardware allocation
 * feature is working correctly. It consists of two stages:
 *
 * 1) write - This stage receives the PROCESSES property of the test and
 *    compares it with the values passed in the CTEST_PROCESS_* environment
 *    variables. If it received all of the resources it expected, then it
 *    writes this information to a log file, which will be read in the verify
 *    stage.
 * 2) verify - This stage compares the log file with the hardware spec file to
 *    make sure that no resources were over-subscribed, deallocated without
 *    being allocated, or allocated without being deallocated.
 */

static int usage(const char* argv0)
{
  std::cout << "Usage: " << argv0 << " (write|verify) <args...>" << std::endl;
  return 1;
}

static int usageWrite(const char* argv0)
{
  std::cout << "Usage: " << argv0
            << " write <log-file> <test-name> <sleep-time-secs>"
               " [<processes-property>]"
            << std::endl;
  return 1;
}

static int usageVerify(const char* argv0)
{
  std::cout << "Usage: " << argv0
            << " verify <log-file> <hardware-spec-file> [<test-names>]"
            << std::endl;
  return 1;
}

static int doWrite(int argc, char const* const* argv)
{
  if (argc < 5 || argc > 6) {
    return usageWrite(argv[0]);
  }
  std::string logFile = argv[2];
  std::string testName = argv[3];
  unsigned int sleepTime = std::atoi(argv[4]);
  std::vector<std::map<
    std::string, std::vector<cmCTestMultiProcessHandler::HardwareAllocation>>>
    hardware;
  if (argc == 6) {
    // Parse processes property
    std::string processesProperty = argv[5];
    std::vector<
      std::vector<cmCTestTestHandler::cmCTestTestResourceRequirement>>
      processes;
    bool result =
      cmCTestTestHandler::ParseProcessesProperty(processesProperty, processes);
    (void)result;
    assert(result);

    // Verify process count
    const char* processCountEnv = cmSystemTools::GetEnv("CTEST_PROCESS_COUNT");
    if (!processCountEnv) {
      std::cout << "CTEST_PROCESS_COUNT should be defined" << std::endl;
      return 1;
    }
    int processCount = std::atoi(processCountEnv);
    if (processes.size() != std::size_t(processCount)) {
      std::cout << "CTEST_PROCESS_COUNT does not match expected processes"
                << std::endl
                << "Expected: " << processes.size() << std::endl
                << "Actual: " << processCount << std::endl;
      return 1;
    }

    if (!cmSystemTools::Touch(logFile + ".lock", true)) {
      std::cout << "Could not create lock file" << std::endl;
      return 1;
    }
    cmFileLock lock;
    auto lockResult =
      lock.Lock(logFile + ".lock", static_cast<unsigned long>(-1));
    if (!lockResult.IsOk()) {
      std::cout << "Could not lock file" << std::endl;
      return 1;
    }
    std::size_t i = 0;
    cmsys::ofstream fout(logFile.c_str(), std::ios::app);
    fout << "begin " << testName << std::endl;
    for (auto& process : processes) {
      try {
        // Build and verify set of expected resources
        std::set<std::string> expectedResources;
        for (auto const& it : process) {
          expectedResources.insert(it.ResourceType);
        }

        std::string prefix = "CTEST_PROCESS_";
        prefix += std::to_string(i);
        const char* actualResourcesCStr = cmSystemTools::GetEnv(prefix);
        if (!actualResourcesCStr) {
          std::cout << prefix << " should be defined" << std::endl;
          return 1;
        }

        auto actualResourcesVec =
          cmSystemTools::SplitString(actualResourcesCStr, ',');
        std::set<std::string> actualResources;
        for (auto const& r : actualResourcesVec) {
          if (!r.empty()) {
            actualResources.insert(r);
          }
        }

        if (actualResources != expectedResources) {
          std::cout << prefix << " did not list expected resources"
                    << std::endl;
          return 1;
        }

        // Verify that we got what we asked for and write it to the log
        prefix += '_';
        std::map<std::string,
                 std::vector<cmCTestMultiProcessHandler::HardwareAllocation>>
          hwEntry;
        for (auto const& type : actualResources) {
          auto it = process.begin();

          std::string varName = prefix;
          varName += cmSystemTools::UpperCase(type);
          const char* varVal = cmSystemTools::GetEnv(varName);
          if (!varVal) {
            std::cout << varName << " should be defined" << std::endl;
            return 1;
          }

          auto received = cmSystemTools::SplitString(varVal, ';');
          for (auto const& r : received) {
            while (it->ResourceType != type || it->UnitsNeeded == 0) {
              ++it;
              if (it == process.end()) {
                std::cout << varName << " did not list expected resources"
                          << std::endl;
                return 1;
              }
            }
            auto split = cmSystemTools::SplitString(r, ',');
            if (split.size() != 2) {
              std::cout << varName << " was ill-formed" << std::endl;
              return 1;
            }
            if (!cmHasLiteralPrefix(split[0], "id:")) {
              std::cout << varName << " was ill-formed" << std::endl;
              return 1;
            }
            auto id = split[0].substr(3);
            if (!cmHasLiteralPrefix(split[1], "slots:")) {
              std::cout << varName << " was ill-formed" << std::endl;
              return 1;
            }
            auto slots = split[1].substr(6);
            unsigned int amount = std::atoi(slots.c_str());
            if (amount != static_cast<unsigned int>(it->SlotsNeeded)) {
              std::cout << varName << " did not list expected resources"
                        << std::endl;
              return 1;
            }
            --it->UnitsNeeded;

            fout << "alloc " << type << " " << id << " " << amount
                 << std::endl;
            hwEntry[type].push_back({ id, amount });
          }

          bool ended = false;
          while (it->ResourceType != type || it->UnitsNeeded == 0) {
            ++it;
            if (it == process.end()) {
              ended = true;
              break;
            }
          }

          if (!ended) {
            std::cout << varName << " did not list expected resources"
                      << std::endl;
            return 1;
          }
        }
        hardware.push_back(hwEntry);

        ++i;
      } catch (...) {
        std::cout << "Unknown error while processing resources" << std::endl;
        return 1;
      }
    }

    auto unlockResult = lock.Release();
    if (!unlockResult.IsOk()) {
      std::cout << "Could not unlock file" << std::endl;
      return 1;
    }
  } else {
    if (cmSystemTools::GetEnv("CTEST_PROCESS_COUNT")) {
      std::cout << "CTEST_PROCESS_COUNT should not be defined" << std::endl;
      return 1;
    }
  }

  std::this_thread::sleep_for(std::chrono::seconds(sleepTime));

  if (argc == 6) {
    if (!cmSystemTools::Touch(logFile + ".lock", true)) {
      std::cout << "Could not create lock file" << std::endl;
      return 1;
    }
    cmFileLock lock;
    auto lockResult =
      lock.Lock(logFile + ".lock", static_cast<unsigned long>(-1));
    if (!lockResult.IsOk()) {
      std::cout << "Could not lock file" << std::endl;
      return 1;
    }
    cmsys::ofstream fout(logFile.c_str(), std::ios::app);
    for (auto const& process : hardware) {
      for (auto const& it : process) {
        for (auto const& it2 : it.second) {
          fout << "dealloc " << it.first << " " << it2.Id << " " << it2.Slots
               << std::endl;
        }
      }
    }

    fout << "end " << testName << std::endl;

    auto unlockResult = lock.Release();
    if (!unlockResult.IsOk()) {
      std::cout << "Could not unlock file" << std::endl;
      return 1;
    }
  }

  return 0;
}

static int doVerify(int argc, char const* const* argv)
{
  if (argc < 4 || argc > 5) {
    return usageVerify(argv[0]);
  }
  std::string logFile = argv[2];
  std::string hwFile = argv[3];
  std::string testNames;
  if (argc == 5) {
    testNames = argv[4];
  }
  auto testNameList = cmExpandedList(testNames, false);
  std::set<std::string> testNameSet(testNameList.begin(), testNameList.end());

  cmCTestHardwareSpec spec;
  if (!spec.ReadFromJSONFile(hwFile)) {
    std::cout << "Could not read hardware spec " << hwFile << std::endl;
    return 1;
  }

  cmCTestHardwareAllocator allocator;
  allocator.InitializeFromHardwareSpec(spec);

  cmsys::ifstream fin(logFile.c_str(), std::ios::in);
  if (!fin) {
    std::cout << "Could not open log file " << logFile << std::endl;
    return 1;
  }

  std::string command;
  std::string resourceName;
  std::string resourceId;
  std::string testName;
  unsigned int amount;
  std::set<std::string> inProgressTests;
  std::set<std::string> completedTests;
  try {
    while (fin >> command) {
      if (command == "begin") {
        if (!(fin >> testName)) {
          std::cout << "Could not read begin line" << std::endl;
          return 1;
        }
        if (!testNameSet.count(testName) || inProgressTests.count(testName) ||
            completedTests.count(testName)) {
          std::cout << "Could not begin test" << std::endl;
          return 1;
        }
        inProgressTests.insert(testName);
      } else if (command == "alloc") {
        if (!(fin >> resourceName) || !(fin >> resourceId) ||
            !(fin >> amount)) {
          std::cout << "Could not read alloc line" << std::endl;
          return 1;
        }
        if (!allocator.AllocateResource(resourceName, resourceId, amount)) {
          std::cout << "Could not allocate resources" << std::endl;
          return 1;
        }
      } else if (command == "dealloc") {
        if (!(fin >> resourceName) || !(fin >> resourceId) ||
            !(fin >> amount)) {
          std::cout << "Could not read dealloc line" << std::endl;
          return 1;
        }
        if (!allocator.DeallocateResource(resourceName, resourceId, amount)) {
          std::cout << "Could not deallocate resources" << std::endl;
          return 1;
        }
      } else if (command == "end") {
        if (!(fin >> testName)) {
          std::cout << "Could not read end line" << std::endl;
          return 1;
        }
        if (!inProgressTests.erase(testName)) {
          std::cout << "Could not end test" << std::endl;
          return 1;
        }
        if (!completedTests.insert(testName).second) {
          std::cout << "Could not end test" << std::endl;
          return 1;
        }
      }
    }
  } catch (...) {
    std::cout << "Unknown error while reading log file" << std::endl;
    return 1;
  }

  auto const& avail = allocator.GetResources();
  for (auto const& it : avail) {
    for (auto const& it2 : it.second) {
      if (it2.second.Locked != 0) {
        std::cout << "Resource was not unlocked" << std::endl;
        return 1;
      }
    }
  }

  if (completedTests != testNameSet) {
    std::cout << "Tests were not ended" << std::endl;
    return 1;
  }

  return 0;
}

int main(int argc, char const* const* argv)
{
  cmsys::Encoding::CommandLineArguments args =
    cmsys::Encoding::CommandLineArguments::Main(argc, argv);
  argc = args.argc();
  argv = args.argv();

  if (argc < 2) {
    return usage(argv[0]);
  }

  std::string argv1 = argv[1];
  if (argv1 == "write") {
    return doWrite(argc, argv);
  }
  if (argv1 == "verify") {
    return doVerify(argc, argv);
  }
  return usage(argv[0]);
}
