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

#include <algorithm>
#include <cstddef>
#include <functional>
#include <map>
#include <string>
#include <vector>

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

#include <cm3p/json/value.h>

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

template <typename T, typename E>
class cmJSONObjectHelper
{
public:
  cmJSONObjectHelper(E&& success, E&& fail, bool allowExtra = true);

  template <typename U, typename M, typename F>
  cmJSONObjectHelper& Bind(const cm::string_view& name, M U::*member, F func,
                           bool required = true);
  template <typename M, typename F>
  cmJSONObjectHelper& Bind(const cm::string_view& name, std::nullptr_t, F func,
                           bool required = true);

  E operator()(T& out, const Json::Value* value) const;

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

  cmJSONObjectHelper& BindPrivate(const cm::string_view& name,
                                  MemberFunction&& func, bool required);
};

template <typename T, typename E>
cmJSONObjectHelper<T, E>::cmJSONObjectHelper(E&& success, E&& fail,
                                             bool allowExtra)
  : Success(std::move(success))
  , Fail(std::move(fail))
  , AllowExtra(allowExtra)
{
}

template <typename T, typename E>
template <typename U, typename M, typename F>
cmJSONObjectHelper<T, E>& cmJSONObjectHelper<T, E>::Bind(
  const cm::string_view& name, M U::*member, F func, bool required)
{
  return this->BindPrivate(
    name,
    [func, member](T& out, const Json::Value* value) -> E {
      return func(out.*member, value);
    },
    required);
}

template <typename T, typename E>
template <typename M, typename F>
cmJSONObjectHelper<T, E>& cmJSONObjectHelper<T, E>::Bind(
  const cm::string_view& name, std::nullptr_t, F func, bool required)
{
  return this->BindPrivate(name,
                           [func](T& /*out*/, const Json::Value* value) -> E {
                             M dummy;
                             return func(dummy, value);
                           },
                           required);
}

template <typename T, typename E>
cmJSONObjectHelper<T, E>& cmJSONObjectHelper<T, E>::BindPrivate(
  const cm::string_view& name, MemberFunction&& func, bool required)
{
  Member m;
  m.Name = name;
  m.Function = std::move(func);
  m.Required = required;
  this->Members.push_back(std::move(m));
  if (required) {
    this->AnyRequired = true;
  }
  return *this;
}

template <typename T, typename E>
E cmJSONObjectHelper<T, E>::operator()(T& out, const Json::Value* value) const
{
  if (!value && this->AnyRequired) {
    return this->Fail;
  }
  if (value && !value->isObject()) {
    return this->Fail;
  }
  Json::Value::Members extraFields;
  if (value) {
    extraFields = value->getMemberNames();
  }

  for (auto const& m : this->Members) {
    std::string name(m.Name.data(), m.Name.size());
    if (value && value->isMember(name)) {
      E result = m.Function(out, &(*value)[name]);
      if (result != this->Success) {
        return result;
      }
      extraFields.erase(
        std::find(extraFields.begin(), extraFields.end(), name));
    } else if (!m.Required) {
      E result = m.Function(out, nullptr);
      if (result != this->Success) {
        return result;
      }
    } else {
      return this->Fail;
    }
  }

  return this->AllowExtra || extraFields.empty() ? this->Success : this->Fail;
}

template <typename E>
cmJSONHelper<std::string, E> cmJSONStringHelper(E success, E fail,
                                                const std::string& defval = "")
{
  return
    [success, fail, defval](std::string& out, const Json::Value* value) -> E {
      if (!value) {
        out = defval;
        return success;
      }
      if (!value->isString()) {
        return fail;
      }
      out = value->asString();
      return success;
    };
}

template <typename E>
cmJSONHelper<int, E> cmJSONIntHelper(E success, E fail, int defval = 0)
{
  return [success, fail, defval](int& out, const Json::Value* value) -> E {
    if (!value) {
      out = defval;
      return success;
    }
    if (!value->isInt()) {
      return fail;
    }
    out = value->asInt();
    return success;
  };
}

template <typename E>
cmJSONHelper<unsigned int, E> cmJSONUIntHelper(E success, E fail,
                                               unsigned int defval = 0)
{
  return
    [success, fail, defval](unsigned int& out, const Json::Value* value) -> E {
      if (!value) {
        out = defval;
        return success;
      }
      if (!value->isUInt()) {
        return fail;
      }
      out = value->asUInt();
      return success;
    };
}

template <typename E>
cmJSONHelper<bool, E> cmJSONBoolHelper(E success, E fail, bool defval = false)
{
  return [success, fail, defval](bool& out, const Json::Value* value) -> E {
    if (!value) {
      out = defval;
      return success;
    }
    if (!value->isBool()) {
      return fail;
    }
    out = value->asBool();
    return success;
  };
}

template <typename T, typename E, typename F, typename Filter>
cmJSONHelper<std::vector<T>, E> cmJSONVectorFilterHelper(E success, E fail,
                                                         F func, Filter filter)
{
  return [success, fail, func, filter](std::vector<T>& out,
                                       const Json::Value* value) -> E {
    if (!value) {
      out.clear();
      return success;
    }
    if (!value->isArray()) {
      return fail;
    }
    out.clear();
    for (auto const& item : *value) {
      T t;
      E result = func(t, &item);
      if (result != success) {
        return result;
      }
      if (!filter(t)) {
        continue;
      }
      out.push_back(t);
    }
    return success;
  };
}

template <typename T, typename E, typename F>
cmJSONHelper<std::vector<T>, E> cmJSONVectorHelper(E success, E fail, F func)
{
  return cmJSONVectorFilterHelper<T, E, F>(success, fail, func,
                                           [](const T&) { return true; });
}

template <typename T, typename E, typename F, typename Filter>
cmJSONHelper<std::map<std::string, T>, E> cmJSONMapFilterHelper(E success,
                                                                E fail, F func,
                                                                Filter filter)
{
  return [success, fail, func, filter](std::map<std::string, T>& out,
                                       const Json::Value* value) -> E {
    if (!value) {
      out.clear();
      return success;
    }
    if (!value->isObject()) {
      return fail;
    }
    out.clear();
    for (auto const& key : value->getMemberNames()) {
      if (!filter(key)) {
        continue;
      }
      T t;
      E result = func(t, &(*value)[key]);
      if (result != success) {
        return result;
      }
      out[key] = std::move(t);
    }
    return success;
  };
}

template <typename T, typename E, typename F>
cmJSONHelper<std::map<std::string, T>, E> cmJSONMapHelper(E success, E fail,
                                                          F func)
{
  return cmJSONMapFilterHelper<T, E, F>(
    success, fail, func, [](const std::string&) { return true; });
}

template <typename T, typename E, typename F>
cmJSONHelper<cm::optional<T>, E> cmJSONOptionalHelper(E success, F func)
{
  return [success, func](cm::optional<T>& out, const Json::Value* value) -> E {
    if (!value) {
      out.reset();
      return success;
    }
    out.emplace();
    return func(*out, value);
  };
}

template <typename T, typename E, typename F>
cmJSONHelper<T, E> cmJSONRequiredHelper(E fail, F func)
{
  return [fail, func](T& out, const Json::Value* value) -> E {
    if (!value) {
      return fail;
    }
    return func(out, value);
  };
}
