using System.Collections.Generic;
using System.Globalization;
using System.Text;

namespace MajacloudSdk.Internal
{
    /// <summary>Recursive-descent JSON parser: strict, depth-limited, no trailing garbage.</summary>
    internal static class JsonParser
    {
        private const int MaxDepth = 64;

        public static JsonNode Parse(string json)
        {
            if (json == null) throw new JsonParseException("JSON text is null", 0);
            var state = new State(json);
            state.SkipBom();
            state.SkipWhitespace();
            var node = ParseValue(state, 0);
            state.SkipWhitespace();
            if (!state.AtEnd) throw new JsonParseException("Unexpected trailing characters", state.Position);
            return node;
        }

        private static JsonNode ParseValue(State s, int depth)
        {
            if (depth > MaxDepth) throw new JsonParseException("JSON nesting is too deep", s.Position);
            if (s.AtEnd) throw new JsonParseException("Unexpected end of JSON", s.Position);
            char c = s.Peek();
            switch (c)
            {
                case '{': return ParseObject(s, depth);
                case '[': return ParseArray(s, depth);
                case '"': return JsonNode.From(ParseString(s));
                case 't': s.Expect("true"); return JsonNode.From(true);
                case 'f': s.Expect("false"); return JsonNode.From(false);
                case 'n': s.Expect("null"); return JsonNode.Null;
                default:
                    if (c == '-' || (c >= '0' && c <= '9')) return ParseNumber(s);
                    throw new JsonParseException("Unexpected character '" + c + "'", s.Position);
            }
        }

        private static JsonNode ParseObject(State s, int depth)
        {
            s.Advance();
            var node = JsonNode.Object();
            s.SkipWhitespace();
            if (s.Peek() == '}')
            {
                s.Advance();
                return node;
            }
            while (true)
            {
                s.SkipWhitespace();
                if (s.Peek() != '"') throw new JsonParseException("Expected a property name", s.Position);
                var key = ParseString(s);
                s.SkipWhitespace();
                if (s.Peek() != ':') throw new JsonParseException("Expected ':'", s.Position);
                s.Advance();
                s.SkipWhitespace();
                node.Fields.Add(new KeyValuePair<string, JsonNode>(key, ParseValue(s, depth + 1)));
                s.SkipWhitespace();
                char c = s.Peek();
                if (c == ',')
                {
                    s.Advance();
                    continue;
                }
                if (c == '}')
                {
                    s.Advance();
                    return node;
                }
                throw new JsonParseException("Expected ',' or '}'", s.Position);
            }
        }

        private static JsonNode ParseArray(State s, int depth)
        {
            s.Advance();
            var node = JsonNode.Array();
            s.SkipWhitespace();
            if (s.Peek() == ']')
            {
                s.Advance();
                return node;
            }
            while (true)
            {
                s.SkipWhitespace();
                node.Items.Add(ParseValue(s, depth + 1));
                s.SkipWhitespace();
                char c = s.Peek();
                if (c == ',')
                {
                    s.Advance();
                    continue;
                }
                if (c == ']')
                {
                    s.Advance();
                    return node;
                }
                throw new JsonParseException("Expected ',' or ']'", s.Position);
            }
        }

        private static string ParseString(State s)
        {
            s.Advance();
            var sb = new StringBuilder();
            while (true)
            {
                if (s.AtEnd) throw new JsonParseException("Unterminated string", s.Position);
                char c = s.Next();
                if (c == '"') return sb.ToString();
                if (c == '\\')
                {
                    if (s.AtEnd) throw new JsonParseException("Unterminated escape", s.Position);
                    char e = s.Next();
                    switch (e)
                    {
                        case '"': sb.Append('"'); break;
                        case '\\': sb.Append('\\'); break;
                        case '/': sb.Append('/'); break;
                        case 'b': sb.Append('\b'); break;
                        case 'f': sb.Append('\f'); break;
                        case 'n': sb.Append('\n'); break;
                        case 'r': sb.Append('\r'); break;
                        case 't': sb.Append('\t'); break;
                        case 'u':
                            int code = s.ReadHex4();
                            if (code >= 0xD800 && code <= 0xDBFF && s.Peek() == '\\' && s.PeekAt(1) == 'u')
                            {
                                s.Advance();
                                s.Advance();
                                int low = s.ReadHex4();
                                if (low >= 0xDC00 && low <= 0xDFFF)
                                {
                                    sb.Append(char.ConvertFromUtf32(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00)));
                                    break;
                                }
                                sb.Append((char)code);
                                sb.Append((char)low);
                                break;
                            }
                            sb.Append((char)code);
                            break;
                        default: throw new JsonParseException("Invalid escape '\\" + e + "'", s.Position);
                    }
                    continue;
                }
                if (c < 0x20) throw new JsonParseException("Control character in string", s.Position);
                sb.Append(c);
            }
        }

        private static JsonNode ParseNumber(State s)
        {
            int start = s.Position;
            if (s.Peek() == '-') s.Advance();
            if (s.AtEnd || !char.IsDigit(s.Peek())) throw new JsonParseException("Invalid number", s.Position);
            bool isInteger = true;
            while (!s.AtEnd && char.IsDigit(s.Peek())) s.Advance();
            if (!s.AtEnd && s.Peek() == '.')
            {
                isInteger = false;
                s.Advance();
                if (s.AtEnd || !char.IsDigit(s.Peek())) throw new JsonParseException("Invalid number", s.Position);
                while (!s.AtEnd && char.IsDigit(s.Peek())) s.Advance();
            }
            if (!s.AtEnd && (s.Peek() == 'e' || s.Peek() == 'E'))
            {
                isInteger = false;
                s.Advance();
                if (!s.AtEnd && (s.Peek() == '+' || s.Peek() == '-')) s.Advance();
                if (s.AtEnd || !char.IsDigit(s.Peek())) throw new JsonParseException("Invalid number", s.Position);
                while (!s.AtEnd && char.IsDigit(s.Peek())) s.Advance();
            }
            var text = s.Slice(start);
            if (isInteger && long.TryParse(text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var longValue))
            {
                return JsonNode.From(longValue);
            }
            if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) throw new JsonParseException("Invalid number", start);
            var node = new JsonNode { Type = JsonType.Number, NumberValue = value, IsInteger = false };
            return node;
        }

        private sealed class State
        {
            private readonly string _text;
            public int Position;

            public State(string text) { _text = text; }

            public bool AtEnd => Position >= _text.Length;
            public char Peek() => AtEnd ? '\0' : _text[Position];
            public char PeekAt(int offset) => Position + offset < _text.Length ? _text[Position + offset] : '\0';
            public char Next() => _text[Position++];
            public void Advance() => Position++;
            public string Slice(int start) => _text.Substring(start, Position - start);

            public void SkipBom()
            {
                if (!AtEnd && _text[Position] == '﻿') Position++;
            }

            public void SkipWhitespace()
            {
                while (!AtEnd)
                {
                    char c = _text[Position];
                    if (c == ' ' || c == '\t' || c == '\n' || c == '\r') Position++;
                    else break;
                }
            }

            public void Expect(string literal)
            {
                if (string.CompareOrdinal(_text, Position, literal, 0, literal.Length) != 0) throw new JsonParseException("Expected '" + literal + "'", Position);
                Position += literal.Length;
            }

            public int ReadHex4()
            {
                if (Position + 4 > _text.Length) throw new JsonParseException("Invalid unicode escape", Position);
                int value = 0;
                for (int i = 0; i < 4; i++)
                {
                    char c = _text[Position++];
                    int digit = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1;
                    if (digit < 0) throw new JsonParseException("Invalid unicode escape", Position);
                    value = (value << 4) | digit;
                }
                return value;
            }
        }
    }
}
