/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst 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/value.h>

#include "cmsys/RegularExpression.hxx"

#include "cmJSONHelpers.h"

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

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

struct TopVersion
{
  struct Version Version;
};

auto const VersionFieldHelper =
  JSONHelperBuilder::Int(cmCTestResourceSpecErrors::INVALID_VERSION);

auto const VersionHelper = JSONHelperBuilder::Required<Version>(
  cmCTestResourceSpecErrors::NO_VERSION,
  JSONHelperBuilder::Object<Version>()
    .Bind("major"_s, &Version::Major, VersionFieldHelper)
    .Bind("minor"_s, &Version::Minor, VersionFieldHelper));

auto const RootVersionHelper = JSONHelperBuilder::Object<TopVersion>().Bind(
  "version"_s, &TopVersion::Version, VersionHelper, false);

bool ResourceIdHelper(std::string& out, Json::Value const* value,
                      cmJSONState* state)
{
  if (!JSONHelperBuilder::String(cmCTestResourceSpecErrors::INVALID_RESOURCE)(
        out, value, state)) {
    return false;
  }
  cmsys::RegularExpressionMatch match;
  if (!IdRegex.find(out.c_str(), match)) {
    cmCTestResourceSpecErrors::INVALID_RESOURCE(value, state);
    return false;
  }
  return true;
}

auto const ResourceHelper =
  JSONHelperBuilder::Object<cmCTestResourceSpec::Resource>()
    .Bind("id"_s, &cmCTestResourceSpec::Resource::Id, ResourceIdHelper)
    .Bind(
      "slots"_s, &cmCTestResourceSpec::Resource::Capacity,
      JSONHelperBuilder::UInt(cmCTestResourceSpecErrors::INVALID_RESOURCE, 1),
      false);

auto const ResourceListHelper =
  JSONHelperBuilder::Vector<cmCTestResourceSpec::Resource>(
    cmCTestResourceSpecErrors::INVALID_RESOURCE_TYPE, ResourceHelper);

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

auto const SocketSetHelper = JSONHelperBuilder::Vector<
  std::map<std::string, std::vector<cmCTestResourceSpec::Resource>>>(
  cmCTestResourceSpecErrors::INVALID_SOCKET_SPEC, ResourceMapHelper);

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

auto const LocalRequiredHelper =
  JSONHelperBuilder::Required<cmCTestResourceSpec::Socket>(
    cmCTestResourceSpecErrors::INVALID_SOCKET_SPEC, SocketHelper);

auto const RootHelper = JSONHelperBuilder::Object<cmCTestResourceSpec>().Bind(
  "local", &cmCTestResourceSpec::LocalSocket, LocalRequiredHelper, false);
}

bool cmCTestResourceSpec::ReadFromJSONFile(std::string const& filename)
{
  Json::Value root;

  this->parseState = cmJSONState(filename, &root);
  if (!this->parseState.errors.empty()) {
    return false;
  }

  TopVersion version;
  bool result;
  if ((result = RootVersionHelper(version, &root, &parseState)) != true) {
    return result;
  }
  if (version.Version.Major != 1 || version.Version.Minor != 0) {
    return false;
  }

  return RootHelper(*this, &root, &parseState);
}

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

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

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

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

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

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