/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */
#pragma once

#include "cmConfigure.h" // IWYU pragma: keep

#include <algorithm>
#include <cstddef>
#include <functional>
#include <iterator>
#include <map>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

#include <cm/optional>
#include <cm/string_view>

#include <cm3p/json/value.h>

#include "cmJSONState.h"
#include "cmStringAlgorithms.h"

template <typename T>
using cmJSONHelper =
  std::function<bool(T& out, Json::Value const* value, cmJSONState* state)>;

using ErrorGenerator = std::function<void(Json::Value const*, cmJSONState*)>;

namespace JsonErrors {
enum ObjectError
{
  RequiredMissing,
  InvalidObject,
  ExtraField,
  MissingRequired
};

using ErrorGenerator = std::function<void(Json::Value const*, cmJSONState*)>;
using ObjectErrorGenerator =
  std::function<ErrorGenerator(ObjectError, Json::Value::Members const&)>;

ErrorGenerator EXPECTED_TYPE(std::string const& type);

void INVALID_STRING(Json::Value const* value, cmJSONState* state);

void INVALID_BOOL(Json::Value const* value, cmJSONState* state);

void INVALID_INT(Json::Value const* value, cmJSONState* state);

void INVALID_UINT(Json::Value const* value, cmJSONState* state);

ObjectErrorGenerator INVALID_NAMED_OBJECT(
  std::function<std::string(Json::Value const*, cmJSONState*)> const&
    nameGenerator);

ErrorGenerator INVALID_OBJECT(ObjectError errorType,
                              Json::Value::Members const& extraFields);

ErrorGenerator INVALID_NAMED_OBJECT_KEY(
  ObjectError errorType, Json::Value::Members const& extraFields);
}

#if __cplusplus >= 201703L
namespace details {
// A meta-function to check if a given callable type
// can be called with the only string ref arg.
template <typename F, typename Enable = void>
struct is_bool_filter
{
  static constexpr bool value = false;
};

template <typename F>
struct is_bool_filter<F,
                      std::enable_if_t<std::is_same_v<
                        std::invoke_result_t<F, std::string const&>, bool>>>
{
  static constexpr bool value = true;
};
}
#endif

struct cmJSONHelperBuilder
{
  template <typename T>
  class Object
  {
  public:
    Object(JsonErrors::ObjectErrorGenerator error = JsonErrors::INVALID_OBJECT,
           bool allowExtra = true)
      : Error(std::move(error))
      , AllowExtra(allowExtra)
    {
    }

    template <typename U, typename M, typename F>
    Object& Bind(cm::string_view name, M U::*member, F func,
                 bool required = true)
    {
      return this->BindPrivate(
        name,
        [func, member](T& out, Json::Value const* value, cmJSONState* state)
          -> bool { return func(out.*member, value, state); },
        required);
    }
    template <typename U, typename M, typename F, typename C>
    Object& Bind(cm::string_view name, M U::*member, F func, C constant,
                 bool required = true)
    {
      return this->BindPrivate(
        name,
        [func, member, constant](T& out, Json::Value const* value,
                                 cmJSONState* state) -> bool {
          return func(out.*member, value, constant, state);
        },
        required);
    }
    template <typename M, typename F>
    Object& Bind(cm::string_view name, std::nullptr_t, F func,
                 bool required = true)
    {
      return this->BindPrivate(
        name,
        [func](T& /*out*/, Json::Value const* value,
               cmJSONState* state) -> bool {
          M dummy;
          return func(dummy, value, state);
        },
        required);
    }
    template <typename F>
    Object& Bind(cm::string_view name, F func, bool required = true)
    {
      return this->BindPrivate(name, MemberFunction(func), required);
    }

    bool operator()(T& out, Json::Value const* value, cmJSONState* state) const
    {
      Json::Value::Members extraFields;
      if (!value && this->AnyRequired) {
        Error(JsonErrors::ObjectError::RequiredMissing, extraFields)(value,
                                                                     state);
        return false;
      }
      if (value && !value->isObject()) {
        Error(JsonErrors::ObjectError::InvalidObject, extraFields)(value,
                                                                   state);
        return false;
      }
      if (value) {
        extraFields = value->getMemberNames();
      }

      if (state->allowComments) {
        extraFields.erase(
          std::remove(extraFields.begin(), extraFields.end(), "$comment"),
          extraFields.end());
      }

      bool success = true;
      for (auto const& m : this->Members) {
        std::string name(m.Name.data(), m.Name.size());
        state->push_stack(name, value);
        if (value && value->isMember(name)) {
          if (!m.Function(out, &(*value)[name], state)) {
            success = false;
          }
          extraFields.erase(
            std::find(extraFields.begin(), extraFields.end(), name));
        } else if (!m.Required) {
          if (!m.Function(out, nullptr, state)) {
            success = false;
          }
        } else {
          Error(JsonErrors::ObjectError::MissingRequired, extraFields)(value,
                                                                       state);
          success = false;
        }
        state->pop_stack();
      }

      if (!this->AllowExtra && !extraFields.empty()) {
        Error(JsonErrors::ObjectError::ExtraField, extraFields)(value, state);
        success = false;
      }
      return success;
    }

  private:
    // Not a true cmJSONHelper, it just happens to match the signature
    using MemberFunction = std::function<bool(T& out, Json::Value const* value,
                                              cmJSONState* state)>;
    struct Member
    {
      Member(cm::string_view name, MemberFunction func, bool required)
        : Name{ name }
        , Function{ std::move(func) }
        , Required{ required }
      {
      }
      cm::string_view Name;
      MemberFunction Function;
      bool Required;
    };
    std::vector<Member> Members;
    bool AnyRequired = false;
    JsonErrors::ObjectErrorGenerator Error;
    bool AllowExtra;

    Object& BindPrivate(cm::string_view name, MemberFunction&& func,
                        bool required)
    {
      this->Members.emplace_back(name, std::move(func), required);
      this->AnyRequired = this->AnyRequired || required;
      return *this;
    }
  };

  static cmJSONHelper<std::string> String(
    JsonErrors::ErrorGenerator const& error = JsonErrors::INVALID_STRING,
    std::string const& defval = "")
  {
    return [error, defval](std::string& out, Json::Value const* value,
                           cmJSONState* state) -> bool {
      if (!value) {
        out = defval;
        return true;
      }
      if (!value->isString()) {
        error(value, state);
        return false;
      }
      out = value->asString();
      return true;
    };
  };

  static cmJSONHelper<std::string> String(std::string const& defval)
  {
    return String(JsonErrors::INVALID_STRING, defval);
  };

  static cmJSONHelper<int> Int(
    JsonErrors::ErrorGenerator const& error = JsonErrors::INVALID_INT,
    int defval = 0)
  {
    return [error, defval](int& out, Json::Value const* value,
                           cmJSONState* state) -> bool {
      if (!value) {
        out = defval;
        return true;
      }
      if (!value->isInt()) {
        error(value, state);
        return false;
      }
      out = value->asInt();
      return true;
    };
  }

  static cmJSONHelper<int> Int(int defval)
  {
    return Int(JsonErrors::INVALID_INT, defval);
  };

  static cmJSONHelper<unsigned int> UInt(
    JsonErrors::ErrorGenerator const& error = JsonErrors::INVALID_UINT,
    unsigned int defval = 0)
  {
    return [error, defval](unsigned int& out, Json::Value const* value,
                           cmJSONState* state) -> bool {
      if (!value) {
        out = defval;
        return true;
      }
      if (!value->isUInt()) {
        error(value, state);
        return false;
      }
      out = value->asUInt();
      return true;
    };
  }

  static cmJSONHelper<unsigned int> UInt(unsigned int defval)
  {
    return UInt(JsonErrors::INVALID_UINT, defval);
  }

  static cmJSONHelper<bool> Bool(
    JsonErrors::ErrorGenerator const& error = JsonErrors::INVALID_BOOL,
    bool defval = false)
  {
    return [error, defval](bool& out, Json::Value const* value,
                           cmJSONState* state) -> bool {
      if (!value) {
        out = defval;
        return true;
      }
      if (!value->isBool()) {
        error(value, state);
        return false;
      }
      out = value->asBool();
      return true;
    };
  }

  static cmJSONHelper<bool> Bool(bool defval)
  {
    return Bool(JsonErrors::INVALID_BOOL, defval);
  }

  template <typename T, typename F, typename Filter>
  static cmJSONHelper<std::vector<T>> VectorFilter(
    JsonErrors::ErrorGenerator const& error, F func, Filter filter)
  {
    return [error, func, filter](std::vector<T>& out, Json::Value const* value,
                                 cmJSONState* state) -> bool {
      bool success = true;
      if (!value) {
        out.clear();
        return true;
      }
      if (!value->isArray()) {
        error(value, state);
        return false;
      }
      out.clear();
      int index = 0;
      for (auto const& item : *value) {
        state->push_stack(cmStrCat("$vector_item_", index++), &item);
        T t;
        if (!func(t, &item, state)) {
          success = false;
        }
        if (!filter(t)) {
          state->pop_stack();
          continue;
        }
        out.push_back(std::move(t));
        state->pop_stack();
      }
      return success;
    };
  }

  template <typename T, typename F>
  static cmJSONHelper<std::vector<T>> Vector(JsonErrors::ErrorGenerator error,
                                             F func)
  {
    return VectorFilter<T, F>(std::move(error), func,
                              [](T const&) { return true; });
  }

  enum class FilterResult
  {
    Continue, ///< A filter has accepted a given key (and value)
    Skip,     ///< A filter has rejected a given key (or value)
    Error     ///< A filter has found and reported an error
  };

  /// Iterate over the object's members and call a filter callable to
  /// decide what to do with the current key/value.
  /// A filter returns one of the `FilterResult` values.
  /// A container type is an associative or a sequence
  /// container of pairs (key, value).
  template <typename Container, typename F, typename Filter>
  static cmJSONHelper<Container> FilteredObject(
    JsonErrors::ErrorGenerator const& error, F func, Filter filter)
  {
    return [error, func, filter](Container& out, Json::Value const* value,
                                 cmJSONState* state) -> bool {
      // NOTE Some compile-time code path don't use `filter` at all.
      // So, suppress "unused lambda capture" warning is needed.
      static_cast<void>(filter);

      if (!value) {
        out.clear();
        return true;
      }
      if (!value->isObject()) {
        error(value, state);
        return false;
      }
      out.clear();
      auto outIt = std::inserter(out, out.end());
      bool success = true;
      for (auto const& key : value->getMemberNames()) {
        state->push_stack(key, &(*value)[key]);
#if __cplusplus >= 201703L
        if constexpr (std::is_same_v<Filter, std::true_type>) {
          // Filtering functionality isn't needed at all...
        } else if constexpr (details::is_bool_filter<Filter>::value) {
          // A given `Filter` is `bool(const std::string&)` callable.
          if (!filter(key)) {
            state->pop_stack();
            continue;
          }
        } else {
#endif
          // A full-featured `Filter` has been given
          auto res = filter(key, &(*value)[key], state);
          if (res == FilterResult::Skip) {
            state->pop_stack();
            continue;
          }
          if (res == FilterResult::Error) {
            state->pop_stack();
            success = false;
            break;
          }
#if __cplusplus >= 201703L
        }
#endif
        typename Container::value_type::second_type t;
        // ATTENTION Call the function first (for it's side-effects),
        // then accumulate the result!
        success = func(t, &(*value)[key], state) && success;
        outIt = typename Container::value_type{ key, std::move(t) };
        state->pop_stack();
      }
      return success;
    };
  }

  template <typename T, typename F, typename Filter>
  static cmJSONHelper<std::map<std::string, T>> MapFilter(
    JsonErrors::ErrorGenerator const& error, F func, Filter filter)
  {
    // clang-format off
    return FilteredObject<std::map<std::string, T>>(
      error, func,
#if __cplusplus >= 201703L
      // In C++ 17 a filter callable can be passed as is.
      // Depending on its type `FilteredObject()` will call
      // it with a key only (backward compatible behavior)
      // or with 3 args supported by the full-featured
      // filtering feature.
      filter
#else
      // For C++14 and below, to keep backward compatibility
      // with CMake Presets code, `MapFilter()` can accept only
      // `bool(const std::string&)` callables.
      [filter](const std::string &key, const Json::Value * /*value*/,
              cmJSONState * /*state*/) -> FilterResult {
        // Simple adaptor to translate `bool` to `FilterResult`
        return filter(key) ? FilterResult::Continue : FilterResult::Skip;
      }
#endif
    );
    // clang-format on
  }

  template <typename T, typename F>
  static cmJSONHelper<std::map<std::string, T>> Map(
    JsonErrors::ErrorGenerator const& error, F func)
  {
    // clang-format off
    return FilteredObject<std::map<std::string, T>>(
      error, func,
#if __cplusplus >= 201703L
      // With C++ 17 and above, pass a marker type, that no
      // filtering is needed at all.
      std::true_type()
#else
      // In C++ 14 and below, pass an always-true dummy functor.
      [](const std::string& /*key*/, const Json::Value* /*value*/,
         cmJSONState* /*state*/) -> FilterResult {
        return FilterResult::Continue;
      }
#endif
    );
    // clang-format on
  }

  template <typename T, typename F>
  static cmJSONHelper<cm::optional<T>> Optional(F func)
  {
    return [func](cm::optional<T>& out, Json::Value const* value,
                  cmJSONState* state) -> bool {
      if (!value) {
        out.reset();
        return true;
      }
      out.emplace();
      return func(*out, value, state);
    };
  }

  template <typename T, typename F>
  static cmJSONHelper<T> Required(JsonErrors::ErrorGenerator const& error,
                                  F func)
  {
    return [error, func](T& out, Json::Value const* value,
                         cmJSONState* state) -> bool {
      if (!value) {
        error(value, state);
        return false;
      }
      return func(out, value, state);
    };
  }

  template <typename T, typename F, typename P>
  static cmJSONHelper<T> Checked(JsonErrors::ErrorGenerator const& error,
                                 F func, P predicate)
  {
    return [error, func, predicate](T& out, Json::Value const* value,
                                    cmJSONState* state) -> bool {
      bool result = func(out, value, state);
      if (result && !predicate(out)) {
        error(value, state);
        result = false;
      }
      return result;
    };
  }
};
