using System;
using System.Globalization;
using System.IO;
using System.Text;

namespace Unity.VisualScripting.FullSerializer
{
    public static class fsJsonPrinter
    {
        /// <summary>
        /// Inserts the given number of indents into the builder.
        /// </summary>
        private static void InsertSpacing(TextWriter stream, int count)
        {
            for (var i = 0; i < count; ++i)
            {
                stream.Write("    ");
            }
        }

        /// <summary>
        /// Escapes a string.
        /// </summary>
        private static string EscapeString(string str)
        {
            // Escaping a string is pretty allocation heavy, so we try hard to
            // not do it.

            var needsEscape = false;
            for (var i = 0; i < str.Length; ++i)
            {
                var c = str[i];

                // unicode code point
                var intChar = Convert.ToInt32(c);
                if (intChar < 0 || intChar > 127)
                {
                    needsEscape = true;
                    break;
                }

                // standard escape character
                switch (c)
                {
                    case '"':
                    case '\\':
                    case '\a':
                    case '\b':
                    case '\f':
                    case '\n':
                    case '\r':
                    case '\t':
                    case '\0':
                        needsEscape = true;
                        break;
                }

                if (needsEscape)
                {
                    break;
                }
            }

            if (needsEscape == false)
            {
                return str;
            }

            var result = new StringBuilder();

            for (var i = 0; i < str.Length; ++i)
            {
                var c = str[i];

                // unicode code point
                var intChar = Convert.ToInt32(c);
                if (intChar < 0 || intChar > 127)
                {
                    result.Append($"\\u{intChar:x4} ".Trim());
                    continue;
                }

                // standard escape character
                switch (c)
                {
                    case '"':
                        result.Append("\\\"");
                        continue;
                    case '\\':
                        result.Append(@"\\");
                        continue;
                    case '\a':
                        result.Append(@"\a");
                        continue;
                    case '\b':
                        result.Append(@"\b");
                        continue;
                    case '\f':
                        result.Append(@"\f");
                        continue;
                    case '\n':
                        result.Append(@"\n");
                        continue;
                    case '\r':
                        result.Append(@"\r");
                        continue;
                    case '\t':
                        result.Append(@"\t");
                        continue;
                    case '\0':
                        result.Append(@"\0");
                        continue;
                }

                // no escaping needed
                result.Append(c);
            }
            return result.ToString();
        }

        private static void BuildCompressedString(fsData data, TextWriter stream)
        {
            switch (data.Type)
            {
                case fsDataType.Null:
                    stream.Write("null");
                    break;

                case fsDataType.Boolean:
                    if (data.AsBool)
                    {
                        stream.Write("true");
                    }
                    else
                    {
                        stream.Write("false");
                    }
                    break;

                case fsDataType.Double:
                    // doubles must *always* include a decimal
                    stream.Write(ConvertDoubleToString(data.AsDouble));
                    break;

                case fsDataType.Int64:
                    stream.Write(data.AsInt64);
                    break;

                case fsDataType.String:
                    stream.Write('"');
                    stream.Write(EscapeString(data.AsString));
                    stream.Write('"');
                    break;

                case fsDataType.Object:
                    {
                        stream.Write('{');
                        var comma = false;
                        foreach (var entry in data.AsDictionary)
                        {
                            if (comma)
                            {
                                stream.Write(',');
                            }
                            comma = true;
                            stream.Write('"');
                            stream.Write(entry.Key);
                            stream.Write('"');
                            stream.Write(":");
                            BuildCompressedString(entry.Value, stream);
                        }
                        stream.Write('}');
                        break;
                    }

                case fsDataType.Array:
                    {
                        stream.Write('[');
                        var comma = false;
                        foreach (var entry in data.AsList)
                        {
                            if (comma)
                            {
                                stream.Write(',');
                            }
                            comma = true;
                            BuildCompressedString(entry, stream);
                        }
                        stream.Write(']');
                        break;
                    }
            }
        }

        /// <summary>
        /// Formats this data into the given builder.
        /// </summary>
        private static void BuildPrettyString(fsData data, TextWriter stream, int depth)
        {
            switch (data.Type)
            {
                case fsDataType.Null:
                    stream.Write("null");
                    break;

                case fsDataType.Boolean:
                    if (data.AsBool)
                    {
                        stream.Write("true");
                    }
                    else
                    {
                        stream.Write("false");
                    }
                    break;

                case fsDataType.Double:
                    stream.Write(ConvertDoubleToString(data.AsDouble));
                    break;

                case fsDataType.Int64:
                    stream.Write(data.AsInt64);
                    break;

                case fsDataType.String:
                    stream.Write('"');
                    stream.Write(EscapeString(data.AsString));
                    stream.Write('"');
                    break;

                case fsDataType.Object:
                    {
                        stream.Write('{');
                        stream.WriteLine();
                        var comma = false;
                        foreach (var entry in data.AsDictionary)
                        {
                            if (comma)
                            {
                                stream.Write(',');
                                stream.WriteLine();
                            }
                            comma = true;
                            InsertSpacing(stream, depth + 1);
                            stream.Write('"');
                            stream.Write(entry.Key);
                            stream.Write('"');
                            stream.Write(": ");
                            BuildPrettyString(entry.Value, stream, depth + 1);
                        }
                        stream.WriteLine();
                        InsertSpacing(stream, depth);
                        stream.Write('}');
                        break;
                    }

                case fsDataType.Array:
                    // special case for empty lists; we don't put an empty line
                    // between the brackets
                    if (data.AsList.Count == 0)
                    {
                        stream.Write("[]");
                    }
                    else
                    {
                        var comma = false;

                        stream.Write('[');
                        stream.WriteLine();
                        foreach (var entry in data.AsList)
                        {
                            if (comma)
                            {
                                stream.Write(',');
                                stream.WriteLine();
                            }
                            comma = true;
                            InsertSpacing(stream, depth + 1);
                            BuildPrettyString(entry, stream, depth + 1);
                        }
                        stream.WriteLine();
                        InsertSpacing(stream, depth);
                        stream.Write(']');
                    }
                    break;
            }
        }

        /// <summary>
        /// Writes the pretty JSON output data to the given stream.
        /// </summary>
        /// <param name="data">The data to print.</param>
        /// <param name="outputStream">Where to write the printed data.</param>
        public static void PrettyJson(fsData data, TextWriter outputStream)
        {
            BuildPrettyString(data, outputStream, 0);
        }

        /// <summary>
        /// Returns the data in a pretty printed JSON format.
        /// </summary>
        public static string PrettyJson(fsData data)
        {
            var sb = new StringBuilder();
            using (var writer = new StringWriter(sb))
            {
                BuildPrettyString(data, writer, 0);
                return sb.ToString();
            }
        }

        /// <summary>
        /// Writes the compressed JSON output data to the given stream.
        /// </summary>
        /// <param name="data">The data to print.</param>
        /// <param name="outputStream">Where to write the printed data.</param>
        public static void CompressedJson(fsData data, StreamWriter outputStream)
        {
            BuildCompressedString(data, outputStream);
        }

        /// <summary>
        /// Returns the data in a relatively compressed JSON format.
        /// </summary>
        public static string CompressedJson(fsData data)
        {
            var sb = new StringBuilder();
            using (var writer = new StringWriter(sb))
            {
                BuildCompressedString(data, writer);
                return sb.ToString();
            }
        }

        /// <summary>
        /// Utility method that converts a double to a string.
        /// </summary>
        private static string ConvertDoubleToString(double d)
        {
            if (Double.IsInfinity(d) || Double.IsNaN(d))
            {
                return d.ToString(CultureInfo.InvariantCulture);
            }

            var doubledString = d.ToString(CultureInfo.InvariantCulture);

            // NOTE/HACK: If we don't serialize with a period or an exponent,
            // then the number will be deserialized as an Int64, not a double.
            if (doubledString.Contains(".") == false &&
                doubledString.Contains("e") == false &&
                doubledString.Contains("E") == false)
            {
                doubledString += ".0";
            }

            return doubledString;
        }
    }
}
