/*
 * Copyright (c) 2022-2024 Philippe Proulx <pproulx@efficios.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef BABELTRACE_CPP_COMMON_BT2C_VAL_REQ_HPP
#define BABELTRACE_CPP_COMMON_BT2C_VAL_REQ_HPP

#include <limits>
#include <memory>
#include <set>
#include <sstream>
#include <string>
#include <unordered_map>

#include "logging.hpp"

#include "exc.hpp"
#include "text-loc.hpp"

namespace bt2c {

/*
 * This set of class templates makes it possible to get basic
 * requirement classes to validate JSON-like value objects, that is, a
 * system of null, boolean, unsigned/signed integer, real, string,
 * array, and object value objects.
 *
 * All the class templates accept a `ValT` parameter which is the base
 * type of the objects to validate, as well as `ValOpsT`, a structure
 * which defines specific value operations.
 *
 * The requirements of `ValOptsT` are:
 *
 * static ValType valType(const ValT& val):
 *     Returns the type of `val` as a `ValType` value.
 *
 * static const char *typeDetStr(ValType type):
 *     Returns the determiner (lowercase) to use for the value type
 *     `type`.
 *
 *     This is required to generate an error message.
 *
 * static const char *typeStr(ValType type):
 *     Returns the name (lowercase) of the value type `type`.
 *
 *     This is required to generate an error message.
 *
 * static constexpr const char *objValPropName:
 *     Name (lowercase) of an object value object property.
 *
 * static const TextLoc& valLoc(const ValT& val):
 *     Returns the location of the value `val`.
 *
 *     This is required to build a text parse error (`TextParseError`).
 *
 * static const SomeUIntVal& asUInt(const ValT& val):
 *     Returns `val` as an unsigned integer value object.
 *
 * static const SomeStrVal& asStr(const ValT& val):
 *     Returns `val` as a string value object.
 *
 * static const SomeArrayVal& asArray(const ValT& val):
 *     Returns `val` as an array value object.
 *
 * static const SomeObjVal& asObj(const ValT& val):
 *     Returns `val` as an object value object.
 *
 * template <typename ScalarValT> using ScalarValRawValT = ...:
 *     Raw value type of the scalar value object type `ScalarValT`.
 *
 * static unsigned long long scalarValRawVal(const SomeUIntVal& val):
 *     Returns the raw value of the unsigned value object `val`.
 *
 * static long long scalarValRawVal(const SomeSIntVal& val):
 *     Returns the raw value of the signed value object `val`.
 *
 * static double scalarValRawVal(const SomeRealVal& val):
 *     Returns the raw value of the real value object `val`.
 *
 * static const std::string& scalarValRawVal(const SomeStrVal& val):
 *     Returns the raw value of the string value object `val`.
 *
 * static std::size_t arrayValSize(const SomeArrayVal& val):
 *     Returns the size of the array value object `val`.
 *
 * static const ValT& arrayValElem(const SomeArrayVal& val, std::size_t index):
 *     Returns the element of the array value object `val` at the index
 *     `index`.
 *
 * static const ValT *objValVal(const SomeObjVal& val, const std::string& key):
 *     Returns the value of the object value object `val` having the key
 *     `key`, or `nullptr` if there's none.
 *
 * static SomeIterator objValBegin(const SomeObjVal& val):
 *     Returns an iterator at the beginning of the object value object
 *     `val`.
 *
 * static SomeIterator objValEnd(const SomeObjVal& val):
 *     Returns an iterator at the end of the object value object `val`.
 *
 * static const std::string& objValItKey(const SomeIterator& it):
 *     Returns the key of the object value object iterator `it`.
 *
 * static const ValT& objValItVal(const SomeIterator& it):
 *     Returns the value object of the object value object iterator
 *     `it`.
 */

/*
 * Value requirement abstract base class.
 */
template <typename ValT, typename ValOpsT>
class ValReq
{
public:
    /* Shared pointer to constant value requirement */
    using SP = std::shared_ptr<const ValReq>;

protected:
    /*
     * Builds a value requirement.
     */
    explicit ValReq(const Logger& parentLogger) noexcept : _mLogger {parentLogger, "VAL-REQ"}
    {
    }

public:
    /* Deleted copy/move operations to simplify */
    ValReq(const ValReq&) = delete;
    ValReq(ValReq&&) = delete;
    ValReq& operator=(const ValReq&) = delete;
    ValReq& operator=(ValReq&&) = delete;

    virtual ~ValReq() = default;

    /*
     * Validates that `val` satisfies this requirement.
     */
    void validate(const ValT& val) const
    {
        this->_validate(val);
    }

protected:
    static const TextLoc& _loc(const ValT& val) noexcept
    {
        return ValOpsT::valLoc(val);
    }

    const Logger& _logger() const noexcept
    {
        return _mLogger;
    }

private:
    /*
     * Requirement-specific validation.
     */
    virtual void _validate(const ValT&) const
    {
    }

protected:
    Logger _mLogger;
};

/*
 * Value type.
 */
enum class ValType
{
    Null,
    Bool,
    SInt,
    UInt,
    Real,
    Str,
    Array,
    Obj,
};

/*
 * "Value has type" requirement.
 *
 * An instance of this class validates that a value has a given type.
 */
template <typename ValT, typename ValOpsT>
class ValHasTypeReq : public ValReq<ValT, ValOpsT>
{
public:
    /*
     * Builds a "value has type" requirement: _validate() validates that
     * the type of the value is `type`.
     */
    explicit ValHasTypeReq(const ValType type, const Logger& parentLogger) noexcept :
        ValReq<ValT, ValOpsT> {parentLogger}, _mType {type}
    {
    }

    /*
     * Returns a shared pointer to "value has type" requirement,
     * forwarding the parameters to the constructor.
     */
    static typename ValReq<ValT, ValOpsT>::SP shared(const ValType type, const Logger& parentLogger)
    {
        return std::make_shared<ValHasTypeReq>(type, parentLogger);
    }

protected:
    void _validate(const ValT& val) const override
    {
        if (ValOpsT::valType(val) != _mType) {
            BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_THROW_SPEC(
                this->_logger(), Error, this->_loc(val), "Expecting {} {}.",
                ValOpsT::typeDetStr(_mType), ValOpsT::typeStr(_mType));
        }
    }

private:
    /* Required value type */
    ValType _mType;
};

/*
 * Any integer value requirement.
 *
 * An instance of this class validates that a value is an integer value
 * (unsigned or signed).
 */
template <typename ValT, typename ValOpsT>
class AnyIntValReq : public ValReq<ValT, ValOpsT>
{
public:
    explicit AnyIntValReq(const Logger& parentLogger) noexcept :
        ValReq<ValT, ValOpsT> {parentLogger}
    {
    }

    /*
     * Returns a shared pointer to any integer value requirement,
     * forwarding the parameters to the constructor.
     */
    static typename ValReq<ValT, ValOpsT>::SP shared(const Logger& parentLogger)
    {
        return std::make_shared<AnyIntValReq>(parentLogger);
    }

protected:
    void _validate(const ValT& val) const override
    {
        if (!val.isUInt() && !val.isSInt()) {
            BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_THROW_SPEC(this->_logger(), Error, this->_loc(val),
                                                            "Expecting an integer.");
        }
    }
};

/*
 * Unsigned integer (range) value requirement.
 *
 * An instance of this class validates that a value is an unsigned
 * integer value.
 */
template <typename ValT, typename ValOpsT>
class UIntValReq : public ValHasTypeReq<ValT, ValOpsT>
{
public:
    /*
     * Builds an unsigned integer value: _validate() validates that the
     * integer value is an unsigned integer type.
     */
    explicit UIntValReq(const Logger& parentLogger) noexcept :
        ValHasTypeReq<ValT, ValOpsT> {ValType::UInt, parentLogger}
    {
    }

    /*
     * Returns a shared pointer to unsigned integer value requirement,
     * forwarding the parameters to the constructor.
     */
    static typename ValReq<ValT, ValOpsT>::SP shared(const Logger& parentLogger)
    {
        return std::make_shared<UIntValReq>(parentLogger);
    }
};

/*
 * Signed integer value (range) requirement.
 *
 * An instance of this class validates that a value is an integer value
 * (unsigned or signed) and that its raw value is between
 * -9,223,372,036,854,775,808 and 9,223,372,036,854,775,807.
 */
template <typename ValT, typename ValOpsT>
class SIntValReq : public AnyIntValReq<ValT, ValOpsT>
{
public:
    explicit SIntValReq(const Logger& parentLogger) noexcept :
        AnyIntValReq<ValT, ValOpsT> {parentLogger}
    {
    }

    /*
     * Returns a shared pointer to signed value requirement, forwarding
     * the parameters to the constructor.
     */
    static typename ValReq<ValT, ValOpsT>::SP shared(const Logger& parentLogger)
    {
        return std::make_shared<SIntValReq>(parentLogger);
    }

protected:
    void _validate(const ValT& val) const override
    {
        /* Validate that it's an integer value */
        AnyIntValReq<ValT, ValOpsT>::_validate(val);

        if (ValOpsT::valType(val) == ValType::SInt) {
            /* Always correct */
            return;
        }

        /* Validate the raw value */
        static constexpr auto llMaxAsUll =
            static_cast<unsigned long long>(std::numeric_limits<long long>::max());

        const auto rawVal = ValOpsT::scalarValRawVal(ValOpsT::asUInt(val));

        if (rawVal > llMaxAsUll) {
            BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_THROW_SPEC(
                this->_logger(), Error, this->_loc(val),
                "Expecting a signed integer: {} is greater than {}.", rawVal, llMaxAsUll);
        }
    }
};

/*
 * "Integer value in range" requirement template.
 *
 * An instance of this class validates that, given a value V of type
 * `IntValT`:
 *
 * • V has the type enumerator `TypeV`.
 * • The raw value of V is within a given range.
 */
template <typename ValT, typename ValOpsT, typename IntValT, ValType TypeV>
class IntValInRangeReq : public ValHasTypeReq<ValT, ValOpsT>
{
private:
    /* Raw value type */
    using _RawVal = typename ValOpsT::template ScalarValRawValT<IntValT>;

public:
    /*
     * Builds an "integer value in range" requirement: _validate()
     * validates that the raw value of the integer value is:
     *
     * • If `minVal` is set: greater than or equal to `*minVal`.
     * • If `maxVal` is set: less than or equal to `*maxVal`.
     */
    explicit IntValInRangeReq(const bt2s::optional<_RawVal>& minVal,
                              const bt2s::optional<_RawVal>& maxVal,
                              const Logger& parentLogger) noexcept :
        ValHasTypeReq<ValT, ValOpsT> {TypeV, parentLogger},
        _mMinVal {minVal ? *minVal : std::numeric_limits<_RawVal>::min()},
        _mMaxVal {maxVal ? *maxVal : std::numeric_limits<_RawVal>::max()}
    {
    }

    /*
     * Builds an "integer value in range" requirement: _validate()
     * validates that the raw value of the integer value is exactly
     * `exactVal`.
     */
    explicit IntValInRangeReq(const _RawVal exactVal, const Logger& parentLogger) noexcept :
        IntValInRangeReq {exactVal, exactVal, parentLogger}
    {
    }

    /*
     * Returns a shared pointer to "integer value in range" requirement,
     * forwarding the parameters to the constructor.
     */
    static typename ValReq<ValT, ValOpsT>::SP shared(const bt2s::optional<_RawVal>& minVal,
                                                     const bt2s::optional<_RawVal>& maxVal,
                                                     const Logger& parentLogger)
    {
        return std::make_shared<IntValInRangeReq>(minVal, maxVal, parentLogger);
    }

    /*
     * Returns a shared pointer to "integer value in range" requirement,
     * forwarding the parameters to the constructor.
     */
    static typename ValReq<ValT, ValOpsT>::SP shared(const _RawVal exactVal,
                                                     const Logger& parentLogger)
    {
        return std::make_shared<IntValInRangeReq>(exactVal, parentLogger);
    }

protected:
    void _validate(const ValT& val) const override
    {
        ValHasTypeReq<ValT, ValOpsT>::_validate(val);

        auto& intVal = static_cast<const IntValT&>(val);
        const auto rawVal = ValOpsT::scalarValRawVal(intVal);

        if (rawVal < _mMinVal) {
            BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_THROW_SPEC(
                this->_logger(), Error, this->_loc(intVal),
                "Integer {} is too small: expecting at least {}.", rawVal, _mMinVal);
        }

        if (rawVal > _mMaxVal) {
            BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_THROW_SPEC(
                this->_logger(), Error, this->_loc(intVal),
                "Integer {} is too large: expecting at most {}.", rawVal, _mMaxVal);
        }
    }

private:
    /* Minimum raw value */
    _RawVal _mMinVal;

    /* Maximum raw value */
    _RawVal _mMaxVal;
};

namespace internal {

template <typename RawValT>
std::string rawValStr(const RawValT& rawVal)
{
    return fmt::to_string(rawVal);
}

template <>
inline std::string rawValStr<std::string>(const std::string& val)
{
    return fmt::format("`{}`", val);
}

template <>
inline std::string rawValStr<bool>(const bool& val)
{
    return val ? "true" : "false";
}

} /* namespace internal */

/*
 * "Scalar value in set" requirement template.
 *
 * An instance of this class validates that, given a value V of type
 * `ScalarValT`:
 *
 * • V has the type enumerator `TypeV`.
 * • The raw value of V is an element of a given set.
 */
template <typename ValT, typename ValOpsT, typename ScalarValT, ValType TypeV>
class ScalarValInSetReq : public ValHasTypeReq<ValT, ValOpsT>
{
private:
    /* Raw value type */
    using _RawVal = typename ValOpsT::template ScalarValRawValT<ScalarValT>;

public:
    /*
     * Raw value set type.
     *
     * Using `std::set` instead of `std::unordered_set` because
     * _setStr() needs the elements in order.
     */
    using Set = std::set<_RawVal>;

    /*
     * Builds a "scalar value in set" requirement: _validate() validates
     * that the raw value of the scalar value is an element of `set`.
     */
    explicit ScalarValInSetReq(Set set, const Logger& parentLogger) :
        ValHasTypeReq<ValT, ValOpsT> {TypeV, parentLogger}, _mSet {std::move(set)}
    {
    }

    /*
     * Builds a "scalar value in set" requirement: _validate() validates
     * that the raw value of the scalar value is exactly `rawVal`.
     */
    explicit ScalarValInSetReq(_RawVal rawVal, const Logger& parentLogger) :
        ScalarValInSetReq {Set {std::move(rawVal)}, parentLogger}
    {
    }

    /*
     * Returns a shared pointer to "scalar value in set" requirement,
     * forwarding the parameters to the constructor.
     */
    static typename ValReq<ValT, ValOpsT>::SP shared(Set set, const Logger& parentLogger)
    {
        return std::make_shared<ScalarValInSetReq>(std::move(set), parentLogger);
    }

    /*
     * Returns a shared pointer to "scalar value in set" requirement,
     * forwarding the parameters to the constructor.
     */
    static typename ValReq<ValT, ValOpsT>::SP shared(_RawVal rawVal, const Logger& parentLogger)
    {
        return std::make_shared<ScalarValInSetReq>(std::move(rawVal), parentLogger);
    }

protected:
    void _validate(const ValT& val) const override
    {
        ValHasTypeReq<ValT, ValOpsT>::_validate(val);

        auto& scalarVal = static_cast<const ScalarValT&>(val);
        const auto& rawVal = ValOpsT::scalarValRawVal(scalarVal);

        if (_mSet.find(rawVal) == _mSet.end()) {
            BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_THROW_SPEC(
                this->_logger(), Error, this->_loc(val), "Unexpected value {}: expecting {}.",
                internal::rawValStr(rawVal), this->_setStr());
        }
    }

private:
    /*
     * Serializes the raw values of `_mSet` and returns the resulting
     * string.
     */
    std::string _setStr() const
    {
        if (_mSet.size() == 1) {
            /* Special case: direct value */
            return internal::rawValStr(*_mSet.begin());
        } else if (_mSet.size() == 2) {
            /* Special case: "or" word without any comma */
            return fmt::format("{} or {}", internal::rawValStr(*_mSet.begin()),
                               internal::rawValStr(*std::next(_mSet.begin())));
        }

        /* Enumeration with at least one comma */
        std::ostringstream ss;

        {
            const auto lastIt = std::prev(_mSet.end());

            for (auto it = _mSet.begin(); it != lastIt; ++it) {
                ss << internal::rawValStr(*it) << ", ";
            }

            ss << "or " << internal::rawValStr(*lastIt);
        }

        return ss.str();
    }

    /* Set of expected raw values */
    Set _mSet;
};

/*
 * Array value requirement.
 *
 * An instance of this class validates that, given a value V:
 *
 * • V is an array value.
 * • The size of V is within a given range.
 * • All the elements of V satisfy a given value requirement.
 */
template <typename ValT, typename ValOpsT>
class ArrayValReq : public ValHasTypeReq<ValT, ValOpsT>
{
public:
    using SP = typename ValReq<ValT, ValOpsT>::SP;

    /*
     * Builds an array value requirement: _validate() validates that,
     * for a given array value V:
     *
     * • If `minSize` is set: the size of V is greater than or equal to
     *   `*minSize`.
     *
     * • If `maxSize` is set: the size of V is less than or equal to
     *   `*maxSize`.
     *
     * • If `elemValReq` is set: all the elements of V satisfy
     *   `*elemValReq`.
     */
    explicit ArrayValReq(const bt2s::optional<std::size_t>& minSize,
                         const bt2s::optional<std::size_t>& maxSize, SP elemValReq,
                         const Logger& parentLogger) :
        ValHasTypeReq<ValT, ValOpsT> {ValType::Array, parentLogger},
        _mMinSize {minSize ? *minSize : std::numeric_limits<std::size_t>::min()},
        _mMaxSize {maxSize ? *maxSize : std::numeric_limits<std::size_t>::max()},
        _mElemValReq {std::move(elemValReq)}
    {
    }

    /*
     * Builds an array value requirement: _validate() validates that,
     * for a given array value V:
     *
     * • If `minSize` is set: the size of V is greater than or equal to
     *   `*minSize`.
     *
     * • If `maxSize` is set: the size of V is less than or equal to
     *   `*maxSize`.
     */
    explicit ArrayValReq(const bt2s::optional<std::size_t>& minSize,
                         const bt2s::optional<std::size_t>& maxSize, const Logger& parentLogger) :
        ArrayValReq {minSize, maxSize, nullptr, parentLogger}
    {
    }

    /*
     * Builds an array value requirement: _validate() validates that,
     * for a given array value V:
     *
     * • The size of V is exactly `exactSize`.
     *
     * • If `elemValReq` is set: all the elements of V satisfy
     *   `*elemValReq`.
     */
    explicit ArrayValReq(const std::size_t exactSize, SP elemValReq, const Logger& parentLogger) :
        ArrayValReq {exactSize, exactSize, std::move(elemValReq), parentLogger}
    {
    }

    /*
     * Builds an array value requirement: _validate() validates that,
     * for a given array value V:
     *
     * • The size of V is exactly `exactSize`.
     */
    explicit ArrayValReq(const std::size_t exactSize, const Logger& parentLogger) :
        ArrayValReq {exactSize, exactSize, nullptr, parentLogger}
    {
    }

    /*
     * Builds an array value requirement: _validate() validates that all
     * the elements of a given array value satisfy `*elemValReq`, if
     * set.
     */
    explicit ArrayValReq(SP elemValReq, const Logger& parentLogger) :
        ArrayValReq {bt2s::nullopt, bt2s::nullopt, std::move(elemValReq), parentLogger}
    {
    }

    /*
     * Builds an array value requirement: _validate() validates that
     * a given value is an array value.
     */
    explicit ArrayValReq(const Logger& parentLogger) :
        ArrayValReq {bt2s::nullopt, bt2s::nullopt, parentLogger}
    {
    }

    /*
     * Returns a shared pointer to array value requirement, forwarding
     * the parameters to the constructor.
     */
    static SP shared(const bt2s::optional<std::size_t>& minSize,
                     const bt2s::optional<std::size_t>& maxSize, SP elemValReq,
                     const Logger& parentLogger)
    {
        return std::make_shared<ArrayValReq>(minSize, maxSize, std::move(elemValReq), parentLogger);
    }

    /*
     * Returns a shared pointer to array value requirement, forwarding
     * the parameters to the constructor.
     */
    static SP shared(const bt2s::optional<std::size_t>& minSize,
                     const bt2s::optional<std::size_t>& maxSize, const Logger& parentLogger)
    {
        return std::make_shared<ArrayValReq>(minSize, maxSize, parentLogger);
    }

    /*
     * Returns a shared pointer to array value requirement, forwarding
     * the parameters to the constructor.
     */
    static SP shared(const std::size_t exactSize, SP elemValReq, const Logger& parentLogger)
    {
        return std::make_shared<ArrayValReq>(exactSize, std::move(elemValReq), parentLogger);
    }

    /*
     * Returns a shared pointer to array value requirement, forwarding
     * the parameters to the constructor.
     */
    static SP shared(const std::size_t exactSize, const Logger& parentLogger)
    {
        return std::make_shared<ArrayValReq>(exactSize, parentLogger);
    }

    /*
     * Returns a shared pointer to array value requirement, forwarding
     * the parameters to the constructor.
     */
    static SP shared(SP elemValReq, const Logger& parentLogger)
    {
        return std::make_shared<ArrayValReq>(std::move(elemValReq), parentLogger);
    }

    /*
     * Returns a shared pointer to array value requirement, forwarding
     * the parameter to the constructor.
     */
    static SP shared(const Logger& parentLogger)
    {
        return std::make_shared<ArrayValReq>(parentLogger);
    }

protected:
    void _validate(const ValT& val) const override
    {
        ValHasTypeReq<ValT, ValOpsT>::_validate(val);

        auto& arrayVal = ValOpsT::asArray(val);
        const auto size = ValOpsT::arrayValSize(arrayVal);

        if (size < _mMinSize) {
            BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_THROW_SPEC(
                this->_logger(), Error, this->_loc(val),
                "Size of array ({}) is too small: expecting at least {} elements.", size,
                _mMinSize);
        }

        if (size > _mMaxSize) {
            BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_THROW_SPEC(
                this->_logger(), Error, this->_loc(val),
                "Size of array ({}) is too large: expecting at most {} elements.", size, _mMaxSize);
        }

        if (_mElemValReq) {
            for (std::size_t i = 0; i < size; ++i) {
                auto& elemVal = ValOpsT::arrayValElem(arrayVal, i);

                try {
                    _mElemValReq->validate(elemVal);
                } catch (const Error&) {
                    BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_RETHROW_SPEC(
                        this->_logger(), this->_loc(elemVal), "Invalid array element #{}.", i + 1);
                }
            }
        }
    }

private:
    std::size_t _mMinSize;
    std::size_t _mMaxSize;
    SP _mElemValReq;
};

/*
 * Object value property requirement.
 *
 * An instance of this class contains the requirements of a single
 * object value property, that is:
 *
 * • Whether or not it's required.
 * • The requirement of the value of the property.
 */
template <typename ValT, typename ValOpsT>
class ObjValPropReq final
{
public:
    /*
     * Builds an object value property requirement, required if
     * `isRequired` is true: if `valReq` is set, then validate()
     * validates that a value satisfies `*valReq`.
     *
     * Not `explicit` to make the construction of `ObjValReq` lighter.
     */
    ObjValPropReq(typename ValReq<ValT, ValOpsT>::SP valReq = nullptr,
                  const bool isRequired = false) :
        _mIsRequired {isRequired},
        _mValReq {std::move(valReq)}
    {
    }

    /*
     * Whether or not the property is required.
     */
    bool isRequired() const noexcept
    {
        return _mIsRequired;
    }

    /*
     * Validates that `val` satisfies this requirement.
     */
    void validate(const ValT& val) const
    {
        if (_mValReq) {
            _mValReq->validate(val);
        }
    }

private:
    /* Whether or not this property is required */
    bool _mIsRequired = false;

    /* Requirement of the value */
    typename ValReq<ValT, ValOpsT>::SP _mValReq;
};

/*
 * Object value requirement.
 *
 * An instance of this class validates that, given a value V:
 *
 * • V is an object value.
 *
 * • The properties of V satisfy a given set of object value property
 *   requirements.
 */
template <typename ValT, typename ValOpsT>
class ObjValReq : public ValHasTypeReq<ValT, ValOpsT>
{
public:
    /* Map of property name to property requirement */
    using PropReqs = std::unordered_map<std::string, ObjValPropReq<ValT, ValOpsT>>;

    /* Single entry (pair) of `PropReqs` */
    using PropReqsEntry = typename PropReqs::value_type;

public:
    /*
     * Builds an object value requirement: _validate() validates that,
     * for a given object value V:
     *
     * • If `allowUnknownProps` is false, then V has no value of which
     *   the key is not an element of the keys of `propReqs`.
     *
     * • For each property requirement PR having the key K in
     *   `propReqs`: if `PR.isRequired()`, then a value having the key K
     *   exists in V.
     *
     * • For each value VV having the key K in V: VV satisfies the value
     *   requirement, if any, of `propReqs[K]`.
     */
    explicit ObjValReq(PropReqs propReqs, const bool allowUnknownProps,
                       const Logger& parentLogger) :
        ValHasTypeReq<ValT, ValOpsT> {ValType::Obj, parentLogger},
        _mPropReqs {std::move(propReqs)}, _mAllowUnknownProps {allowUnknownProps}
    {
    }

    /*
     * Builds an object value requirement: _validate() validates that,
     * for a given object value V:
     *
     * • V has no value of which the key is not an element of the keys
     *   of `propReqs`.
     *
     * • For each property requirement PR having the key K in
     *   `propReqs`: if `PR.isRequired()`, then a value having the key K
     *   exists in V.
     *
     * • For each value VV having the key K in V: VV satisfies the value
     *   requirement, if any, of `propReqs[K]`.
     */
    explicit ObjValReq(PropReqs propReqs, const Logger& parentLogger) :
        ObjValReq {std::move(propReqs), false, parentLogger}
    {
    }

    /*
     * Returns a shared pointer to object value requirement, forwarding
     * the parameters to the constructor.
     */
    static typename ValReq<ValT, ValOpsT>::SP
    shared(PropReqs propReqs, const bool allowUnknownProps, const Logger& parentLogger)
    {
        return std::make_shared<ObjValReq>(std::move(propReqs), allowUnknownProps, parentLogger);
    }

    /*
     * Returns a shared pointer to object value requirement, forwarding
     * the parameters to the constructor.
     */
    static typename ValReq<ValT, ValOpsT>::SP shared(PropReqs propReqs, const Logger& parentLogger)
    {
        return std::make_shared<ObjValReq>(std::move(propReqs), parentLogger);
    }

protected:
    void _validate(const ValT& val) const override
    {
        ValHasTypeReq<ValT, ValOpsT>::_validate(val);

        auto& objVal = ValOpsT::asObj(val);
        const auto objValTypeStr = ValOpsT::typeStr(ValType::Obj);

        for (auto& keyPropReqPair : _mPropReqs) {
            auto& key = keyPropReqPair.first;

            if (keyPropReqPair.second.isRequired() && !ValOpsT::objValVal(objVal, key)) {
                BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_THROW_SPEC(
                    this->_logger(), Error, this->_loc(objVal), "Missing mandatory {} {} `{}`.",
                    objValTypeStr, ValOpsT::objValPropName, key);
            }
        }

        for (auto it = ValOpsT::objValBegin(objVal); it != ValOpsT::objValEnd(objVal); ++it) {
            auto& key = ValOpsT::objValItKey(it);
            auto& propVal = ValOpsT::objValItVal(it);
            const auto keyPropReqPairIt = _mPropReqs.find(key);

            if (keyPropReqPairIt == _mPropReqs.end()) {
                /* No property requirement found */
                if (_mAllowUnknownProps) {
                    continue;
                } else {
                    BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_THROW_SPEC(
                        this->_logger(), Error, this->_loc(propVal), "Unknown {} {} `{}`.",
                        objValTypeStr, ValOpsT::objValPropName, key);
                }
            }

            try {
                keyPropReqPairIt->second.validate(propVal);
            } catch (const Error&) {
                BT_CPPLOGE_TEXT_LOC_APPEND_CAUSE_AND_RETHROW_SPEC(
                    this->_logger(), this->_loc(propVal), "Invalid {} {} `{}`.", objValTypeStr,
                    ValOpsT::objValPropName, key);
            }
        }
    }

private:
    PropReqs _mPropReqs;
    bool _mAllowUnknownProps;
};

} /* namespace bt2c */

#endif /* BABELTRACE_CPP_COMMON_BT2C_VAL_REQ_HPP */
