﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using Microsoft.CodeAnalysis.EmbeddedLanguages.Common;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;

namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.Json
{
    using static EmbeddedSyntaxHelpers;

    using JsonToken = EmbeddedSyntaxToken<JsonKind>;

    internal partial struct JsonParser
    {
        private static class JsonNetSyntaxChecker
        {
            public static EmbeddedDiagnostic? CheckSyntax(JsonNode node)
            {
                var diagnostic = node.Kind switch
                {
                    JsonKind.Array => CheckArray((JsonArrayNode)node),
                    JsonKind.Object => CheckObject((JsonObjectNode)node),
                    JsonKind.Constructor => CheckConstructor((JsonConstructorNode)node),
                    JsonKind.Property => CheckProperty((JsonPropertyNode)node),
                    JsonKind.Literal => CheckLiteral((JsonLiteralNode)node),
                    JsonKind.NegativeLiteral => CheckNegativeLiteral((JsonNegativeLiteralNode)node),
                    _ => null,
                };

                return Earliest(diagnostic, CheckChildren(node));

                static EmbeddedDiagnostic? CheckChildren(JsonNode node)
                {
                    foreach (var child in node)
                    {
                        if (child.IsNode)
                        {
                            var diagnostic = CheckSyntax(child.Node);
                            if (diagnostic != null)
                                return diagnostic;
                        }
                    }

                    return null;
                }
            }

            private static EmbeddedDiagnostic? CheckLiteral(JsonLiteralNode node)
                => node.LiteralToken.Kind == JsonKind.NumberToken
                    ? CheckNumber(node.LiteralToken)
                    : null;

            private static EmbeddedDiagnostic? CheckNegativeLiteral(JsonNegativeLiteralNode node)
                => node.LiteralToken.Kind == JsonKind.NumberToken
                    ? CheckNumber(node.LiteralToken)
                    : null;

            private static EmbeddedDiagnostic? CheckNumber(JsonToken numberToken)
            {
                // This code was effectively copied from:
                // https://github.com/JamesNK/Newtonsoft.Json/blob/993215529562866719689206e27e413013d4439c/Src/Newtonsoft.Json/JsonTextReader.cs#L1926
                // So as to match Newtonsoft.Json's behavior around number parsing.
                var chars = numberToken.VirtualChars;
                var firstChar = chars[0];

                var singleDigit = firstChar.IsDigit && chars.Length == 1;
                if (singleDigit)
                    return null;

                var nonBase10 =
                    firstChar == '0' && chars.Length > 1 &&
                    chars[1] != '.' && chars[1] != 'e' && chars[1] != 'E';

                var literalText = numberToken.VirtualChars.CreateString();
                if (nonBase10)
                {
                    Debug.Assert(chars.Length > 1);
                    var b = chars[1] == 'x' || chars[1] == 'X' ? 16 : 8;

                    try
                    {
                        Convert.ToInt64(literalText, b);
                    }
                    catch (Exception)
                    {
                        return new EmbeddedDiagnostic(FeaturesResources.Invalid_number, GetSpan(chars));
                    }
                }
                else if (!double.TryParse(
                    literalText, NumberStyles.Float,
                    CultureInfo.InvariantCulture, out _))
                {
                    return new EmbeddedDiagnostic(FeaturesResources.Invalid_number, GetSpan(chars));
                }

                return null;
            }

            private static EmbeddedDiagnostic? CheckArray(JsonArrayNode node)
                => CheckCommasBetweenSequenceElements(node.Sequence);

            private static EmbeddedDiagnostic? CheckConstructor(JsonConstructorNode node)
                => !IsValidConstructorName(node.NameToken)
                    ? new EmbeddedDiagnostic(FeaturesResources.Invalid_constructor_name, node.NameToken.GetSpan())
                    : CheckCommasBetweenSequenceElements(node.Sequence);

            private static bool IsValidConstructorName(JsonToken nameToken)
            {
                foreach (var vc in nameToken.VirtualChars)
                {
                    if (!vc.IsLetterOrDigit)
                        return false;
                }

                return true;
            }

            private static EmbeddedDiagnostic? CheckCommasBetweenSequenceElements(ImmutableArray<JsonValueNode> sequence)
            {
                // Json.net allows sequences of commas.  But after every non-comma value, you need
                // a comma.
                for (int i = 0, n = sequence.Length - 1; i < n; i++)
                {
                    var child = sequence[i];
                    var nextChild = sequence[i + 1];
                    if (child.Kind != JsonKind.CommaValue && nextChild.Kind != JsonKind.CommaValue)
                        return new EmbeddedDiagnostic(string.Format(FeaturesResources._0_expected, ','), GetFirstToken(nextChild).GetSpan());
                }

                return null;
            }

            private static EmbeddedDiagnostic? CheckObject(JsonObjectNode node)
            {
                foreach (var child in node.Sequence)
                {
                    if (child.Kind != JsonKind.Property)
                        return new EmbeddedDiagnostic(FeaturesResources.Only_properties_allowed_in_an_object, GetFirstToken(child).GetSpan());
                }

                return null;
            }

            private static EmbeddedDiagnostic? CheckProperty(JsonPropertyNode node)
                => node.NameToken.Kind != JsonKind.StringToken && !IsLegalPropertyNameText(node.NameToken)
                    ? new EmbeddedDiagnostic(FeaturesResources.Invalid_property_name, node.NameToken.GetSpan())
                    : null;

            private static bool IsLegalPropertyNameText(JsonToken textToken)
            {
                foreach (var ch in textToken.VirtualChars)
                {
                    if (!IsLegalPropertyNameChar(ch))
                        return false;
                }

                return true;
            }

            private static bool IsLegalPropertyNameChar(VirtualChar ch)
                => ch.IsLetterOrDigit || ch == '_' || ch == '$';
        }
    }
}
