// Copyright (c) 2015-2017 Michael Popoloski
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using UnsafeIL;

namespace System.Text.Formatting {
	/// <summary>
	/// Specifies an interface for types that act as a set of formatting arguments.
	/// </summary>
	public interface IArgSet {
        /// <summary>
        /// The number of arguments in the set.
        /// </summary>
        int Count { get; }

        /// <summary>
        /// Format one of the arguments in the set into the given string buffer.
        /// </summary>
        /// <param name="buffer">The buffer to which to append the argument.</param>
        /// <param name="index">The index of the argument to format.</param>
        /// <param name="format">A specifier indicating how the argument should be formatted.</param>
        void Format (StringBuffer buffer, int index, StringView format);
    }

    /// <summary>
    /// Defines an interface for types that can be formatted into a string buffer.
    /// </summary>
    public interface IStringFormattable {
        /// <summary>
        /// Format the current instance into the given string buffer.
        /// </summary>
        /// <param name="buffer">The buffer to which to append.</param>
        /// <param name="format">A specifier indicating how the argument should be formatted.</param>
        void Format (StringBuffer buffer, StringView format);
    }

    /// <summary>
    /// A low-allocation version of the built-in <see cref="StringBuilder"/> type.
    /// </summary>
    public unsafe sealed partial class StringBuffer {
        CachedCulture culture;
        char[] buffer;
        int currentCount;

        /// <summary>
        /// The number of characters in the buffer.
        /// </summary>
        public int Count {
            get { return currentCount; }
        }

        /// <summary>
        /// The culture used to format string data.
        /// </summary>
        public CultureInfo Culture {
            get { return culture.Culture; }
            set {
                if (culture.Culture == value)
                    return;

                if (value == CultureInfo.InvariantCulture)
                    culture = CachedInvariantCulture;
                else if (value == CachedCurrentCulture.Culture)
                    culture = CachedCurrentCulture;
                else
                    culture = new CachedCulture(value);
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="StringBuffer"/> class.
        /// </summary>
        public StringBuffer ()
            : this(DefaultCapacity) {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="StringBuffer"/> class.
        /// </summary>
        /// <param name="capacity">The initial size of the string buffer.</param>
        public StringBuffer (int capacity) {
            buffer = new char[capacity];
            culture = CachedCurrentCulture;
        }

        /// <summary>
        /// Sets a custom formatter to use when converting instances of a given type to a string.
        /// </summary>
        /// <typeparam name="T">The type for which to set the formatter.</typeparam>
        /// <param name="formatter">A delegate that will be called to format instances of the specified type.</param>
        public static void SetCustomFormatter<T>(Action<StringBuffer, T, StringView> formatter) {
            ValueHelper<T>.Formatter = formatter;
        }

        /// <summary>
        /// Clears the buffer.
        /// </summary>
        public void Clear () {
            currentCount = 0;
        }

        /// <summary>
        /// Copies the contents of the buffer to the given array.
        /// </summary>
        /// <param name="sourceIndex">The index within the buffer to begin copying.</param>
        /// <param name="destination">The destination array.</param>
        /// <param name="destinationIndex">The index within the destination array to which to begin copying.</param>
        /// <param name="count">The number of characters to copy.</param>
        public void CopyTo (int sourceIndex, char[] destination, int destinationIndex, int count) {
            if (destination == null)
                throw new ArgumentNullException(nameof(destination));
            if (destinationIndex + count > destination.Length || destinationIndex < 0)
                throw new ArgumentOutOfRangeException(nameof(destinationIndex));

            fixed (char* destPtr = &destination[destinationIndex])
                CopyTo(destPtr, sourceIndex, count);
        }

        /// <summary>
        /// Copies the contents of the buffer to the given array.
        /// </summary>
        /// <param name="dest">A pointer to the destination array.</param>
        /// <param name="sourceIndex">The index within the buffer to begin copying.</param>
        /// <param name="count">The number of characters to copy.</param>
        public void CopyTo (char* dest, int sourceIndex, int count) {
            if (count < 0)
                throw new ArgumentOutOfRangeException(nameof(count));
            if (sourceIndex + count > currentCount || sourceIndex < 0)
                throw new ArgumentOutOfRangeException(nameof(sourceIndex));

            fixed (char* s = buffer)
            {
                var src = s + sourceIndex;
                for (int i = 0; i < count; i++)
                    *dest++ = *src++;
            }
        }

        /// <summary>
        /// Copies the contents of the buffer to the given byte array.
        /// </summary>
        /// <param name="dest">A pointer to the destination byte array.</param>
        /// <param name="sourceIndex">The index within the buffer to begin copying.</param>
        /// <param name="count">The number of characters to copy.</param>
        /// <param name="encoding">The encoding to use to convert characters to bytes.</param>
        /// <returns>The number of bytes written to the destination.</returns>
        public int CopyTo (byte* dest, int sourceIndex, int count, Encoding encoding) {
            if (count < 0)
                throw new ArgumentOutOfRangeException(nameof(count));
            if (sourceIndex + count > currentCount || sourceIndex < 0)
                throw new ArgumentOutOfRangeException(nameof(sourceIndex));
            if (encoding == null)
                throw new ArgumentNullException(nameof(encoding));

            fixed (char* s = buffer)
                return encoding.GetBytes(s, count, dest, count);
        }

        /// <summary>
        /// Converts the buffer to a string instance.
        /// </summary>
        /// <returns>A new string representing the characters currently in the buffer.</returns>
        public override string ToString () {
            return new string(buffer, 0, currentCount);
        }

        /// <summary>
        /// Appends a character to the current buffer.
        /// </summary>
        /// <param name="c">The character to append.</param>
        public void Append (char c) {
            Append(c, 1);
        }

        /// <summary>
        /// Appends a character to the current buffer several times.
        /// </summary>
        /// <param name="c">The character to append.</param>
        /// <param name="count">The number of times to append the character.</param>
        public void Append (char c, int count) {
            if (count < 0)
                throw new ArgumentOutOfRangeException(nameof(count));

            CheckCapacity(count);
            fixed (char* b = &buffer[currentCount])
            {
                var ptr = b;
                for (int i = 0; i < count; i++)
                    *ptr++ = c;
                currentCount += count;
            }
        }

        /// <summary>
        /// Appends the specified string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        public void Append (string value) {
            if (value == null)
                throw new ArgumentNullException(nameof(value));

            Append(value, 0, value.Length);
        }

        /// <summary>
        /// Appends a string subset to the current buffer.
        /// </summary>
        /// <param name="value">The string to append.</param>
        /// <param name="startIndex">The starting index within the string to begin reading characters.</param>
        /// <param name="count">The number of characters to append.</param>
        public void Append (string value, int startIndex, int count) {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            if (startIndex < 0 || startIndex + count > value.Length)
                throw new ArgumentOutOfRangeException(nameof(startIndex));

            fixed (char* s = value)
                Append(s + startIndex, count);
        }

        /// <summary>
        /// Appends an array of characters to the current buffer.
        /// </summary>
        /// <param name="values">The characters to append.</param>
        /// <param name="startIndex">The starting index within the array to begin reading characters.</param>
        /// <param name="count">The number of characters to append.</param>
        public void Append (char[] values, int startIndex, int count) {
            if (values == null)
                throw new ArgumentNullException(nameof(values));
            if (startIndex < 0 || startIndex + count > values.Length)
                throw new ArgumentOutOfRangeException(nameof(startIndex));

            fixed (char* s = &values[startIndex])
                Append(s, count);
        }

        /// <summary>
        /// Appends an array of characters to the current buffer.
        /// </summary>
        /// <param name="str">A pointer to the array of characters to append.</param>
        /// <param name="count">The number of characters to append.</param>
        public void Append (char* str, int count) {
            CheckCapacity(count);
            fixed (char* b = &buffer[currentCount])
            {
                var dest = b;
                for (int i = 0; i < count; i++)
                    *dest++ = *str++;
                currentCount += count;
            }
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        public void Append (bool value) {
            if (value)
                Append(TrueLiteral);
            else
                Append(FalseLiteral);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (sbyte value, StringView format) {
            Numeric.FormatSByte(this, value, format, culture);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (byte value, StringView format) {
            // widening here is fine
            Numeric.FormatUInt32(this, value, format, culture);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (short value, StringView format) {
            Numeric.FormatInt16(this, value, format, culture);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (ushort value, StringView format) {
            // widening here is fine
            Numeric.FormatUInt32(this, value, format, culture);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (int value, StringView format) {
            Numeric.FormatInt32(this, value, format, culture);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (uint value, StringView format) {
            Numeric.FormatUInt32(this, value, format, culture);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (long value, StringView format) {
            Numeric.FormatInt64(this, value, format, culture);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (ulong value, StringView format) {
            Numeric.FormatUInt64(this, value, format, culture);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (float value, StringView format) {
            Numeric.FormatSingle(this, value, format, culture);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (double value, StringView format) {
            Numeric.FormatDouble(this, value, format, culture);
        }

        /// <summary>
        /// Appends the specified value as a string to the current buffer.
        /// </summary>
        /// <param name="value">The value to append.</param>
        /// <param name="format">A format specifier indicating how to convert <paramref name="value"/> to a string.</param>
        public void Append (decimal value, StringView format) {
            Numeric.FormatDecimal(this, (uint*)&value, format, culture);
        }

        /// <summary>
        /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance.
        /// Each format item is replaced by the string representation of a single argument.
        /// </summary>
        /// <typeparam name="T">The type of argument set being formatted.</typeparam>
        /// <param name="format">A composite format string.</param>
        /// <param name="args">The set of args to insert into the format string.</param>
        public void AppendArgSet<T>(string format, ref T args) where T : IArgSet {
            if (format == null)
                throw new ArgumentNullException(nameof(format));

            fixed (char* formatPtr = format)
            {
                var curr = formatPtr;
                var end = curr + format.Length;
                var segmentsLeft = false;
                var prevArgIndex = 0;
                do {
                    CheckCapacity((int)(end - curr) + 1);
                    fixed (char* bufferPtr = &buffer[currentCount])
                        segmentsLeft = AppendSegment(ref curr, end, bufferPtr, ref prevArgIndex, ref args);
                }
                while (segmentsLeft);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        void CheckCapacity (int count) {
			int newCount = currentCount + count;
			if (newCount > buffer.Length) {
				int newCapacity = buffer.Length * 2;
				if (newCapacity < newCount) newCapacity = newCount;
				char[] newBuffer = new char[newCapacity];
				fixed (void* nptr = newBuffer, ptr = buffer) {
					Unsafe.CopyBlock(nptr, ptr, (uint)(currentCount * sizeof(char)));
				}
				buffer = newBuffer;
			}
        }

        bool AppendSegment<T>(ref char* currRef, char* end, char* dest, ref int prevArgIndex, ref T args) where T : IArgSet {
            char* curr = currRef;
            char c = '\x0';
            while (curr < end) {
                c = *curr++;
                if (c == '}') {
                    // check for escape character for }}
                    if (curr < end && *curr == '}')
                        curr++;
                    else
                        ThrowError();
                }
                else if (c == '{') {
                    // check for escape character for {{
                    if (curr == end)
                        ThrowError();
                    else if (*curr == '{')
                        curr++;
                    else
                        break;
                }

                *dest++ = c;
                currentCount++;
            }

            if (curr == end)
                return false;

            int index;
            if (*curr == '}')
                index = prevArgIndex;
            else
                index = ParseNum(ref curr, end, MaxArgs);
            if (index >= args.Count)
                throw new FormatException(string.Format(SR.ArgIndexOutOfRange, index));

            // check for a spacing specifier
            c = SkipWhitespace(ref curr, end);
            var width = 0;
            var leftJustify = false;
            var oldCount = currentCount;
            if (c == ',') {
                curr++;
                c = SkipWhitespace(ref curr, end);

                // spacing can be left-justified
                if (c == '-') {
                    leftJustify = true;
                    curr++;
                    if (curr == end)
                        ThrowError();
                }

                width = ParseNum(ref curr, end, MaxSpacing);
                c = SkipWhitespace(ref curr, end);
            }

            // check for format specifier
            curr++;
            if (c == ':') {
                var specifierBuffer = stackalloc char[MaxSpecifierSize];
                var specifierEnd = specifierBuffer + MaxSpecifierSize;
                var specifierPtr = specifierBuffer;

                while (true) {
                    if (curr == end)
                        ThrowError();

                    c = *curr++;
                    if (c == '{') {
                        // check for escape character for {{
                        if (curr < end && *curr == '{')
                            curr++;
                        else
                            ThrowError();
                    }
                    else if (c == '}') {
                        // check for escape character for }}
                        if (curr < end && *curr == '}')
                            curr++;
                        else {
                            // found the end of the specifier
                            // kick off the format job
                            var specifier = new StringView(specifierBuffer, (int)(specifierPtr - specifierBuffer));
                            args.Format(this, index, specifier);
                            break;
                        }
                    }

                    if (specifierPtr == specifierEnd)
                        ThrowError();
                    *specifierPtr++ = c;
                }
            }
            else {
                // no specifier. make sure we're at the end of the format block
                if (c != '}')
                    ThrowError();

                // format without any specifier
                args.Format(this, index, StringView.Empty);
            }

            // finish off padding, if necessary
            var padding = width - (currentCount - oldCount);
            if (padding > 0) {
                if (leftJustify)
                    Append(' ', padding);
                else {
                    // copy the recently placed chars up in memory to make room for padding
                    CheckCapacity(padding);
                    for (int i = currentCount - 1; i >= oldCount; i--)
                        buffer[i + padding] = buffer[i];

                    // fill in padding
                    for (int i = 0; i < padding; i++)
                        buffer[i + oldCount] = ' ';
                    currentCount += padding;
                }
            }

            prevArgIndex = index + 1;
            currRef = curr;
            return true;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal void AppendGeneric<T>(T value, StringView format) {
			// this looks gross, but T is known at JIT-time so this call tree
			// gets compiled down to a direct call with no branching
			if (typeof(T) == typeof(sbyte))
				Append(*(sbyte*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(byte))
				Append(*(byte*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(short))
				Append(*(short*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(ushort))
				Append(*(ushort*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(int))
				Append(*(int*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(uint))
				Append(*(uint*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(long))
				Append(*(long*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(ulong))
				Append(*(ulong*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(float))
				Append(*(float*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(double))
				Append(*(double*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(decimal))
				Append(*(decimal*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(bool))
				Append(*(bool*)Unsafe.AsPointer(ref value));
			else if (typeof(T) == typeof(char))
				Append(*(char*)Unsafe.AsPointer(ref value), format);
			else if (typeof(T) == typeof(string))
				Append(Unsafe.As<string>(value));
			else {
                // first, check to see if it's a value type implementing IStringFormattable
                var formatter = ValueHelper<T>.Formatter;
                if (formatter != null)
                    formatter(this, value, format);
                else {
                    // We could handle this case by calling ToString() on the object and paying the
                    // allocation, but presumably if the user is using us instead of the built-in
                    // formatting utilities they would rather be notified of this case, so we'll throw.
                    throw new InvalidOperationException(string.Format(SR.TypeNotFormattable, typeof(T)));
                }
            }
        }

        static int ParseNum (ref char* currRef, char* end, int maxValue) {
            char* curr = currRef;
            char c = *curr;
            if (c < '0' || c > '9')
                ThrowError();

            int value = 0;
            do {
                value = value * 10 + c - '0';
                curr++;
                if (curr == end)
                    ThrowError();
                c = *curr;
            } while (c >= '0' && c <= '9' && value < maxValue);

            currRef = curr;
            return value;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        static char SkipWhitespace (ref char* currRef, char* end) {
            char* curr = currRef;
            while (curr < end && *curr == ' ') curr++;

            if (curr == end)
                ThrowError();

            currRef = curr;
            return *curr;
        }

        static void ThrowError () {
            throw new FormatException(SR.InvalidFormatString);
        }

        static StringBuffer Acquire (int capacity) {
            if (capacity <= MaxCachedSize) {
                var buffer = CachedInstance;
                if (buffer != null) {
                    CachedInstance = null;
                    buffer.Clear();
                    buffer.CheckCapacity(capacity);
                    return buffer;
                }
            }

            return new StringBuffer(capacity);
        }

        static void Release (StringBuffer buffer) {
            if (buffer.buffer.Length <= MaxCachedSize)
                CachedInstance = buffer;
        }

        [ThreadStatic]
        static StringBuffer CachedInstance;

        static readonly CachedCulture CachedInvariantCulture = new CachedCulture(CultureInfo.InvariantCulture);
        static readonly CachedCulture CachedCurrentCulture = new CachedCulture(CultureInfo.CurrentCulture);

        const int DefaultCapacity = 32;
        const int MaxCachedSize = 360;  // same as BCL's StringBuilderCache
        const int MaxArgs = 256;
        const int MaxSpacing = 1000000;
        const int MaxSpecifierSize = 32;

        const string TrueLiteral = "True";
        const string FalseLiteral = "False";

        // The point of this class is to allow us to generate a direct call to a known
        // method on an unknown, unconstrained generic value type. Normally this would
        // be impossible; you'd have to cast the generic argument and introduce boxing.
        // Instead we pay a one-time startup cost to create a delegate that will forward
        // the parameter to the appropriate method in a strongly typed fashion.
        static class ValueHelper<T> {
            public static Action<StringBuffer, T, StringView> Formatter = Prepare();

            static Action<StringBuffer, T, StringView> Prepare () {
                // we only use this class for value types that also implement IStringFormattable
                var type = typeof(T);
                if (!typeof(IStringFormattable).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()))
                    return null;

                var result = typeof(ValueHelper<T>)
                    .GetTypeInfo()
                    .GetDeclaredMethod("Assign")
                    .MakeGenericMethod(type)
                    .Invoke(null, null);
                return (Action<StringBuffer, T, StringView>)result;
            }

            public static Action<StringBuffer, U, StringView> Assign<U>() where U : IStringFormattable {
                return (f, u, v) => u.Format(f, v);
            }
        }
    }

    // TODO: clean this up
    public unsafe struct StringView {
        public static readonly StringView Empty = new StringView();

        public readonly char* Data;
        public readonly int Length;

        public bool IsEmpty {
            get { return Length == 0; }
        }

        public StringView (char* data, int length) {
            Data = data;
            Length = length;
        }

        public static bool operator ==(StringView lhs, string rhs) {
            var count = lhs.Length;
            if (count != rhs.Length)
                return false;

            fixed (char* r = rhs)
            {
                var lhsPtr = lhs.Data;
                var rhsPtr = r;
                for (int i = 0; i < count; i++) {
                    if (*lhsPtr++ != *rhsPtr++)
                        return false;
                }
            }

            return true;
        }

        public static bool operator !=(StringView lhs, string rhs) {
            return !(lhs == rhs);
        }
    }

    // caches formatting information from culture data
    // some of the accessors on NumberFormatInfo allocate copies of their data
    sealed class CachedCulture {
        public readonly CultureInfo Culture;

        public readonly NumberFormatData CurrencyData;
        public readonly NumberFormatData FixedData;
        public readonly NumberFormatData NumberData;
        public readonly NumberFormatData ScientificData;
        public readonly NumberFormatData PercentData;

        public readonly string CurrencyNegativePattern;
        public readonly string CurrencyPositivePattern;
        public readonly string CurrencySymbol;

        public readonly string NumberNegativePattern;
        public readonly string NumberPositivePattern;

        public readonly string PercentNegativePattern;
        public readonly string PercentPositivePattern;
        public readonly string PercentSymbol;
		public readonly string PerMilleSymbol;

		public readonly string NegativeSign;
        public readonly string PositiveSign;

        public readonly string NaN;
        public readonly string PositiveInfinity;
        public readonly string NegativeInfinity;

        public readonly int DecimalBufferSize;

        public CachedCulture (CultureInfo culture) {
            Culture = culture;

            var info = culture.NumberFormat;
            CurrencyData = new NumberFormatData(
                info.CurrencyDecimalDigits,
                info.NegativeSign,
                info.CurrencyDecimalSeparator,
                info.CurrencyGroupSeparator,
                info.CurrencyGroupSizes,
                info.CurrencySymbol.Length
            );

            FixedData = new NumberFormatData(
                info.NumberDecimalDigits,
                info.NegativeSign,
                info.NumberDecimalSeparator,
                null,
                null,
                0
            );

            NumberData = new NumberFormatData(
                info.NumberDecimalDigits,
                info.NegativeSign,
                info.NumberDecimalSeparator,
                info.NumberGroupSeparator,
                info.NumberGroupSizes,
                0
            );

            ScientificData = new NumberFormatData(
                6,
                info.NegativeSign,
                info.NumberDecimalSeparator,
                null,
                null,
                info.NegativeSign.Length + info.PositiveSign.Length * 2 // for number and exponent
            );

            PercentData = new NumberFormatData(
                info.PercentDecimalDigits,
                info.NegativeSign,
                info.PercentDecimalSeparator,
                info.PercentGroupSeparator,
                info.PercentGroupSizes,
                info.PercentSymbol.Length
            );

            CurrencyNegativePattern = NegativeCurrencyFormats[info.CurrencyNegativePattern];
            CurrencyPositivePattern = PositiveCurrencyFormats[info.CurrencyPositivePattern];
            CurrencySymbol = info.CurrencySymbol;
            NumberNegativePattern = NegativeNumberFormats[info.NumberNegativePattern];
            NumberPositivePattern = PositiveNumberFormat;
            PercentNegativePattern = NegativePercentFormats[info.PercentNegativePattern];
            PercentPositivePattern = PositivePercentFormats[info.PercentPositivePattern];
            PercentSymbol = info.PercentSymbol;
			PerMilleSymbol = info.PerMilleSymbol;
            NegativeSign = info.NegativeSign;
            PositiveSign = info.PositiveSign;
            NaN = info.NaNSymbol;
            PositiveInfinity = info.PositiveInfinitySymbol;
            NegativeInfinity = info.NegativeInfinitySymbol;
            DecimalBufferSize =
                NumberFormatData.MinBufferSize +
                info.NumberDecimalSeparator.Length +
                (NegativeSign.Length + PositiveSign.Length) * 2;
        }

        static readonly string[] PositiveCurrencyFormats = {
            "$#", "#$", "$ #", "# $"
        };

        static readonly string[] NegativeCurrencyFormats = {
            "($#)", "-$#", "$-#", "$#-",
            "(#$)", "-#$", "#-$", "#$-",
            "-# $", "-$ #", "# $-", "$ #-",
            "$ -#", "#- $", "($ #)", "(# $)"
        };

        static readonly string[] PositivePercentFormats = {
            "# %", "#%", "%#", "% #"
        };

        static readonly string[] NegativePercentFormats = {
            "-# %", "-#%", "-%#",
            "%-#", "%#-",
            "#-%", "#%-",
            "-% #", "# %-", "% #-",
            "% -#", "#- %"
        };

        static readonly string[] NegativeNumberFormats = {
            "(#)", "-#", "- #", "#-", "# -",
        };

        static readonly string PositiveNumberFormat = "#";
    }

    // contains format information for a specific kind of format string
    // e.g. (fixed, number, currency)
    sealed class NumberFormatData {
        readonly int bufferLength;
        readonly int perDigitLength;

        public readonly int DecimalDigits;
        public readonly string NegativeSign;
        public readonly string DecimalSeparator;
        public readonly string GroupSeparator;
        public readonly int[] GroupSizes;

        public NumberFormatData (int decimalDigits, string negativeSign, string decimalSeparator, string groupSeparator, int[] groupSizes, int extra) {
            DecimalDigits = decimalDigits;
            NegativeSign = negativeSign;
            DecimalSeparator = decimalSeparator;
            GroupSeparator = groupSeparator;
            GroupSizes = groupSizes;

            bufferLength = MinBufferSize;
            bufferLength += NegativeSign.Length;
            bufferLength += DecimalSeparator.Length;
            bufferLength += extra;

            if (GroupSeparator != null)
                perDigitLength = GroupSeparator.Length;
        }

        public int GetBufferSize (ref int maxDigits, int scale) {
            if (maxDigits < 0)
                maxDigits = DecimalDigits;

            var digitCount = scale >= 0 ? scale + maxDigits : 0;
            long len = bufferLength;

            // calculate buffer size
            len += digitCount;
            len += perDigitLength * digitCount;
            return checked((int)len);
        }

        internal const int MinBufferSize = 105;
    }

    // Most of the implementation of this file was ported from the native versions built into the CLR
    // See: https://github.com/dotnet/coreclr/blob/838807429a0828a839958e3b7d392d65886c8f2e/src/classlibnative/bcltype/number.cpp
    // Also see: https://github.com/dotnet/coreclr/blob/02084af832c2900cf6eac2a168c41f261409be97/src/mscorlib/src/System/Number.cs
    // Standard numeric format string reference: https://msdn.microsoft.com/en-us/library/dwhawy9k%28v=vs.110%29.aspx

    unsafe static partial class Numeric {
        public static void FormatSByte (StringBuffer formatter, sbyte value, StringView specifier, CachedCulture culture) {
            if (value < 0 && !specifier.IsEmpty) {
                // if we're negative and doing a hex format, mask out the bits for the conversion
                char c = specifier.Data[0];
                if (c == 'X' || c == 'x') {
                    FormatUInt32(formatter, (uint)(value & 0xFF), specifier, culture);
                    return;
                }
            }

            FormatInt32(formatter, value, specifier, culture);
        }

        public static void FormatInt16 (StringBuffer formatter, short value, StringView specifier, CachedCulture culture) {
            if (value < 0 && !specifier.IsEmpty) {
                // if we're negative and doing a hex format, mask out the bits for the conversion
                char c = specifier.Data[0];
                if (c == 'X' || c == 'x') {
                    FormatUInt32(formatter, (uint)(value & 0xFFFF), specifier, culture);
                    return;
                }
            }

            FormatInt32(formatter, value, specifier, culture);
        }

        public static void FormatInt32 (StringBuffer formatter, int value, StringView specifier, CachedCulture culture) {
            int digits;
            var fmt = ParseFormatSpecifier(specifier, out digits);

            // ANDing with 0xFFDF has the effect of uppercasing the character
            switch (fmt & 0xFFDF) {
                case 'G':
                    if (digits > 0)
                        goto default;
                    else
                        goto case 'D';

                case 'D':
                    Int32ToDecStr(formatter, value, digits, culture.NegativeSign);
                    break;

                case 'X':
                    // fmt-('X'-'A'+1) gives us the base hex character in either
                    // uppercase or lowercase, depending on the casing of fmt
                    Int32ToHexStr(formatter, (uint)value, fmt - ('X' - 'A' + 10), digits);
                    break;

                default:
                    var number = new Number();
                    var buffer = stackalloc char[MaxNumberDigits + 1];
                    number.Digits = buffer;
                    Int32ToNumber(value, ref number);
                    if (fmt != 0)
                        NumberToString(formatter, ref number, fmt, digits, culture);
                    else
                        NumberToCustomFormatString(formatter, ref number, specifier, culture);
                    break;
            }
        }

        public static void FormatUInt32 (StringBuffer formatter, uint value, StringView specifier, CachedCulture culture) {
            int digits;
            var fmt = ParseFormatSpecifier(specifier, out digits);

            // ANDing with 0xFFDF has the effect of uppercasing the character
            switch (fmt & 0xFFDF) {
                case 'G':
                    if (digits > 0)
                        goto default;
                    else
                        goto case 'D';

                case 'D':
                    UInt32ToDecStr(formatter, value, digits);
                    break;

                case 'X':
                    // fmt-('X'-'A'+1) gives us the base hex character in either
                    // uppercase or lowercase, depending on the casing of fmt
                    Int32ToHexStr(formatter, value, fmt - ('X' - 'A' + 10), digits);
                    break;

                default:
                    var number = new Number();
                    var buffer = stackalloc char[MaxNumberDigits + 1];
                    number.Digits = buffer;
                    UInt32ToNumber(value, ref number);
                    if (fmt != 0)
                        NumberToString(formatter, ref number, fmt, digits, culture);
                    else
                        NumberToCustomFormatString(formatter, ref number, specifier, culture);
                    break;
            }
        }

        public static void FormatInt64 (StringBuffer formatter, long value, StringView specifier, CachedCulture culture) {
            int digits;
            var fmt = ParseFormatSpecifier(specifier, out digits);

            // ANDing with 0xFFDF has the effect of uppercasing the character
            switch (fmt & 0xFFDF) {
                case 'G':
                    if (digits > 0)
                        goto default;
                    else
                        goto case 'D';

                case 'D':
                    Int64ToDecStr(formatter, value, digits, culture.NegativeSign);
                    break;

                case 'X':
                    // fmt-('X'-'A'+1) gives us the base hex character in either
                    // uppercase or lowercase, depending on the casing of fmt
                    Int64ToHexStr(formatter, (ulong)value, fmt - ('X' - 'A' + 10), digits);
                    break;

                default:
                    var number = new Number();
                    var buffer = stackalloc char[MaxNumberDigits + 1];
                    number.Digits = buffer;
                    Int64ToNumber(value, ref number);
                    if (fmt != 0)
                        NumberToString(formatter, ref number, fmt, digits, culture);
                    else
                        NumberToCustomFormatString(formatter, ref number, specifier, culture);
                    break;
            }
        }

        public static void FormatUInt64 (StringBuffer formatter, ulong value, StringView specifier, CachedCulture culture) {
            int digits;
            var fmt = ParseFormatSpecifier(specifier, out digits);

            // ANDing with 0xFFDF has the effect of uppercasing the character
            switch (fmt & 0xFFDF) {
                case 'G':
                    if (digits > 0)
                        goto default;
                    else
                        goto case 'D';

                case 'D':
                    UInt64ToDecStr(formatter, value, digits);
                    break;

                case 'X':
                    // fmt-('X'-'A'+1) gives us the base hex character in either
                    // uppercase or lowercase, depending on the casing of fmt
                    Int64ToHexStr(formatter, value, fmt - ('X' - 'A' + 10), digits);
                    break;

                default:
                    var number = new Number();
                    var buffer = stackalloc char[MaxNumberDigits + 1];
                    number.Digits = buffer;
                    UInt64ToNumber(value, ref number);
                    if (fmt != 0)
                        NumberToString(formatter, ref number, fmt, digits, culture);
                    else
                        NumberToCustomFormatString(formatter, ref number, specifier, culture);
                    break;
            }
        }

        public static void FormatSingle (StringBuffer formatter, float value, StringView specifier, CachedCulture culture) {
            int digits;
            int precision = FloatPrecision;
            var fmt = ParseFormatSpecifier(specifier, out digits);

            // ANDing with 0xFFDF has the effect of uppercasing the character
            switch (fmt & 0xFFDF) {
                case 'G':
                    if (digits > 7)
                        precision = 9;
                    break;

                case 'E':
                    if (digits > 6)
                        precision = 9;
                    break;
            }

            var number = new Number();
            var buffer = stackalloc char[MaxFloatingDigits + 1];
            number.Digits = buffer;
            DoubleToNumber(value, precision, ref number);

            if (number.Scale == ScaleNaN) {
                formatter.Append(culture.NaN);
                return;
            }

            if (number.Scale == ScaleInf) {
                if (number.Sign > 0)
                    formatter.Append(culture.NegativeInfinity);
                else
                    formatter.Append(culture.PositiveInfinity);
                return;
            }

            if (fmt != 0)
                NumberToString(formatter, ref number, fmt, digits, culture);
            else
                NumberToCustomFormatString(formatter, ref number, specifier, culture);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void FormatDouble (StringBuffer formatter, double value, StringView specifier, CachedCulture culture) {
            int digits;
            int precision = DoublePrecision;
            var fmt = ParseFormatSpecifier(specifier, out digits);

            // ANDing with 0xFFDF has the effect of uppercasing the character
            switch (fmt & 0xFFDF) {
                case 'G':
                    if (digits > 15)
                        precision = 17;
                    break;

                case 'E':
                    if (digits > 14)
                        precision = 17;
                    break;
            }

            var number = new Number();
            var buffer = stackalloc char[MaxFloatingDigits + 1];
            number.Digits = buffer;
            DoubleToNumber(value, precision, ref number);

            if (number.Scale == ScaleNaN) {
                formatter.Append(culture.NaN);
                return;
            }

            if (number.Scale == ScaleInf) {
                if (number.Sign > 0)
                    formatter.Append(culture.NegativeInfinity);
                else
                    formatter.Append(culture.PositiveInfinity);
                return;
            }

            if (fmt != 0)
                NumberToString(formatter, ref number, fmt, digits, culture);
            else
                NumberToCustomFormatString(formatter, ref number, specifier, culture);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void FormatDecimal (StringBuffer formatter, uint* value, StringView specifier, CachedCulture culture) {
            int digits;
            var fmt = ParseFormatSpecifier(specifier, out digits);

            var number = new Number();
            var buffer = stackalloc char[MaxNumberDigits + 1];
            number.Digits = buffer;
            DecimalToNumber(value, ref number);
            if (fmt != 0)
                NumberToString(formatter, ref number, fmt, digits, culture, isDecimal: true);
            else
                NumberToCustomFormatString(formatter, ref number, specifier, culture);
        }

        static void NumberToString (StringBuffer formatter, ref Number number, char format, int maxDigits, CachedCulture culture, bool isDecimal = false) {
            // ANDing with 0xFFDF has the effect of uppercasing the character
            switch (format & 0xFFDF) {
                case 'C':
                    {
                        var cultureData = culture.CurrencyData;
                        var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale);
                        RoundNumber(ref number, number.Scale + maxDigits);

                        var buffer = stackalloc char[bufferSize];
                        var ptr = FormatCurrency(
                            buffer,
                            ref number,
                            maxDigits,
                            cultureData,
                            number.Sign > 0 ? culture.CurrencyNegativePattern : culture.CurrencyPositivePattern,
                            culture.CurrencySymbol
                        );

                        formatter.Append(buffer, (int)(ptr - buffer));
                        break;
                    }

                case 'F':
                    {
                        var cultureData = culture.FixedData;
                        var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale);
                        RoundNumber(ref number, number.Scale + maxDigits);

                        var buffer = stackalloc char[bufferSize];
                        var ptr = buffer;
                        if (number.Sign > 0)
                            AppendString(&ptr, cultureData.NegativeSign);

                        ptr = FormatFixed(ptr, ref number, maxDigits, cultureData);
                        formatter.Append(buffer, (int)(ptr - buffer));
                        break;
                    }

                case 'N':
                    {
                        var cultureData = culture.NumberData;
                        var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale);
                        RoundNumber(ref number, number.Scale + maxDigits);

                        var buffer = stackalloc char[bufferSize];
                        var ptr = FormatNumber(
                            buffer,
                            ref number,
                            maxDigits,
                            number.Sign > 0 ? culture.NumberNegativePattern : culture.NumberPositivePattern,
                            cultureData
                        );

                        formatter.Append(buffer, (int)(ptr - buffer));
                        break;
                    }

                case 'E':
                    {
                        var cultureData = culture.ScientificData;
                        var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale);
                        maxDigits++;

                        RoundNumber(ref number, maxDigits);

                        var buffer = stackalloc char[bufferSize];
                        var ptr = buffer;
                        if (number.Sign > 0)
                            AppendString(&ptr, cultureData.NegativeSign);

                        ptr = FormatScientific(
                            ptr,
                            ref number,
                            maxDigits,
                            format, // TODO: fix casing
                            cultureData.DecimalSeparator,
                            culture.PositiveSign,
                            culture.NegativeSign
                        );

                        formatter.Append(buffer, (int)(ptr - buffer));
                        break;
                    }

                case 'P':
                    {
                        number.Scale += 2;
                        var cultureData = culture.PercentData;
                        var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale);
                        RoundNumber(ref number, number.Scale + maxDigits);

                        var buffer = stackalloc char[bufferSize];
                        var ptr = FormatPercent(
                            buffer,
                            ref number,
                            maxDigits,
                            cultureData,
                            number.Sign > 0 ? culture.PercentNegativePattern : culture.PercentPositivePattern,
                            culture.PercentSymbol
                        );

                        formatter.Append(buffer, (int)(ptr - buffer));
                        break;
                    }

                case 'G':
                    {
                        var enableRounding = true;
                        if (maxDigits < 1) {
                            if (isDecimal && maxDigits == -1) {
                                // if we're formatting a decimal, default to 29 digits precision
                                // only for G formatting without a precision specifier
                                maxDigits = DecimalPrecision;
                                enableRounding = false;
                            }
                            else
                                maxDigits = number.Precision;
                        }

                        var bufferSize = maxDigits + culture.DecimalBufferSize;
                        var buffer = stackalloc char[bufferSize];
                        var ptr = buffer;

                        // round for G formatting only if a precision is given
                        // we need to handle the minus zero case also
                        if (enableRounding)
                            RoundNumber(ref number, maxDigits);
                        else if (isDecimal && number.Digits[0] == 0)
                            number.Sign = 0;

                        if (number.Sign > 0)
                            AppendString(&ptr, culture.NegativeSign);

                        ptr = FormatGeneral(
                            ptr,
                            ref number,
                            maxDigits,
                            (char)(format - ('G' - 'E')),
                            culture.NumberData.DecimalSeparator,
                            culture.PositiveSign,
                            culture.NegativeSign,
                            !enableRounding
                        );

                        formatter.Append(buffer, (int)(ptr - buffer));
                        break;
                    }

                default:
                    throw new FormatException(string.Format(SR.UnknownFormatSpecifier, format));
            }
        }

        static char* FormatCurrency (char* buffer, ref Number number, int maxDigits, NumberFormatData data, string currencyFormat, string currencySymbol) {
            for (int i = 0; i < currencyFormat.Length; i++) {
                char c = currencyFormat[i];
                switch (c) {
                    case '#': buffer = FormatFixed(buffer, ref number, maxDigits, data); break;
                    case '-': AppendString(&buffer, data.NegativeSign); break;
                    case '$': AppendString(&buffer, currencySymbol); break;
                    default: *buffer++ = c; break;
                }
            }

            return buffer;
        }

        static char* FormatNumber (char* buffer, ref Number number, int maxDigits, string format, NumberFormatData data) {
            for (int i = 0; i < format.Length; i++) {
                char c = format[i];
                switch (c) {
                    case '#': buffer = FormatFixed(buffer, ref number, maxDigits, data); break;
                    case '-': AppendString(&buffer, data.NegativeSign); break;
                    default: *buffer++ = c; break;
                }
            }

            return buffer;
        }

        static char* FormatPercent (char* buffer, ref Number number, int maxDigits, NumberFormatData data, string format, string percentSymbol) {
            for (int i = 0; i < format.Length; i++) {
                char c = format[i];
                switch (c) {
                    case '#': buffer = FormatFixed(buffer, ref number, maxDigits, data); break;
                    case '-': AppendString(&buffer, data.NegativeSign); break;
                    case '%': AppendString(&buffer, percentSymbol); break;
                    default: *buffer++ = c; break;
                }
            }

            return buffer;
        }

        static char* FormatGeneral (
            char* buffer, ref Number number, int maxDigits, char expChar,
            string decimalSeparator, string positiveSign, string negativeSign,
            bool suppressScientific) {

            var digitPos = number.Scale;
            var scientific = false;
            if (!suppressScientific) {
                if (digitPos > maxDigits || digitPos < -3) {
                    digitPos = 1;
                    scientific = true;
                }
            }

            var digits = number.Digits;
            if (digitPos <= 0)
                *buffer++ = '0';
            else {
                do {
                    *buffer++ = *digits != 0 ? *digits++ : '0';
                } while (--digitPos > 0);
            }

            if (*digits != 0 || digitPos < 0) {
                AppendString(&buffer, decimalSeparator);
                while (digitPos < 0) {
                    *buffer++ = '0';
                    digitPos++;
                }

                while (*digits != 0)
                    *buffer++ = *digits++;
            }

            if (scientific)
                buffer = FormatExponent(buffer, number.Scale - 1, expChar, positiveSign, negativeSign, 2);

            return buffer;
        }

        static char* FormatScientific (
            char* buffer, ref Number number, int maxDigits, char expChar,
            string decimalSeparator, string positiveSign, string negativeSign) {

            var digits = number.Digits;
            *buffer++ = *digits != 0 ? *digits++ : '0';
            if (maxDigits != 1)
                AppendString(&buffer, decimalSeparator);

            while (--maxDigits > 0)
                *buffer++ = *digits != 0 ? *digits++ : '0';

            int e = number.Digits[0] == 0 ? 0 : number.Scale - 1;
            return FormatExponent(buffer, e, expChar, positiveSign, negativeSign, 3);
        }

        static char* FormatExponent (char* buffer, int value, char expChar, string positiveSign, string negativeSign, int minDigits) {
            *buffer++ = expChar;
            if (value < 0) {
                AppendString(&buffer, negativeSign);
                value = -value;
            }
            else if (positiveSign != null)
                AppendString(&buffer, positiveSign);

            var digits = stackalloc char[11];
            var ptr = Int32ToDecChars(digits + 10, (uint)value, minDigits);
            var len = (int)(digits + 10 - ptr);
            while (--len >= 0)
                *buffer++ = *ptr++;

            return buffer;
        }

        static char* FormatFixed (char* buffer, ref Number number, int maxDigits, NumberFormatData data) {
            var groups = data.GroupSizes;
            var digits = number.Digits;
            var digitPos = number.Scale;
            if (digitPos <= 0)
                *buffer++ = '0';
            else if (groups != null) {
                var groupIndex = 0;
                var groupSizeCount = groups[0];
                var groupSizeLen = groups.Length;
                var newBufferSize = digitPos;
                var groupSeparatorLen = data.GroupSeparator.Length;
                var groupSize = 0;

                // figure out the size of the result
                if (groupSizeLen != 0) {
                    while (digitPos > groupSizeCount) {
                        groupSize = groups[groupIndex];
                        if (groupSize == 0)
                            break;

                        newBufferSize += groupSeparatorLen;
                        if (groupIndex < groupSizeLen - 1)
                            groupIndex++;

                        groupSizeCount += groups[groupIndex];
                        if (groupSizeCount < 0 || newBufferSize < 0)
                            throw new ArgumentOutOfRangeException(SR.InvalidGroupSizes);
                    }

                    if (groupSizeCount == 0)
                        groupSize = 0;
                    else
                        groupSize = groups[0];
                }

                groupIndex = 0;
                var digitCount = 0;
                var digitLength = StrLen(digits);
                var digitStart = digitPos < digitLength ? digitPos : digitLength;
                var ptr = buffer + newBufferSize - 1;

                for (int i = digitPos - 1; i >= 0; i--) {
                    *(ptr--) = i < digitStart ? digits[i] : '0';

                    // check if we need to add a group separator
                    if (groupSize > 0) {
                        digitCount++;
                        if (digitCount == groupSize && i != 0) {
                            for (int j = groupSeparatorLen - 1; j >= 0; j--)
                                *(ptr--) = data.GroupSeparator[j];

                            if (groupIndex < groupSizeLen - 1) {
                                groupIndex++;
                                groupSize = groups[groupIndex];
                            }
                            digitCount = 0;
                        }
                    }
                }

                buffer += newBufferSize;
                digits += digitStart;
            }
            else {
                do {
                    *buffer++ = *digits != 0 ? *digits++ : '0';
                }
                while (--digitPos > 0);
            }

            if (maxDigits > 0) {
                AppendString(&buffer, data.DecimalSeparator);
                while (digitPos < 0 && maxDigits > 0) {
                    *buffer++ = '0';
                    digitPos++;
                    maxDigits--;
                }

                while (maxDigits > 0) {
                    *buffer++ = *digits != 0 ? *digits++ : '0';
                    maxDigits--;
                }
            }

            return buffer;
        }

        static void Int32ToDecStr (StringBuffer formatter, int value, int digits, string negativeSign) {
            if (digits < 1)
                digits = 1;

            var maxDigits = digits > 15 ? digits : 15;
            var bufferLength = maxDigits > 100 ? maxDigits : 100;
            var negativeLength = 0;

            if (value < 0) {
                negativeLength = negativeSign.Length;
                if (negativeLength > bufferLength - maxDigits)
                    bufferLength = negativeLength + maxDigits;
            }

            var buffer = stackalloc char[bufferLength];
            var p = Int32ToDecChars(buffer + bufferLength, value >= 0 ? (uint)value : (uint)-value, digits);
            if (value < 0) {
                // add the negative sign
                for (int i = negativeLength - 1; i >= 0; i--)
                    *(--p) = negativeSign[i];
            }

            formatter.Append(p, (int)(buffer + bufferLength - p));
        }

        static void UInt32ToDecStr (StringBuffer formatter, uint value, int digits) {
            var buffer = stackalloc char[100];
            if (digits < 1)
                digits = 1;

            var p = Int32ToDecChars(buffer + 100, value, digits);
            formatter.Append(p, (int)(buffer + 100 - p));
        }

        static void Int32ToHexStr (StringBuffer formatter, uint value, int hexBase, int digits) {
            var buffer = stackalloc char[100];
            if (digits < 1)
                digits = 1;

            var p = Int32ToHexChars(buffer + 100, value, hexBase, digits);
            formatter.Append(p, (int)(buffer + 100 - p));
        }

        static void Int64ToDecStr (StringBuffer formatter, long value, int digits, string negativeSign) {
            if (digits < 1)
                digits = 1;

            var sign = (int)High32((ulong)value);
            var maxDigits = digits > 20 ? digits : 20;
            var bufferLength = maxDigits > 100 ? maxDigits : 100;

            if (sign < 0) {
                value = -value;
                var negativeLength = negativeSign.Length;
                if (negativeLength > bufferLength - maxDigits)
                    bufferLength = negativeLength + maxDigits;
            }

            var buffer = stackalloc char[bufferLength];
            var p = buffer + bufferLength;
            var uv = (ulong)value;
            while (High32(uv) != 0) {
                p = Int32ToDecChars(p, Int64DivMod(ref uv), 9);
                digits -= 9;
            }

            p = Int32ToDecChars(p, Low32(uv), digits);
            if (sign < 0) {
                // add the negative sign
                for (int i = negativeSign.Length - 1; i >= 0; i--)
                    *(--p) = negativeSign[i];
            }

            formatter.Append(p, (int)(buffer + bufferLength - p));
        }

        static void UInt64ToDecStr (StringBuffer formatter, ulong value, int digits) {
            if (digits < 1)
                digits = 1;

            var buffer = stackalloc char[100];
            var p = buffer + 100;
            while (High32(value) != 0) {
                p = Int32ToDecChars(p, Int64DivMod(ref value), 9);
                digits -= 9;
            }

            p = Int32ToDecChars(p, Low32(value), digits);
            formatter.Append(p, (int)(buffer + 100 - p));
        }

        static void Int64ToHexStr (StringBuffer formatter, ulong value, int hexBase, int digits) {
            var buffer = stackalloc char[100];
            char* ptr;
            if (High32(value) != 0) {
                Int32ToHexChars(buffer + 100, Low32(value), hexBase, 8);
                ptr = Int32ToHexChars(buffer + 100 - 8, High32(value), hexBase, digits - 8);
            }
            else {
                if (digits < 1)
                    digits = 1;
                ptr = Int32ToHexChars(buffer + 100, Low32(value), hexBase, digits);
            }

            formatter.Append(ptr, (int)(buffer + 100 - ptr));
        }

        static char* Int32ToDecChars (char* p, uint value, int digits) {
            while (value != 0) {
                *--p = (char)(value % 10 + '0');
                value /= 10;
                digits--;
            }

            while (--digits >= 0)
                *--p = '0';
            return p;
        }

        static char* Int32ToHexChars (char* p, uint value, int hexBase, int digits) {
            while (--digits >= 0 || value != 0) {
                var digit = value & 0xF;
                *--p = (char)(digit + (digit < 10 ? '0' : hexBase));
                value >>= 4;
            }
            return p;
        }

        static char ParseFormatSpecifier (StringView specifier, out int digits) {
            if (specifier.IsEmpty) {
                digits = -1;
                return 'G';
            }

            char* curr = specifier.Data;
            char first = *curr++;
            if ((first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z')) {
                int n = -1;
                char c = *curr++;
                if (c >= '0' && c <= '9') {
                    n = c - '0';
                    c = *curr++;
                    while (c >= '0' && c <= '9') {
                        n = n * 10 + c - '0';
                        c = *curr++;
                        if (n >= 10)
                            break;
                    }
                }

                if (c == 0) {
                    digits = n;
                    return first;
                }
            }

            digits = -1;
            return (char)0;
        }

        static void Int32ToNumber (int value, ref Number number) {
            number.Precision = Int32Precision;
            if (value >= 0)
                number.Sign = 0;
            else {
                number.Sign = 1;
                value = -value;
            }

            var buffer = stackalloc char[Int32Precision + 1];
            var ptr = Int32ToDecChars(buffer + Int32Precision, (uint)value, 0);
            var len = (int)(buffer + Int32Precision - ptr);
            number.Scale = len;

            var dest = number.Digits;
            while (--len >= 0)
                *dest++ = *ptr++;
            *dest = '\0';
        }

        static void UInt32ToNumber (uint value, ref Number number) {
            number.Precision = UInt32Precision;
            number.Sign = 0;

            var buffer = stackalloc char[UInt32Precision + 1];
            var ptr = Int32ToDecChars(buffer + UInt32Precision, value, 0);
            var len = (int)(buffer + UInt32Precision - ptr);
            number.Scale = len;

            var dest = number.Digits;
            while (--len >= 0)
                *dest++ = *ptr++;
            *dest = '\0';
        }

        static void Int64ToNumber (long value, ref Number number) {
            number.Precision = Int64Precision;
            if (value >= 0)
                number.Sign = 0;
            else {
                number.Sign = 1;
                value = -value;
            }

            var buffer = stackalloc char[Int64Precision + 1];
            var ptr = buffer + Int64Precision;
            var uv = (ulong)value;
            while (High32(uv) != 0)
                ptr = Int32ToDecChars(ptr, Int64DivMod(ref uv), 9);

            ptr = Int32ToDecChars(ptr, Low32(uv), 0);
            var len = (int)(buffer + Int64Precision - ptr);
            number.Scale = len;

            var dest = number.Digits;
            while (--len >= 0)
                *dest++ = *ptr++;
            *dest = '\0';
        }

        static void UInt64ToNumber (ulong value, ref Number number) {
            number.Precision = UInt64Precision;
            number.Sign = 0;

            var buffer = stackalloc char[UInt64Precision + 1];
            var ptr = buffer + UInt64Precision;
            while (High32(value) != 0)
                ptr = Int32ToDecChars(ptr, Int64DivMod(ref value), 9);

            ptr = Int32ToDecChars(ptr, Low32(value), 0);

            var len = (int)(buffer + UInt64Precision - ptr);
            number.Scale = len;

            var dest = number.Digits;
            while (--len >= 0)
                *dest++ = *ptr++;
            *dest = '\0';
        }

        static void DoubleToNumber (double value, int precision, ref Number number) {
            number.Precision = precision;

            uint sign, exp, mantHi, mantLo;
            ExplodeDouble(value, out sign, out exp, out mantHi, out mantLo);

            if (exp == 0x7FF) {
                // special value handling (infinity and NaNs)
                number.Scale = (mantLo != 0 || mantHi != 0) ? ScaleNaN : ScaleInf;
                number.Sign = (int)sign;
                number.Digits[0] = '\0';
            }
            else {
                // convert the digits of the number to characters
                if (value < 0) {
                    number.Sign = 1;
                    value = -value;
                }

                var digits = number.Digits;
                var end = digits + MaxFloatingDigits;
                var p = end;
                var shift = 0;
                double intPart;
                double reducedInt;
                var fracPart = ModF(value, out intPart);

                if (intPart != 0) {
                    // format the integer part
                    while (intPart != 0) {
                        reducedInt = ModF(intPart / 10, out intPart);
                        *--p = (char)((int)((reducedInt + 0.03) * 10) + '0');
                        shift++;
                    }
                    while (p < end)
                        *digits++ = *p++;
                }
                else if (fracPart > 0) {
                    // normalize the fractional part
                    while ((reducedInt = fracPart * 10) < 1) {
                        fracPart = reducedInt;
                        shift--;
                    }
                }

                // concat the fractional part, padding the remainder with zeros
                p = number.Digits + precision;
                while (digits <= p && digits < end) {
                    fracPart *= 10;
                    fracPart = ModF(fracPart, out reducedInt);
                    *digits++ = (char)((int)reducedInt + '0');
                }

                // round the result if necessary
                digits = p;
                *p = (char)(*p + 5);
                while (*p > '9') {
                    *p = '0';
                    if (p > number.Digits)
                        ++*--p;
                    else {
                        *p = '1';
                        shift++;
                    }
                }

                number.Scale = shift;
                *digits = '\0';
            }
        }

        static void DecimalToNumber (uint* value, ref Number number) {
            // bit 31 of the decimal is the sign bit
            // bits 16-23 contain the scale
            number.Sign = (int)(*value >> 31);
            number.Scale = (int)((*value >> 16) & 0xFF);
            number.Precision = DecimalPrecision;

            // loop for as long as the decimal is larger than 32 bits
            var buffer = stackalloc char[DecimalPrecision + 1];
            var p = buffer + DecimalPrecision;
            var hi = *(value + 1);
            var lo = *(value + 2);
            var mid = *(value + 3);

            while ((mid | hi) != 0) {
                // keep dividing down by one billion at a time
                ulong n = hi;
                hi = (uint)(n / OneBillion);
                n = (n % OneBillion) << 32 | mid;
                mid = (uint)(n / OneBillion);
                n = (n % OneBillion) << 32 | lo;
                lo = (uint)(n / OneBillion);

                // format this portion of the number
                p = Int32ToDecChars(p, (uint)(n % OneBillion), 9);
            }

            // finish off with the low 32-bits of the decimal, if anything is left over
            p = Int32ToDecChars(p, lo, 0);

            var len = (int)(buffer + DecimalPrecision - p);
            number.Scale = len - number.Scale;

            var dest = number.Digits;
            while (--len >= 0)
                *dest++ = *p++;
            *dest = '\0';
        }

        static void RoundNumber (ref Number number, int pos) {
            var digits = number.Digits;
            int i = 0;
            while (i < pos && digits[i] != 0) i++;
            if (i == pos && digits[i] >= '5') {
                while (i > 0 && digits[i - 1] == '9') i--;
                if (i > 0)
                    digits[i - 1]++;
                else {
                    number.Scale++;
                    digits[0] = '1';
                    i = 1;
                }
            }
            else {
                while (i > 0 && digits[i - 1] == '0')
                    i--;
            }

            if (i == 0) {
                number.Scale = 0;
                number.Sign = 0;
            }

            digits[i] = '\0';
        }

        static void AppendString (char** buffer, string value) {
            fixed (char* pinnedString = value)
            {
                var length = value.Length;
                for (var src = pinnedString; src < pinnedString + length; (*buffer)++, src++)
                    **buffer = *src;
            }
        }

        static int StrLen (char* str) {
            int count = 0;
            while (*str++ != 0)
                count++;

            return count;
        }

        static uint Int64DivMod (ref ulong value) {
            var rem = (uint)(value % 1000000000);
            value /= 1000000000;
            return rem;
        }

        static double ModF (double value, out double intPart) {
            intPart = Math.Truncate(value);
            return value - intPart;
        }

        static void ExplodeDouble (double value, out uint sign, out uint exp, out uint mantHi, out uint mantLo) {
            var bits = *(ulong*)&value;
            if (BitConverter.IsLittleEndian) {
                mantLo = (uint)(bits & 0xFFFFFFFF);         // bits 0 - 31
                mantHi = (uint)((bits >> 32) & 0xFFFFF);    // bits 32 - 51
                exp = (uint)((bits >> 52) & 0x7FF);         // bits 52 - 62
                sign = (uint)((bits >> 63) & 0x1);          // bit 63
            }
            else {
                sign = (uint)(bits & 0x1);                  // bit 0
                exp = (uint)((bits >> 1) & 0x7FF);          // bits 1 - 11
                mantHi = (uint)((bits >> 12) & 0xFFFFF);    // bits 12 - 31
                mantLo = (uint)(bits >> 32);                // bits 32 - 63
            }
        }

        static uint Low32 (ulong value) {
            return (uint)value;
        }

        static uint High32 (ulong value) {
            return (uint)((value & 0xFFFFFFFF00000000) >> 32);
        }

        struct Number {
            public int Precision;
            public int Scale;
            public int Sign;
            public char* Digits;

            // useful for debugging
            public override string ToString () {
                return new string(Digits);
            }
        }

        const int MaxNumberDigits = 50;
        const int MaxFloatingDigits = 352;
        const int Int32Precision = 10;
        const int UInt32Precision = 10;
        const int Int64Precision = 19;
        const int UInt64Precision = 20;
        const int FloatPrecision = 7;
        const int DoublePrecision = 15;
        const int DecimalPrecision = 29;
        const int ScaleNaN = unchecked((int)0x80000000);
        const int ScaleInf = 0x7FFFFFFF;
        const int OneBillion = 1000000000;
    }

    // currently just contains some hardcoded exception messages
    static class SR {
        public const string InvalidGroupSizes = "Invalid group sizes in NumberFormatInfo.";
        public const string UnknownFormatSpecifier = "Unknown format specifier '{0}'.";
        public const string ArgIndexOutOfRange = "No format argument exists for index '{0}'.";
        public const string TypeNotFormattable = "Type '{0}' is not a built-in type, does not implement IStringFormattable, and no custom formatter was found for it.";
        public const string InvalidFormatString = "Invalid format string.";
    }

    /// <summary>
    /// A low-allocation version of the built-in <see cref="StringBuilder"/> type.
    /// </summary>
    partial class StringBuffer {
        /// <summary>
        /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>   
        public void AppendFormat<T0>(string format, T0 arg0) {
            var args = new Arg1<T0>(arg0);
            AppendArgSet(format, ref args);
        }

        /// <summary>
        /// Converts the value of objects to strings based on the formats specified and inserts them into another string.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>   
        public static string Format<T0>(string format, T0 arg0) {
            var buffer = Acquire(format.Length + 8);
            buffer.AppendFormat(format, arg0);
            var result = buffer.ToString();
            Release(buffer);
            return result;
        }

        /// <summary>
        /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>   
        public void AppendFormat<T0, T1>(string format, T0 arg0, T1 arg1) {
            var args = new Arg2<T0, T1>(arg0, arg1);
            AppendArgSet(format, ref args);
        }

        /// <summary>
        /// Converts the value of objects to strings based on the formats specified and inserts them into another string.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>   
        public static string Format<T0, T1>(string format, T0 arg0, T1 arg1) {
            var buffer = Acquire(format.Length + 16);
            buffer.AppendFormat(format, arg0, arg1);
            var result = buffer.ToString();
            Release(buffer);
            return result;
        }

        /// <summary>
        /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>   
        public void AppendFormat<T0, T1, T2>(string format, T0 arg0, T1 arg1, T2 arg2) {
            var args = new Arg3<T0, T1, T2>(arg0, arg1, arg2);
            AppendArgSet(format, ref args);
        }

        /// <summary>
        /// Converts the value of objects to strings based on the formats specified and inserts them into another string.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>   
        public static string Format<T0, T1, T2>(string format, T0 arg0, T1 arg1, T2 arg2) {
            var buffer = Acquire(format.Length + 24);
            buffer.AppendFormat(format, arg0, arg1, arg2);
            var result = buffer.ToString();
            Release(buffer);
            return result;
        }

        /// <summary>
        /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>
        /// <param name="arg3">A value to format.</param>   
        public void AppendFormat<T0, T1, T2, T3>(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3) {
            var args = new Arg4<T0, T1, T2, T3>(arg0, arg1, arg2, arg3);
            AppendArgSet(format, ref args);
        }

        /// <summary>
        /// Converts the value of objects to strings based on the formats specified and inserts them into another string.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>
        /// <param name="arg3">A value to format.</param>   
        public static string Format<T0, T1, T2, T3>(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3) {
            var buffer = Acquire(format.Length + 32);
            buffer.AppendFormat(format, arg0, arg1, arg2, arg3);
            var result = buffer.ToString();
            Release(buffer);
            return result;
        }

        /// <summary>
        /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>
        /// <param name="arg3">A value to format.</param>
        /// <param name="arg4">A value to format.</param>   
        public void AppendFormat<T0, T1, T2, T3, T4>(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) {
            var args = new Arg5<T0, T1, T2, T3, T4>(arg0, arg1, arg2, arg3, arg4);
            AppendArgSet(format, ref args);
        }

        /// <summary>
        /// Converts the value of objects to strings based on the formats specified and inserts them into another string.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>
        /// <param name="arg3">A value to format.</param>
        /// <param name="arg4">A value to format.</param>   
        public static string Format<T0, T1, T2, T3, T4>(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) {
            var buffer = Acquire(format.Length + 40);
            buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4);
            var result = buffer.ToString();
            Release(buffer);
            return result;
        }

        /// <summary>
        /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>
        /// <param name="arg3">A value to format.</param>
        /// <param name="arg4">A value to format.</param>
        /// <param name="arg5">A value to format.</param>   
        public void AppendFormat<T0, T1, T2, T3, T4, T5>(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) {
            var args = new Arg6<T0, T1, T2, T3, T4, T5>(arg0, arg1, arg2, arg3, arg4, arg5);
            AppendArgSet(format, ref args);
        }

        /// <summary>
        /// Converts the value of objects to strings based on the formats specified and inserts them into another string.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>
        /// <param name="arg3">A value to format.</param>
        /// <param name="arg4">A value to format.</param>
        /// <param name="arg5">A value to format.</param>   
        public static string Format<T0, T1, T2, T3, T4, T5>(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) {
            var buffer = Acquire(format.Length + 48);
            buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4, arg5);
            var result = buffer.ToString();
            Release(buffer);
            return result;
        }

        /// <summary>
        /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>
        /// <param name="arg3">A value to format.</param>
        /// <param name="arg4">A value to format.</param>
        /// <param name="arg5">A value to format.</param>
        /// <param name="arg6">A value to format.</param>   
        public void AppendFormat<T0, T1, T2, T3, T4, T5, T6>(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) {
            var args = new Arg7<T0, T1, T2, T3, T4, T5, T6>(arg0, arg1, arg2, arg3, arg4, arg5, arg6);
            AppendArgSet(format, ref args);
        }

        /// <summary>
        /// Converts the value of objects to strings based on the formats specified and inserts them into another string.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>
        /// <param name="arg3">A value to format.</param>
        /// <param name="arg4">A value to format.</param>
        /// <param name="arg5">A value to format.</param>
        /// <param name="arg6">A value to format.</param>   
        public static string Format<T0, T1, T2, T3, T4, T5, T6>(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) {
            var buffer = Acquire(format.Length + 56);
            buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4, arg5, arg6);
            var result = buffer.ToString();
            Release(buffer);
            return result;
        }

        /// <summary>
        /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>
        /// <param name="arg3">A value to format.</param>
        /// <param name="arg4">A value to format.</param>
        /// <param name="arg5">A value to format.</param>
        /// <param name="arg6">A value to format.</param>
        /// <param name="arg7">A value to format.</param>   
        public void AppendFormat<T0, T1, T2, T3, T4, T5, T6, T7>(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) {
            var args = new Arg8<T0, T1, T2, T3, T4, T5, T6, T7>(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
            AppendArgSet(format, ref args);
        }

        /// <summary>
        /// Converts the value of objects to strings based on the formats specified and inserts them into another string.
        /// </summary>
        /// <param name="format">A composite format string.</param>
        /// <param name="arg0">A value to format.</param>
        /// <param name="arg1">A value to format.</param>
        /// <param name="arg2">A value to format.</param>
        /// <param name="arg3">A value to format.</param>
        /// <param name="arg4">A value to format.</param>
        /// <param name="arg5">A value to format.</param>
        /// <param name="arg6">A value to format.</param>
        /// <param name="arg7">A value to format.</param>   
        public static string Format<T0, T1, T2, T3, T4, T5, T6, T7>(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) {
            var buffer = Acquire(format.Length + 64);
            buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
            var result = buffer.ToString();
            Release(buffer);
            return result;
        }
    }

    unsafe struct Arg1<T0> : IArgSet {
        T0 t0;

        public int Count => 1;

        public Arg1 (T0 t0) {
            this.t0 = t0;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Format (StringBuffer buffer, int index, StringView format) {
            switch (index) {
                case 0: buffer.AppendGeneric(t0, format); break;
            }
        }
    }

    unsafe struct Arg2<T0, T1> : IArgSet {
        T0 t0;
        T1 t1;

        public int Count => 2;

        public Arg2 (T0 t0, T1 t1) {
            this.t0 = t0;
            this.t1 = t1;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Format (StringBuffer buffer, int index, StringView format) {
            switch (index) {
                case 0: buffer.AppendGeneric(t0, format); break;
                case 1: buffer.AppendGeneric(t1, format); break;
            }
        }
    }

    unsafe struct Arg3<T0, T1, T2> : IArgSet {
        T0 t0;
        T1 t1;
        T2 t2;

        public int Count => 3;

        public Arg3 (T0 t0, T1 t1, T2 t2) {
            this.t0 = t0;
            this.t1 = t1;
            this.t2 = t2;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Format (StringBuffer buffer, int index, StringView format) {
            switch (index) {
                case 0: buffer.AppendGeneric(t0, format); break;
                case 1: buffer.AppendGeneric(t1, format); break;
                case 2: buffer.AppendGeneric(t2, format); break;
            }
        }
    }

    unsafe struct Arg4<T0, T1, T2, T3> : IArgSet {
        T0 t0;
        T1 t1;
        T2 t2;
        T3 t3;

        public int Count => 4;

        public Arg4 (T0 t0, T1 t1, T2 t2, T3 t3) {
            this.t0 = t0;
            this.t1 = t1;
            this.t2 = t2;
            this.t3 = t3;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Format (StringBuffer buffer, int index, StringView format) {
            switch (index) {
                case 0: buffer.AppendGeneric(t0, format); break;
                case 1: buffer.AppendGeneric(t1, format); break;
                case 2: buffer.AppendGeneric(t2, format); break;
                case 3: buffer.AppendGeneric(t3, format); break;
            }
        }
    }

    unsafe struct Arg5<T0, T1, T2, T3, T4> : IArgSet {
        T0 t0;
        T1 t1;
        T2 t2;
        T3 t3;
        T4 t4;

        public int Count => 5;

        public Arg5 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4) {
            this.t0 = t0;
            this.t1 = t1;
            this.t2 = t2;
            this.t3 = t3;
            this.t4 = t4;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Format (StringBuffer buffer, int index, StringView format) {
            switch (index) {
                case 0: buffer.AppendGeneric(t0, format); break;
                case 1: buffer.AppendGeneric(t1, format); break;
                case 2: buffer.AppendGeneric(t2, format); break;
                case 3: buffer.AppendGeneric(t3, format); break;
                case 4: buffer.AppendGeneric(t4, format); break;
            }
        }
    }

    unsafe struct Arg6<T0, T1, T2, T3, T4, T5> : IArgSet {
        T0 t0;
        T1 t1;
        T2 t2;
        T3 t3;
        T4 t4;
        T5 t5;

        public int Count => 6;

        public Arg6 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5) {
            this.t0 = t0;
            this.t1 = t1;
            this.t2 = t2;
            this.t3 = t3;
            this.t4 = t4;
            this.t5 = t5;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Format (StringBuffer buffer, int index, StringView format) {
            switch (index) {
                case 0: buffer.AppendGeneric(t0, format); break;
                case 1: buffer.AppendGeneric(t1, format); break;
                case 2: buffer.AppendGeneric(t2, format); break;
                case 3: buffer.AppendGeneric(t3, format); break;
                case 4: buffer.AppendGeneric(t4, format); break;
                case 5: buffer.AppendGeneric(t5, format); break;
            }
        }
    }

    unsafe struct Arg7<T0, T1, T2, T3, T4, T5, T6> : IArgSet {
        T0 t0;
        T1 t1;
        T2 t2;
        T3 t3;
        T4 t4;
        T5 t5;
        T6 t6;

        public int Count => 7;

        public Arg7 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6) {
            this.t0 = t0;
            this.t1 = t1;
            this.t2 = t2;
            this.t3 = t3;
            this.t4 = t4;
            this.t5 = t5;
            this.t6 = t6;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Format (StringBuffer buffer, int index, StringView format) {
            switch (index) {
                case 0: buffer.AppendGeneric(t0, format); break;
                case 1: buffer.AppendGeneric(t1, format); break;
                case 2: buffer.AppendGeneric(t2, format); break;
                case 3: buffer.AppendGeneric(t3, format); break;
                case 4: buffer.AppendGeneric(t4, format); break;
                case 5: buffer.AppendGeneric(t5, format); break;
                case 6: buffer.AppendGeneric(t6, format); break;
            }
        }
    }

    unsafe struct Arg8<T0, T1, T2, T3, T4, T5, T6, T7> : IArgSet {
        T0 t0;
        T1 t1;
        T2 t2;
        T3 t3;
        T4 t4;
        T5 t5;
        T6 t6;
        T7 t7;

        public int Count => 8;

        public Arg8 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7) {
            this.t0 = t0;
            this.t1 = t1;
            this.t2 = t2;
            this.t3 = t3;
            this.t4 = t4;
            this.t5 = t5;
            this.t6 = t6;
            this.t7 = t7;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Format (StringBuffer buffer, int index, StringView format) {
            switch (index) {
                case 0: buffer.AppendGeneric(t0, format); break;
                case 1: buffer.AppendGeneric(t1, format); break;
                case 2: buffer.AppendGeneric(t2, format); break;
                case 3: buffer.AppendGeneric(t3, format); break;
                case 4: buffer.AppendGeneric(t4, format); break;
                case 5: buffer.AppendGeneric(t5, format); break;
                case 6: buffer.AppendGeneric(t6, format); break;
                case 7: buffer.AppendGeneric(t7, format); break;
            }
        }
    }
}
