#nullable enable

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

namespace HyperstoneSdk
{
    public static class Json
    {
        const string TerminalAlphabet = "abcdefABCDEF01234567890truefalsenull+-.";

        public static Dictionary<string, JsonToken>? ParseObject(string json)
        {
            var tokens = new Dictionary<string, JsonToken>();
            var jsonSpan = json.AsSpan();
            var pos = 0;

            var sb = new StringBuilder();
            var result = ParseObject(jsonSpan, ref pos, tokens, sb);
            if (!result)
            {
                return null;
            }

            return tokens;
        }

        public static bool ParseObject(ReadOnlySpan<char> json, ref int pos, Dictionary<string, JsonToken>? tokens, StringBuilder? sb)
        {
            var state = JsonObjectParserState.WsThenObjectStart;
            string? key = null;

            while (pos < json.Length)
            {
                var ch = json[pos];
                switch (state)
                {
                    case JsonObjectParserState.WsThenObjectStart:
                        if (char.IsWhiteSpace(ch))
                        {
                            pos++;
                        }
                        else if (ch == '{')
                        {
                            state = JsonObjectParserState.WsThenKeyOrEnd;
                            pos++;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    case JsonObjectParserState.WsThenKeyOrEnd:
                        if (char.IsWhiteSpace(ch))
                        {
                            pos++;
                        }
                        else if (ch == '"')
                        {
                            sb?.Clear();
                            if (ParseQuotedString(json, ref pos, sb))
                            {
                                key = sb?.ToString();
                                state = JsonObjectParserState.WsThenColon;
                            }
                            else
                            {
                                return false;
                            }
                        }
                        else if (ch == '}')
                        {
                            pos++;
                            return true;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    case JsonObjectParserState.WsThenColon:
                        if (char.IsWhiteSpace(ch))
                        {
                            pos++;
                        }
                        else if (ch == ':')
                        {
                            state = JsonObjectParserState.WsThenValue;
                            pos++;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    case JsonObjectParserState.WsThenValue:
                        if (char.IsWhiteSpace(ch))
                        {
                            pos++;
                        }
                        else
                        {
                            if (ParseAnyValue(json, ref pos, out var token))
                            {
                                if (tokens != null && key != null)
                                {
                                    tokens[key] = token;
                                }
                                state = JsonObjectParserState.WsThenCommaOrObjectEnd;
                            }
                            else
                            {
                                return false;
                            }
                        }
                        break;
                    case JsonObjectParserState.WsThenCommaOrObjectEnd:
                        if (char.IsWhiteSpace(ch))
                        {
                            pos++;
                        }
                        else if (ch == '}')
                        {
                            // Object parsing completed
                            pos++;
                            return true;
                        }
                        else if (ch == ',')
                        {
                            state = JsonObjectParserState.WsThenKeyOrEnd;
                            pos++;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    default:
                        return false; // unexpected state
                }
            }

            return false;
        }

        public static bool ParseArray(ReadOnlySpan<char> json, ref int pos, List<JsonToken>? tokens)
        {
            var state = JsonArrayParserState.WsThenArrayStart;
            while (pos < json.Length)
            {
                var ch = json[pos];
                switch (state)
                {
                    case JsonArrayParserState.WsThenArrayStart:
                        if (char.IsWhiteSpace(ch))
                        {
                            pos++;
                        }
                        else if (ch == '[')
                        {
                            state = JsonArrayParserState.WsThenValueOrEnd;
                            pos++;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    case JsonArrayParserState.WsThenValueOrEnd:
                        if (char.IsWhiteSpace(ch))
                        {
                            pos++;
                        }
                        else if (ch == ']')
                        {
                            pos++;
                            return true;
                        }
                        else
                        {
                            if (ParseAnyValue(json, ref pos, out var token))
                            {
                                tokens?.Add(token);
                                state = JsonArrayParserState.WsThenCommaOrArrayEnd;
                            }
                            else
                            {
                                return false;
                            }
                        }
                        break;
                    case JsonArrayParserState.WsThenCommaOrArrayEnd:
                        if (char.IsWhiteSpace(ch))
                        {
                            pos++;
                            break;
                        }
                        else if (ch == ']')
                        {
                            // array parsing completed
                            pos++;
                            return true;
                        }
                        else if (ch == ',')
                        {
                            state = JsonArrayParserState.WsThenValueOrEnd;
                            pos++;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    default:
                        return false; // unexpected state
                }
            }

            return false; // eof
        }

        private static bool ParseAnyValue(ReadOnlySpan<char> json, ref int pos, out JsonToken token)
        {
            var oldPos = pos;
            var ch = json[pos];
            if (ch == '{')
            {
                if (ParseObject(json, ref pos, null, null))
                {
                    token = new JsonToken(JsonTokenType.ObjectValue, oldPos, pos);
                    return true;
                }
                else
                {
                    token = default;
                    return false;
                }
            }
            else if (ch == '[')
            {
                if (ParseArray(json, ref pos, null))
                {
                    token = new JsonToken(JsonTokenType.ArrayValue, oldPos, pos);
                    return true;
                }
                else
                {
                    token = default;
                    return false;
                }
            }
            else if (ch == '"')
            {
                if (ParseQuotedString(json, ref pos, null))
                {
                    token = new JsonToken(JsonTokenType.QuotedStringValue, oldPos, pos);
                    return true;
                }
                else
                {
                    token = default;
                    return false;
                }
            }
            else
            {
                if (ParseNonStringTerminal(json, ref pos))
                {
                    token = new JsonToken(JsonTokenType.TerminalValue, oldPos, pos);
                    return true;
                }
                else
                {
                    token = default;
                    return false;
                }
            }
        }

        public static bool ParseQuotedString(ReadOnlySpan<char> json, ref int pos, StringBuilder? sb)
        {
            if (pos >= json.Length)
                return false; // eof

            if (json[pos] != '"')
                return false; // strings should start with a quote

            pos++;

            var state = JsonStringParsingState.Default;
            var uCodePoint = 0;
            var uDigits = 0;

            while (pos < json.Length)
            {
                var ch = json[pos];

                if (state == JsonStringParsingState.Default)
                {
                    if (ch == '"')
                    {
                        pos++;
                        return true; // completed
                    }
                    else if (ch == '\\')
                    {
                        state = JsonStringParsingState.Escaped;
                        pos++;
                    }
                    else if (char.IsControl(ch))
                    {
                        // unescaped control sequence in json string
                        return false;
                    }
                    else
                    {
                        sb?.Append(ch);
                        pos++;
                    }
                }
                else if (state == JsonStringParsingState.Escaped)
                {
                    switch (ch)
                    {
                        case '"':
                            sb?.Append('"');
                            state = JsonStringParsingState.Default;
                            break;
                        case '\\':
                            sb?.Append('\\');
                            state = JsonStringParsingState.Default;
                            break;
                        case '/':
                            sb?.Append('/');
                            state = JsonStringParsingState.Default;
                            break;
                        case 'b':
                            sb?.Append('\b');
                            state = JsonStringParsingState.Default;
                            break;
                        case 'f':
                            sb?.Append('\f');
                            state = JsonStringParsingState.Default;
                            break;
                        case 'n':
                            sb?.Append('\n');
                            state = JsonStringParsingState.Default;
                            break;
                        case 'r':
                            sb?.Append('\r');
                            state = JsonStringParsingState.Default;
                            break;
                        case 't':
                            sb?.Append('\t');
                            state = JsonStringParsingState.Default;
                            break;
                        case 'u':
                            state = JsonStringParsingState.UEscaped;
                            uCodePoint = 0;
                            uDigits = 0;
                            break;
                        default:
                            // illigal escape secuence
                            return false;
                    }
                    pos++;
                }
                else if (state == JsonStringParsingState.UEscaped)
                {
                    int hex;

                    if (ch >= '0' && ch <= '9')
                        hex = ch - '0';
                    else if (ch >= 'a' && ch <= 'f')
                        hex = ch - 'a' + 10;
                    else if (ch >= 'A' && ch <= 'F')
                        hex = ch - 'A' + 10;
                    else
                        return false;

                    uCodePoint = (uCodePoint << 4) | hex;
                    uDigits++;

                    if (uDigits == 4)
                    {
                        // Surrogate handling
                        if (char.IsHighSurrogate((char)uCodePoint))
                        {
                            // Expect second \uXXXX
                            if (pos + 2 + 4 >= json.Length || json[pos + 1] != '\\' || json[pos + 2] != 'u')
                                return false;

                            pos += 2;

                            int low = 0;
                            for (int i = 0; i < 4; i++)
                            {
                                pos++;
                                ch = json[pos];

                                if (ch >= '0' && ch <= '9')
                                    hex = ch - '0';
                                else if (ch >= 'a' && ch <= 'f')
                                    hex = ch - 'a' + 10;
                                else if (ch >= 'A' && ch <= 'F')
                                    hex = ch - 'A' + 10;
                                else
                                    return false;

                                low = (low << 4) | hex;
                            }

                            if (!char.IsLowSurrogate((char)low))
                                return false;

                            int codePoint = 0x10000 + ((uCodePoint - 0xD800) << 10) + (low - 0xDC00);
                            sb?.Append(char.ConvertFromUtf32(codePoint));
                        }
                        else
                        {
                            sb?.Append((char)uCodePoint);
                        }

                        state = JsonStringParsingState.Default;
                    }

                    pos++;
                }
            }

            return false;
        }

        public static bool ParseNonStringTerminal(ReadOnlySpan<char> json, ref int pos)
        {
            var start = pos;
            var end = pos;
            while (pos < json.Length)
            {
                var ch = json[pos];
                if (TerminalAlphabet.Contains(ch))
                {
                    end++;
                    pos++;
                }
                else
                {
                    break;

                }
            }

            if (end == start)
            {
                return false;  // empty sequence
            }
            return true; // actual value
        }

        public static bool ParseQuotedStringSimple(string json, int pos, out string result)
        {
            var sb = new StringBuilder();
            if (ParseQuotedString(json, ref pos, sb))
            {
                result = sb.ToString();
                return true;
            }

            result = string.Empty;
            return false;
        }
    }

    public enum JsonTokenType
    {
        Key,
        ObjectValue,
        ArrayValue,
        QuotedStringValue,
        TerminalValue,
    }

    public readonly struct JsonToken
    {
        public readonly int Start;
        public readonly int End;
        public readonly JsonTokenType Type;

        public JsonToken(JsonTokenType type, int start, int end)
        {
            Start = start;
            End = end;
            Type = type;
        }
    }

    internal enum JsonStringParsingState
    {
        Default,
        Escaped,
        UEscaped,
    }

    internal enum JsonObjectParserState
    {
        WsThenObjectStart,
        WsThenKeyOrEnd,
        WsThenColon,
        WsThenValue,
        WsThenCommaOrObjectEnd,
    }

    internal enum JsonArrayParserState
    {
        WsThenArrayStart,
        WsThenValueOrEnd,
        WsThenCommaOrArrayEnd,
        WsThenCommaOrObjectEnd,
    }


    [Serializable]
    public class JsonBuilder
    {
        private StringBuilder _sb;
        private bool _needComma;

        public JsonBuilder()
        {
            _sb = new StringBuilder();
            _needComma = false;
        }
        private void BeginValue()
        {
            if (_needComma)
            {
                _sb.Append(',');
            }
        }

        private void EndValue()
        {
            _needComma = true;
        }

        public void BeginObject()
        {
            BeginValue();
            _sb.Append('{');
            _needComma = false;
        }

        public void EndObject()
        {
            _sb.Append('}');
            EndValue();
        }

        public void BeginArray()
        {
            BeginValue();
            _sb.Append('[');
            _needComma = false;
        }

        public void EndArray()
        {
            _sb.Append(']');
            EndValue();
        }

        public void WriteNull()
        {
            BeginValue();
            _sb.Append("null");
            EndValue();
        }

        public void WriteObjectProperty(string key, object value)
        {
            WriteKey(key);
            WriteValue(value);
        }

        private void WriteKey(string key)
        {
            BeginValue();
            _sb.Append('\"');
            WriteEscapedString(key);
            _sb.Append("\":");
            _needComma = false;
        }

        public void WriteValue(int value)
        {
            BeginValue();
            _sb.Append(value.ToString(CultureInfo.InvariantCulture));
            EndValue();
        }

        public void WriteValue(long value)
        {
            BeginValue();
            _sb.Append(value.ToString(CultureInfo.InvariantCulture));
            EndValue();
        }

        public void WriteValue(float value)
        {
            BeginValue();
            _sb.Append(value.ToString(CultureInfo.InvariantCulture));
            EndValue();
        }

        public void WriteValue(double value)
        {
            BeginValue();
            _sb.Append(value.ToString(CultureInfo.InvariantCulture));
            EndValue();
        }

        public void WriteValue(bool value)
        {
            BeginValue();
            if (value)
            {
                _sb.Append("true");
            }
            else
            {
                _sb.Append("false");
            }
            EndValue();
        }

        public void WriteValue(string? value)
        {
            BeginValue();
            if (value == null)
            {
                _sb.Append("null");
            }
            else
            {
                _sb.Append('"');
                WriteEscapedString(value);
                _sb.Append('"');
            }
            EndValue();
        }

        public void WriteValue<T>(IDictionary<string, T> obj)
        {
            BeginObject();
            foreach (var kv in obj)
            {
                WriteKey(kv.Key);
                WriteValue(kv.Value);
            }
            EndObject();
        }

        private void WriteGenericDictValue(IDictionary obj)
        {
            BeginObject();
            var enumerator = obj.GetEnumerator();
            while (enumerator.MoveNext())
            {
                if (enumerator.Key is string stringKey)
                {
                    WriteKey(stringKey);
                    WriteValue(enumerator.Value);
                }
            }
            EndObject();
        }

        public void WriteValue(IEnumerable obj)
        {
            BeginArray();
            foreach (var o in obj)
            {
                WriteValue(o);
            }
            EndArray();
        }

        public void WriteValue(object? o)
        {
            if (o == null) WriteNull();
            else if (o is string stringVal) WriteValue(stringVal);
            else if (o is int intVal) WriteValue(intVal);
            else if (o is bool boolVal) WriteValue(boolVal);
            else if (o is float floatVal) WriteValue(floatVal);
            else if (o is double doubleVal) WriteValue(doubleVal);
            else if (o is long longVal) WriteValue(longVal);
            else if (o is IDictionary genericDict)
            {
                if (genericDict is IDictionary<string, string> dsVal) WriteValue(o);
                else if (genericDict is IDictionary<string, int> diVal) WriteValue(diVal);
                else if (genericDict is IDictionary<string, float> dfVal) WriteValue(dfVal);
                else if (genericDict is IDictionary<string, object> doVal) WriteValue(doVal);
                else WriteGenericDictValue(genericDict);
            }
            else if (o is IDictionary<string, object> dictVal) WriteValue(dictVal);
            else if (o is IEnumerable arrayVal) WriteValue(arrayVal);
            else throw new ArgumentException($"Unsupported object type: {o.GetType()}");
        }


        private void WriteEscapedString(string value)
        {
            foreach (var c in value)
            {
                switch (c)
                {
                    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;
                    default:
                        if (c < 0x20)
                            _sb.Append("\\u").Append(((int)c).ToString("x4"));
                        else
                            _sb.Append(c);
                        break;
                }
            }
        }

        public void Clear()
        {
            _sb.Clear();
        }

        public override string ToString()
        {
            return _sb.ToString();
        }
    }
}
