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>;
    }
}