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

#include <functional>
#include <map>
#include <string>
#include <utility>
#include <vector>

#include <cmext/string_view>

#include <cm3p/json/reader.h>
#include <cm3p/json/value.h>

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

#include "cmJSONHelpers.h"

namespace {
const cmsys::RegularExpression IdentifierRegex{ "^[a-z_][a-z0-9_]*$" };
const cmsys::RegularExpression IdRegex{ "^[a-z0-9_]+$" };

struct Version
{
  int Major = 1;
  int Minor = 0;
};

struct TopVersion
{
  struct Version Version;
};

auto const VersionFieldHelper =
  cmJSONIntHelper<cmCTestResourceSpec::ReadFileResult>(
    cmCTestResourceSpec::ReadFileResult::READ_OK,
    cmCTestResourceSpec::ReadFileResult::INVALID_VERSION);

auto const VersionHelper =
  cmJSONRequiredHelper<Version, cmCTestResourceSpec::ReadFileResult>(
    cmCTestResourceSpec::ReadFileResult::NO_VERSION,
    cmJSONObjectHelper<Version, cmCTestResourceSpec::ReadFileResult>(
      cmCTestResourceSpec::ReadFileResult::READ_OK,
      cmCTestResourceSpec::ReadFileResult::INVALID_VERSION)
      .Bind("major"_s, &Version::Major, VersionFieldHelper)
      .Bind("minor"_s, &Version::Minor, VersionFieldHelper));

auto const RootVersionHelper =
  cmJSONObjectHelper<TopVersion, cmCTestResourceSpec::ReadFileResult>(
    cmCTestResourceSpec::ReadFileResult::READ_OK,
    cmCTestResourceSpec::ReadFileResult::INVALID_ROOT)
    .Bind("version"_s, &TopVersion::Version, VersionHelper, false);

cmCTestResourceSpec::ReadFileResult ResourceIdHelper(std::string& out,
                                                     const Json::Value* value)
{
  auto result = cmJSONStringHelper(
    cmCTestResourceSpec::ReadFileResult::READ_OK,
    cmCTestResourceSpec::ReadFileResult::INVALID_RESOURCE)(out, value);
  if (result != cmCTestResourceSpec::ReadFileResult::READ_OK) {
    return result;
  }
  cmsys::RegularExpressionMatch match;
  if (!IdRegex.find(out.c_str(), match)) {
    return cmCTestResourceSpec::ReadFileResult::INVALID_RESOURCE;
  }
  return cmCTestResourceSpec::ReadFileResult::READ_OK;
}

auto const ResourceHelper =
  cmJSONObjectHelper<cmCTestResourceSpec::Resource,
                     cmCTestResourceSpec::ReadFileResult>(
    cmCTestResourceSpec::ReadFileResult::READ_OK,
    cmCTestResourceSpec::ReadFileResult::INVALID_RESOURCE)
    .Bind("id"_s, &cmCTestResourceSpec::Resource::Id, ResourceIdHelper)
    .Bind("slots"_s, &cmCTestResourceSpec::Resource::Capacity,
          cmJSONUIntHelper(
            cmCTestResourceSpec::ReadFileResult::READ_OK,
            cmCTestResourceSpec::ReadFileResult::INVALID_RESOURCE, 1),
          false);

auto const ResourceListHelper =
  cmJSONVectorHelper<cmCTestResourceSpec::Resource,
                     cmCTestResourceSpec::ReadFileResult>(
    cmCTestResourceSpec::ReadFileResult::READ_OK,
    cmCTestResourceSpec::ReadFileResult::INVALID_RESOURCE_TYPE,
    ResourceHelper);

auto const ResourceMapHelper =
  cmJSONMapFilterHelper<std::vector<cmCTestResourceSpec::Resource>,
                        cmCTestResourceSpec::ReadFileResult>(
    cmCTestResourceSpec::ReadFileResult::READ_OK,
    cmCTestResourceSpec::ReadFileResult::INVALID_SOCKET_SPEC,
    ResourceListHelper, [](const std::string& key) -> bool {
      cmsys::RegularExpressionMatch match;
      return IdentifierRegex.find(key.c_str(), match);
    });

auto const SocketSetHelper = cmJSONVectorHelper<
  std::map<std::string, std::vector<cmCTestResourceSpec::Resource>>>(
  cmCTestResourceSpec::ReadFileResult::READ_OK,
  cmCTestResourceSpec::ReadFileResult::INVALID_SOCKET_SPEC, ResourceMapHelper);

cmCTestResourceSpec::ReadFileResult SocketHelper(
  cmCTestResourceSpec::Socket& out, const Json::Value* value)
{
  std::vector<
    std::map<std::string, std::vector<cmCTestResourceSpec::Resource>>>
    sockets;
  cmCTestResourceSpec::ReadFileResult result = SocketSetHelper(sockets, value);
  if (result != cmCTestResourceSpec::ReadFileResult::READ_OK) {
    return result;
  }
  if (sockets.size() > 1) {
    return cmCTestResourceSpec::ReadFileResult::INVALID_SOCKET_SPEC;
  }
  if (sockets.empty()) {
    out.Resources.clear();
  } else {
    out.Resources = std::move(sockets[0]);
  }
  return cmCTestResourceSpec::ReadFileResult::READ_OK;
}

auto const LocalRequiredHelper =
  cmJSONRequiredHelper<cmCTestResourceSpec::Socket,
                       cmCTestResourceSpec::ReadFileResult>(
    cmCTestResourceSpec::ReadFileResult::INVALID_SOCKET_SPEC, SocketHelper);

auto const RootHelper =
  cmJSONObjectHelper<cmCTestResourceSpec, cmCTestResourceSpec::ReadFileResult>(
    cmCTestResourceSpec::ReadFileResult::READ_OK,
    cmCTestResourceSpec::ReadFileResult::INVALID_ROOT)
    .Bind("local", &cmCTestResourceSpec::LocalSocket, LocalRequiredHelper,
          false);
}

cmCTestResourceSpec::ReadFileResult cmCTestResourceSpec::ReadFromJSONFile(
  const std::string& filename)
{
  cmsys::ifstream fin(filename.c_str());
  if (!fin) {
    return ReadFileResult::FILE_NOT_FOUND;
  }

  Json::Value root;
  Json::CharReaderBuilder builder;
  if (!Json::parseFromStream(builder, fin, &root, nullptr)) {
    return ReadFileResult::JSON_PARSE_ERROR;
  }

  TopVersion version;
  ReadFileResult result;
  if ((result = RootVersionHelper(version, &root)) !=
      ReadFileResult::READ_OK) {
    return result;
  }
  if (version.Version.Major != 1 || version.Version.Minor != 0) {
    return ReadFileResult::UNSUPPORTED_VERSION;
  }

  return RootHelper(*this, &root);
}

const char* cmCTestResourceSpec::ResultToString(ReadFileResult result)
{
  switch (result) {
    case ReadFileResult::READ_OK:
      return "OK";

    case ReadFileResult::FILE_NOT_FOUND:
      return "File not found";

    case ReadFileResult::JSON_PARSE_ERROR:
      return "JSON parse error";

    case ReadFileResult::INVALID_ROOT:
      return "Invalid root object";

    case ReadFileResult::NO_VERSION:
      return "No version specified";

    case ReadFileResult::INVALID_VERSION:
      return "Invalid version object";

    case ReadFileResult::UNSUPPORTED_VERSION:
      return "Unsupported version";

    case ReadFileResult::INVALID_SOCKET_SPEC:
      return "Invalid socket object";

    case ReadFileResult::INVALID_RESOURCE_TYPE:
      return "Invalid resource type object";

    case ReadFileResult::INVALID_RESOURCE:
      return "Invalid resource object";

    default:
      return "Unknown";
  }
}

bool cmCTestResourceSpec::operator==(const cmCTestResourceSpec& other) const
{
  return LocalSocket == other.LocalSocket;
}

bool cmCTestResourceSpec::operator!=(const cmCTestResourceSpec& other) const
{
  return !(*this == other);
}

bool cmCTestResourceSpec::Socket::operator==(
  const cmCTestResourceSpec::Socket& other) const
{
  return Resources == other.Resources;
}

bool cmCTestResourceSpec::Socket::operator!=(
  const cmCTestResourceSpec::Socket& other) const
{
  return !(*this == other);
}

bool cmCTestResourceSpec::Resource::operator==(
  const cmCTestResourceSpec::Resource& other) const
{
  return Id == other.Id && Capacity == other.Capacity;
}

bool cmCTestResourceSpec::Resource::operator!=(
  const cmCTestResourceSpec::Resource& other) const
{
  return !(*this == other);
}
