using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.IO; using System.Runtime.CompilerServices; namespace Dashboard.Drawing { public class DrawQueue : IEnumerable<ICommandFrame>, IDisposable { private readonly HashList<IDrawExtension> _extensions = new HashList<IDrawExtension>(); private readonly HashList<IDrawCommand> _commands = new HashList<IDrawCommand>(); private readonly HashList<IDrawResource> _resources = new HashList<IDrawResource>(); private readonly DrawController _controller; private readonly MemoryStream _commandStream = new MemoryStream(); /// <summary> /// The absolute boundary of all graphics objects. /// </summary> public Box3d Bounds { get; set; } /// <summary> /// The extensions required to draw the image. /// </summary> public IReadOnlyList<IDrawExtension> Extensions => _extensions; /// <summary> /// The resources used by this draw queue. /// </summary> public IReadOnlyList<IDrawResource> Resources => _resources; /// <summary> /// The list of commands used by the extension. /// </summary> public IReadOnlyList<IDrawCommand> Command => _commands; public DrawQueue() { _controller = new DrawController(this); } /// <summary> /// Clear the queue. /// </summary> public void Clear() { _resources.Clear(); _commands.Clear(); _extensions.Clear(); _commandStream.SetLength(0); } public int RequireExtension(IDrawExtension extension) { foreach (IDrawExtension super in extension.Requires) RequireExtension(super); return _extensions.Intern(extension); } public int RequireResource(IDrawResource resource) { RequireExtension(resource.Kind); return _resources.Intern(resource); } internal IDrawController GetController(IDrawExtension extension) { _extensions.Intern(extension); return _controller; } private void Write(IDrawCommand command) { if (command.Length > 0) throw new InvalidOperationException("This command has a finite length argument."); int cmdIndex = _commands.Intern(command); Span<byte> cmd = stackalloc byte[6]; int sz; if (command.Length == 0) { // Write a fixed command. sz = ToVlq(cmdIndex, cmd); } else { // Write a variadic with zero length. sz = ToVlq(cmdIndex, cmd); cmd[sz++] = 0; } _commandStream.Write(cmd[..sz]); } private void Write(IDrawCommand command, ReadOnlySpan<byte> param) { if (command.Length < 0) { Span<byte> cmd = stackalloc byte[10]; int cmdIndex = _commands.Intern(command); int sz = ToVlq(cmdIndex, cmd); sz += ToVlq(param.Length, cmd[sz..]); _commandStream.Write(cmd[..sz]); } else { if (command.Length != param.Length) throw new ArgumentOutOfRangeException(nameof(param.Length), "Length of the parameter does not match the command."); Span<byte> cmd = stackalloc byte[5]; int cmdIndex = _commands.Intern(command); int sz = ToVlq(cmdIndex, cmd); _commandStream.Write(cmd[..sz]); _commandStream.Write(param); } } public Enumerator GetEnumerator() => new Enumerator(this); IEnumerator<ICommandFrame> IEnumerable<ICommandFrame>.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); private static int ToVlq(int value, Span<byte> bytes) { if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "Must be a positive integer."); else if (bytes.Length < 5) throw new ArgumentOutOfRangeException(nameof(bytes), "Must at least be five bytes long."); if (value == 0) { bytes[0] = 0; return 1; } int i; for (i = 0; i < 5 && value != 0; i++, value >>= 7) { if (i > 0) bytes[i - 1] |= 1 << 7; bytes[i] = (byte)(value & 0x7F); } return i; } private static int FromVlq(ReadOnlySpan<byte> bytes, out int value) { value = 0; int i; for (i = 0; i < bytes.Length; i++) { byte b = bytes[i]; value = (value << 7) | b; if ((b & (1 << 7)) == 0) { i++; break; } } return i; } public void Dispose() { throw new NotImplementedException(); } private class DrawController(DrawQueue Queue) : IDrawController { public void EnsureBounds(Box3d bounds) { Queue.Bounds = Box3d.Union(Queue.Bounds, bounds); } public void Write(IDrawCommand command) { Queue.Write(command); } public void Write(IDrawCommand command, ReadOnlySpan<byte> bytes) { Queue.Write(command, bytes); } public void Write<T>(IDrawCommand command, T param) where T : IParameterSerializer<T> { int length = param.Serialize(Queue, Span<byte>.Empty); Span<byte> bytes = stackalloc byte[length]; param.Serialize(Queue, bytes); Write(command, bytes); } public void Write<T1, T2>(T2 command, T1 param) where T2 : IDrawCommand<T1> { int length = command.WriteParams(Queue, param, Span<byte>.Empty); Span<byte> bytes = stackalloc byte[length]; command.WriteParams(Queue, param, bytes); Write(command, bytes); } } public class Enumerator : ICommandFrame, IEnumerator<ICommandFrame> { private readonly DrawQueue _queue; private readonly byte[] _stream; private int _length; private int _index = -1; private int _paramsIndex = -1; private int _paramLength = 0; private IDrawCommand? _current = null; public ICommandFrame Current => this; object? IEnumerator.Current => Current; public IDrawCommand Command => _current ?? throw new InvalidOperationException(); public bool HasParameters { get; private set; } public Enumerator(DrawQueue queue) { _queue = queue; _stream = queue._commandStream.GetBuffer(); _length = (int)queue._commandStream.Length; } public bool MoveNext() { if (_index >= _length) return false; if (_index == -1) _index = 0; _index += FromVlq(_stream[_index .. (_index + 5)], out int command); _current = _queue.Command[command]; HasParameters = _current.Length != 0; if (!HasParameters) { _paramsIndex = -1; return true; } int length; if (_current.Length < 0) { _index += FromVlq(_stream[_index .. (_index + 5)], out length); } else { length = _current.Length; } _paramsIndex = _index; _paramLength = length; _index += length; return true; } public void Reset() { _index = -1; _current = null; } public object? GetParameter() { return _current?.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength)); } public T GetParameter<T>() { if (_current is IDrawCommand<T> command) { return command.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength))!; } else { throw new InvalidOperationException(); } } public bool TryGetParameter<T>([NotNullWhen(true)] out T? parameter) { if (_current is IDrawCommand<T> command) { parameter = command.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength))!; return true; } else { parameter = default; return false; } } public void Dispose() { } } } public interface ICommandFrame { public IDrawCommand Command { get; } public bool HasParameters { get; } public object? GetParameter(); public T GetParameter<T>(); public bool TryGetParameter<T>([NotNullWhen(true)] out T? parameter); } public interface IDrawController { /// <summary> /// Ensures that the canvas is at least a certain size. /// </summary> /// <param name="bounds">The bounding box.</param> void EnsureBounds(Box3d bounds); /// <summary> /// Write into the command stream. /// </summary> /// <param name="command">The command to write.</param> void Write(IDrawCommand command); /// <summary> /// Write into the command stream. /// </summary> /// <param name="command">The command to write.</param> /// <param name="param">Any data associated with the command.</param> void Write(IDrawCommand command, ReadOnlySpan<byte> param); /// <summary> /// Write into the command stream. /// </summary> /// <param name="command">The command to write.</param> /// <param name="param">Any data associated with the command.</param> void Write<T>(IDrawCommand command, T param) where T : IParameterSerializer<T>; /// <summary> /// Write into the command stream. /// </summary> /// <param name="command">The command to write.</param> /// <param name="param">Any data associated with the command.</param> void Write<T1, T2>(T2 command, T1 param) where T2 : IDrawCommand<T1>; } }