// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.OpenApi;

using OpenApiConstants = Microsoft.AspNetCore.OpenApi.OpenApiConstants;

internal sealed partial class OpenApiJsonSchema
{
    /// <summary>
    /// Read a list from the given JSON reader instance.
    /// </summary>
    /// <typeparam name="T">The type of the elements that will populate the list.</typeparam>
    /// <param name="reader">The <see cref="Utf8JsonReader"/> to consume the list from.</param>
    /// <returns>A list parsed from the JSON array.</returns>
    public static List<T>? ReadList<T>(ref Utf8JsonReader reader)
    {
        if (reader.TokenType == JsonTokenType.Null)
        {
            return default;
        }

        if (reader.TokenType == JsonTokenType.StartArray)
        {
            var values = new List<T>();
            reader.Read();
            while (reader.TokenType != JsonTokenType.EndArray)
            {
                values.Add((T)JsonSerializer.Deserialize(ref reader, typeof(T), OpenApiJsonSchemaContext.Default)!);
                reader.Read();
            }

            return values;
        }

        if (reader.TokenType == JsonTokenType.Null)
        {
            return default;
        }

        return default;
    }

    /// <summary>
    /// Read a dictionary from the given JSON reader instance.
    /// </summary>
    /// <typeparam name="T">The type associated with the values in the dictionary.</typeparam>
    /// <param name="reader">The <see cref="Utf8JsonReader"/> to consume the dictionary from.</param>
    /// <returns>A dictionary parsed from the JSON object.</returns>
    /// <exception cref="JsonException">Thrown if JSON object is not valid.</exception>
    public static Dictionary<string, T>? ReadDictionary<T>(ref Utf8JsonReader reader)
    {
        if (reader.TokenType == JsonTokenType.Null)
        {
            return default;
        }

        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException("Expected StartObject or Null");
        }

        var values = new Dictionary<string, T>();
        reader.Read();
        while (reader.TokenType != JsonTokenType.EndObject)
        {
            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException("Expected PropertyName");
            }

            var key = reader.GetString()!;
            reader.Read();
            values[key] = (T)JsonSerializer.Deserialize(ref reader, typeof(T), OpenApiJsonSchemaContext.Default)!;
            reader.Read();
        }

        return values;
    }

    internal static JsonNode? ReadJsonNode(ref Utf8JsonReader reader)
        => ReadJsonNode(ref reader, out _);

    internal static JsonNode? ReadJsonNode(ref Utf8JsonReader reader, out JsonSchemaType? type)
    {
        type = null;
        if (reader.TokenType == JsonTokenType.Null)
        {
            return null;
        }

        if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False)
        {
            type = JsonSchemaType.Boolean;
            return reader.GetBoolean();
        }

        if (reader.TokenType == JsonTokenType.Number)
        {
            if (reader.TryGetInt32(out var intValue))
            {
                type = JsonSchemaType.Integer;
                return intValue;
            }

            if (reader.TryGetInt64(out var longValue))
            {
                type = JsonSchemaType.Integer;
                return longValue;
            }

            if (reader.TryGetSingle(out var floatValue) && !float.IsInfinity(floatValue))
            {
                type = JsonSchemaType.Number;
                return floatValue;
            }

            if (reader.TryGetDouble(out var doubleValue))
            {
                type = JsonSchemaType.Number;
                return doubleValue;
            }
        }

        if (reader.TokenType == JsonTokenType.String)
        {
            type = JsonSchemaType.String;
            return reader.GetString();
        }

        if (reader.TokenType == JsonTokenType.StartArray)
        {
            type = JsonSchemaType.Array;
            var array = new JsonArray();
            // Read to process JsonTokenType.StartArray before advancing
            reader.Read();
            while (reader.TokenType != JsonTokenType.EndArray)
            {
                array.Add(ReadJsonNode(ref reader));
                reader.Read();
            }
            return array;
        }

        if (reader.TokenType == JsonTokenType.StartObject)
        {
            type = JsonSchemaType.Object;
            var obj = new JsonObject();
            reader.Read();
            while (reader.TokenType != JsonTokenType.EndObject)
            {
                if (reader.TokenType != JsonTokenType.PropertyName)
                {
                    throw new JsonException("Expected PropertyName");
                }

                var key = reader.GetString()!;
                reader.Read();
                obj[key] = ReadJsonNode(ref reader);
                reader.Read();
            }
            return obj;
        }

        return default;
    }

    /// <summary>
    /// Read a property node from the given JSON reader instance.
    /// </summary>
    /// <param name="reader">The <see cref="Utf8JsonReader"/> to consume the property value from.</param>
    /// <param name="propertyName">The name of the property the editor is currently consuming.</param>
    /// <param name="schema">The <see cref="OpenApiSchema"/> to write the given values to.</param>
    /// <param name="options">The <see cref="JsonSerializerOptions"/> instance.</param>
    public static void ReadProperty(ref Utf8JsonReader reader, string propertyName, OpenApiSchema schema, JsonSerializerOptions options)
    {
        switch (propertyName)
        {
            case OpenApiSchemaKeywords.TypeKeyword:
                reader.Read();
                if (reader.TokenType == JsonTokenType.StartArray)
                {
                    var types = ReadList<string>(ref reader);
                    foreach (var type in types ?? [])
                    {
                        if (schema.Type is not null)
                        {
                            schema.Type |= Enum.Parse<JsonSchemaType>(type, ignoreCase: true);
                        }
                        else
                        {
                            schema.Type = Enum.Parse<JsonSchemaType>(type, ignoreCase: true);
                        }
                    }
                }
                else
                {
                    var type = reader.GetString();
                    Debug.Assert(type is not null);
                    schema.Type = Enum.Parse<JsonSchemaType>(type, ignoreCase: true);
                }
                break;
            case OpenApiSchemaKeywords.EnumKeyword:
                reader.Read();
                var enumValues = ReadList<JsonNode>(ref reader);
                if (enumValues is not null)
                {
                    schema.Enum = enumValues;
                }
                break;
            case OpenApiSchemaKeywords.DefaultKeyword:
                reader.Read();
                schema.Default = ReadJsonNode(ref reader);
                break;
            case OpenApiSchemaKeywords.ItemsKeyword:
                reader.Read();
                var valueConverter = (JsonConverter<OpenApiJsonSchema>)options.GetTypeInfo(typeof(OpenApiJsonSchema)).Converter;
                schema.Items = valueConverter.Read(ref reader, typeof(OpenApiJsonSchema), options)?.Schema;
                break;
            case OpenApiSchemaKeywords.DescriptionKeyword:
                reader.Read();
                schema.Description = reader.GetString();
                break;
            case OpenApiSchemaKeywords.FormatKeyword:
                reader.Read();
                schema.Format = reader.GetString();
                break;
            case OpenApiSchemaKeywords.RequiredKeyword:
                reader.Read();
                schema.Required = ReadList<string>(ref reader)?.ToHashSet();
                break;
            case OpenApiSchemaKeywords.MinLengthKeyword:
                reader.Read();
                var minLength = reader.GetInt32();
                schema.MinLength = minLength;
                break;
            case OpenApiSchemaKeywords.MinItemsKeyword:
                reader.Read();
                var minItems = reader.GetInt32();
                schema.MinItems = minItems;
                break;
            case OpenApiSchemaKeywords.MaxLengthKeyword:
                reader.Read();
                var maxLength = reader.GetInt32();
                schema.MaxLength = maxLength;
                break;
            case OpenApiSchemaKeywords.MaxItemsKeyword:
                reader.Read();
                var maxItems = reader.GetInt32();
                schema.MaxItems = maxItems;
                break;
            case OpenApiSchemaKeywords.MinimumKeyword:
                reader.Read();
                var minimum = reader.GetDecimal();
                schema.Minimum = minimum.ToString(CultureInfo.InvariantCulture);
                break;
            case OpenApiSchemaKeywords.ExclusiveMinimum:
                reader.Read();
                var exclusiveMinimum = reader.GetDecimal();
                schema.ExclusiveMinimum = exclusiveMinimum.ToString(CultureInfo.InvariantCulture);
                break;
            case OpenApiSchemaKeywords.MaximumKeyword:
                reader.Read();
                var maximum = reader.GetDecimal();
                schema.Maximum = maximum.ToString(CultureInfo.InvariantCulture);
                break;
            case OpenApiSchemaKeywords.ExclusiveMaximum:
                reader.Read();
                var exclusiveMaximum = reader.GetDecimal();
                schema.ExclusiveMaximum = exclusiveMaximum.ToString(CultureInfo.InvariantCulture);
                break;
            case OpenApiSchemaKeywords.PatternKeyword:
                reader.Read();
                var pattern = reader.GetString();
                schema.Pattern = pattern;
                break;
            case OpenApiSchemaKeywords.PropertiesKeyword:
                reader.Read();
                var props = ReadDictionary<OpenApiJsonSchema>(ref reader);
                schema.Properties = props?.ToDictionary(p => p.Key, p => p.Value.Schema as IOpenApiSchema);
                break;
            case OpenApiSchemaKeywords.AdditionalPropertiesKeyword:
                reader.Read();
                if (reader.TokenType == JsonTokenType.False)
                {
                    schema.AdditionalPropertiesAllowed = false;
                    break;
                }
                var additionalPropsConverter = (JsonConverter<OpenApiJsonSchema>)options.GetTypeInfo(typeof(OpenApiJsonSchema)).Converter;
                schema.AdditionalProperties = additionalPropsConverter.Read(ref reader, typeof(OpenApiJsonSchema), options)?.Schema;
                break;
            case OpenApiSchemaKeywords.AnyOfKeyword:
                reader.Read();
                schema.Type = JsonSchemaType.Object;
                var anyOfSchemas = ReadList<OpenApiJsonSchema>(ref reader);
                schema.AnyOf = anyOfSchemas?.Select(s => s.Schema as IOpenApiSchema).ToList();
                break;
            case OpenApiSchemaKeywords.OneOfKeyword:
                reader.Read();
                schema.Type = JsonSchemaType.Object;
                var oneOfSchemas = ReadList<OpenApiJsonSchema>(ref reader);
                schema.OneOf = oneOfSchemas?.Select(s => s.Schema as IOpenApiSchema).ToList();
                break;
            case OpenApiSchemaKeywords.DiscriminatorKeyword:
                reader.Read();
                var discriminator = reader.GetString();
                if (discriminator is not null)
                {
                    schema.Discriminator = new OpenApiDiscriminator { PropertyName = discriminator };
                }
                break;
            case OpenApiSchemaKeywords.DiscriminatorMappingKeyword:
                reader.Read();
                var mappings = ReadDictionary<string>(ref reader);
                if (mappings is not null)
                {
                    schema.Discriminator ??= new OpenApiDiscriminator();
                    foreach (var kvp in mappings)
                    {
                        schema.Discriminator.Mapping ??= new Dictionary<string, OpenApiSchemaReference>();
                        schema.Discriminator.Mapping[kvp.Key] = new OpenApiSchemaReference(kvp.Value);
                    }
                }
                break;
            case OpenApiConstants.SchemaId:
                reader.Read();
                schema.Metadata ??= new Dictionary<string, object>();
                schema.Metadata.Add(OpenApiConstants.SchemaId, reader.GetString() ?? string.Empty);
                break;
            case OpenApiConstants.NullableProperty:
                reader.Read();
                schema.Metadata ??= new Dictionary<string, object>();
                schema.Metadata.Add(OpenApiConstants.NullableProperty, reader.GetBoolean());
                break;
            // OpenAPI does not support the `const` keyword in its schema implementation, so
            // we map it to its closest approximation, an enum with a single value, here.
            case OpenApiSchemaKeywords.ConstKeyword:
                reader.Read();
                schema.Enum = ReadJsonNode(ref reader, out var constType) is { } jsonNode ? [jsonNode] : [];
                schema.Type = constType;
                break;
            case OpenApiConstants.RefDescriptionAnnotation:
                reader.Read();
                schema.Metadata ??= new Dictionary<string, object>();
                schema.Metadata[OpenApiConstants.RefDescriptionAnnotation] = reader.GetString() ?? string.Empty;
                break;
            case OpenApiConstants.RefDefaultAnnotation:
                reader.Read();
                schema.Metadata ??= new Dictionary<string, object>();
                schema.Metadata[OpenApiConstants.RefDefaultAnnotation] = ReadJsonNode(ref reader)!;
                break;
            default:
                reader.Skip();
                break;
        }
    }
}
