using System.Numerics;
using System.Runtime.CompilerServices;
using OpenTK.Graphics.OpenGL;

namespace Dashboard.Drawing.OpenGL
{
    public class MappableBuffer<T> : IDisposable
    {
        public int Handle { get; private set; } = 0;
        public int Capacity { get; set; } = BASE_CAPACITY;
        public IntPtr Pointer { get; private set; } = IntPtr.Zero;

        private bool _isDisposed = false;
        private const int BASE_CAPACITY = 4 << 10;  // 4 KiB
        private const int MAX_INCREMENT = 4 << 20;  // 4 MiB

        ~MappableBuffer()
        {
            Dispose(false);
        }

        public void Initialize()
        {
            Handle = GL.GenBuffer();
            GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
            GL.BufferData(BufferTarget.ArrayBuffer, Capacity, IntPtr.Zero, BufferUsage.DynamicDraw);
        }

        public void EnsureCapacity(int count)
        {
            if (count < 0)
                throw new ArgumentOutOfRangeException(nameof(count));

            if (Capacity >= count)
                return;

            AssertInitialized();
            Unmap();

            int sz = Unsafe.SizeOf<T>();
            int oldsize = Capacity * sz;
            int request = count * sz;
            int newsize;
            if (request > MAX_INCREMENT)
            {
                newsize = ((request + MAX_INCREMENT - 1) / MAX_INCREMENT) * MAX_INCREMENT;
            }
            else
            {
                newsize = checked((int)BitOperations.RoundUpToPowerOf2((ulong)request));
            }

            int dest = GL.GenBuffer();

            GL.BindBuffer(BufferTarget.CopyWriteBuffer, dest);
            GL.BindBuffer(BufferTarget.CopyReadBuffer, Handle);

            GL.BufferData(BufferTarget.CopyWriteBuffer, newsize, IntPtr.Zero, BufferUsage.DynamicDraw);
            GL.CopyBufferSubData(CopyBufferSubDataTarget.CopyReadBuffer, CopyBufferSubDataTarget.CopyWriteBuffer, 0, 0, oldsize);

            GL.DeleteBuffer(Handle);
            Handle = dest;
            Capacity = newsize;
        }

        public unsafe void Map()
        {
            if (Pointer != IntPtr.Zero)
                return;

            AssertInitialized();

            GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
            Pointer = (IntPtr)GL.MapBuffer(BufferTarget.ArrayBuffer, BufferAccess.ReadWrite);
        }

        public void Unmap()
        {
            if (Pointer == IntPtr.Zero)
                return;

            AssertInitialized();

            GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
            GL.UnmapBuffer(BufferTarget.ArrayBuffer);
        }

        public unsafe Span<T> AsSpan()
        {
            if (Pointer == IntPtr.Zero)
                throw new InvalidOperationException("The buffer is not currently mapped.");

            AssertInitialized();

            return new Span<T>(Pointer.ToPointer(), Capacity / Unsafe.SizeOf<T>());
        }

        private void AssertInitialized()
        {
            if (Handle == 0)
                throw new InvalidOperationException("The buffer is not initialized.");
        }

        private void Dispose(bool disposing)
        {
            if (_isDisposed)
                return;
            _isDisposed = true;

            if (disposing)
                GC.SuppressFinalize(this);

            ContextCollector.Global.DeleteBufffer(Handle);
        }

        public void Dispose() => Dispose(true);
    }
}