using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Text.Unicode; namespace Dalamud.Bindings.ImGui; [InterpolatedStringHandler] public ref struct ImU8String { public const int AllocFreeBufferSize = 512; private const int MinimumRentSize = AllocFreeBufferSize * 2; private IFormatProvider? formatProvider; private byte[]? rentedBuffer; private ref readonly byte externalFirstByte; private State state; private FixedBufferContainer fixedBuffer; [Flags] private enum State : byte { None = 0, Initialized = 1 << 0, NullTerminated = 1 << 1, Interpolation = 1 << 2, } public ImU8String() { Unsafe.SkipInit(out this.fixedBuffer); this.FixedBufferByteRef = 0; } public ImU8String(int literalLength, int formattedCount) : this(""u8) { this.state |= State.Interpolation; literalLength += formattedCount * 4; this.Reserve(literalLength); } public ImU8String(int literalLength, int formattedCount, IFormatProvider? formatProvider) : this(literalLength, formattedCount) { this.formatProvider = formatProvider; } public ImU8String(ReadOnlySpan text, bool ensureNullTermination = false) : this() { if (Unsafe.IsNullRef(in MemoryMarshal.GetReference(text))) { this.state = State.None; return; } this.state = State.Initialized; if (text.IsEmpty) { this.state |= State.NullTerminated; } else if (ensureNullTermination) { this.Reserve(text.Length + 1); var buffer = this.Buffer; text.CopyTo(buffer); buffer[^1] = 0; this.Length = text.Length; this.state |= State.NullTerminated; } else { this.externalFirstByte = ref text[0]; this.Length = text.Length; if (Unsafe.Add(ref Unsafe.AsRef(in this.externalFirstByte), this.Length) == 0) this.state |= State.NullTerminated; } } public ImU8String(ReadOnlyMemory text, bool ensureNullTermination = false) : this(text.Span, ensureNullTermination) { } public ImU8String(ReadOnlySpan text) : this() { if (Unsafe.IsNullRef(in MemoryMarshal.GetReference(text))) { this.state = State.None; return; } this.state = State.Initialized | State.NullTerminated; this.Length = Encoding.UTF8.GetByteCount(text); if (this.Length + 1 < AllocFreeBufferSize) { var newSpan = this.FixedBufferSpan[..this.Length]; Encoding.UTF8.GetBytes(text, newSpan); this.FixedBufferSpan[this.Length] = 0; } else { this.rentedBuffer = ArrayPool.Shared.Rent(this.Length + 1); var newSpan = this.rentedBuffer.AsSpan(0, this.Length); Encoding.UTF8.GetBytes(text, newSpan); this.rentedBuffer[this.Length] = 0; } } public ImU8String(ReadOnlyMemory text) : this(text.Span) { } public ImU8String(string? text) : this(text.AsSpan()) { } public unsafe ImU8String(byte* text) : this(MemoryMarshal.CreateReadOnlySpanFromNullTerminated(text)) { this.state |= State.NullTerminated; } public unsafe ImU8String(char* text) : this(MemoryMarshal.CreateReadOnlySpanFromNullTerminated(text)) { } public static ImU8String Empty => default; public readonly ReadOnlySpan Span => !Unsafe.IsNullRef(in this.externalFirstByte) ? MemoryMarshal.CreateReadOnlySpan(in this.externalFirstByte, this.Length) : this.rentedBuffer is { } rented ? rented.AsSpan(0, this.Length) : Unsafe.AsRef(in this).FixedBufferSpan[..this.Length]; public int Length { get; private set; } public readonly bool IsNull => (this.state & State.Initialized) == 0; public readonly bool IsEmpty => this.Length == 0; internal Span Buffer { get { if (Unsafe.IsNullRef(in this.externalFirstByte)) this.ConvertToOwned(); return this.rentedBuffer is { } buf ? buf.AsSpan() : MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer))); } } private Span RemainingBuffer => this.Buffer[this.Length..]; private ref byte FixedBufferByteRef => ref this.FixedBufferSpan[0]; private Span FixedBufferSpan => MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer))); public static implicit operator ImU8String(ReadOnlySpan text) => new(text); public static implicit operator ImU8String(ReadOnlyMemory text) => new(text); public static implicit operator ImU8String(Span text) => new(text); public static implicit operator ImU8String(Memory text) => new(text); public static implicit operator ImU8String(byte[]? text) => new(text.AsSpan()); public static implicit operator ImU8String(ReadOnlySpan text) => new(text); public static implicit operator ImU8String(ReadOnlyMemory text) => new(text); public static implicit operator ImU8String(Span text) => new(text); public static implicit operator ImU8String(Memory text) => new(text); public static implicit operator ImU8String(char[]? text) => new(text.AsSpan()); public static implicit operator ImU8String(string? text) => new(text); public static unsafe implicit operator ImU8String(byte* text) => new(text); public static unsafe implicit operator ImU8String(char* text) => new(text); public ref readonly byte GetPinnableReference() { if (this.IsNull) return ref Unsafe.NullRef(); if (this.IsEmpty) return ref this.FixedBufferSpan[0]; return ref this.Span.GetPinnableReference(); } public ref readonly byte GetPinnableReference(ReadOnlySpan defaultValue) { if (this.IsNull) return ref defaultValue.GetPinnableReference(); if (this.IsEmpty) return ref this.FixedBufferSpan[0]; return ref this.Span.GetPinnableReference(); } public ref readonly byte GetPinnableNullTerminatedReference(ReadOnlySpan defaultValue = default) { if (this.IsNull) return ref defaultValue.GetPinnableReference(); if (this.IsEmpty) { ref var t = ref this.FixedBufferSpan[0]; t = 0; return ref t; } if ((this.state & State.NullTerminated) == 0) this.ConvertToOwned(); return ref this.Span[0]; } private void ConvertToOwned() { if (Unsafe.IsNullRef(in this.externalFirstByte)) return; Debug.Assert(this.rentedBuffer is null); if (this.Length + 1 < AllocFreeBufferSize) { var fixedBufferSpan = this.FixedBufferSpan; this.Span.CopyTo(fixedBufferSpan); fixedBufferSpan[this.Length] = 0; } else { var newBuffer = ArrayPool.Shared.Rent(this.Length + 1); this.Span.CopyTo(newBuffer); newBuffer[this.Length] = 0; this.rentedBuffer = newBuffer; } this.state |= State.NullTerminated; this.externalFirstByte = ref Unsafe.NullRef(); } public void Recycle() { if (this.rentedBuffer is { } buf) { this.rentedBuffer = null; ArrayPool.Shared.Return(buf); } } public ImU8String MoveOrDefault(ImU8String other) { if (!this.IsNull) { other.Recycle(); var res = this; this = default; return res; } return other; } public override readonly string ToString() => Encoding.UTF8.GetString(this.Span); public void AppendLiteral(string value) { if (string.IsNullOrEmpty(value)) return; var remaining = this.RemainingBuffer; var len = Encoding.UTF8.GetByteCount(value); if (remaining.Length <= len) this.IncreaseBuffer(out remaining, this.Length + len + 1); this.Buffer[this.Length += Encoding.UTF8.GetBytes(value.AsSpan(), remaining)] = 0; } public void AppendFormatted(ReadOnlySpan value) => this.AppendFormatted(value, null); public void AppendFormatted(ReadOnlySpan value, string? format) { var remaining = this.RemainingBuffer; if (remaining.Length < value.Length + 1) this.IncreaseBuffer(out remaining, this.Length + value.Length + 1); value.CopyTo(remaining); this.Buffer[this.Length += value.Length] = 0; } public void AppendFormatted(ReadOnlySpan value, int alignment) => this.AppendFormatted(value, alignment, null); public void AppendFormatted(ReadOnlySpan value, int alignment, string? format) { var startingPos = this.Length; this.AppendFormatted(value, format); this.FixAlignment(startingPos, alignment); } public void AppendFormatted(ReadOnlySpan value) => this.AppendFormatted(value, null); public void AppendFormatted(ReadOnlySpan value, string? format) { var remaining = this.RemainingBuffer; var len = Encoding.UTF8.GetByteCount(value); if (remaining.Length < len + 1) this.IncreaseBuffer(out remaining, this.Length + len + 1); this.Buffer[this.Length += Encoding.UTF8.GetBytes(value, remaining)] = 0; } public void AppendFormatted(ReadOnlySpan value, int alignment) => this.AppendFormatted(value, alignment, null); public void AppendFormatted(ReadOnlySpan value, int alignment, string? format) { var startingPos = this.Length; this.AppendFormatted(value, format); this.FixAlignment(startingPos, alignment); } public void AppendFormatted(object? value) => this.AppendFormatted(value!); public void AppendFormatted(object? value, string? format) => this.AppendFormatted(value!, format); public void AppendFormatted(object? value, int alignment) => this.AppendFormatted(value!, alignment); public void AppendFormatted(object? value, int alignment, string? format) => this.AppendFormatted(value!, alignment, format); public void AppendFormatted(T value) => this.AppendFormatted(value, null); public void AppendFormatted(T value, string? format) { var remaining = this.RemainingBuffer; if (remaining.Length < 1) this.IncreaseBuffer(out remaining); int written; while (true) { var handler = new Utf8.TryWriteInterpolatedStringHandler(1, 1, remaining[..^1], this.formatProvider, out _); handler.AppendFormatted(value, format); if (Utf8.TryWrite(remaining, this.formatProvider, ref handler, out written)) break; this.IncreaseBuffer(out remaining); } this.Buffer[this.Length += written] = 0; } public void AppendFormatted(T value, int alignment) => this.AppendFormatted(value, alignment, null); public void AppendFormatted(T value, int alignment, string? format) { var startingPos = this.Length; this.AppendFormatted(value, format); this.FixAlignment(startingPos, alignment); } public void Reserve(int length) { if (length >= AllocFreeBufferSize) this.IncreaseBuffer(out _, length); } private void FixAlignment(int startingPos, int alignment) { var appendedLength = this.Length - startingPos; var leftAlign = alignment < 0; if (leftAlign) alignment = -alignment; var fillLength = alignment - appendedLength; if (fillLength <= 0) return; var destination = this.Buffer; if (fillLength > destination.Length - this.Length) { this.IncreaseBuffer(out _, fillLength + 1); destination = this.Buffer; } if (leftAlign) { destination.Slice(this.Length, fillLength).Fill((byte)' '); } else { destination.Slice(startingPos, appendedLength).CopyTo(destination[(startingPos + fillLength)..]); destination.Slice(startingPos, fillLength).Fill((byte)' '); } this.Buffer[this.Length += fillLength] = 0; } private void IncreaseBuffer(out Span remaining, int minCapacity = 0) { minCapacity = Math.Max(minCapacity, Math.Max(this.Buffer.Length * 2, MinimumRentSize)); var newBuffer = ArrayPool.Shared.Rent(minCapacity); this.Span.CopyTo(newBuffer); newBuffer[this.Length] = 0; if (this.rentedBuffer is not null) ArrayPool.Shared.Return(this.rentedBuffer); this.rentedBuffer = newBuffer; this.externalFirstByte = ref Unsafe.NullRef(); remaining = newBuffer.AsSpan(this.Length); } [StructLayout(LayoutKind.Sequential, Size = AllocFreeBufferSize)] private struct FixedBufferContainer; }