// Copyright (c) 2016-2020 Dr. Colin Hirsch and Daniel Frey
// Please see LICENSE for license or visit https://github.com/taocpp/json/

#ifndef TAO_JSON_CONTRIB_SCHEMA_HPP
#define TAO_JSON_CONTRIB_SCHEMA_HPP

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <memory>
#include <regex>
#include <set>
#include <stdexcept>
#include <string>
#include <type_traits>

#include "reference.hpp"

#include "../events/compare.hpp"
#include "../events/from_value.hpp"
#include "../events/hash.hpp"
#include "../pointer.hpp"
#include "../value.hpp"

#include "../external/pegtl/contrib/uri.hpp"
#include "../external/pegtl/parse.hpp"

namespace tao::json
{
   namespace internal
   {
      // TODO: Check if these grammars are correct.
      struct local_part_label
         : pegtl::plus< pegtl::sor< pegtl::alnum, pegtl::one< '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~' > > >
      {};

      struct local_part
         : pegtl::list_must< local_part_label, pegtl::one< '.' > >
      {};

      struct hostname_label
         : pegtl::seq< pegtl::alnum, pegtl::rep_max< 62, pegtl::ranges< 'a', 'z', 'A', 'Z', '0', '9', '-' > > >
      {};

      struct hostname
         : pegtl::list_must< hostname_label, pegtl::one< '.' > >
      {};

      struct email
         : pegtl::seq< local_part, pegtl::one< '@' >, hostname >
      {};

      template< typename Rule >
      [[nodiscard]] bool parse( const std::string_view v )
      {
         pegtl::memory_input in( v.data(), v.size(), "" );
         return pegtl::parse< pegtl::seq< Rule, pegtl::eof > >( in );
      }

      [[nodiscard]] inline bool parse_date_time( const std::string_view v )
      {
         static std::regex re( "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:[0-5]\\d)$" );
         if( !std::regex_search( v.begin(), v.end(), re ) ) {
            return false;
         }

         const unsigned year = ( v[ 0 ] - '0' ) * 1000 + ( v[ 1 ] - '0' ) * 100 + ( v[ 2 ] - '0' ) * 10 + ( v[ 3 ] - '0' );
         const unsigned month = ( v[ 5 ] - '0' ) * 10 + ( v[ 6 ] - '0' );
         const unsigned day = ( v[ 8 ] - '0' ) * 10 + ( v[ 9 ] - '0' );

         if( month == 0 || month > 12 ) {
            return false;
         }
         if( day == 0 || day > 31 ) {
            return false;
         }
         if( month == 2 ) {
            const bool is_leap_year = ( year % 4 == 0 ) && ( year % 100 != 0 || year % 400 == 0 );
            if( day > ( is_leap_year ? 29U : 28U ) ) {
               return false;
            }
         }
         else if( day == 31 ) {
            switch( month ) {
               case 4:
               case 6:
               case 9:
               case 11:
                  return false;
               default:;
            }
         }

         const unsigned hour = ( v[ 11 ] - '0' ) * 10 + ( v[ 12 ] - '0' );
         if( hour >= 24 ) {
            return false;
         }

         if( *v.rbegin() != 'Z' ) {
            const auto s = v.size();
            const unsigned tz_hour = ( v[ s - 5 ] - '0' ) * 10 + ( v[ s - 4 ] - '0' );
            if( tz_hour >= 24 ) {
               return false;
            }
         }

         return true;
      }

      [[nodiscard]] inline std::size_t unicode_size( const std::string_view v ) noexcept
      {
         std::size_t r = 0;
         for( char c : v ) {
            r += static_cast< std::size_t >( ( c & 0xC0 ) != 0x80 );
         }
         return r;
      }

      enum schema_flags
      {
         NONE = 0,

         HAS_TYPE = 1 << 0,
         NULL_ = 1 << 1,
         BOOLEAN = 1 << 2,
         INTEGER = 1 << 3,
         NUMBER = 1 << 4,
         STRING = 1 << 5,
         ARRAY = 1 << 6,
         OBJECT = 1 << 7,

         HAS_ENUM = 1 << 8,

         HAS_MULTIPLE_OF_UNSIGNED = 1 << 9,
         HAS_MULTIPLE_OF_DOUBLE = 1 << 10,
         HAS_MULTIPLE_OF = 3 << 9,

         HAS_MAXIMUM_SIGNED = 1 << 11,
         HAS_MAXIMUM_UNSIGNED = 1 << 12,
         HAS_MAXIMUM_DOUBLE = 3 << 11,
         HAS_MAXIMUM = 3 << 11,
         EXCLUSIVE_MAXIMUM = 1 << 13,

         HAS_MINIMUM_SIGNED = 1 << 14,
         HAS_MINIMUM_UNSIGNED = 1 << 15,
         HAS_MINIMUM_DOUBLE = 3 << 14,
         HAS_MINIMUM = 3 << 14,
         EXCLUSIVE_MINIMUM = 1 << 16,

         HAS_MAX_LENGTH = 1 << 17,
         HAS_MIN_LENGTH = 1 << 18,

         HAS_MAX_ITEMS = 1 << 19,
         HAS_MIN_ITEMS = 1 << 20,
         HAS_UNIQUE_ITEMS = 1 << 21,

         HAS_MAX_PROPERTIES = 1 << 22,
         HAS_MIN_PROPERTIES = 1 << 23,
         NO_ADDITIONAL_PROPERTIES = 1 << 24,
         HAS_DEPENDENCIES = 1 << 25
      };

      enum class schema_format
      {
         none,
         date_time,
         email,
         hostname,
         ipv4,
         ipv6,
         uri
      };

      [[nodiscard]] inline constexpr schema_flags operator|( const schema_flags lhs, const schema_flags rhs ) noexcept
      {
         return static_cast< schema_flags >( static_cast< std::underlying_type< schema_flags >::type >( lhs ) | static_cast< std::underlying_type< schema_flags >::type >( rhs ) );
      }

      union schema_limit
      {
         std::int64_t i;
         std::uint64_t u;
         double d;
      };

      template< template< typename... > class Traits >
      class schema_container;

      template< template< typename... > class Traits >
      struct schema_node
      {
         const schema_container< Traits >* m_container;
         const basic_value< Traits >* m_value;
         const basic_value< Traits >* m_all_of = nullptr;
         const basic_value< Traits >* m_any_of = nullptr;
         const basic_value< Traits >* m_one_of = nullptr;
         const basic_value< Traits >* m_not = nullptr;
         const basic_value< Traits >* m_items = nullptr;
         const basic_value< Traits >* m_additional_items = nullptr;
         const basic_value< Traits >* m_properties = nullptr;
         const basic_value< Traits >* m_additional_properties = nullptr;

         std::map< std::string, std::set< std::string > > m_property_dependencies;
         std::map< std::string, const basic_value< Traits >* > m_schema_dependencies;

         std::vector< std::pair< std::regex, const basic_value< Traits >* > > m_pattern_properties;

         std::set< const basic_value< Traits >* > m_referenced_pointers;

         // number
         schema_limit m_multiple_of;
         schema_limit m_maximum;
         schema_limit m_minimum;

         // string
         std::uint64_t m_max_length;
         std::uint64_t m_min_length;
         std::unique_ptr< std::regex > m_pattern;

         // array
         std::uint64_t m_max_items;
         std::uint64_t m_min_items;

         // object
         std::uint64_t m_max_properties;
         std::uint64_t m_min_properties;
         std::set< std::string > m_required;

         schema_flags m_flags = NONE;
         schema_format m_format = schema_format::none;

         void add_type( const schema_flags v )
         {
            if( ( m_flags & v ) != 0 ) {
               throw std::runtime_error( "invalid JSON Schema: duplicate primitive type" );
            }
            m_flags = m_flags | v;
         }

         void add_type( const std::string_view v )
         {
            if( !v.empty() ) {
               switch( v[ 0 ] ) {
                  case 'n':
                     if( v == "number" ) {
                        return add_type( NUMBER );
                     }
                     else if( v == "null" ) {
                        return add_type( NULL_ );
                     }
                     break;
                  case 'b':
                     if( v == "boolean" ) {
                        return add_type( BOOLEAN );
                     }
                     break;
                  case 'i':
                     if( v == "integer" ) {
                        return add_type( INTEGER );
                     }
                     break;
                  case 's':
                     if( v == "string" ) {
                        return add_type( STRING );
                     }
                     break;
                  case 'a':
                     if( v == "array" ) {
                        return add_type( ARRAY );
                     }
                     break;
                  case 'o':
                     if( v == "object" ) {
                        return add_type( OBJECT );
                     }
                     break;
               }
            }
            throw std::runtime_error( "invalid JSON Schema: invalid primitive type '" + std::string( v ) + '\'' );
         }

         [[nodiscard]] const basic_value< Traits >* find( const char* s ) const
         {
            const auto* p = m_value->find( s );
            if( p != nullptr ) {
               p = &p->skip_value_ptr();
            }
            return p;
         }

         schema_node( const schema_container< Traits >* c, const basic_value< Traits >& v )
            : m_container( c ),
              m_value( &v )
         {
            // general
            if( !m_value->is_object() ) {
               throw std::runtime_error( "invalid JSON Schema: a schema must be of type 'object'" );
            }

            // title
            if( const auto* p = find( "title" ) ) {
               if( !p->is_string() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"title\" must be of type 'string'" );
               }
            }

            // description
            if( const auto* p = find( "description" ) ) {
               if( !p->is_string() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"description\" must be of type 'string'" );
               }
            }

            // type
            if( const auto* p = find( "type" ) ) {
               switch( p->type() ) {
                  case type::STRING:
                     add_type( p->get_string_type() );
                     break;
                  case type::ARRAY:
                     for( const auto& e : p->get_array() ) {
                        if( !e.is_string() ) {
                           throw std::runtime_error( "invalid JSON Schema: elements in array \"type\" must be of type 'string'" );
                        }
                        add_type( e.get_string_type() );
                     }
                     break;
                  default:
                     throw std::runtime_error( "invalid JSON Schema: \"type\" must be of type 'string' or 'array'" );
               }
               m_flags = m_flags | HAS_TYPE;
            }

            // enum
            if( const auto* p = find( "enum" ) ) {
               if( !p->is_array() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"enum\" must be of type 'array'" );
               }
               m_flags = m_flags | HAS_ENUM;
            }

            // const
            // TODO: Implement me!

            // allOf
            if( const auto* p = find( "allOf" ) ) {
               if( !p->is_array() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"allOf\" must be of type 'array'" );
               }
               if( p->get_array().empty() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"allOf\" must have at least one element" );
               }
               for( const auto& e : p->get_array() ) {
                  m_referenced_pointers.insert( &e.skip_value_ptr() );
               }
               m_all_of = p;
            }

            // anyOf
            if( const auto* p = find( "anyOf" ) ) {
               if( !p->is_array() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"anyOf\" must be of type 'array'" );
               }
               if( p->get_array().empty() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"anyOf\" must have at least one element" );
               }
               for( const auto& e : p->get_array() ) {
                  m_referenced_pointers.insert( &e.skip_value_ptr() );
               }
               m_any_of = p;
            }

            // oneOf
            if( const auto* p = find( "oneOf" ) ) {
               if( !p->is_array() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"oneOf\" must be of type 'array'" );
               }
               if( p->get_array().empty() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"oneOf\" must have at least one element" );
               }
               for( const auto& e : p->get_array() ) {
                  m_referenced_pointers.insert( &e.skip_value_ptr() );
               }
               m_one_of = p;
            }

            // not
            if( const auto* p = find( "not" ) ) {
               m_referenced_pointers.insert( p );
               m_not = p;
            }

            // dependentSchemas
            // TODO: Implement me!

            // definitions
            if( const auto* p = find( "definitions" ) ) {
               if( !p->is_object() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"definitions\" must be of type 'object'" );
               }
               for( const auto& e : p->get_object() ) {
                  m_referenced_pointers.insert( &e.second.skip_value_ptr() );
               }
            }

            // multipleOf
            if( const auto* p = find( "multipleOf" ) ) {
               switch( p->type() ) {
                  case type::SIGNED: {
                     const auto i = p->get_signed();
                     if( i <= 0 ) {
                        throw std::runtime_error( "invalid JSON Schema: \"multipleOf\" must be strictly greater than zero" );
                     }
                     m_multiple_of.u = i;
                     m_flags = m_flags | HAS_MULTIPLE_OF_UNSIGNED;
                  } break;
                  case type::UNSIGNED: {
                     const auto u = p->get_unsigned();
                     if( u == 0 ) {
                        throw std::runtime_error( "invalid JSON Schema: \"multipleOf\" must be strictly greater than zero" );
                     }
                     m_multiple_of.u = u;
                     m_flags = m_flags | HAS_MULTIPLE_OF_UNSIGNED;
                  } break;
                  case type::DOUBLE: {
                     const auto d = p->get_double();
                     if( d <= 0 ) {
                        throw std::runtime_error( "invalid JSON Schema: \"multipleOf\" must be strictly greater than zero" );
                     }
                     m_multiple_of.d = d;
                     m_flags = m_flags | HAS_MULTIPLE_OF_DOUBLE;
                  } break;
                  default:
                     throw std::runtime_error( "invalid JSON Schema: \"multipleOf\" must be of type 'number'" );
               }
            }

            // maximum
            if( const auto* p = find( "maximum" ) ) {
               switch( p->type() ) {
                  case type::SIGNED:
                     m_maximum.i = p->get_signed();
                     m_flags = m_flags | HAS_MAXIMUM_SIGNED;
                     break;
                  case type::UNSIGNED:
                     m_maximum.u = p->get_unsigned();
                     m_flags = m_flags | HAS_MAXIMUM_UNSIGNED;
                     break;
                  case type::DOUBLE:
                     m_maximum.d = p->get_double();
                     m_flags = m_flags | HAS_MAXIMUM_DOUBLE;
                     break;
                  default:
                     throw std::runtime_error( "invalid JSON Schema: \"maximum\" must be of type 'number'" );
               }
            }

            // exclusiveMaximum
            if( const auto* p = find( "exclusiveMaximum" ) ) {
               if( !p->is_boolean() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"exclusiveMaximum\" must be of type 'boolean'" );
               }
               if( ( m_flags & HAS_MAXIMUM ) == 0 ) {
                  throw std::runtime_error( "invalid JSON Schema: \"exclusiveMaximum\" requires presence of \"maximum\"" );
               }
               if( p->get_boolean() ) {
                  m_flags = m_flags | EXCLUSIVE_MAXIMUM;
               }
            }

            // minimum
            if( const auto* p = find( "minimum" ) ) {
               switch( p->type() ) {
                  case type::SIGNED:
                     m_minimum.i = p->get_signed();
                     m_flags = m_flags | HAS_MINIMUM_SIGNED;
                     break;
                  case type::UNSIGNED:
                     m_minimum.u = p->get_unsigned();
                     m_flags = m_flags | HAS_MINIMUM_UNSIGNED;
                     break;
                  case type::DOUBLE:
                     m_minimum.d = p->get_double();
                     m_flags = m_flags | HAS_MINIMUM_DOUBLE;
                     break;
                  default:
                     throw std::runtime_error( "invalid JSON Schema: \"minimum\" must be of type 'number'" );
               }
            }

            // exclusiveMinimum
            if( const auto* p = find( "exclusiveMinimum" ) ) {
               if( !p->is_boolean() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"exclusiveMinimum\" must be of type 'boolean'" );
               }
               if( ( m_flags & HAS_MINIMUM ) == 0 ) {
                  throw std::runtime_error( "invalid JSON Schema: \"exclusiveMinimum\" requires presence of \"minimum\"" );
               }
               if( p->get_boolean() ) {
                  m_flags = m_flags | EXCLUSIVE_MINIMUM;
               }
            }

            // maxLength
            if( const auto* p = find( "maxLength" ) ) {
               switch( p->type() ) {
                  case type::SIGNED: {
                     const auto i = p->get_signed();
                     if( i < 0 ) {
                        throw std::runtime_error( "invalid JSON Schema: \"maxLength\" must be greater than or equal to zero" );
                     }
                     m_max_length = i;
                     m_flags = m_flags | HAS_MAX_LENGTH;
                  } break;
                  case type::UNSIGNED:
                     m_max_length = p->get_unsigned();
                     m_flags = m_flags | HAS_MAX_LENGTH;
                     break;
                  default:
                     throw std::runtime_error( "invalid JSON Schema: \"maxLength\" must be of type 'integer'" );
               }
            }

            // minLength
            if( const auto* p = find( "minLength" ) ) {
               switch( p->type() ) {
                  case type::SIGNED: {
                     const auto i = p->get_signed();
                     if( i < 0 ) {
                        throw std::runtime_error( "invalid JSON Schema: \"minLength\" must be greater than or equal to zero" );
                     }
                     if( i > 0 ) {
                        m_min_length = i;
                        m_flags = m_flags | HAS_MIN_LENGTH;
                     }
                  } break;
                  case type::UNSIGNED: {
                     const auto u = p->get_unsigned();
                     if( u > 0 ) {
                        m_min_length = u;
                        m_flags = m_flags | HAS_MIN_LENGTH;
                     }
                  } break;
                  default:
                     throw std::runtime_error( "invalid JSON Schema: \"minLength\" must be of type 'integer'" );
               }
            }

            // pattern
            if( const auto* p = find( "pattern" ) ) {
               if( !p->is_string() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"pattern\" must be of type 'string'" );
               }
               try {
                  m_pattern = std::make_unique< std::regex >( p->get_string() );
               }
               catch( const std::regex_error& e ) {
                  throw std::runtime_error( "invalid JSON Schema: \"pattern\" must be a regular expression: " + std::string( e.what() ) );
               }
            }

            // format
            // TODO: offer an option to disable "format" support?
            if( const auto* p = find( "format" ) ) {
               if( !p->is_string() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"format\" must be of type 'string'" );
               }
               const auto& s = p->get_string();
               if( s == "date-time" ) {
                  m_format = schema_format::date_time;
               }
               else if( s == "email" ) {
                  m_format = schema_format::email;
               }
               else if( s == "hostname" ) {
                  m_format = schema_format::hostname;
               }
               else if( s == "ipv4" ) {
                  m_format = schema_format::ipv4;
               }
               else if( s == "ipv6" ) {
                  m_format = schema_format::ipv6;
               }
               else if( s == "uri" ) {
                  m_format = schema_format::uri;
               }
               // unknown "format" values are ignored
            }

            // items
            if( const auto* p = find( "items" ) ) {
               if( p->is_array() ) {
                  for( const auto& e : p->get_array() ) {
                     m_referenced_pointers.insert( &e.skip_value_ptr() );
                  }
               }
               else if( p->is_object() ) {
                  m_referenced_pointers.insert( p );
               }
               else {
                  throw std::runtime_error( "invalid JSON Schema: \"items\" must be of type 'object' or 'array'" );
               }
               m_items = p;
            }

            // additionalItems
            if( const auto* p = find( "additionalItems" ) ) {
               if( p->is_object() ) {
                  m_referenced_pointers.insert( p );
               }
               else if( !p->is_boolean() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"additionalItems\" must be of type 'boolean' or 'object'" );
               }
               m_additional_items = p;
            }

            // unevaluatedItems
            // TODO: Implement me!

            // contains
            // TODO: Implement me!

            // maxItems
            if( const auto* p = find( "maxItems" ) ) {
               switch( p->type() ) {
                  case type::SIGNED: {
                     const auto i = p->get_signed();
                     if( i < 0 ) {
                        throw std::runtime_error( "invalid JSON Schema: \"maxItems\" must be greater than or equal to zero" );
                     }
                     m_max_items = i;
                     m_flags = m_flags | HAS_MAX_ITEMS;
                  } break;
                  case type::UNSIGNED:
                     m_max_items = p->get_unsigned();
                     m_flags = m_flags | HAS_MAX_ITEMS;
                     break;
                  default:
                     throw std::runtime_error( "invalid JSON Schema: \"maxItems\" must be of type 'integer'" );
               }
            }

            // minItems
            if( const auto* p = find( "minItems" ) ) {
               switch( p->type() ) {
                  case type::SIGNED: {
                     const auto i = p->get_signed();
                     if( i < 0 ) {
                        throw std::runtime_error( "invalid JSON Schema: \"minItems\" must be greater than or equal to zero" );
                     }
                     m_min_items = i;
                     m_flags = m_flags | HAS_MIN_ITEMS;
                  } break;
                  case type::UNSIGNED:
                     m_min_items = p->get_unsigned();
                     m_flags = m_flags | HAS_MIN_ITEMS;
                     break;
                  default:
                     throw std::runtime_error( "invalid JSON Schema: \"minItems\" must be of type 'integer'" );
               }
            }

            // uniqueItems
            if( const auto* p = find( "uniqueItems" ) ) {
               if( p->get_boolean() ) {
                  m_flags = m_flags | HAS_UNIQUE_ITEMS;
               }
            }

            // maxContains
            // TODO: Implement me!

            // minContains
            // TODO: Implement me!

            // maxProperties
            if( const auto* p = find( "maxProperties" ) ) {
               switch( p->type() ) {
                  case type::SIGNED: {
                     const auto i = p->get_signed();
                     if( i < 0 ) {
                        throw std::runtime_error( "invalid JSON Schema: \"maxProperties\" must be greater than or equal to zero" );
                     }
                     m_max_properties = i;
                     m_flags = m_flags | HAS_MAX_PROPERTIES;
                  } break;
                  case type::UNSIGNED:
                     m_max_properties = p->get_unsigned();
                     m_flags = m_flags | HAS_MAX_PROPERTIES;
                     break;
                  default:
                     throw std::runtime_error( "invalid JSON Schema: \"maxProperties\" must be of type 'integer'" );
               }
            }

            // minProperties
            if( const auto* p = find( "minProperties" ) ) {
               switch( p->type() ) {
                  case type::SIGNED: {
                     const auto i = p->get_signed();
                     if( i < 0 ) {
                        throw std::runtime_error( "invalid JSON Schema: \"minProperties\" must be greater than or equal to zero" );
                     }
                     m_min_properties = i;
                     m_flags = m_flags | HAS_MIN_PROPERTIES;
                  } break;
                  case type::UNSIGNED:
                     m_min_properties = p->get_unsigned();
                     m_flags = m_flags | HAS_MIN_PROPERTIES;
                     break;
                  default:
                     throw std::runtime_error( "invalid JSON Schema: \"minProperties\" must be of type 'integer'" );
               }
            }

            // required
            if( const auto* p = find( "required" ) ) {
               if( !p->is_array() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"required\" must be of type 'array'" );
               }
               if( p->get_array().empty() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"required\" must have at least one element" );
               }
               for( const auto& e : p->get_array() ) {
                  if( !m_required.insert( e.get_string() ).second ) {
                     throw std::runtime_error( "invalid JSON Schema: duplicate required key" );
                  }
               }
            }

            // dependentRequired
            // TODO: Implement me!

            // properties
            if( const auto* p = find( "properties" ) ) {
               if( !p->is_object() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"properties\" must be of type 'object'" );
               }
               for( const auto& e : p->get_object() ) {
                  m_referenced_pointers.insert( &e.second.skip_value_ptr() );
               }
               m_properties = p;
            }

            // patternProperties
            if( const auto* p = find( "patternProperties" ) ) {
               if( !p->is_object() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"patternProperties\" must be of type 'object'" );
               }
               for( const auto& e : p->get_object() ) {
                  try {
                     m_pattern_properties.emplace_back( std::regex( e.first ), &e.second.skip_value_ptr() );
                  }
                  catch( const std::regex_error& ex ) {
                     throw std::runtime_error( "invalid JSON Schema: keys in object \"patternProperties\" must be regular expressions: " + std::string( ex.what() ) );
                  }
                  m_referenced_pointers.insert( &e.second.skip_value_ptr() );
               }
            }

            // additionalProperties
            if( const auto* p = find( "additionalProperties" ) ) {
               const type t = p->type();
               if( t == type::OBJECT ) {
                  m_referenced_pointers.insert( p );
               }
               else if( t != type::BOOLEAN ) {
                  throw std::runtime_error( "invalid JSON Schema: \"additionalProperties\" must be of type 'boolean' or 'object'" );
               }
               m_additional_properties = p;
            }

            // unevaluatedProperties
            // TODO: Implement me!

            // dependencies
            if( const auto* p = find( "dependencies" ) ) {
               if( !p->is_object() ) {
                  throw std::runtime_error( "invalid JSON Schema: \"dependencies\" must be of type 'object'" );
               }
               for( const auto& e : p->get_object() ) {
                  const auto* p2 = &e.second.skip_value_ptr();
                  if( p2->is_object() ) {
                     m_schema_dependencies.emplace( e.first, p2 );
                     m_referenced_pointers.insert( p2 );
                  }
                  else if( p2->is_array() ) {
                     if( p2->get_array().empty() ) {
                        throw std::runtime_error( "invalid JSON Schema: values in object \"dependencies\" of type 'array' must have at least one element" );
                     }
                     std::set< std::string > s;
                     for( const auto& r : p2->get_array() ) {
                        if( !r.is_string() ) {
                           throw std::runtime_error( "invalid JSON Schema: values in object \"dependencies\" of type 'array' must contain elements of type 'string'" );
                        }
                        if( !s.emplace( r.get_string() ).second ) {
                           throw std::runtime_error( "invalid JSON Schema: values in object \"dependencies\" of type 'array' must contain unique elements of type 'string'" );
                        }
                     }
                     m_property_dependencies.emplace( e.first, std::move( s ) );
                  }
                  else {
                     throw std::runtime_error( "invalid JSON Schema: values in object \"dependencies\" must be of type 'object' or 'array'" );
                  }
               }
               if( !p->get_object().empty() ) {
                  m_flags = m_flags | HAS_DEPENDENCIES;
               }
            }

            // default
            if( const auto* p = find( "default" ) ) {
               // TODO: the value should validate against the JSON Schema itself
            }
         }

         schema_node( const schema_node& ) = delete;
         schema_node( schema_node&& ) = delete;

         ~schema_node() = default;

         void operator=( const schema_node& ) = delete;
         void operator=( schema_node&& ) = delete;

         [[nodiscard]] const std::set< const basic_value< Traits >* >& referenced_pointers() const noexcept
         {
            return m_referenced_pointers;
         }
      };

      template< template< typename... > class Traits >
      class schema_consumer
      {
      private:
         const std::shared_ptr< const schema_container< Traits > > m_container;
         const schema_node< Traits >* const m_node;

         std::vector< std::unique_ptr< events_compare< Traits > > > m_enum;
         std::unique_ptr< events::hash > m_hash;
         std::set< std::string > m_unique;
         std::set< std::string > m_keys;
         std::vector< std::size_t > m_count;
         std::vector< std::unique_ptr< schema_consumer > > m_properties;
         std::vector< std::unique_ptr< schema_consumer > > m_all_of;
         std::vector< std::unique_ptr< schema_consumer > > m_any_of;
         std::vector< std::unique_ptr< schema_consumer > > m_one_of;
         std::map< std::string, std::unique_ptr< schema_consumer > > m_schema_dependencies;
         std::unique_ptr< schema_consumer > m_not;
         std::unique_ptr< schema_consumer > m_item;
         bool m_match = true;

         void validate_type( const schema_flags t )
         {
            if( !m_count.empty() ) {
               return;
            }
            if( ( m_node->m_flags & HAS_TYPE ) == 0 ) {
               return;
            }
            if( ( m_node->m_flags & t ) == 0 ) {
               m_match = false;
            }
         }

         // note: lambda returns true if validation failure detected
         template< typename F >
         void validate_enum( F&& f )
         {
            assert( m_match );
            if( m_node->m_flags & HAS_ENUM ) {
               m_enum.erase( std::remove_if( m_enum.begin(), m_enum.end(), [ & ]( const std::unique_ptr< events_compare< Traits > >& p ) { return f( *p ); } ), m_enum.end() );
               if( m_enum.empty() ) {
                  m_match = false;
               }
            }
         }

         template< typename F >
         void validate_item( F&& f )
         {
            if( m_item ) {
               if( f( m_item ) ) {
                  m_match = false;
               }
            }
         }

         template< typename F >
         void validate_properties( F&& f )
         {
            for( auto& p : m_properties ) {
               if( f( p ) ) {
                  m_match = false;
                  break;
               }
            }
         }

         template< typename F >
         void validate_schema_dependencies( F&& f )
         {
            auto it = m_schema_dependencies.begin();
            while( it != m_schema_dependencies.end() ) {
               if( f( it->second ) ) {
                  it = m_schema_dependencies.erase( it );
               }
               else {
                  ++it;
               }
            }
         }

         template< typename F >
         void validate_all_of( F&& f )
         {
            for( auto& p : m_all_of ) {
               if( f( p ) ) {
                  m_match = false;
                  break;
               }
            }
         }

         template< typename F >
         void validate_any_of( F&& f )
         {
            if( !m_any_of.empty() ) {
               m_any_of.erase( std::remove_if( m_any_of.begin(), m_any_of.end(), f ), m_any_of.end() );
               if( m_any_of.empty() ) {
                  m_match = false;
               }
            }
         }

         template< typename F >
         void validate_one_of( F&& f )
         {
            if( !m_one_of.empty() ) {
               m_one_of.erase( std::remove_if( m_one_of.begin(), m_one_of.end(), f ), m_one_of.end() );
               if( m_one_of.empty() ) {
                  m_match = false;
               }
            }
         }

         template< typename F >
         void validate_not( F&& f )
         {
            if( m_not ) {
               if( f( m_not ) ) {
                  m_not.reset();
               }
            }
         }

         // note: lambda returns true if validation failure detected
         template< typename F >
         void validate_collections( F&& f )
         {
            assert( m_match );
            const auto f2 = [ & ]( const std::unique_ptr< schema_consumer >& p ) { return f( *p ); };
            if( m_match ) {
               validate_item( f2 );
            }
            if( m_match ) {
               validate_properties( f2 );
            }
            if( m_match ) {
               validate_all_of( f2 );
            }
            if( m_match ) {
               validate_any_of( f2 );
            }
            if( m_match ) {
               validate_one_of( f2 );
            }
            if( m_match ) {
               validate_not( f2 );
            }
            if( m_match ) {
               validate_schema_dependencies( f2 );
            }
         }

         [[nodiscard]] static bool is_multiple_of( const double v, const double d )
         {
            const auto r = std::fmod( v, d );
            if( std::fabs( r ) < std::numeric_limits< double >::epsilon() ) {
               return true;
            }
            if( std::fabs( r - d ) < std::numeric_limits< double >::epsilon() ) {
               return true;
            }
            return false;
         }

         void validate_multiple_of( const std::int64_t v )
         {
            switch( m_node->m_flags & HAS_MULTIPLE_OF ) {
               case HAS_MULTIPLE_OF_UNSIGNED:
                  if( v < 0 ) {
                     if( ( -v % m_node->m_multiple_of.u ) != 0 ) {
                        m_match = false;
                     }
                  }
                  else {
                     if( ( v % m_node->m_multiple_of.u ) != 0 ) {
                        m_match = false;
                     }
                  }
                  break;
               case HAS_MULTIPLE_OF_DOUBLE:
                  if( !is_multiple_of( double( v ), m_node->m_multiple_of.d ) ) {
                     m_match = false;
                  }
                  break;
            }
         }

         void validate_multiple_of( const std::uint64_t v )
         {
            switch( m_node->m_flags & HAS_MULTIPLE_OF ) {
               case HAS_MULTIPLE_OF_UNSIGNED:
                  if( ( v % m_node->m_multiple_of.u ) != 0 ) {
                     m_match = false;
                  }
                  break;
               case HAS_MULTIPLE_OF_DOUBLE:
                  if( !is_multiple_of( double( v ), m_node->m_multiple_of.d ) ) {
                     m_match = false;
                  }
                  break;
            }
         }

         void validate_multiple_of( const double v )
         {
            switch( m_node->m_flags & HAS_MULTIPLE_OF ) {
               case HAS_MULTIPLE_OF_UNSIGNED:
                  if( !is_multiple_of( v, double( m_node->m_multiple_of.u ) ) ) {
                     m_match = false;
                  }
                  break;
               case HAS_MULTIPLE_OF_DOUBLE:
                  if( !is_multiple_of( v, m_node->m_multiple_of.d ) ) {
                     m_match = false;
                  }
                  break;
            }
         }

         void validate_number( const std::int64_t v )
         {
            validate_multiple_of( v );
            switch( m_node->m_flags & ( HAS_MAXIMUM | EXCLUSIVE_MAXIMUM ) ) {
               case HAS_MAXIMUM_SIGNED:
                  if( v > m_node->m_maximum.i ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_SIGNED | EXCLUSIVE_MAXIMUM:
                  if( v >= m_node->m_maximum.i ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_UNSIGNED:
                  if( v >= 0 && static_cast< std::uint64_t >( v ) > m_node->m_maximum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_UNSIGNED | EXCLUSIVE_MAXIMUM:
                  if( v >= 0 && static_cast< std::uint64_t >( v ) >= m_node->m_maximum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_DOUBLE:
                  if( v > m_node->m_maximum.d ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_DOUBLE | EXCLUSIVE_MAXIMUM:
                  if( v >= m_node->m_maximum.d ) {
                     m_match = false;
                  }
                  break;
            }
            switch( m_node->m_flags & ( HAS_MINIMUM | EXCLUSIVE_MINIMUM ) ) {
               case HAS_MINIMUM_SIGNED:
                  if( v < m_node->m_minimum.i ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_SIGNED | EXCLUSIVE_MINIMUM:
                  if( v <= m_node->m_minimum.i ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_UNSIGNED:
                  if( v < 0 || static_cast< std::uint64_t >( v ) < m_node->m_minimum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_UNSIGNED | EXCLUSIVE_MINIMUM:
                  if( v < 0 || static_cast< std::uint64_t >( v ) <= m_node->m_minimum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_DOUBLE:
                  if( v < m_node->m_minimum.d ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_DOUBLE | EXCLUSIVE_MINIMUM:
                  if( v <= m_node->m_minimum.d ) {
                     m_match = false;
                  }
                  break;
            }
         }

         void validate_number( const std::uint64_t v )
         {
            validate_multiple_of( v );
            switch( m_node->m_flags & ( HAS_MAXIMUM | EXCLUSIVE_MAXIMUM ) ) {
               case HAS_MAXIMUM_SIGNED:
                  if( m_node->m_maximum.i < 0 || v > static_cast< std::uint64_t >( m_node->m_maximum.i ) ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_SIGNED | EXCLUSIVE_MAXIMUM:
                  if( m_node->m_maximum.i < 0 || v >= static_cast< std::uint64_t >( m_node->m_maximum.i ) ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_UNSIGNED:
                  if( v > m_node->m_maximum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_UNSIGNED | EXCLUSIVE_MAXIMUM:
                  if( v >= m_node->m_maximum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_DOUBLE:
                  if( v > m_node->m_maximum.d ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_DOUBLE | EXCLUSIVE_MAXIMUM:
                  if( v >= m_node->m_maximum.d ) {
                     m_match = false;
                  }
                  break;
            }
            switch( m_node->m_flags & ( HAS_MINIMUM | EXCLUSIVE_MINIMUM ) ) {
               case HAS_MINIMUM_SIGNED:
                  if( m_node->m_minimum.i >= 0 && v < static_cast< std::uint64_t >( m_node->m_minimum.i ) ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_SIGNED | EXCLUSIVE_MINIMUM:
                  if( m_node->m_minimum.i >= 0 && v <= static_cast< std::uint64_t >( m_node->m_minimum.i ) ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_UNSIGNED:
                  if( v < m_node->m_minimum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_UNSIGNED | EXCLUSIVE_MINIMUM:
                  if( v <= m_node->m_minimum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_DOUBLE:
                  if( v < m_node->m_minimum.d ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_DOUBLE | EXCLUSIVE_MINIMUM:
                  if( v <= m_node->m_minimum.d ) {
                     m_match = false;
                  }
                  break;
            }
         }

         void validate_number( const double v )
         {
            validate_multiple_of( v );
            switch( m_node->m_flags & ( HAS_MAXIMUM | EXCLUSIVE_MAXIMUM ) ) {
               case HAS_MAXIMUM_SIGNED:
                  if( v > m_node->m_maximum.i ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_SIGNED | EXCLUSIVE_MAXIMUM:
                  if( v >= m_node->m_maximum.i ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_UNSIGNED:
                  if( v > m_node->m_maximum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_UNSIGNED | EXCLUSIVE_MAXIMUM:
                  if( v >= m_node->m_maximum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_DOUBLE:
                  if( v > m_node->m_maximum.d ) {
                     m_match = false;
                  }
                  break;
               case HAS_MAXIMUM_DOUBLE | EXCLUSIVE_MAXIMUM:
                  if( v >= m_node->m_maximum.d ) {
                     m_match = false;
                  }
                  break;
            }
            switch( m_node->m_flags & ( HAS_MINIMUM | EXCLUSIVE_MINIMUM ) ) {
               case HAS_MINIMUM_SIGNED:
                  if( v < m_node->m_minimum.i ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_SIGNED | EXCLUSIVE_MINIMUM:
                  if( v <= m_node->m_minimum.i ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_UNSIGNED:
                  if( v < m_node->m_minimum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_UNSIGNED | EXCLUSIVE_MINIMUM:
                  if( v <= m_node->m_minimum.u ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_DOUBLE:
                  if( v < m_node->m_minimum.d ) {
                     m_match = false;
                  }
                  break;
               case HAS_MINIMUM_DOUBLE | EXCLUSIVE_MINIMUM:
                  if( v <= m_node->m_minimum.d ) {
                     m_match = false;
                  }
                  break;
            }
         }

         void validate_string( const std::string_view v )
         {
            if( ( m_node->m_flags & HAS_MAX_LENGTH ) && ( unicode_size( v ) > m_node->m_max_length ) ) {
               m_match = false;
            }
            if( ( m_node->m_flags & HAS_MIN_LENGTH ) && ( unicode_size( v ) < m_node->m_min_length ) ) {
               m_match = false;
            }
            if( m_match && m_node->m_pattern ) {
               if( !std::regex_search( v.begin(), v.end(), *m_node->m_pattern ) ) {
                  m_match = false;
               }
            }
            if( m_match && m_node->m_format != schema_format::none ) {
               switch( m_node->m_format ) {
                  case schema_format::date_time:
                     if( !internal::parse_date_time( v ) ) {
                        m_match = false;
                     }
                     break;
                  case schema_format::email:
                     if( ( v.size() > 255 ) || !internal::parse< internal::email >( v ) ) {
                        m_match = false;
                     }
                     break;
                  case schema_format::hostname:
                     if( ( v.size() > 255 ) || !internal::parse< internal::hostname >( v ) ) {
                        m_match = false;
                     }
                     break;
                  case schema_format::ipv4:
                     if( !internal::parse< pegtl::uri::IPv4address >( v ) ) {
                        m_match = false;
                     }
                     break;
                  case schema_format::ipv6:
                     if( !internal::parse< pegtl::uri::IPv6address >( v ) ) {
                        m_match = false;
                     }
                     break;
                  case schema_format::uri:
                     // TODO: What rule exactly should we apply here?? JSON Schema is not exactly the best spec I've ever read...
                     if( !internal::parse< pegtl::uri::URI >( v ) ) {
                        m_match = false;
                     }
                     break;
                  case schema_format::none:;
               }
            }
         }

         void validate_elements( const std::size_t v )
         {
            if( m_node->m_flags & HAS_MAX_ITEMS && v > m_node->m_max_items ) {
               m_match = false;
            }
            if( m_node->m_flags & HAS_MIN_ITEMS && v < m_node->m_min_items ) {
               m_match = false;
            }
         }

         void validate_members( const std::size_t v )
         {
            if( m_node->m_flags & HAS_MAX_PROPERTIES && v > m_node->m_max_properties ) {
               m_match = false;
            }
            if( m_node->m_flags & HAS_MIN_PROPERTIES && v < m_node->m_min_properties ) {
               m_match = false;
            }
         }

      public:
         schema_consumer( const std::shared_ptr< const schema_container< Traits > >& c, const schema_node< Traits >& n )
            : m_container( c ),
              m_node( &n )
         {
            if( m_node->m_flags & HAS_ENUM ) {
               const auto& a = m_node->m_value->at( "enum" ).get_array();
               m_enum.reserve( a.size() );
               for( const auto& e : a ) {
                  m_enum.emplace_back( std::make_unique< events_compare< Traits > >() );
                  m_enum.back()->push( &e );
               }
            }
            if( const auto* p = m_node->m_all_of ) {
               for( const auto& e : p->get_array() ) {
                  m_all_of.push_back( m_container->consumer( &e.skip_value_ptr() ) );
               }
            }
            if( const auto* p = m_node->m_any_of ) {
               for( const auto& e : p->get_array() ) {
                  m_any_of.push_back( m_container->consumer( &e.skip_value_ptr() ) );
               }
            }
            if( const auto* p = m_node->m_one_of ) {
               for( const auto& e : p->get_array() ) {
                  m_one_of.push_back( m_container->consumer( &e.skip_value_ptr() ) );
               }
            }
            if( const auto* p = m_node->m_not ) {
               m_not = m_container->consumer( p );
            }
            for( const auto& e : m_node->m_schema_dependencies ) {
               m_schema_dependencies.emplace( e.first, m_container->consumer( e.second ) );
            }
         }

         schema_consumer( const schema_consumer& ) = delete;
         schema_consumer( schema_consumer&& ) = delete;

         ~schema_consumer() = default;

         void operator=( const schema_consumer& ) = delete;
         void operator=( schema_consumer&& ) = delete;

         [[nodiscard]] bool finalize()
         {
            if( m_match && !m_all_of.empty() ) {
               for( auto& e : m_all_of ) {
                  if( !e->finalize() ) {
                     m_match = false;
                     break;
                  }
               }
            }
            if( m_match && !m_any_of.empty() ) {
               m_any_of.erase( std::remove_if( m_any_of.begin(), m_any_of.end(), []( const std::unique_ptr< schema_consumer >& c ) { return !c->finalize(); } ), m_any_of.end() );
               if( m_any_of.empty() ) {
                  m_match = false;
               }
            }
            if( m_match && !m_one_of.empty() ) {
               m_one_of.erase( std::remove_if( m_one_of.begin(), m_one_of.end(), []( const std::unique_ptr< schema_consumer >& c ) { return !c->finalize(); } ), m_one_of.end() );
               if( m_one_of.size() != 1 ) {
                  m_match = false;
               }
            }
            if( m_match && m_not && m_not->finalize() ) {
               m_match = false;
            }
            if( m_match && m_node->m_flags & HAS_DEPENDENCIES ) {
               for( const auto& e : m_node->m_schema_dependencies ) {
                  if( m_keys.count( e.first ) != 0 ) {
                     const auto it = m_schema_dependencies.find( e.first );
                     if( it == m_schema_dependencies.end() ) {
                        m_match = false;
                        break;
                     }
                     if( !it->second->finalize() ) {
                        m_match = false;
                        break;
                     }
                  }
               }
            }
            return m_match;
         }

         [[nodiscard]] bool match() const noexcept
         {
            return m_match;
         }

         void null()
         {
            if( m_match ) {
               validate_type( NULL_ );
            }
            if( m_match ) {
               validate_enum( []( events_compare< Traits >& c ) { c.null(); return ! c.match(); } );
            }
            if( m_match ) {
               validate_collections( []( schema_consumer& c ) { c.null(); return ! c.match(); } );
            }
            if( m_match && m_hash ) {
               m_hash->null();
            }
         }

         void boolean( const bool v )
         {
            if( m_match ) {
               validate_type( BOOLEAN );
            }
            if( m_match ) {
               validate_enum( [ = ]( events_compare< Traits >& c ) { c.boolean( v ); return ! c.match(); } );
            }
            if( m_match ) {
               validate_collections( [ = ]( schema_consumer& c ) { c.boolean( v ); return ! c.match(); } );
            }
            if( m_match && m_hash ) {
               m_hash->boolean( v );
            }
         }

         void number( const std::int64_t v )
         {
            if( m_match ) {
               validate_type( INTEGER | NUMBER );
            }
            if( m_match ) {
               validate_enum( [ = ]( events_compare< Traits >& c ) { c.number( v ); return ! c.match(); } );
            }
            if( m_match ) {
               validate_collections( [ = ]( schema_consumer& c ) { c.number( v ); return ! c.match(); } );
            }
            if( m_match && m_count.empty() ) {
               validate_number( v );
            }
            if( m_match && m_hash ) {
               m_hash->number( v );
            }
         }

         void number( const std::uint64_t v )
         {
            if( m_match ) {
               validate_type( INTEGER | NUMBER );
            }
            if( m_match ) {
               validate_enum( [ = ]( events_compare< Traits >& c ) { c.number( v ); return ! c.match(); } );
            }
            if( m_match ) {
               validate_collections( [ = ]( schema_consumer& c ) { c.number( v ); return ! c.match(); } );
            }
            if( m_match && m_count.empty() ) {
               validate_number( v );
            }
            if( m_match && m_hash ) {
               m_hash->number( v );
            }
         }

         void number( const double v )
         {
            if( m_match ) {
               validate_type( NUMBER );
            }
            if( m_match ) {
               validate_enum( [ = ]( events_compare< Traits >& c ) { c.number( v ); return ! c.match(); } );
            }
            if( m_match ) {
               validate_collections( [ = ]( schema_consumer& c ) { c.number( v ); return ! c.match(); } );
            }
            if( m_match && m_count.empty() ) {
               validate_number( v );
            }
            if( m_match && m_hash ) {
               m_hash->number( v );
            }
         }

         void string( const std::string_view v )
         {
            if( m_match ) {
               validate_type( STRING );
            }
            if( m_match ) {
               validate_enum( [ & ]( events_compare< Traits >& c ) { c.string( v ); return ! c.match(); } );
            }
            if( m_match ) {
               validate_collections( [ & ]( schema_consumer& c ) { c.string( v ); return ! c.match(); } );
            }
            if( m_match && m_count.empty() ) {
               validate_string( v );
            }
            if( m_match && m_hash ) {
               m_hash->string( v );
            }
         }

         void binary( const tao::binary_view /*unused*/ )
         {
            // TODO: What?
         }

         void begin_array( const std::size_t /*unused*/ = 0 )
         {
            if( m_match ) {
               validate_type( ARRAY );
            }
            if( m_match ) {
               validate_enum( []( events_compare< Traits >& c ) { c.begin_array(); return ! c.match(); } );
            }
            if( m_match ) {
               validate_collections( []( schema_consumer& c ) { c.begin_array(); return ! c.match(); } );
            }
            if( m_match ) {
               if( m_hash ) {
                  m_hash->begin_array();
               }
               else if( m_count.empty() && ( ( m_node->m_flags & HAS_UNIQUE_ITEMS ) != 0 ) ) {
                  m_hash = std::make_unique< events::hash >();
               }
            }
            if( m_match && m_count.empty() ) {
               if( const auto* p = m_node->m_items ) {
                  if( p->is_object() ) {
                     m_item = m_container->consumer( p );
                  }
                  else {
                     const auto& a = p->get_array();
                     if( !a.empty() ) {
                        m_item = m_container->consumer( &a[ 0 ].skip_value_ptr() );
                     }
                  }
               }
               if( !m_item ) {
                  if( const auto* p = m_node->m_additional_items ) {
                     if( p->is_object() ) {
                        m_item = m_container->consumer( p );
                     }
                  }
               }
            }
            m_count.push_back( 0 );
         }

         void element()
         {
            if( m_match ) {
               validate_enum( []( events_compare< Traits >& c ) { c.element(); return ! c.match(); } );
            }
            if( m_match && m_item ) {
               if( m_count.size() == 1 ) {
                  if( !m_item->finalize() ) {
                     m_match = false;
                  }
                  m_item.reset();
               }
            }
            if( m_match ) {
               validate_collections( []( schema_consumer& c ) { c.element(); return ! c.match(); } );
            }
            if( m_match && m_hash ) {
               if( m_count.size() == 1 ) {
                  if( !m_unique.emplace( m_hash->value() ).second ) {
                     m_match = false;
                  }
                  m_hash->reset();
               }
               else {
                  m_hash->element();
               }
            }
            const auto next = ++m_count.back();
            if( m_match && ( m_count.size() == 1 ) ) {
               if( const auto* p = m_node->m_items ) {
                  if( p->is_object() ) {
                     m_item = m_container->consumer( p );
                  }
                  else {
                     const auto& a = p->get_array();
                     if( next < a.size() ) {
                        m_item = m_container->consumer( &a[ next ].skip_value_ptr() );
                     }
                  }
               }
               if( !m_item ) {
                  if( const auto* p = m_node->m_additional_items ) {
                     if( p->is_object() ) {
                        m_item = m_container->consumer( p );
                     }
                  }
               }
            }
         }

         void end_array( const std::size_t /*unused*/ = 0 )
         {
            if( m_match ) {
               validate_enum( []( events_compare< Traits >& c ) { c.end_array(); return ! c.match(); } );
            }
            if( m_match && m_item && ( m_count.size() == 1 ) ) {
               if( !m_item->finalize() ) {
                  m_match = false;
               }
               m_item.reset();
            }
            if( m_match && ( m_count.size() == 1 ) ) {
               if( m_node->m_items && m_node->m_items->is_array() ) {
                  if( m_node->m_additional_items && m_node->m_additional_items->is_boolean() ) {
                     if( !m_node->m_additional_items->get_boolean() ) {
                        if( m_count.back() > m_node->m_items->get_array().size() ) {
                           m_match = false;
                        }
                     }
                  }
               }
            }
            if( m_match ) {
               validate_collections( []( schema_consumer& c ) { c.end_array(); return ! c.match(); } );
            }
            if( m_match && m_hash ) {
               m_hash->end_array();
            }
            if( m_match && ( m_count.size() == 1 ) ) {
               validate_elements( m_count.back() );
            }
            m_count.pop_back();
         }

         void begin_object( const std::size_t /*unused*/ = 0 )
         {
            if( m_match ) {
               validate_type( OBJECT );
            }
            if( m_match ) {
               validate_enum( []( events_compare< Traits >& c ) { c.begin_object(); return ! c.match(); } );
            }
            if( m_match ) {
               validate_collections( []( schema_consumer& c ) { c.begin_object(); return ! c.match(); } );
            }
            if( m_match && m_hash ) {
               m_hash->begin_object();
            }
            m_count.push_back( 0 );
         }

         void key( const std::string_view sv )
         {
            if( m_match ) {
               validate_enum( [ & ]( events_compare< Traits >& c ) { c.key( sv ); return ! c.match(); } );
            }
            if( m_match ) {
               validate_collections( [ & ]( schema_consumer& c ) { c.key( sv ); return ! c.match(); } );
            }
            if( m_match && m_hash ) {
               m_hash->key( sv );
            }
            if( m_match && ( m_count.size() == 1 ) && ( m_node->m_flags & HAS_DEPENDENCIES || !m_node->m_required.empty() ) ) {
               if( !m_keys.insert( std::string( sv ) ).second ) {
                  // duplicate keys immediately invalidate!
                  // TODO: throw?
                  m_match = false;
               }
            }
            if( m_match && m_properties.empty() && ( m_count.size() == 1 ) ) {
               if( const auto* p = m_node->m_properties ) {
                  const auto& o = p->get_object();
                  const auto it = o.find( sv );
                  if( it != o.end() ) {
                     m_properties.push_back( m_container->consumer( &it->second.skip_value_ptr() ) );
                  }
               }
               for( const auto& e : m_node->m_pattern_properties ) {
                  if( std::regex_search( sv.begin(), sv.end(), e.first ) ) {
                     m_properties.push_back( m_container->consumer( e.second ) );
                  }
               }
               if( m_properties.empty() ) {
                  if( const auto* p = m_node->m_additional_properties ) {
                     if( p->is_boolean() ) {
                        if( !p->get_boolean() ) {
                           m_match = false;
                        }
                     }
                     else {
                        m_properties.push_back( m_container->consumer( p ) );
                     }
                  }
               }
            }
         }

         void member()
         {
            if( m_match ) {
               validate_enum( []( events_compare< Traits >& c ) { c.member(); return ! c.match(); } );
            }
            if( m_match && !m_properties.empty() && ( m_count.size() == 1 ) ) {
               for( auto& e : m_properties ) {
                  if( !e->finalize() ) {
                     m_match = false;
                     break;
                  }
               }
               m_properties.clear();
            }
            if( m_match ) {
               validate_collections( []( schema_consumer& c ) { c.member(); return ! c.match(); } );
            }
            if( m_match && m_hash ) {
               m_hash->member();
            }
            ++m_count.back();
         }

         void end_object( const std::size_t /*unused*/ = 0 )
         {
            if( m_match ) {
               validate_enum( []( events_compare< Traits >& c ) { c.end_object(); return ! c.match(); } );
            }
            if( m_match ) {
               validate_collections( []( schema_consumer& c ) { c.end_object(); return ! c.match(); } );
            }
            if( m_match && m_hash ) {
               m_hash->end_object();
            }
            if( m_match && ( m_count.size() == 1 ) ) {
               validate_members( m_count.back() );
            }
            if( m_match && ( m_count.size() == 1 ) && !m_node->m_required.empty() ) {
               if( !std::includes( m_keys.begin(), m_keys.end(), m_node->m_required.begin(), m_node->m_required.end() ) ) {
                  m_match = false;
               }
            }
            if( m_match && ( m_count.size() == 1 ) && m_node->m_flags & HAS_DEPENDENCIES ) {
               for( const auto& e : m_node->m_property_dependencies ) {
                  if( m_keys.count( e.first ) != 0 ) {
                     if( !std::includes( m_keys.begin(), m_keys.end(), e.second.begin(), e.second.end() ) ) {
                        m_match = false;
                        break;
                     }
                  }
               }
            }
            m_count.pop_back();
         }
      };

      template< template< typename... > class Traits >
      class schema_container
         : public std::enable_shared_from_this< schema_container< Traits > >
      {
      private:
         basic_value< Traits > m_value;

         using nodes_t = std::map< const basic_value< Traits >*, std::unique_ptr< schema_node< Traits > > >;
         nodes_t m_nodes;

         void make_node( const basic_value< Traits >* p )
         {
            m_nodes.emplace( p, std::make_unique< schema_node< Traits > >( this, *p ) );
         }

      public:
         explicit schema_container( const basic_value< Traits >& v )
            : m_value( v.skip_value_ptr() )
         {
            resolve_references( m_value );
            make_node( &m_value );
            while( true ) {
               std::set< const basic_value< Traits >* > required;
               for( const auto& e : m_nodes ) {
                  auto s = e.second->referenced_pointers();
                  required.insert( s.begin(), s.end() );
               }
               for( const auto& e : m_nodes ) {
                  required.erase( e.first );
               }
               if( required.empty() ) {
                  break;
               }
               for( const auto& e : required ) {
                  make_node( e );
               }
            }
         }

         [[nodiscard]] std::unique_ptr< schema_consumer< Traits > > consumer( const basic_value< Traits >* p ) const
         {
            const auto it = m_nodes.find( p );
            if( it == m_nodes.end() ) {
               throw std::logic_error( "invalid node ptr, no schema registered" );
            }
            return std::make_unique< schema_consumer< Traits > >( this->shared_from_this(), *it->second );
         }

         [[nodiscard]] std::unique_ptr< schema_consumer< Traits > > consumer() const
         {
            return consumer( &m_value );
         }
      };

   }  // namespace internal

   template< template< typename... > class Traits >
   class basic_schema
   {
   private:
      const std::shared_ptr< const internal::schema_container< Traits > > m_container;

   public:
      explicit basic_schema( const basic_value< Traits >& v )
         : m_container( std::make_shared< internal::schema_container< Traits > >( v ) )
      {
      }

      [[nodiscard]] std::unique_ptr< internal::schema_consumer< Traits > > consumer() const
      {
         return m_container->consumer();
      }

      [[nodiscard]] bool validate( const basic_value< Traits >& v ) const
      {
         // TODO: Value validation should be implemented independently,
         // as it could be more efficient than Events validation!
         const auto c = consumer();
         events::from_value( *c, v );
         return c->finalize();
      }
   };

   using schema = basic_schema< traits >;

   template< template< typename... > class Traits >
   [[nodiscard]] basic_schema< Traits > make_schema( const basic_value< Traits >& v )
   {
      return basic_schema< Traits >( v );
   }

}  // namespace tao::json

#endif
