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

namespace Unity.VisualScripting.FullSerializer
{
    // TODO: properly propagate warnings/etc for fsResult states

    /// <summary>
    /// A simple recursive descent parser for JSON.
    /// </summary>
    public class fsJsonParser
    {
        private fsJsonParser(string input)
        {
            _input = input;
            _start = 0;
        }

        private readonly StringBuilder _cachedStringBuilder = new StringBuilder(256);
        private int _start;
        private string _input;

        private fsResult MakeFailure(string message)
        {
            var start = Math.Max(0, _start - 20);
            var length = Math.Min(50, _input.Length - start);

            var error = "Error while parsing: " + message + "; context = <" +
                _input.Substring(start, length) + ">";
            return fsResult.Fail(error);
        }

        private bool TryMoveNext()
        {
            if (_start < _input.Length)
            {
                ++_start;
                return true;
            }

            return false;
        }

        private bool HasValue()
        {
            return HasValue(0);
        }

        private bool HasValue(int offset)
        {
            return (_start + offset) >= 0 && (_start + offset) < _input.Length;
        }

        private char Character()
        {
            return Character(0);
        }

        private char Character(int offset)
        {
            return _input[_start + offset];
        }

        /// <summary>
        /// Skips input such that Character() will return a non-whitespace
        /// character
        /// </summary>
        private void SkipSpace()
        {
            while (HasValue())
            {
                var c = Character();

                // whitespace; fine to skip
                if (char.IsWhiteSpace(c))
                {
                    TryMoveNext();
                    continue;
                }

                // comment?
                if (HasValue(1) && Character(0) == '/')
                {
                    if (Character(1) == '/')
                    {
                        // skip the rest of the line
                        while (HasValue() && Environment.NewLine.Contains("" + Character()) == false)
                        {
                            TryMoveNext();
                        }
                        continue;
                    }
                    else if (Character(1) == '*')
                    {
                        // skip to comment close
                        TryMoveNext();
                        TryMoveNext();
                        while (HasValue(1))
                        {
                            if (Character(0) == '*' && Character(1) == '/')
                            {
                                TryMoveNext();
                                TryMoveNext();
                                TryMoveNext();
                                break;
                            }
                            else
                            {
                                TryMoveNext();
                            }
                        }
                    }
                    // let other checks to check fail
                    continue;
                }

                break;
            }
        }

        private fsResult TryParseExact(string content)
        {
            for (var i = 0; i < content.Length; ++i)
            {
                if (Character() != content[i])
                {
                    return MakeFailure("Expected " + content[i]);
                }

                if (TryMoveNext() == false)
                {
                    return MakeFailure("Unexpected end of content when parsing " + content);
                }
            }

            return fsResult.Success;
        }

        private fsResult TryParseTrue(out fsData data)
        {
            var fail = TryParseExact("true");

            if (fail.Succeeded)
            {
                data = new fsData(true);
                return fsResult.Success;
            }

            data = null;
            return fail;
        }

        private fsResult TryParseFalse(out fsData data)
        {
            var fail = TryParseExact("false");

            if (fail.Succeeded)
            {
                data = new fsData(false);
                return fsResult.Success;
            }

            data = null;
            return fail;
        }

        private fsResult TryParseNull(out fsData data)
        {
            var fail = TryParseExact("null");

            if (fail.Succeeded)
            {
                data = new fsData();
                return fsResult.Success;
            }

            data = null;
            return fail;
        }

        private bool IsSeparator(char c)
        {
            return char.IsWhiteSpace(c) || c == ',' || c == '}' || c == ']';
        }

        /// <summary>
        /// Parses numbers that follow the regular expression [-+](\d+|\d*\.\d*)
        /// </summary>
        private fsResult TryParseNumber(out fsData data)
        {
            var start = _start;

            // read until we get to a separator
            while (
                TryMoveNext() &&
                (HasValue() && IsSeparator(Character()) == false)) { }

            // try to parse the value
            var numberString = _input.Substring(start, _start - start);

            // double -- includes a .
            if (numberString.Contains(".") || numberString.Contains("e") || numberString.Contains("E") ||
                numberString == "Infinity" || numberString == "-Infinity" || numberString == "NaN")
            {
                double doubleValue;
                if (double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue) == false)
                {
                    data = null;
                    return MakeFailure("Bad double format with " + numberString);
                }

                data = new fsData(doubleValue);
                return fsResult.Success;
            }
            else
            {
                Int64 intValue;
                if (Int64.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out intValue) == false)
                {
                    data = null;
                    return MakeFailure("Bad Int64 format with " + numberString);
                }

                data = new fsData(intValue);
                return fsResult.Success;
            }
        }

        /// <summary>
        /// Parses a string
        /// </summary>
        private fsResult TryParseString(out string str)
        {
            _cachedStringBuilder.Length = 0;

            // skip the first "
            if (Character() != '"' || TryMoveNext() == false)
            {
                str = string.Empty;
                return MakeFailure("Expected initial \" when parsing a string");
            }

            // read until the next "
            while (HasValue() && Character() != '\"')
            {
                var c = Character();

                // escape if necessary
                if (c == '\\')
                {
                    char unescaped;
                    var fail = TryUnescapeChar(out unescaped);
                    if (fail.Failed)
                    {
                        str = string.Empty;
                        return fail;
                    }

                    _cachedStringBuilder.Append(unescaped);
                }
                // no escaping necessary
                else
                {
                    _cachedStringBuilder.Append(c);

                    // get the next character
                    if (TryMoveNext() == false)
                    {
                        str = string.Empty;
                        return MakeFailure("Unexpected end of input when reading a string");
                    }
                }
            }

            // skip the first "
            if (HasValue() == false || Character() != '"' || TryMoveNext() == false)
            {
                str = string.Empty;
                return MakeFailure("No closing \" when parsing a string");
            }

            str = _cachedStringBuilder.ToString();
            return fsResult.Success;
        }

        /// <summary>
        /// Parses an array
        /// </summary>
        private fsResult TryParseArray(out fsData arr)
        {
            if (Character() != '[')
            {
                arr = null;
                return MakeFailure("Expected initial [ when parsing an array");
            }

            // skip '['
            if (TryMoveNext() == false)
            {
                arr = null;
                return MakeFailure("Unexpected end of input when parsing an array");
            }
            SkipSpace();

            var result = new List<fsData>();

            while (HasValue() && Character() != ']')
            {
                // parse the element
                fsData element;
                var fail = RunParse(out element);
                if (fail.Failed)
                {
                    arr = null;
                    return fail;
                }

                result.Add(element);

                // parse the comma
                SkipSpace();
                if (HasValue() && Character() == ',')
                {
                    if (TryMoveNext() == false)
                    {
                        break;
                    }
                    SkipSpace();
                }
            }

            // skip the final ]
            if (HasValue() == false || Character() != ']' || TryMoveNext() == false)
            {
                arr = null;
                return MakeFailure("No closing ] for array");
            }

            arr = new fsData(result);
            return fsResult.Success;
        }

        private fsResult TryParseObject(out fsData obj)
        {
            if (Character() != '{')
            {
                obj = null;
                return MakeFailure("Expected initial { when parsing an object");
            }

            // skip '{'
            if (TryMoveNext() == false)
            {
                obj = null;
                return MakeFailure("Unexpected end of input when parsing an object");
            }
            SkipSpace();

            var result = new Dictionary<string, fsData>(
                fsGlobalConfig.IsCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);

            while (HasValue() && Character() != '}')
            {
                fsResult failure;

                // parse the key
                SkipSpace();
                string key;
                failure = TryParseString(out key);
                if (failure.Failed)
                {
                    obj = null;
                    return failure;
                }
                SkipSpace();

                // parse the ':' after the key
                if (HasValue() == false || Character() != ':' || TryMoveNext() == false)
                {
                    obj = null;
                    return MakeFailure("Expected : after key \"" + key + "\"");
                }
                SkipSpace();

                // parse the value
                fsData value;
                failure = RunParse(out value);
                if (failure.Failed)
                {
                    obj = null;
                    return failure;
                }

                result.Add(key, value);

                // parse the comma
                SkipSpace();
                if (HasValue() && Character() == ',')
                {
                    if (TryMoveNext() == false)
                    {
                        break;
                    }
                    SkipSpace();
                }
            }

            // skip the final }
            if (HasValue() == false || Character() != '}' || TryMoveNext() == false)
            {
                obj = null;
                return MakeFailure("No closing } for object");
            }

            obj = new fsData(result);
            return fsResult.Success;
        }

        private fsResult RunParse(out fsData data)
        {
            SkipSpace();

            if (HasValue() == false)
            {
                data = default(fsData);
                return MakeFailure("Unexpected end of input");
            }

            switch (Character())
            {
                case 'I': // Infinity
                case 'N': // NaN
                case '.':
                case '+':
                case '-':
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    return TryParseNumber(out data);
                case '"':
                    {
                        string str;
                        var fail = TryParseString(out str);
                        if (fail.Failed)
                        {
                            data = null;
                            return fail;
                        }
                        data = new fsData(str);
                        return fsResult.Success;
                    }
                case '[':
                    return TryParseArray(out data);
                case '{':
                    return TryParseObject(out data);
                case 't':
                    return TryParseTrue(out data);
                case 'f':
                    return TryParseFalse(out data);
                case 'n':
                    return TryParseNull(out data);
                default:
                    data = null;
                    return MakeFailure("unable to parse; invalid token \"" + Character() + "\"");
            }
        }

        /// <summary>
        /// Parses the specified input. Returns a failure state if parsing
        /// failed.
        /// </summary>
        /// <param name="input">The input to parse.</param>
        /// <param name="data">
        /// The parsed data. This is undefined if parsing fails.
        /// </param>
        /// <returns>The parsed input.</returns>
        public static fsResult Parse(string input, out fsData data)
        {
            if (string.IsNullOrEmpty(input))
            {
                data = default(fsData);
                return fsResult.Fail("No input");
            }

            var context = new fsJsonParser(input);
            return context.RunParse(out data);
        }

        /// <summary>
        /// Helper method for Parse that does not allow the error information to
        /// be recovered.
        /// </summary>
        public static fsData Parse(string input)
        {
            fsData data;
            Parse(input, out data).AssertSuccess();
            return data;
        }

        #region Escaping

        private bool IsHex(char c)
        {
            return ((c >= '0' && c <= '9') ||
                (c >= 'a' && c <= 'f') ||
                (c >= 'A' && c <= 'F'));
        }

        private uint ParseSingleChar(char c1, uint multipliyer)
        {
            uint p1 = 0;
            if (c1 >= '0' && c1 <= '9')
            {
                p1 = (uint)(c1 - '0') * multipliyer;
            }
            else if (c1 >= 'A' && c1 <= 'F')
            {
                p1 = (uint)((c1 - 'A') + 10) * multipliyer;
            }
            else if (c1 >= 'a' && c1 <= 'f')
            {
                p1 = (uint)((c1 - 'a') + 10) * multipliyer;
            }
            return p1;
        }

        private uint ParseUnicode(char c1, char c2, char c3, char c4)
        {
            var p1 = ParseSingleChar(c1, 0x1000);
            var p2 = ParseSingleChar(c2, 0x100);
            var p3 = ParseSingleChar(c3, 0x10);
            var p4 = ParseSingleChar(c4, 0x1);

            return p1 + p2 + p3 + p4;
        }

        private fsResult TryUnescapeChar(out char escaped)
        {
            // skip leading backslash '\'
            TryMoveNext();
            if (HasValue() == false)
            {
                escaped = ' ';
                return MakeFailure("Unexpected end of input after \\");
            }

            switch (Character())
            {
                case '\\':
                    TryMoveNext();
                    escaped = '\\';
                    return fsResult.Success;
                case '/':
                    TryMoveNext();
                    escaped = '/';
                    return fsResult.Success;
                case '"':
                    TryMoveNext();
                    escaped = '\"';
                    return fsResult.Success;
                case 'a':
                    TryMoveNext();
                    escaped = '\a';
                    return fsResult.Success;
                case 'b':
                    TryMoveNext();
                    escaped = '\b';
                    return fsResult.Success;
                case 'f':
                    TryMoveNext();
                    escaped = '\f';
                    return fsResult.Success;
                case 'n':
                    TryMoveNext();
                    escaped = '\n';
                    return fsResult.Success;
                case 'r':
                    TryMoveNext();
                    escaped = '\r';
                    return fsResult.Success;
                case 't':
                    TryMoveNext();
                    escaped = '\t';
                    return fsResult.Success;
                case '0':
                    TryMoveNext();
                    escaped = '\0';
                    return fsResult.Success;
                case 'u':
                    TryMoveNext();
                    if (IsHex(Character(0))
                        && IsHex(Character(1))
                        && IsHex(Character(2))
                        && IsHex(Character(3)))
                    {
                        var codePoint = ParseUnicode(Character(0), Character(1), Character(2), Character(3));

                        TryMoveNext();
                        TryMoveNext();
                        TryMoveNext();
                        TryMoveNext();

                        escaped = (char)codePoint;
                        return fsResult.Success;
                    }

                    // invalid escape sequence
                    escaped = (char)0;
                    return MakeFailure(
                        $"invalid escape sequence '\\u{Character(0)}{Character(1)}{Character(2)}{Character(3)}'\n");
                default:
                    escaped = (char)0;
                    return MakeFailure($"Invalid escape sequence \\{Character()}");
            }
        }

        #endregion Escaping
    }
}
