#region License
// Copyright (c) 2007 James Newton-King
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#endregion

using System;
using System.Collections.Generic;
#if HAVE_BIG_INTEGER
using System.Numerics;
#endif
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using Newtonsoft.Json.Utilities;
using System.Globalization;
using System.Text.RegularExpressions;
using System.IO;
#if !HAVE_LINQ
using Newtonsoft.Json.Utilities.LinqBridge;
#else
using System.Linq;

#endif

#nullable disable

namespace Newtonsoft.Json
{
    /// <summary>
    /// <para>
    /// Represents a reader that provides <see cref="JsonSchema"/> validation.
    /// </para>
    /// <note type="caution">
    /// JSON Schema validation has been moved to its own package. See <see href="https://www.newtonsoft.com/jsonschema">https://www.newtonsoft.com/jsonschema</see> for more details.
    /// </note>
    /// </summary>
    [Obsolete("JSON Schema validation has been moved to its own package. See https://www.newtonsoft.com/jsonschema for more details.")]
    public class JsonValidatingReader : JsonReader, IJsonLineInfo
    {
        private class SchemaScope
        {
            private readonly JTokenType _tokenType;
            private readonly IList<JsonSchemaModel> _schemas;
            private readonly Dictionary<string, bool> _requiredProperties;

            public string CurrentPropertyName { get; set; }
            public int ArrayItemCount { get; set; }
            public bool IsUniqueArray { get; }
            public IList<JToken> UniqueArrayItems { get; }
            public JTokenWriter CurrentItemWriter { get; set; }

            public IList<JsonSchemaModel> Schemas => _schemas;

            public Dictionary<string, bool> RequiredProperties => _requiredProperties;

            public JTokenType TokenType => _tokenType;

            public SchemaScope(JTokenType tokenType, IList<JsonSchemaModel> schemas)
            {
                _tokenType = tokenType;
                _schemas = schemas;

                _requiredProperties = schemas.SelectMany<JsonSchemaModel, string>(GetRequiredProperties).Distinct().ToDictionary(p => p, p => false);

                if (tokenType == JTokenType.Array && schemas.Any(s => s.UniqueItems))
                {
                    IsUniqueArray = true;
                    UniqueArrayItems = new List<JToken>();
                }
            }

            private IEnumerable<string> GetRequiredProperties(JsonSchemaModel schema)
            {
                if (schema?.Properties == null)
                {
                    return Enumerable.Empty<string>();
                }

                return schema.Properties.Where(p => p.Value.Required).Select(p => p.Key);
            }
        }

        private readonly JsonReader _reader;
        private readonly Stack<SchemaScope> _stack;
        private JsonSchema _schema;
        private JsonSchemaModel _model;
        private SchemaScope _currentScope;

        /// <summary>
        /// Sets an event handler for receiving schema validation errors.
        /// </summary>
        public event ValidationEventHandler ValidationEventHandler;

        /// <summary>
        /// Gets the text value of the current JSON token.
        /// </summary>
        /// <value></value>
        public override object Value => _reader.Value;

        /// <summary>
        /// Gets the depth of the current token in the JSON document.
        /// </summary>
        /// <value>The depth of the current token in the JSON document.</value>
        public override int Depth => _reader.Depth;

        /// <summary>
        /// Gets the path of the current JSON token. 
        /// </summary>
        public override string Path => _reader.Path;

        /// <summary>
        /// Gets the quotation mark character used to enclose the value of a string.
        /// </summary>
        /// <value></value>
        public override char QuoteChar
        {
            get { return _reader.QuoteChar; }
            protected internal set { }
        }

        /// <summary>
        /// Gets the type of the current JSON token.
        /// </summary>
        /// <value></value>
        public override JsonToken TokenType => _reader.TokenType;

        /// <summary>
        /// Gets the .NET type for the current JSON token.
        /// </summary>
        /// <value></value>
        public override Type ValueType => _reader.ValueType;

        private void Push(SchemaScope scope)
        {
            _stack.Push(scope);
            _currentScope = scope;
        }

        private SchemaScope Pop()
        {
            SchemaScope poppedScope = _stack.Pop();
            _currentScope = (_stack.Count != 0)
                ? _stack.Peek()
                : null;

            return poppedScope;
        }

        private IList<JsonSchemaModel> CurrentSchemas => _currentScope.Schemas;

        private static readonly IList<JsonSchemaModel> EmptySchemaList = new List<JsonSchemaModel>();

        private IList<JsonSchemaModel> CurrentMemberSchemas
        {
            get
            {
                if (_currentScope == null)
                {
                    return new List<JsonSchemaModel>(new[] { _model });
                }

                if (_currentScope.Schemas == null || _currentScope.Schemas.Count == 0)
                {
                    return EmptySchemaList;
                }

                switch (_currentScope.TokenType)
                {
                    case JTokenType.None:
                        return _currentScope.Schemas;
                    case JTokenType.Object:
                        {
                            if (_currentScope.CurrentPropertyName == null)
                            {
                                throw new JsonReaderException("CurrentPropertyName has not been set on scope.");
                            }

                            IList<JsonSchemaModel> schemas = new List<JsonSchemaModel>();

                            foreach (JsonSchemaModel schema in CurrentSchemas)
                            {
                                if (schema.Properties != null && schema.Properties.TryGetValue(_currentScope.CurrentPropertyName, out JsonSchemaModel propertySchema))
                                {
                                    schemas.Add(propertySchema);
                                }
                                if (schema.PatternProperties != null)
                                {
                                    foreach (KeyValuePair<string, JsonSchemaModel> patternProperty in schema.PatternProperties)
                                    {
                                        if (Regex.IsMatch(_currentScope.CurrentPropertyName, patternProperty.Key))
                                        {
                                            schemas.Add(patternProperty.Value);
                                        }
                                    }
                                }

                                if (schemas.Count == 0 && schema.AllowAdditionalProperties && schema.AdditionalProperties != null)
                                {
                                    schemas.Add(schema.AdditionalProperties);
                                }
                            }

                            return schemas;
                        }
                    case JTokenType.Array:
                        {
                            IList<JsonSchemaModel> schemas = new List<JsonSchemaModel>();

                            foreach (JsonSchemaModel schema in CurrentSchemas)
                            {
                                if (!schema.PositionalItemsValidation)
                                {
                                    if (schema.Items != null && schema.Items.Count > 0)
                                    {
                                        schemas.Add(schema.Items[0]);
                                    }
                                }
                                else
                                {
                                    if (schema.Items != null && schema.Items.Count > 0)
                                    {
                                        if (schema.Items.Count > (_currentScope.ArrayItemCount - 1))
                                        {
                                            schemas.Add(schema.Items[_currentScope.ArrayItemCount - 1]);
                                        }
                                    }

                                    if (schema.AllowAdditionalItems && schema.AdditionalItems != null)
                                    {
                                        schemas.Add(schema.AdditionalItems);
                                    }
                                }
                            }

                            return schemas;
                        }
                    case JTokenType.Constructor:
                        return EmptySchemaList;
                    default:
                        throw new ArgumentOutOfRangeException("TokenType", "Unexpected token type: {0}".FormatWith(CultureInfo.InvariantCulture, _currentScope.TokenType));
                }
            }
        }

        private void RaiseError(string message, JsonSchemaModel schema)
        {
            IJsonLineInfo lineInfo = this;

            string exceptionMessage = (lineInfo.HasLineInfo())
                ? message + " Line {0}, position {1}.".FormatWith(CultureInfo.InvariantCulture, lineInfo.LineNumber, lineInfo.LinePosition)
                : message;

            OnValidationEvent(new JsonSchemaException(exceptionMessage, null, Path, lineInfo.LineNumber, lineInfo.LinePosition));
        }

        private void OnValidationEvent(JsonSchemaException exception)
        {
            ValidationEventHandler handler = ValidationEventHandler;
            if (handler != null)
            {
                handler(this, new ValidationEventArgs(exception));
            }
            else
            {
                throw exception;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="JsonValidatingReader"/> class that
        /// validates the content returned from the given <see cref="JsonReader"/>.
        /// </summary>
        /// <param name="reader">The <see cref="JsonReader"/> to read from while validating.</param>
        public JsonValidatingReader(JsonReader reader)
        {
            ValidationUtils.ArgumentNotNull(reader, nameof(reader));
            _reader = reader;
            _stack = new Stack<SchemaScope>();
        }

        /// <summary>
        /// Gets or sets the schema.
        /// </summary>
        /// <value>The schema.</value>
        public JsonSchema Schema
        {
            get => _schema;
            set
            {
                if (TokenType != JsonToken.None)
                {
                    throw new InvalidOperationException("Cannot change schema while validating JSON.");
                }

                _schema = value;
                _model = null;
            }
        }

        /// <summary>
        /// Gets the <see cref="JsonReader"/> used to construct this <see cref="JsonValidatingReader"/>.
        /// </summary>
        /// <value>The <see cref="JsonReader"/> specified in the constructor.</value>
        public JsonReader Reader => _reader;

        /// <summary>
        /// Changes the reader's state to <see cref="JsonReader.State.Closed"/>.
        /// If <see cref="JsonReader.CloseInput"/> is set to <c>true</c>, the underlying <see cref="JsonReader"/> is also closed.
        /// </summary>
        public override void Close()
        {
            base.Close();
            if (CloseInput)
            {
                _reader?.Close();
            }
        }

        private void ValidateNotDisallowed(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return;
            }

            JsonSchemaType? currentNodeType = GetCurrentNodeSchemaType();
            if (currentNodeType != null)
            {
                if (JsonSchemaGenerator.HasFlag(schema.Disallow, currentNodeType.GetValueOrDefault()))
                {
                    RaiseError("Type {0} is disallowed.".FormatWith(CultureInfo.InvariantCulture, currentNodeType), schema);
                }
            }
        }

        private JsonSchemaType? GetCurrentNodeSchemaType()
        {
            switch (_reader.TokenType)
            {
                case JsonToken.StartObject:
                    return JsonSchemaType.Object;
                case JsonToken.StartArray:
                    return JsonSchemaType.Array;
                case JsonToken.Integer:
                    return JsonSchemaType.Integer;
                case JsonToken.Float:
                    return JsonSchemaType.Float;
                case JsonToken.String:
                    return JsonSchemaType.String;
                case JsonToken.Boolean:
                    return JsonSchemaType.Boolean;
                case JsonToken.Null:
                    return JsonSchemaType.Null;
                default:
                    return null;
            }
        }

        /// <summary>
        /// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="Int32"/>.
        /// </summary>
        /// <returns>A <see cref="Nullable{T}"/> of <see cref="Int32"/>.</returns>
        public override int? ReadAsInt32()
        {
            int? i = _reader.ReadAsInt32();

            ValidateCurrentToken();
            return i;
        }

        /// <summary>
        /// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Byte"/>[].
        /// </summary>
        /// <returns>
        /// A <see cref="Byte"/>[] or <c>null</c> if the next JSON token is null.
        /// </returns>
        public override byte[] ReadAsBytes()
        {
            byte[] data = _reader.ReadAsBytes();

            ValidateCurrentToken();
            return data;
        }

        /// <summary>
        /// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="Decimal"/>.
        /// </summary>
        /// <returns>A <see cref="Nullable{T}"/> of <see cref="Decimal"/>.</returns>
        public override decimal? ReadAsDecimal()
        {
            decimal? d = _reader.ReadAsDecimal();

            ValidateCurrentToken();
            return d;
        }

        /// <summary>
        /// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="Double"/>.
        /// </summary>
        /// <returns>A <see cref="Nullable{T}"/> of <see cref="Double"/>.</returns>
        public override double? ReadAsDouble()
        {
            double? d = _reader.ReadAsDouble();

            ValidateCurrentToken();
            return d;
        }

        /// <summary>
        /// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="Boolean"/>.
        /// </summary>
        /// <returns>A <see cref="Nullable{T}"/> of <see cref="Boolean"/>.</returns>
        public override bool? ReadAsBoolean()
        {
            bool? b = _reader.ReadAsBoolean();

            ValidateCurrentToken();
            return b;
        }

        /// <summary>
        /// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="String"/>.
        /// </summary>
        /// <returns>A <see cref="String"/>. This method will return <c>null</c> at the end of an array.</returns>
        public override string ReadAsString()
        {
            string s = _reader.ReadAsString();

            ValidateCurrentToken();
            return s;
        }

        /// <summary>
        /// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="DateTime"/>.
        /// </summary>
        /// <returns>A <see cref="Nullable{T}"/> of <see cref="DateTime"/>. This method will return <c>null</c> at the end of an array.</returns>
        public override DateTime? ReadAsDateTime()
        {
            DateTime? dateTime = _reader.ReadAsDateTime();

            ValidateCurrentToken();
            return dateTime;
        }

#if HAVE_DATE_TIME_OFFSET
        /// <summary>
        /// Reads the next JSON token from the underlying <see cref="JsonReader"/> as a <see cref="Nullable{T}"/> of <see cref="DateTimeOffset"/>.
        /// </summary>
        /// <returns>A <see cref="Nullable{T}"/> of <see cref="DateTimeOffset"/>.</returns>
        public override DateTimeOffset? ReadAsDateTimeOffset()
        {
            DateTimeOffset? dateTimeOffset = _reader.ReadAsDateTimeOffset();

            ValidateCurrentToken();
            return dateTimeOffset;
        }
#endif

        /// <summary>
        /// Reads the next JSON token from the underlying <see cref="JsonReader"/>.
        /// </summary>
        /// <returns>
        /// <c>true</c> if the next token was read successfully; <c>false</c> if there are no more tokens to read.
        /// </returns>
        public override bool Read()
        {
            if (!_reader.Read())
            {
                return false;
            }

            if (_reader.TokenType == JsonToken.Comment)
            {
                return true;
            }

            ValidateCurrentToken();
            return true;
        }

        private void ValidateCurrentToken()
        {
            // first time validate has been called. build model
            if (_model == null)
            {
                JsonSchemaModelBuilder builder = new JsonSchemaModelBuilder();
                _model = builder.Build(_schema);

                if (!JsonTokenUtils.IsStartToken(_reader.TokenType))
                {
                    Push(new SchemaScope(JTokenType.None, CurrentMemberSchemas));
                }
            }

            switch (_reader.TokenType)
            {
                case JsonToken.StartObject:
                    ProcessValue();
                    IList<JsonSchemaModel> objectSchemas = CurrentMemberSchemas.Where(ValidateObject).ToList();
                    Push(new SchemaScope(JTokenType.Object, objectSchemas));
                    WriteToken(CurrentSchemas);
                    break;
                case JsonToken.StartArray:
                    ProcessValue();
                    IList<JsonSchemaModel> arraySchemas = CurrentMemberSchemas.Where(ValidateArray).ToList();
                    Push(new SchemaScope(JTokenType.Array, arraySchemas));
                    WriteToken(CurrentSchemas);
                    break;
                case JsonToken.StartConstructor:
                    ProcessValue();
                    Push(new SchemaScope(JTokenType.Constructor, null));
                    WriteToken(CurrentSchemas);
                    break;
                case JsonToken.PropertyName:
                    WriteToken(CurrentSchemas);
                    foreach (JsonSchemaModel schema in CurrentSchemas)
                    {
                        ValidatePropertyName(schema);
                    }
                    break;
                case JsonToken.Raw:
                    ProcessValue();
                    break;
                case JsonToken.Integer:
                    ProcessValue();
                    WriteToken(CurrentMemberSchemas);
                    foreach (JsonSchemaModel schema in CurrentMemberSchemas)
                    {
                        ValidateInteger(schema);
                    }
                    break;
                case JsonToken.Float:
                    ProcessValue();
                    WriteToken(CurrentMemberSchemas);
                    foreach (JsonSchemaModel schema in CurrentMemberSchemas)
                    {
                        ValidateFloat(schema);
                    }
                    break;
                case JsonToken.String:
                    ProcessValue();
                    WriteToken(CurrentMemberSchemas);
                    foreach (JsonSchemaModel schema in CurrentMemberSchemas)
                    {
                        ValidateString(schema);
                    }
                    break;
                case JsonToken.Boolean:
                    ProcessValue();
                    WriteToken(CurrentMemberSchemas);
                    foreach (JsonSchemaModel schema in CurrentMemberSchemas)
                    {
                        ValidateBoolean(schema);
                    }
                    break;
                case JsonToken.Null:
                    ProcessValue();
                    WriteToken(CurrentMemberSchemas);
                    foreach (JsonSchemaModel schema in CurrentMemberSchemas)
                    {
                        ValidateNull(schema);
                    }
                    break;
                case JsonToken.EndObject:
                    WriteToken(CurrentSchemas);
                    foreach (JsonSchemaModel schema in CurrentSchemas)
                    {
                        ValidateEndObject(schema);
                    }
                    Pop();
                    break;
                case JsonToken.EndArray:
                    WriteToken(CurrentSchemas);
                    foreach (JsonSchemaModel schema in CurrentSchemas)
                    {
                        ValidateEndArray(schema);
                    }
                    Pop();
                    break;
                case JsonToken.EndConstructor:
                    WriteToken(CurrentSchemas);
                    Pop();
                    break;
                case JsonToken.Undefined:
                case JsonToken.Date:
                case JsonToken.Bytes:
                    // these have no equivalent in JSON schema
                    WriteToken(CurrentMemberSchemas);
                    break;
                case JsonToken.None:
                    // no content, do nothing
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private void WriteToken(IList<JsonSchemaModel> schemas)
        {
            foreach (SchemaScope schemaScope in _stack)
            {
                bool isInUniqueArray = (schemaScope.TokenType == JTokenType.Array && schemaScope.IsUniqueArray && schemaScope.ArrayItemCount > 0);

                if (isInUniqueArray || schemas.Any(s => s.Enum != null))
                {
                    if (schemaScope.CurrentItemWriter == null)
                    {
                        if (JsonTokenUtils.IsEndToken(_reader.TokenType))
                        {
                            continue;
                        }

                        schemaScope.CurrentItemWriter = new JTokenWriter();
                    }

                    schemaScope.CurrentItemWriter.WriteToken(_reader, false);

                    // finished writing current item
                    if (schemaScope.CurrentItemWriter.Top == 0 && _reader.TokenType != JsonToken.PropertyName)
                    {
                        JToken finishedItem = schemaScope.CurrentItemWriter.Token;

                        // start next item with new writer
                        schemaScope.CurrentItemWriter = null;

                        if (isInUniqueArray)
                        {
                            if (schemaScope.UniqueArrayItems.Contains(finishedItem, JToken.EqualityComparer))
                            {
                                RaiseError("Non-unique array item at index {0}.".FormatWith(CultureInfo.InvariantCulture, schemaScope.ArrayItemCount - 1), schemaScope.Schemas.First(s => s.UniqueItems));
                            }

                            schemaScope.UniqueArrayItems.Add(finishedItem);
                        }
                        else if (schemas.Any(s => s.Enum != null))
                        {
                            foreach (JsonSchemaModel schema in schemas)
                            {
                                if (schema.Enum != null)
                                {
                                    if (!schema.Enum.ContainsValue(finishedItem, JToken.EqualityComparer))
                                    {
                                        StringWriter sw = new StringWriter(CultureInfo.InvariantCulture);
                                        finishedItem.WriteTo(new JsonTextWriter(sw));

                                        RaiseError("Value {0} is not defined in enum.".FormatWith(CultureInfo.InvariantCulture, sw.ToString()), schema);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        private void ValidateEndObject(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return;
            }

            Dictionary<string, bool> requiredProperties = _currentScope.RequiredProperties;

            if (requiredProperties != null && requiredProperties.Values.Any(v => !v))
            {
                IEnumerable<string> unmatchedRequiredProperties = requiredProperties.Where(kv => !kv.Value).Select(kv => kv.Key);
                RaiseError("Required properties are missing from object: {0}.".FormatWith(CultureInfo.InvariantCulture, string.Join(", ", unmatchedRequiredProperties
#if !HAVE_STRING_JOIN_WITH_ENUMERABLE
                    .ToArray()
#endif
                    )), schema);
            }
        }

        private void ValidateEndArray(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return;
            }

            int arrayItemCount = _currentScope.ArrayItemCount;

            if (schema.MaximumItems != null && arrayItemCount > schema.MaximumItems)
            {
                RaiseError("Array item count {0} exceeds maximum count of {1}.".FormatWith(CultureInfo.InvariantCulture, arrayItemCount, schema.MaximumItems), schema);
            }

            if (schema.MinimumItems != null && arrayItemCount < schema.MinimumItems)
            {
                RaiseError("Array item count {0} is less than minimum count of {1}.".FormatWith(CultureInfo.InvariantCulture, arrayItemCount, schema.MinimumItems), schema);
            }
        }

        private void ValidateNull(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return;
            }

            if (!TestType(schema, JsonSchemaType.Null))
            {
                return;
            }

            ValidateNotDisallowed(schema);
        }

        private void ValidateBoolean(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return;
            }

            if (!TestType(schema, JsonSchemaType.Boolean))
            {
                return;
            }

            ValidateNotDisallowed(schema);
        }

        private void ValidateString(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return;
            }

            if (!TestType(schema, JsonSchemaType.String))
            {
                return;
            }

            ValidateNotDisallowed(schema);

            string value = _reader.Value.ToString();

            if (schema.MaximumLength != null && value.Length > schema.MaximumLength)
            {
                RaiseError("String '{0}' exceeds maximum length of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.MaximumLength), schema);
            }

            if (schema.MinimumLength != null && value.Length < schema.MinimumLength)
            {
                RaiseError("String '{0}' is less than minimum length of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.MinimumLength), schema);
            }

            if (schema.Patterns != null)
            {
                foreach (string pattern in schema.Patterns)
                {
                    if (!Regex.IsMatch(value, pattern))
                    {
                        RaiseError("String '{0}' does not match regex pattern '{1}'.".FormatWith(CultureInfo.InvariantCulture, value, pattern), schema);
                    }
                }
            }
        }

        private void ValidateInteger(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return;
            }

            if (!TestType(schema, JsonSchemaType.Integer))
            {
                return;
            }

            ValidateNotDisallowed(schema);

            object value = _reader.Value;

            if (schema.Maximum != null)
            {
                if (JValue.Compare(JTokenType.Integer, value, schema.Maximum) > 0)
                {
                    RaiseError("Integer {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema);
                }
                if (schema.ExclusiveMaximum && JValue.Compare(JTokenType.Integer, value, schema.Maximum) == 0)
                {
                    RaiseError("Integer {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema);
                }
            }

            if (schema.Minimum != null)
            {
                if (JValue.Compare(JTokenType.Integer, value, schema.Minimum) < 0)
                {
                    RaiseError("Integer {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema);
                }
                if (schema.ExclusiveMinimum && JValue.Compare(JTokenType.Integer, value, schema.Minimum) == 0)
                {
                    RaiseError("Integer {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema);
                }
            }

            if (schema.DivisibleBy != null)
            {
                bool notDivisible;
#if HAVE_BIG_INTEGER
                if (value is BigInteger i)
                {
                    // not that this will lose any decimal point on DivisibleBy
                    // so manually raise an error if DivisibleBy is not an integer and value is not zero
                    bool divisibleNonInteger = !Math.Abs(schema.DivisibleBy.Value - Math.Truncate(schema.DivisibleBy.Value)).Equals(0);
                    if (divisibleNonInteger)
                    {
                        notDivisible = i != 0;
                    }
                    else
                    {
                        notDivisible = i % new BigInteger(schema.DivisibleBy.Value) != 0;
                    }
                }
                else
#endif
                {
                    notDivisible = !IsZero(Convert.ToInt64(value, CultureInfo.InvariantCulture) % schema.DivisibleBy.GetValueOrDefault());
                }

                if (notDivisible)
                {
                    RaiseError("Integer {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema);
                }
            }
        }

        private void ProcessValue()
        {
            if (_currentScope != null && _currentScope.TokenType == JTokenType.Array)
            {
                _currentScope.ArrayItemCount++;

                foreach (JsonSchemaModel currentSchema in CurrentSchemas)
                {
                    // if there is positional validation and the array index is past the number of item validation schemas and there are no additional items then error
                    if (currentSchema != null
                        && currentSchema.PositionalItemsValidation
                        && !currentSchema.AllowAdditionalItems
                        && (currentSchema.Items == null || _currentScope.ArrayItemCount - 1 >= currentSchema.Items.Count))
                    {
                        RaiseError("Index {0} has not been defined and the schema does not allow additional items.".FormatWith(CultureInfo.InvariantCulture, _currentScope.ArrayItemCount), currentSchema);
                    }
                }
            }
        }

        private void ValidateFloat(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return;
            }

            if (!TestType(schema, JsonSchemaType.Float))
            {
                return;
            }

            ValidateNotDisallowed(schema);

            double value = Convert.ToDouble(_reader.Value, CultureInfo.InvariantCulture);

            if (schema.Maximum != null)
            {
                if (value > schema.Maximum)
                {
                    RaiseError("Float {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema);
                }
                if (schema.ExclusiveMaximum && value == schema.Maximum)
                {
                    RaiseError("Float {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema);
                }
            }

            if (schema.Minimum != null)
            {
                if (value < schema.Minimum)
                {
                    RaiseError("Float {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema);
                }
                if (schema.ExclusiveMinimum && value == schema.Minimum)
                {
                    RaiseError("Float {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema);
                }
            }

            if (schema.DivisibleBy != null)
            {
                double remainder = FloatingPointRemainder(value, schema.DivisibleBy.GetValueOrDefault());

                if (!IsZero(remainder))
                {
                    RaiseError("Float {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema);
                }
            }
        }

        private static double FloatingPointRemainder(double dividend, double divisor)
        {
            return dividend - Math.Floor(dividend / divisor) * divisor;
        }

        private static bool IsZero(double value)
        {
            const double epsilon = 2.2204460492503131e-016;

            return Math.Abs(value) < 20.0 * epsilon;
        }

        private void ValidatePropertyName(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return;
            }

            string propertyName = Convert.ToString(_reader.Value, CultureInfo.InvariantCulture);

            if (_currentScope.RequiredProperties.ContainsKey(propertyName))
            {
                _currentScope.RequiredProperties[propertyName] = true;
            }

            if (!schema.AllowAdditionalProperties)
            {
                bool propertyDefinied = IsPropertyDefinied(schema, propertyName);

                if (!propertyDefinied)
                {
                    RaiseError("Property '{0}' has not been defined and the schema does not allow additional properties.".FormatWith(CultureInfo.InvariantCulture, propertyName), schema);
                }
            }

            _currentScope.CurrentPropertyName = propertyName;
        }

        private bool IsPropertyDefinied(JsonSchemaModel schema, string propertyName)
        {
            if (schema.Properties != null && schema.Properties.ContainsKey(propertyName))
            {
                return true;
            }

            if (schema.PatternProperties != null)
            {
                foreach (string pattern in schema.PatternProperties.Keys)
                {
                    if (Regex.IsMatch(propertyName, pattern))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private bool ValidateArray(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return true;
            }

            return (TestType(schema, JsonSchemaType.Array));
        }

        private bool ValidateObject(JsonSchemaModel schema)
        {
            if (schema == null)
            {
                return true;
            }

            return (TestType(schema, JsonSchemaType.Object));
        }

        private bool TestType(JsonSchemaModel currentSchema, JsonSchemaType currentType)
        {
            if (!JsonSchemaGenerator.HasFlag(currentSchema.Type, currentType))
            {
                RaiseError("Invalid type. Expected {0} but got {1}.".FormatWith(CultureInfo.InvariantCulture, currentSchema.Type, currentType), currentSchema);
                return false;
            }

            return true;
        }

        bool IJsonLineInfo.HasLineInfo()
        {
            return _reader is IJsonLineInfo lineInfo && lineInfo.HasLineInfo();
        }

        int IJsonLineInfo.LineNumber => (_reader is IJsonLineInfo lineInfo) ? lineInfo.LineNumber : 0;

        int IJsonLineInfo.LinePosition => (_reader is IJsonLineInfo lineInfo) ? lineInfo.LinePosition : 0;
    }
}