using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

#if !BURST_COMPILER_SHARED
using Unity.Collections.LowLevel.Unsafe;
#endif

namespace Unity.Burst
{
#if BURST_COMPILER_SHARED
    internal static partial class BurstStringInternal
#else
    internal static partial class BurstString
#endif
    {
        // Prevent Format from being stripped, otherwise, the string format transform passes will fail, and code that was compileable
        //before stripping, will no longer compile. 
        internal class PreserveAttribute : System.Attribute {}
        /// <summary>
        /// Copies a Burst managed UTF8 string prefixed by a ushort length to a FixedString with the specified maximum length.
        /// </summary>
        /// <param name="dest">Pointer to the fixed string.</param>
        /// <param name="destLength">Maximum number of UTF8 the fixed string supports without including the zero character.</param>
        /// <param name="src">The UTF8 Burst managed string prefixed by a ushort length and zero terminated.
        /// <param name="srcLength">Number of UTF8 the fixed string supports without including the zero character.</param>
        /// </param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        [Preserve]
        public static unsafe void CopyFixedString(byte* dest, int destLength, byte* src, int srcLength)
        {
            // TODO: should we throw an exception instead if the string doesn't fit?
            var finalLength = srcLength > destLength ? destLength : srcLength;
            // Write the length and zero null terminated
            *((ushort*)dest - 1) = (ushort)finalLength;
            dest[finalLength] = 0;
#if BURST_COMPILER_SHARED
            Unsafe.CopyBlock(dest, src, (uint)finalLength);
#else
            UnsafeUtility.MemCpy(dest, src, finalLength);
#endif
        }

        /// <summary>
        /// Format a UTF-8 string (with a specified source length) to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="src">The source buffer of the string to copy from.</param>
        /// <param name="srcLength">The length of the string from the source buffer.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, byte* src, int srcLength, int formatOptionsRaw)
        {
            var options = *(FormatOptions*)&formatOptionsRaw;

            // Align left
            if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, srcLength)) return;

            int maxToCopy = destLength - destIndex;
            int toCopyLength = srcLength > maxToCopy ? maxToCopy : srcLength;
            if (toCopyLength > 0)
            {
#if BURST_COMPILER_SHARED
                Unsafe.CopyBlock(dest + destIndex, src, (uint)toCopyLength);
#else
                UnsafeUtility.MemCpy(dest + destIndex, src, toCopyLength);
#endif
                destIndex += toCopyLength;

                // Align right
                AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, srcLength);
            }
        }

        /// <summary>
        /// Format a float value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, float value, int formatOptionsRaw)
        {
            var options = *(FormatOptions*)&formatOptionsRaw;
            ConvertFloatToString(dest, ref destIndex, destLength, value, options);
        }

        /// <summary>
        /// Format a double value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, double value, int formatOptionsRaw)
        {
            var options = *(FormatOptions*)&formatOptionsRaw;
            ConvertDoubleToString(dest, ref destIndex, destLength, value, options);
        }

        /// <summary>
        /// Format a bool value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, bool value, int formatOptionsRaw)
        {
            var length = value ? 4 : 5; // True = 4 chars, False = 5 chars
            var options = *(FormatOptions*)&formatOptionsRaw;

            // Align left
            if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, length)) return;

            if (value)
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'T';
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'r';
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'u';
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'e';
            }
            else
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'F';
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'a';
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'l';
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'s';
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'e';
            }

            // Align right
            AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, length);
        }

        /// <summary>
        /// Format a char value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, char value, int formatOptionsRaw)
        {
            var length = value <= 0x7f ? 1 : value <= 0x7FF ? 2 : 3;
            var options = *(FormatOptions*)&formatOptionsRaw;

            // Align left - Special case for char, make the length as it was always one byte (one char)
            // so that alignment is working fine (on a char basis)
            if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, 1)) return;

            // Basic encoding of UTF16 to UTF8, doesn't handle high/low surrogate as we are given only one char
            if (length == 1)
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)value;
            }
            else if (length == 2)
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)((value >> 6) | 0xC0);

                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)((value & 0x3F) | 0x80);
            }
            else if (length == 3)
            {
                // We don't handle high/low surrogate, so we replace the char with the replacement char
                // 0xEF, 0xBF, 0xBD
                bool isHighOrLowSurrogate = value >= '\xD800' && value <= '\xDFFF';
                if (isHighOrLowSurrogate)
                {
                    if (destIndex >= destLength) return;
                    dest[destIndex++] = 0xEF;

                    if (destIndex >= destLength) return;
                    dest[destIndex++] = 0xBF;

                    if (destIndex >= destLength) return;
                    dest[destIndex++] = 0xBD;
                }
                else
                {
                    if (destIndex >= destLength) return;
                    dest[destIndex++] = (byte)((value >> 12) | 0xE0);

                    if (destIndex >= destLength) return;
                    dest[destIndex++] = (byte)(((value >> 6) & 0x3F) | 0x80);

                    if (destIndex >= destLength) return;
                    dest[destIndex++] = (byte)((value & 0x3F) | 0x80);
                }
            }

            // Align right - Special case for char, make the length as it was always one byte (one char)
            // so that alignment is working fine (on a char basis)
            AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, 1);
        }

        /// <summary>
        /// Format a byte value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, byte value, int formatOptionsRaw)
        {
            Format(dest, ref destIndex, destLength, (ulong)value, formatOptionsRaw);
        }

        /// <summary>
        /// Format an ushort value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, ushort value, int formatOptionsRaw)
        {
            Format(dest, ref destIndex, destLength, (ulong)value, formatOptionsRaw);
        }

        /// <summary>
        /// Format an uint value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, uint value, int formatOptionsRaw)
        {
            var options = *(FormatOptions*)&formatOptionsRaw;
            ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, value, options);
        }

        /// <summary>
        /// Format a ulong value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, ulong value, int formatOptionsRaw)
        {
            var options = *(FormatOptions*)&formatOptionsRaw;
            ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, value, options);
        }

        /// <summary>
        /// Format a sbyte value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, sbyte value, int formatOptionsRaw)
        {
            var options = *(FormatOptions*)&formatOptionsRaw;
            if (options.Kind == NumberFormatKind.Hexadecimal)
            {
                ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (byte)value, options);
            }
            else
            {
                ConvertIntegerToString(dest, ref destIndex, destLength, value, options);
            }
        }

        /// <summary>
        /// Format a short value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, short value, int formatOptionsRaw)
        {
            var options = *(FormatOptions*)&formatOptionsRaw;
            if (options.Kind == NumberFormatKind.Hexadecimal)
            {
                ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (ushort)value, options);
            }
            else
            {
                ConvertIntegerToString(dest, ref destIndex, destLength, value, options);
            }

        }

        /// <summary>
        /// Format an int value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, int value, int formatOptionsRaw)
        {
            var options = *(FormatOptions*)&formatOptionsRaw;
            if (options.Kind == NumberFormatKind.Hexadecimal)
            {
                ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (uint)value, options);
            }
            else
            {
                ConvertIntegerToString(dest, ref destIndex, destLength, value, options);
            }
        }

        /// <summary>
        /// Format a long value to a destination buffer.
        /// </summary>
        /// <param name="dest">Destination buffer.</param>
        /// <param name="destIndex">Current index in destination buffer.</param>
        /// <param name="destLength">Maximum length of destination buffer.</param>
        /// <param name="value">The value to format.</param>
        /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
        [Preserve]
        public static unsafe void Format(byte* dest, ref int destIndex, int destLength, long value, int formatOptionsRaw)
        {
            var options = *(FormatOptions*)&formatOptionsRaw;
            if (options.Kind == NumberFormatKind.Hexadecimal)
            {
                ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (ulong)value, options);
            }
            else
            {
                ConvertIntegerToString(dest, ref destIndex, destLength, value, options);
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static unsafe void ConvertUnsignedIntegerToString(byte* dest, ref int destIndex, int destLength, ulong value, FormatOptions options)
        {
            var basis = (uint)options.GetBase();
            if (basis < 2 || basis > 36) return;

            // Calculate the full length (including zero padding)
            int length = 0;
            var tmp = value;
            do
            {
                tmp /= basis;
                length++;
            } while (tmp != 0);

            // Write the characters for the numbers to a temp buffer
            int tmpIndex = length - 1;
            byte* tmpBuffer = stackalloc byte[length + 1];

            tmp = value;
            do
            {
                tmpBuffer[tmpIndex--] = ValueToIntegerChar((int)(tmp % basis), options.Uppercase);
                tmp /= basis;
            } while (tmp != 0);

            tmpBuffer[length] = 0;

            var numberBuffer = new NumberBuffer(NumberBufferKind.Integer, tmpBuffer, length, length, false);
            FormatNumber(dest, ref destIndex, destLength, ref numberBuffer, options.Specifier, options);
        }

        private static int GetLengthIntegerToString(long value, int basis, int zeroPadding)
        {
            int length = 0;
            var tmp = value;
            do
            {
                tmp /= basis;
                length++;
            } while (tmp != 0);

            if (length < zeroPadding)
            {
                length = zeroPadding;
            }

            if (value < 0) length++;
            return length;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static unsafe void ConvertIntegerToString(byte* dest, ref int destIndex, int destLength, long value, FormatOptions options)
        {
            var basis = options.GetBase();
            if (basis < 2 || basis > 36) return;

            // Calculate the full length (including zero padding)
            int length = 0;
            var tmp = value;
            do
            {
                tmp /= basis;
                length++;
            } while (tmp != 0);

            // Write the characters for the numbers to a temp buffer
            byte* tmpBuffer = stackalloc byte[length + 1];

            tmp = value;
            int tmpIndex = length - 1;
            do
            {
                tmpBuffer[tmpIndex--] = ValueToIntegerChar((int)(tmp % basis), options.Uppercase);
                tmp /= basis;
            } while (tmp != 0);
            tmpBuffer[length] = 0;

            var numberBuffer = new NumberBuffer(NumberBufferKind.Integer, tmpBuffer, length, length, value < 0);
            FormatNumber(dest, ref destIndex, destLength, ref numberBuffer, options.Specifier, options);
        }

        private static unsafe void FormatNumber(byte* dest, ref int destIndex, int destLength, ref NumberBuffer number, int nMaxDigits, FormatOptions options)
        {
            bool isCorrectlyRounded = (number.Kind == NumberBufferKind.Float);

            // If we have an integer, and the rendering is the default `G`, then use Decimal rendering which is faster
            if (number.Kind == NumberBufferKind.Integer && options.Kind == NumberFormatKind.General && options.Specifier == 0)
            {
                options.Kind = NumberFormatKind.Decimal;
            }

            int length;
            switch (options.Kind)
            {
                case NumberFormatKind.DecimalForceSigned:
                case NumberFormatKind.Decimal:
                case NumberFormatKind.Hexadecimal:
                    length = number.DigitsCount;

                    var zeroPadding = (int)options.Specifier;
                    int actualZeroPadding = 0;
                    if (length < zeroPadding)
                    {
                        actualZeroPadding = zeroPadding - length;
                        length = zeroPadding;
                    }

                    bool outputPositiveSign = options.Kind == NumberFormatKind.DecimalForceSigned;
                    length += number.IsNegative || outputPositiveSign ? 1 : 0;

                    // Perform left align
                    if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, length)) return;

                    FormatDecimalOrHexadecimal(dest, ref destIndex, destLength, ref number, actualZeroPadding, outputPositiveSign);

                    // Perform right align
                    AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, length);

                    break;

                default:
                case NumberFormatKind.General:

                    if (nMaxDigits < 1)
                    {
                        // This ensures that the PAL code pads out to the correct place even when we use the default precision
                        nMaxDigits = number.DigitsCount;
                    }

                    RoundNumber(ref number, nMaxDigits, isCorrectlyRounded);

                    // Calculate final rendering length
                    length = GetLengthForFormatGeneral(ref number, nMaxDigits);

                    // Perform left align
                    if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, length)) return;

                    // Format using general formatting
                    FormatGeneral(dest, ref destIndex, destLength, ref number, nMaxDigits, options.Uppercase ? (byte)'E' : (byte)'e');

                    // Perform right align
                    AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, length);
                    break;
            }
        }

        private static unsafe void FormatDecimalOrHexadecimal(byte* dest, ref int destIndex, int destLength, ref NumberBuffer number, int zeroPadding, bool outputPositiveSign)
        {
            if (number.IsNegative)
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'-';
            }
            else if (outputPositiveSign)
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'+';
            }

            // Zero Padding
            for (int i = 0; i < zeroPadding; i++)
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'0';
            }

            var digitCount = number.DigitsCount;
            byte* digits = number.GetDigitsPointer();
            for (int i = 0; i < digitCount; i++)
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = digits[i];
            }
        }

        private static byte ValueToIntegerChar(int value, bool uppercase)
        {
            value = value < 0 ? -value : value;
            if (value <= 9)
                return (byte)('0' + value);
            if (value < 36)
                return (byte)((uppercase ? 'A' : 'a') + (value - 10));

            return (byte)'?';
        }

        private static readonly char[] SplitByColon = new char[] { ':' };

#if !NET_DOTS
        private static void OptsSplit(string fullFormat, out string padding, out string format)
        {
            var split = fullFormat.Split(SplitByColon, StringSplitOptions.RemoveEmptyEntries);
            format = split[0];
            padding = null;
            if (split.Length == 2)
            {
                padding = format;
                format = split[1];
            }
            else if (split.Length == 1)
            {
                if (format[0] == ',')
                {
                    padding = format;
                    format = null;
                }
            }
            else
            {
                throw new ArgumentException($"Format `{format}` not supported. Invalid number {split.Length} of :. Expecting no more than one.");
            }
        }
#else
        // Tiny BCL is missing StringSplitOptions
        private static void OptsSplit(string fullFormat, out string padding, out string format)
        {
            var idx0 = 0;
            var idx1 = 1;
            var length = 0;
            var split = fullFormat.Split(SplitByColon);
            for (int chk=0;chk<split.Length;chk++)
            {
                if (split[chk].Length>0)
                    length++;
            }
            while (idx0<split.Length)
            {
                if (split[idx0].Length>0)
                {
                    idx1=idx0+1;
                    break;
                }

                idx0++;
            }
            while (idx1<split.Length)
            {
                if (split[idx1].Length>0)
                {
                    break;
                }

                idx1++;
            }
            format = split[idx0];
            padding = null;
            if (length == 2)
            {
                padding = format;
                format=split[idx1];
            }
            else if (length == 1)
            {
                if (format[0]==',')
                {
                    padding = format;
                    format = null;
                }
            }
            else
            {
                throw new ArgumentException($"Format `{format}` not supported. Invalid number {length} of :. Expecting no more than one.");
            }
        }
#endif

        /// <summary>
        /// Parse a format string as specified .NET string.Format https://docs.microsoft.com/en-us/dotnet/api/system.string.format?view=netframework-4.8
        /// - Supports only Left/Right Padding (e.g {0,-20} {0, 8})
        /// - 'G' 'g' General formatting for numbers with precision specifier (e.g G4 or g4)
        /// - 'D' 'd' General formatting for numbers with precision specifier (e.g D5 or d5)
        /// - 'X' 'x' General formatting for integers with precision specifier (e.g X8 or x8)
        /// </summary>
        /// <param name="fullFormat"></param>
        /// <returns></returns>
        public static FormatOptions ParseFormatToFormatOptions(string fullFormat)
        {
            if (string.IsNullOrWhiteSpace(fullFormat)) return new FormatOptions();

            OptsSplit(fullFormat, out var padding, out var format);

            format = format?.Trim();
            padding = padding?.Trim();

            int alignAndSize = 0;
            var formatKind = NumberFormatKind.General;
            bool lowercase = false;
            int specifier = 0;

            if (!string.IsNullOrEmpty(format))
            {
                switch (format[0])
                {
                    case 'G':
                        formatKind = NumberFormatKind.General;
                        break;
                    case 'g':
                        formatKind = NumberFormatKind.General;
                        lowercase = true;
                        break;
                    case 'D':
                        formatKind = NumberFormatKind.Decimal;
                        break;
                    case 'd':
                        formatKind = NumberFormatKind.Decimal;
                        lowercase = true;
                        break;
                    case 'X':
                        formatKind = NumberFormatKind.Hexadecimal;
                        break;
                    case 'x':
                        formatKind = NumberFormatKind.Hexadecimal;
                        lowercase = true;
                        break;
                    default:
                        throw new ArgumentException($"Format `{format}` not supported. Only G, g, D, d, X, x are supported.");
                }

                if (format.Length > 1)
                {
                    var specifierString = format.Substring(1);
#if !NET_DOTS
                    if (!uint.TryParse(specifierString, out var unsignedSpecifier))
#else
                    // Tiny BCL is missing string->uint
                    if (!ParseUnsigned(specifierString, out var unsignedSpecifier))
#endif
                    {
                        throw new ArgumentException($"Expecting an unsigned integer for specifier `{format}` instead of {specifierString}.");
                    }
                    specifier = (int)unsignedSpecifier;
                }
            }

            if (!string.IsNullOrEmpty(padding))
            {
                if (padding[0] != ',')
                {
                    throw new ArgumentException($"Invalid padding `{padding}`, expecting to start with a leading `,` comma.");
                }

                var numberStr = padding.Substring(1);
#if !NET_DOTS
                if (!int.TryParse(numberStr, out alignAndSize))
#else
                // Tiny BCL is missing string->int
                if (!ParseSigned(numberStr, out alignAndSize))
#endif
                {
                    throw new ArgumentException($"Expecting an integer for align/size padding `{numberStr}`.");
                }
            }

            return new FormatOptions(formatKind, (sbyte)alignAndSize, (byte)specifier, lowercase);
        }

#if NET_DOTS
        // Won't handle anything but simple ascii unsigned integers
        private static bool ParseUnsigned(string inputString, out uint unsignedValue, int startIdx = 0)
        {
            var length = inputString.Length;
            unsignedValue = 0;
            for (int i = startIdx; i < inputString.Length; i++)
            {
                var c = inputString[i];
                if (c>='0' && c<='9')
                {
                    if (unsignedValue>=0x19999999)    // overflow
                        return false;
                    unsignedValue*=10;
                    unsignedValue+=(uint)(c-'0');
                }
                else
                {
                    return false;
                }
            }

            return true;
        }

        // Won't handle anything but simple ascii signed integers
        private static bool ParseSigned(string inputString, out int signedValue)
        {
            signedValue = 0;
            int startIdx = 0;
            bool negative = false;
            if (inputString[0] == '-' || inputString[0] == '+')
            {
                negative = inputString[0] == '-';
                startIdx++;
            }

            if (!ParseUnsigned(inputString, out var unsignedValue, startIdx))
            {
                return false;
            }

            if (negative)
            {
                if (unsignedValue > 0x80000000)
                    return false;
                signedValue = (int) ((~unsignedValue) + 1);
            }
            else
            {
                if (unsignedValue > 0x7FFFFFFF)
                    return false;
                signedValue = (int) unsignedValue;
            }

            return true;
        }
#endif
        private static unsafe bool AlignRight(byte* dest, ref int destIndex, int destLength, int align, int length)
        {
            // right align
            if (align < 0)
            {
                align = -align;
                return AlignLeft(dest, ref destIndex, destLength, align, length);
            }

            return false;
        }

        private static unsafe bool AlignLeft(byte* dest, ref int destIndex, int destLength, int align, int length)
        {
            // left align
            if (align > 0)
            {
                while (length < align)
                {
                    if (destIndex >= destLength) return true;
                    dest[destIndex++] = (byte)' ';
                    length++;
                }
            }

            return false;
        }

        private static unsafe int GetLengthForFormatGeneral(ref NumberBuffer number, int nMaxDigits)
        {
            // NOTE: Must be kept in sync with FormatGeneral!
            int length = 0;
            int scale = number.Scale;
            int digPos = scale;
            bool scientific = false;

            // Don't switch to scientific notation
            if (digPos > nMaxDigits || digPos < -3)
            {
                digPos = 1;
                scientific = true;
            }

            byte* dig = number.GetDigitsPointer();

            if (number.IsNegative)
            {
                length++; // (byte)'-';
            }

            if (digPos > 0)
            {
                do
                {
                    if (*dig != 0)
                    {
                        dig++;
                    }
                    length++;
                } while (--digPos > 0);
            }
            else
            {
                length++;
            }

            if (*dig != 0 || digPos < 0)
            {
                length++; // (byte)'.';

                while (digPos < 0)
                {
                    length++; // (byte)'0';
                    digPos++;
                }

                while (*dig != 0)
                {
                    length++; // *dig++;
                    dig++;
                }
            }

            if (scientific)
            {
                length++; // e or E
                int exponent = number.Scale - 1;
                if (exponent >= 0) length++;
                length += GetLengthIntegerToString(exponent, 10, 2);
            }

            return length;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static unsafe void FormatGeneral(byte* dest, ref int destIndex, int destLength, ref NumberBuffer number, int nMaxDigits, byte expChar)
        {
            int scale = number.Scale;
            int digPos = scale;
            bool scientific = false;

            // Don't switch to scientific notation
            if (digPos > nMaxDigits || digPos < -3)
            {
                digPos = 1;
                scientific = true;
            }

            byte* dig = number.GetDigitsPointer();

            if (number.IsNegative)
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'-';
            }

            if (digPos > 0)
            {
                do
                {
                    if (destIndex >= destLength) return;
                    dest[destIndex++] = (*dig != 0) ? (byte)(*dig++) : (byte)'0';
                } while (--digPos > 0);
            }
            else
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'0';
            }

            if (*dig != 0 || digPos < 0)
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = (byte)'.';

                while (digPos < 0)
                {
                    if (destIndex >= destLength) return;
                    dest[destIndex++] = (byte)'0';
                    digPos++;
                }

                while (*dig != 0)
                {
                    if (destIndex >= destLength) return;
                    dest[destIndex++] = *dig++;
                }
            }

            if (scientific)
            {
                if (destIndex >= destLength) return;
                dest[destIndex++] = expChar;

                int exponent = number.Scale - 1;
                var exponentFormatOptions = new FormatOptions(NumberFormatKind.DecimalForceSigned, 0, 2, false);

                ConvertIntegerToString(dest, ref destIndex, destLength, exponent, exponentFormatOptions);
            }
        }

        private static unsafe void RoundNumber(ref NumberBuffer number, int pos, bool isCorrectlyRounded)
        {
            byte* dig = number.GetDigitsPointer();

            int i = 0;
            while (i < pos && dig[i] != (byte)'\0')
                i++;

            if ((i == pos) && ShouldRoundUp(dig, i, isCorrectlyRounded))
            {
                while (i > 0 && dig[i - 1] == (byte)'9')
                    i--;

                if (i > 0)
                {
                    dig[i - 1]++;
                }
                else
                {
                    number.Scale++;
                    dig[0] = (byte)('1');
                    i = 1;
                }
            }
            else
            {
                while (i > 0 && dig[i - 1] == (byte)'0')
                    i--;
            }

            if (i == 0)
            {
                number.Scale = 0;      // Decimals with scale ('0.00') should be rounded.
            }

            dig[i] = (byte)('\0');
            number.DigitsCount = i;
        }

        private static unsafe bool ShouldRoundUp(byte* dig, int i, bool isCorrectlyRounded)
        {
            // We only want to round up if the digit is greater than or equal to 5 and we are
            // not rounding a floating-point number. If we are rounding a floating-point number
            // we have one of two cases.
            //
            // In the case of a standard numeric-format specifier, the exact and correctly rounded
            // string will have been produced. In this scenario, pos will have pointed to the
            // terminating null for the buffer and so this will return false.
            //
            // However, in the case of a custom numeric-format specifier, we currently fall back
            // to generating Single/DoublePrecisionCustomFormat digits and then rely on this
            // function to round correctly instead. This can unfortunately lead to double-rounding
            // bugs but is the best we have right now due to back-compat concerns.

            byte digit = dig[i];

            if ((digit == '\0') || isCorrectlyRounded)
            {
                // Fast path for the common case with no rounding
                return false;
            }

            // Values greater than or equal to 5 should round up, otherwise we round down. The IEEE
            // 754 spec actually dictates that ties (exactly 5) should round to the nearest even number
            // but that can have undesired behavior for custom numeric format strings. This probably
            // needs further thought for .NET 5 so that we can be spec compliant and so that users
            // can get the desired rounding behavior for their needs.

            return digit >= '5';
        }

        private enum NumberBufferKind
        {
            Integer,
            Float,
        }

        /// <summary>
        /// Information about a number: pointer to digit buffer, scale and if negative.
        /// </summary>
        private unsafe struct NumberBuffer
        {
            private readonly byte* _buffer;

            public NumberBuffer(NumberBufferKind kind, byte* buffer, int digitsCount, int scale, bool isNegative)
            {
                Kind = kind;
                _buffer = buffer;
                DigitsCount = digitsCount;
                Scale = scale;
                IsNegative = isNegative;
            }

            public NumberBufferKind Kind;

            public int DigitsCount;

            public int Scale;

            public readonly bool IsNegative;

            public byte* GetDigitsPointer() => _buffer;
        }

        /// <summary>
        /// Type of formatting
        /// </summary>
        public enum NumberFormatKind : byte
        {
            /// <summary>
            /// General 'G' or 'g' formatting.
            /// </summary>
            General,

            /// <summary>
            /// Decimal 'D' or 'd' formatting.
            /// </summary>
            Decimal,

            /// <summary>
            /// Internal use only. Decimal 'D' or 'd' formatting with a `+` positive in front of the decimal if positive
            /// </summary>
            DecimalForceSigned,

            /// <summary>
            /// Hexadecimal 'X' or 'x' formatting.
            /// </summary>
            Hexadecimal,
        }

        /// <summary>
        /// Formatting options. Must be sizeof(int)
        /// </summary>
        public struct FormatOptions
        {
            public FormatOptions(NumberFormatKind kind, sbyte alignAndSize, byte specifier, bool lowercase) : this()
            {
                Kind = kind;
                AlignAndSize = alignAndSize;
                Specifier = specifier;
                Lowercase = lowercase;
            }

            public NumberFormatKind Kind;
            public sbyte AlignAndSize;
            public byte Specifier;
            public bool Lowercase;

            public bool Uppercase => !Lowercase;

            /// <summary>
            /// Encode this options to a single integer.
            /// </summary>
            /// <returns></returns>
            public unsafe int EncodeToRaw()
            {
#if !NET_DOTS
                Debug.Assert(sizeof(FormatOptions) == sizeof(int));
#endif
                var value = this;
                return *(int*)&value;
            }

            /// <summary>
            /// Get the base used for formatting this number.
            /// </summary>
            /// <returns></returns>
            public int GetBase()
            {
                switch (Kind)
                {
                    case NumberFormatKind.Hexadecimal:
                        return 16;
                    default:
                        return 10;
                }
            }

            public override string ToString()
            {
                return $"{nameof(Kind)}: {Kind}, {nameof(AlignAndSize)}: {AlignAndSize}, {nameof(Specifier)}: {Specifier}, {nameof(Uppercase)}: {Uppercase}";
            }
        }
    }
}