/**
 * @file
 *
 * @brief   Class definitions to support JSON Schema constraints
 *
 * This file contains class definitions for all of the constraints required to
 * support JSON Schema. These classes all inherit from the BasicConstraint
 * template class, which implements the common parts of the Constraint
 * interface.
 *
 * @see BasicConstraint
 * @see Constraint
 */

#pragma once

#include <limits>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <cmath>

#include <valijson/constraints/basic_constraint.hpp>
#include <valijson/internal/custom_allocator.hpp>
#include <valijson/internal/frozen_value.hpp>
#include <valijson/schema.hpp>
#include <valijson/exceptions.hpp>
#include <valijson/validation_results.hpp>

#ifdef _MSC_VER
#pragma warning( push )
#pragma warning( disable : 4702 )
#endif

namespace valijson {

class ValidationResults;

namespace constraints {

/**
 * @brief  Represents an 'allOf' constraint.
 *
 * An allOf constraint provides a collection of sub-schemas that a value must
 * validate against. If a value fails to validate against any of these sub-
 * schemas, then validation fails.
 */
class AllOfConstraint: public BasicConstraint<AllOfConstraint>
{
public:
    AllOfConstraint()
      : m_subschemas(Allocator::rebind<const Subschema *>::other(m_allocator)) { }

    AllOfConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_subschemas(Allocator::rebind<const Subschema *>::other(m_allocator)) { }

    void addSubschema(const Subschema *subschema)
    {
        m_subschemas.push_back(subschema);
    }

    template<typename FunctorType>
    void applyToSubschemas(const FunctorType &fn) const
    {
        unsigned int index = 0;
        for (const Subschema *subschema : m_subschemas) {
            if (!fn(index, subschema)) {
                return;
            }

            index++;
        }
    }

private:
    typedef std::vector<const Subschema *, internal::CustomAllocator<const Subschema *>> Subschemas;

    /// Collection of sub-schemas, all of which must be satisfied
    Subschemas m_subschemas;
};

/**
 * @brief  Represents an 'anyOf' constraint
 *
 * An anyOf constraint provides a collection of sub-schemas that a value can
 * validate against. If a value validates against one of these sub-schemas,
 * then the validation passes.
 */
class AnyOfConstraint: public BasicConstraint<AnyOfConstraint>
{
public:
    AnyOfConstraint()
      : m_subschemas(Allocator::rebind<const Subschema *>::other(m_allocator)) { }

    AnyOfConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_subschemas(Allocator::rebind<const Subschema *>::other(m_allocator)) { }

    void addSubschema(const Subschema *subschema)
    {
        m_subschemas.push_back(subschema);
    }

    template<typename FunctorType>
    void applyToSubschemas(const FunctorType &fn) const
    {
        unsigned int index = 0;
        for (const Subschema *subschema : m_subschemas) {
            if (!fn(index, subschema)) {
                return;
            }

            index++;
        }
    }

private:
    typedef std::vector<const Subschema *, internal::CustomAllocator<const Subschema *>> Subschemas;

    /// Collection of sub-schemas, at least one of which must be satisfied
    Subschemas m_subschemas;
};

/**
 * @brief  Represents a combination 'if', 'then' and 'else' constraints
 *
 * The schema provided by an 'if' constraint is used as the expression for a conditional. When the
 * target validates against that schema, the 'then' subschema will be also be tested. Otherwise,
 * the 'else' subschema will be tested.
 */
class ConditionalConstraint: public BasicConstraint<ConditionalConstraint>
{
public:
    ConditionalConstraint()
      : m_ifSubschema(nullptr),
        m_thenSubschema(nullptr),
        m_elseSubschema(nullptr) { }

    ConditionalConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_ifSubschema(nullptr),
        m_thenSubschema(nullptr),
        m_elseSubschema(nullptr) { }

    const Subschema * getIfSubschema() const
    {
        return m_ifSubschema;
    }

    const Subschema * getThenSubschema() const
    {
        return m_thenSubschema;
    }

    const Subschema * getElseSubschema() const
    {
        return m_elseSubschema;
    }

    void setIfSubschema(const Subschema *subschema)
    {
        m_ifSubschema = subschema;
    }

    void setThenSubschema(const Subschema *subschema)
    {
        m_thenSubschema = subschema;
    }

    void setElseSubschema(const Subschema *subschema)
    {
        m_elseSubschema = subschema;
    }

private:
    const Subschema *m_ifSubschema;
    const Subschema *m_thenSubschema;
    const Subschema *m_elseSubschema;
};

class ConstConstraint: public BasicConstraint<ConstConstraint>
{
public:
    ConstConstraint()
      : m_value(nullptr) { }

    ConstConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_value(nullptr) { }

    ConstConstraint(const ConstConstraint &other)
      : BasicConstraint(other),
        m_value(other.m_value->clone()) { }

    adapters::FrozenValue * getValue() const
    {
        return m_value.get();
    }

    void setValue(const adapters::Adapter &value)
    {
        m_value = std::unique_ptr<adapters::FrozenValue>(value.freeze());
    }

private:
    std::unique_ptr<adapters::FrozenValue> m_value;
};

/**
 * @brief  Represents a 'contains' constraint
 *
 * A 'contains' constraint specifies a schema that must be satisfied by at least one
 * of the values in an array.
 */
class ContainsConstraint: public BasicConstraint<ContainsConstraint>
{
public:
    ContainsConstraint()
      : m_subschema(nullptr) { }

    ContainsConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_subschema(nullptr) { }

    const Subschema * getSubschema() const
    {
        return m_subschema;
    }

    void setSubschema(const Subschema *subschema)
    {
        m_subschema = subschema;
    }

private:
    const Subschema *m_subschema;
};

/**
 * @brief  Represents a 'dependencies' constraint.
 *
 * A dependency constraint ensures that a given property is valid only if the
 * properties that it depends on are present.
 */
class DependenciesConstraint: public BasicConstraint<DependenciesConstraint>
{
public:
    DependenciesConstraint()
      : m_propertyDependencies(std::less<String>(), m_allocator),
        m_schemaDependencies(std::less<String>(), m_allocator)
    { }

    DependenciesConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_propertyDependencies(std::less<String>(), m_allocator),
        m_schemaDependencies(std::less<String>(), m_allocator)
    { }

    template<typename StringType>
    DependenciesConstraint & addPropertyDependency(
            const StringType &propertyName,
            const StringType &dependencyName)
    {
        const String key(propertyName.c_str(), m_allocator);
        auto itr = m_propertyDependencies.find(key);
        if (itr == m_propertyDependencies.end()) {
            itr = m_propertyDependencies.insert(PropertyDependencies::value_type(
                    key, PropertySet(std::less<String>(), m_allocator))).first;
        }

        itr->second.insert(String(dependencyName.c_str(), m_allocator));

        return *this;
    }

    template<typename StringType, typename ContainerType>
    DependenciesConstraint & addPropertyDependencies(
            const StringType &propertyName,
            const ContainerType &dependencyNames)
    {
        const String key(propertyName.c_str(), m_allocator);
        auto itr = m_propertyDependencies.find(key);
        if (itr == m_propertyDependencies.end()) {
            itr = m_propertyDependencies.insert(PropertyDependencies::value_type(
                    key, PropertySet(std::less<String>(), m_allocator))).first;
        }

        typedef typename ContainerType::value_type ValueType;
        for (const ValueType &dependencyName : dependencyNames) {
            itr->second.insert(String(dependencyName.c_str(), m_allocator));
        }

        return *this;
    }

    template<typename StringType>
    DependenciesConstraint & addSchemaDependency(const StringType &propertyName, const Subschema *schemaDependency)
    {
        if (m_schemaDependencies.insert(SchemaDependencies::value_type(
                String(propertyName.c_str(), m_allocator),
                schemaDependency)).second) {
            return *this;
        }

        throwRuntimeError("Dependencies constraint already contains a dependent "
                "schema for the property '" + propertyName + "'");
    }

    template<typename FunctorType>
    void applyToPropertyDependencies(const FunctorType &fn) const
    {
        for (const PropertyDependencies::value_type &v : m_propertyDependencies) {
            if (!fn(v.first, v.second)) {
                return;
            }
        }
    }

    template<typename FunctorType>
    void applyToSchemaDependencies(const FunctorType &fn) const
    {
        for (const SchemaDependencies::value_type &v : m_schemaDependencies) {
            if (!fn(v.first, v.second)) {
                return;
            }
        }
    }

private:
    typedef std::set<String, std::less<String>, internal::CustomAllocator<String>> PropertySet;

    typedef std::map<String, PropertySet, std::less<String>,
            internal::CustomAllocator<std::pair<const String, PropertySet>>> PropertyDependencies;

    typedef std::map<String, const Subschema *, std::less<String>,
            internal::CustomAllocator<std::pair<const String, const Subschema *>>> SchemaDependencies;

    /// Mapping from property names to their property-based dependencies
    PropertyDependencies m_propertyDependencies;

    /// Mapping from property names to their schema-based dependencies
    SchemaDependencies m_schemaDependencies;
};

/**
 * @brief  Represents an 'enum' constraint
 *
 * An enum constraint provides a collection of permissible values for a JSON
 * node. The node will only validate against this constraint if it matches one
 * or more of the values in the collection.
 */
class EnumConstraint: public BasicConstraint<EnumConstraint>
{
public:
    EnumConstraint()
      : m_enumValues(Allocator::rebind<const EnumValue *>::other(m_allocator)) { }

    EnumConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_enumValues(Allocator::rebind<const EnumValue *>::other(m_allocator)) { }

    EnumConstraint(const EnumConstraint &other)
      : BasicConstraint(other),
        m_enumValues(Allocator::rebind<const EnumValue *>::other(m_allocator))
    {
#if VALIJSON_USE_EXCEPTIONS
        try {
#endif
            // Clone individual enum values
            for (const EnumValue *otherValue : other.m_enumValues) {
                const EnumValue *value = otherValue->clone();
#if VALIJSON_USE_EXCEPTIONS
                try {
#endif
                    m_enumValues.push_back(value);
#if VALIJSON_USE_EXCEPTIONS
                } catch (...) {
                    delete value;
                    value = nullptr;
                    throw;
                }
            }
        } catch (...) {
            // Delete values already added to constraint
            for (const EnumValue *value : m_enumValues) {
                delete value;
            }
            throw;
#endif
        }
    }

    ~EnumConstraint() override
    {
        for (const EnumValue *value : m_enumValues) {
            delete value;
        }
    }

    void addValue(const adapters::Adapter &value)
    {
        // TODO: Freeze value using custom alloc/free functions
        m_enumValues.push_back(value.freeze());
    }

    void addValue(const adapters::FrozenValue &value)
    {
        // TODO: Clone using custom alloc/free functions
        m_enumValues.push_back(value.clone());
    }

    template<typename FunctorType>
    void applyToValues(const FunctorType &fn) const
    {
        for (const EnumValue *value : m_enumValues) {
            if (!fn(*value)) {
                return;
            }
        }
    }

private:
    typedef adapters::FrozenValue EnumValue;

    typedef std::vector<const EnumValue *, internal::CustomAllocator<const EnumValue *>> EnumValues;

    EnumValues m_enumValues;
};

/**
 * @brief  Represent a 'format' constraint
 *
 * A format constraint restricts the content of string values, as defined by a set of commonly used formats.
 *
 * As this is an optional feature in JSON Schema, unrecognised formats will be treated as valid for any string value.
 */
class FormatConstraint: public BasicConstraint<FormatConstraint>
{
public:
    FormatConstraint()
        : m_format() { }

    const std::string & getFormat() const
    {
        return m_format;
    }

    void setFormat(const std::string & format)
    {
        m_format = format;
    }

private:
    std::string m_format;
};

/**
 * @brief  Represents non-singular 'items' and 'additionalItems' constraints
 *
 * Unlike the SingularItemsConstraint class, this class represents an 'items'
 * constraint that specifies an array of sub-schemas, which should be used to
 * validate each item in an array, in sequence. It also represents an optional
 * 'additionalItems' sub-schema that should be used when an array contains
 * more values than there are sub-schemas in the 'items' constraint.
 *
 * The prefix 'Linear' comes from the fact that this class contains a list of
 * sub-schemas that corresponding array items must be validated against, and
 * this validation is performed linearly (i.e. in sequence).
 */
class LinearItemsConstraint: public BasicConstraint<LinearItemsConstraint>
{
public:
    LinearItemsConstraint()
      : m_itemSubschemas(Allocator::rebind<const Subschema *>::other(m_allocator)),
        m_additionalItemsSubschema(nullptr) { }

    LinearItemsConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_itemSubschemas(Allocator::rebind<const Subschema *>::other(m_allocator)),
        m_additionalItemsSubschema(nullptr) { }

    void addItemSubschema(const Subschema *subschema)
    {
        m_itemSubschemas.push_back(subschema);
    }

    template<typename FunctorType>
    void applyToItemSubschemas(const FunctorType &fn) const
    {
        unsigned int index = 0;
        for (const Subschema *subschema : m_itemSubschemas) {
            if (!fn(index, subschema)) {
                return;
            }

            index++;
        }
    }

    const Subschema * getAdditionalItemsSubschema() const
    {
        return m_additionalItemsSubschema;
    }

    size_t getItemSubschemaCount() const
    {
        return m_itemSubschemas.size();
    }

    void setAdditionalItemsSubschema(const Subschema *subschema)
    {
        m_additionalItemsSubschema = subschema;
    }

private:
    typedef std::vector<const Subschema *, internal::CustomAllocator<const Subschema *>> Subschemas;

    Subschemas m_itemSubschemas;

    const Subschema* m_additionalItemsSubschema;
};

/**
 * @brief   Represents 'maximum' and 'exclusiveMaximum' constraints
 */
class MaximumConstraint: public BasicConstraint<MaximumConstraint>
{
public:
    MaximumConstraint()
      : m_maximum(std::numeric_limits<double>::infinity()),
        m_exclusiveMaximum(false) { }

    MaximumConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_maximum(std::numeric_limits<double>::infinity()),
        m_exclusiveMaximum(false) { }

    bool getExclusiveMaximum() const
    {
        return m_exclusiveMaximum;
    }

    void setExclusiveMaximum(bool newExclusiveMaximum)
    {
        m_exclusiveMaximum = newExclusiveMaximum;
    }

    double getMaximum() const
    {
        return m_maximum;
    }

    void setMaximum(double newMaximum)
    {
        m_maximum = newMaximum;
    }

private:
    double m_maximum;
    bool m_exclusiveMaximum;
};

/**
 * @brief   Represents a 'maxItems' constraint
 */
class MaxItemsConstraint: public BasicConstraint<MaxItemsConstraint>
{
public:
    MaxItemsConstraint()
      : m_maxItems(std::numeric_limits<uint64_t>::max()) { }

    MaxItemsConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_maxItems(std::numeric_limits<uint64_t>::max()) { }

    uint64_t getMaxItems() const
    {
        return m_maxItems;
    }

    void setMaxItems(uint64_t newMaxItems)
    {
        m_maxItems = newMaxItems;
    }

private:
    uint64_t m_maxItems;
};

/**
 * @brief   Represents a 'maxLength' constraint
 */
class MaxLengthConstraint: public BasicConstraint<MaxLengthConstraint>
{
public:
    MaxLengthConstraint()
      : m_maxLength(std::numeric_limits<uint64_t>::max()) { }

    MaxLengthConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_maxLength(std::numeric_limits<uint64_t>::max()) { }

    uint64_t getMaxLength() const
    {
        return m_maxLength;
    }

    void setMaxLength(uint64_t newMaxLength)
    {
        m_maxLength = newMaxLength;
    }

private:
    uint64_t m_maxLength;
};

/**
 * @brief   Represents a 'maxProperties' constraint
 */
class MaxPropertiesConstraint: public BasicConstraint<MaxPropertiesConstraint>
{
public:
    MaxPropertiesConstraint()
      : m_maxProperties(std::numeric_limits<uint64_t>::max()) { }

    MaxPropertiesConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_maxProperties(std::numeric_limits<uint64_t>::max()) { }

    uint64_t getMaxProperties() const
    {
        return m_maxProperties;
    }

    void setMaxProperties(uint64_t newMaxProperties)
    {
        m_maxProperties = newMaxProperties;
    }

private:
    uint64_t m_maxProperties;
};

/**
 * @brief   Represents 'minimum' and 'exclusiveMinimum' constraints
 */
class MinimumConstraint: public BasicConstraint<MinimumConstraint>
{
public:
    MinimumConstraint()
      : m_minimum(-std::numeric_limits<double>::infinity()),
        m_exclusiveMinimum(false) { }

    MinimumConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_minimum(-std::numeric_limits<double>::infinity()),
        m_exclusiveMinimum(false) { }

    bool getExclusiveMinimum() const
    {
        return m_exclusiveMinimum;
    }

    void setExclusiveMinimum(bool newExclusiveMinimum)
    {
        m_exclusiveMinimum = newExclusiveMinimum;
    }

    double getMinimum() const
    {
        return m_minimum;
    }

    void setMinimum(double newMinimum)
    {
        m_minimum = newMinimum;
    }

private:
    double m_minimum;
    bool m_exclusiveMinimum;
};

/**
 * @brief   Represents a 'minItems' constraint
 */
class MinItemsConstraint: public BasicConstraint<MinItemsConstraint>
{
public:
    MinItemsConstraint()
      : m_minItems(0) { }

    MinItemsConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_minItems(0) { }

    uint64_t getMinItems() const
    {
        return m_minItems;
    }

    void setMinItems(uint64_t newMinItems)
    {
        m_minItems = newMinItems;
    }

private:
    uint64_t m_minItems;
};

/**
 * @brief   Represents a 'minLength' constraint
 */
class MinLengthConstraint: public BasicConstraint<MinLengthConstraint>
{
public:
    MinLengthConstraint()
      : m_minLength(0) { }

    MinLengthConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_minLength(0) { }

    uint64_t getMinLength() const
    {
        return m_minLength;
    }

    void setMinLength(uint64_t newMinLength)
    {
        m_minLength = newMinLength;
    }

private:
    uint64_t m_minLength;
};

/**
 * @brief   Represents a 'minProperties' constraint
 */
class MinPropertiesConstraint: public BasicConstraint<MinPropertiesConstraint>
{
public:
    MinPropertiesConstraint()
      : m_minProperties(0) { }

    MinPropertiesConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_minProperties(0) { }

    uint64_t getMinProperties() const
    {
        return m_minProperties;
    }

    void setMinProperties(uint64_t newMinProperties)
    {
        m_minProperties = newMinProperties;
    }

private:
    uint64_t m_minProperties;
};

/**
 * @brief  Represents either 'multipleOf' or 'divisibleBy' constraints where
 *         the divisor is a floating point number
 */
class MultipleOfDoubleConstraint:
        public BasicConstraint<MultipleOfDoubleConstraint>
{
public:
    MultipleOfDoubleConstraint()
      : m_value(1.) { }

    MultipleOfDoubleConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_value(1.) { }

    double getDivisor() const
    {
        return m_value;
    }

    void setDivisor(double newValue)
    {
        if (!std::isfinite(newValue) || newValue <= 0.0) {
            throwRuntimeError(
                "Divisor for 'multipleOf' or 'divisibleBy' must be positive");
        }

        m_value = newValue;
    }

private:
    double m_value;
};

/**
 * @brief  Represents either 'multipleOf' or 'divisibleBy' constraints where
 *         the divisor is of integer type
 */
class MultipleOfIntConstraint:
        public BasicConstraint<MultipleOfIntConstraint>
{
public:
    MultipleOfIntConstraint()
      : m_value(1) { }

    MultipleOfIntConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_value(1) { }

    int64_t getDivisor() const
    {
        return m_value;
    }

    void setDivisor(int64_t newValue)
    {
        if (newValue <= 0) {
            throwRuntimeError(
                "Divisor for 'multipleOf' or 'divisibleBy' must be positive");
        }

        m_value = newValue;
    }

private:
    int64_t m_value;
};

/**
 * @brief   Represents a 'not' constraint
 */
class NotConstraint: public BasicConstraint<NotConstraint>
{
public:
    NotConstraint()
      : m_subschema(nullptr) { }

    NotConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_subschema(nullptr) { }

    const Subschema * getSubschema() const
    {
        return m_subschema;
    }

    void setSubschema(const Subschema *subschema)
    {
        m_subschema = subschema;
    }

private:
    const Subschema *m_subschema;
};

/**
 * @brief   Represents a 'oneOf' constraint.
 */
class OneOfConstraint: public BasicConstraint<OneOfConstraint>
{
public:
    OneOfConstraint()
      : m_subschemas(Allocator::rebind<const Subschema *>::other(m_allocator)) { }

    OneOfConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_subschemas(Allocator::rebind<const Subschema *>::other(m_allocator)) { }

    void addSubschema(const Subschema *subschema)
    {
        m_subschemas.push_back(subschema);
    }

    template<typename FunctorType>
    void applyToSubschemas(const FunctorType &fn) const
    {
        unsigned int index = 0;
        for (const Subschema *subschema : m_subschemas) {
            if (!fn(index, subschema)) {
                return;
            }

            index++;
        }
    }

private:
    typedef std::vector<const Subschema *, internal::CustomAllocator<const Subschema *>> Subschemas;

    /// Collection of sub-schemas, exactly one of which must be satisfied
    Subschemas m_subschemas;
};

/**
 * @brief   Represents a 'pattern' constraint
 */
class PatternConstraint: public BasicConstraint<PatternConstraint>
{
public:
    PatternConstraint()
      : m_pattern(Allocator::rebind<char>::other(m_allocator)) { }

    PatternConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_pattern(Allocator::rebind<char>::other(m_allocator)) { }

    template<typename AllocatorType>
    bool getPattern(std::basic_string<char, std::char_traits<char>, AllocatorType> &result) const
    {
        result.assign(m_pattern.c_str());
        return true;
    }

    template<typename AllocatorType>
    std::basic_string<char, std::char_traits<char>, AllocatorType> getPattern(
            const AllocatorType &alloc = AllocatorType()) const
    {
        return std::basic_string<char, std::char_traits<char>, AllocatorType>(m_pattern.c_str(), alloc);
    }

    template<typename AllocatorType>
    void setPattern(const std::basic_string<char, std::char_traits<char>, AllocatorType> &pattern)
    {
        m_pattern.assign(pattern.c_str());
    }

private:
    String m_pattern;
};

class PolyConstraint : public Constraint
{
public:
    bool accept(ConstraintVisitor &visitor) const override
    {
        return visitor.visit(*static_cast<const PolyConstraint*>(this));
    }

    OwningPointer clone(CustomAlloc allocFn, CustomFree freeFn) const override
    {
        // smart pointer to automatically free raw memory on exception
        typedef std::unique_ptr<Constraint, CustomFree> RawOwningPointer;
        auto ptr = RawOwningPointer(static_cast<Constraint*>(allocFn(sizeOf())), freeFn);
        if (!ptr) {
            throwRuntimeError("Failed to allocate memory for cloned constraint");
        }

        // constructor might throw but the memory will be taken care of anyways
        (void)cloneInto(ptr.get());

        // implicitly convert to smart pointer that will also destroy object instance
        return ptr;
    }

    virtual bool validate(const adapters::Adapter &target,
            const valijson::ValidationResults::Path &path,
            valijson::ValidationResults *results) const = 0;

private:
    virtual Constraint * cloneInto(void *) const = 0;

    virtual size_t sizeOf() const = 0;
};

/**
 * @brief   Represents a combination of 'properties', 'patternProperties' and
 *          'additionalProperties' constraints
 */
class PropertiesConstraint: public BasicConstraint<PropertiesConstraint>
{
public:
    PropertiesConstraint()
      : m_properties(std::less<String>(), m_allocator),
        m_patternProperties(std::less<String>(), m_allocator),
        m_additionalProperties(nullptr) { }

    PropertiesConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_properties(std::less<String>(), m_allocator),
        m_patternProperties(std::less<String>(), m_allocator),
        m_additionalProperties(nullptr) { }

    bool addPatternPropertySubschema(const char *patternProperty, const Subschema *subschema)
    {
        return m_patternProperties.insert(PropertySchemaMap::value_type(
                String(patternProperty, m_allocator), subschema)).second;
    }

    template<typename AllocatorType>
    bool addPatternPropertySubschema(const std::basic_string<char,
            std::char_traits<char>, AllocatorType> &patternProperty,
            const Subschema *subschema)
    {
        return addPatternPropertySubschema(patternProperty.c_str(), subschema);
    }

    bool addPropertySubschema(const char *propertyName,
            const Subschema *subschema)
    {
        return m_properties.insert(PropertySchemaMap::value_type(
                String(propertyName, m_allocator), subschema)).second;
    }

    template<typename AllocatorType>
    bool addPropertySubschema(const std::basic_string<char,
            std::char_traits<char>, AllocatorType> &propertyName,
            const Subschema *subschema)
    {
        return addPropertySubschema(propertyName.c_str(), subschema);
    }

    template<typename FunctorType>
    void applyToPatternProperties(const FunctorType &fn) const
    {
        typedef typename PropertySchemaMap::value_type ValueType;
        for (const ValueType &value : m_patternProperties) {
            if (!fn(value.first, value.second)) {
                return;
            }
        }
    }

    template<typename FunctorType>
    void applyToProperties(const FunctorType &fn) const
    {
        typedef typename PropertySchemaMap::value_type ValueType;
        for (const ValueType &value : m_properties) {
            if (!fn(value.first, value.second)) {
                return;
            }
        }
    }

    const Subschema * getAdditionalPropertiesSubschema() const
    {
        return m_additionalProperties;
    }

    void setAdditionalPropertiesSubschema(const Subschema *subschema)
    {
        m_additionalProperties = subschema;
    }

private:
    typedef std::map<
            String,
            const Subschema *,
            std::less<String>,
            internal::CustomAllocator<std::pair<const String, const Subschema *>>
        > PropertySchemaMap;

    PropertySchemaMap m_properties;
    PropertySchemaMap m_patternProperties;

    const Subschema *m_additionalProperties;
};

class PropertyNamesConstraint: public BasicConstraint<PropertyNamesConstraint>
{
public:
    PropertyNamesConstraint()
      : m_subschema(nullptr) { }

    PropertyNamesConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_subschema(nullptr) { }

    const Subschema * getSubschema() const
    {
        return m_subschema;
    }

    void setSubschema(const Subschema *subschema)
    {
        m_subschema = subschema;
    }

private:
    const Subschema *m_subschema;
};

/**
 * @brief   Represents a 'required' constraint
 */
class RequiredConstraint: public BasicConstraint<RequiredConstraint>
{
public:
    RequiredConstraint()
      : m_requiredProperties(std::less<String>(), m_allocator) { }

    RequiredConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_requiredProperties(std::less<String>(), m_allocator) { }

    bool addRequiredProperty(const char *propertyName)
    {
        return m_requiredProperties.insert(String(propertyName,
                Allocator::rebind<char>::other(m_allocator))).second;
    }

    template<typename AllocatorType>
    bool addRequiredProperty(const std::basic_string<char, std::char_traits<char>, AllocatorType> &propertyName)
    {
        return addRequiredProperty(propertyName.c_str());
    }

    template<typename FunctorType>
    void applyToRequiredProperties(const FunctorType &fn) const
    {
        for (const String &propertyName : m_requiredProperties) {
            if (!fn(propertyName)) {
                return;
            }
        }
    }

private:
    typedef std::set<String, std::less<String>,
            internal::CustomAllocator<String>> RequiredProperties;

    RequiredProperties m_requiredProperties;
};

/**
 * @brief  Represents an 'items' constraint that specifies one sub-schema
 *
 * A value is considered valid against this constraint if it is an array, and
 * each item in the array validates against the sub-schema specified by this
 * constraint.
 *
 * The prefix 'Singular' comes from the fact that array items must validate
 * against exactly one sub-schema.
 */
class SingularItemsConstraint: public BasicConstraint<SingularItemsConstraint>
{
public:
    SingularItemsConstraint()
      : m_itemsSubschema(nullptr) { }

    SingularItemsConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_itemsSubschema(nullptr) { }

    const Subschema * getItemsSubschema() const
    {
        return m_itemsSubschema;
    }

    void setItemsSubschema(const Subschema *subschema)
    {
        m_itemsSubschema = subschema;
    }

private:
    const Subschema *m_itemsSubschema;
};

/**
 * @brief   Represents a 'type' constraint.
 */
class TypeConstraint: public BasicConstraint<TypeConstraint>
{
public:
    enum JsonType {
        kAny,
        kArray,
        kBoolean,
        kInteger,
        kNull,
        kNumber,
        kObject,
        kString
    };

    TypeConstraint()
      : m_namedTypes(std::less<JsonType>(), m_allocator),
        m_schemaTypes(Allocator::rebind<const Subschema *>::other(m_allocator)) { }

    TypeConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn),
        m_namedTypes(std::less<JsonType>(), m_allocator),
        m_schemaTypes(Allocator::rebind<const Subschema *>::other(m_allocator)) { }

    void addNamedType(JsonType type)
    {
        m_namedTypes.insert(type);
    }

    void addSchemaType(const Subschema *subschema)
    {
        m_schemaTypes.push_back(subschema);
    }

    template<typename FunctorType>
    void applyToNamedTypes(const FunctorType &fn) const
    {
        for (const JsonType namedType : m_namedTypes) {
            if (!fn(namedType)) {
                return;
            }
        }
    }

    template<typename FunctorType>
    void applyToSchemaTypes(const FunctorType &fn) const
    {
        unsigned int index = 0;
        for (const Subschema *subschema : m_schemaTypes) {
            if (!fn(index, subschema)) {
                return;
            }

            index++;
        }
    }

    template<typename AllocatorType>
    static JsonType jsonTypeFromString(const std::basic_string<char,
            std::char_traits<char>, AllocatorType> &typeName)
    {
        if (typeName.compare("any") == 0) {
            return kAny;
        } else if (typeName.compare("array") == 0) {
            return kArray;
        } else if (typeName.compare("boolean") == 0) {
            return kBoolean;
        } else if (typeName.compare("integer") == 0) {
            return kInteger;
        } else if (typeName.compare("null") == 0) {
            return kNull;
        } else if (typeName.compare("number") == 0) {
            return kNumber;
        } else if (typeName.compare("object") == 0) {
            return kObject;
        } else if (typeName.compare("string") == 0) {
            return kString;
        }

        throwRuntimeError("Unrecognised JSON type name '" +
                std::string(typeName.c_str()) + "'");
        abort();
    }

private:
    typedef std::set<JsonType, std::less<JsonType>, internal::CustomAllocator<JsonType>> NamedTypes;

    typedef std::vector<const Subschema *,
            Allocator::rebind<const Subschema *>::other> SchemaTypes;

    /// Set of named JSON types that serve as valid types
    NamedTypes m_namedTypes;

    /// Set of sub-schemas that serve as valid types
    SchemaTypes m_schemaTypes;
};

/**
 * @brief   Represents a 'uniqueItems' constraint
 */
class UniqueItemsConstraint: public BasicConstraint<UniqueItemsConstraint>
{
public:
    UniqueItemsConstraint() = default;

    UniqueItemsConstraint(CustomAlloc allocFn, CustomFree freeFn)
      : BasicConstraint(allocFn, freeFn) { }
};

} // namespace constraints
} // namespace valijson

#ifdef _MSC_VER
#pragma warning( pop )
#endif
