using System.Collections.Generic;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.LightTransport;
using UnityEngine.Rendering;

namespace UnityEngine.PathTracing.Core
{
    internal class UnityComputeDeviceContext : IDeviceContext
    {
        private readonly Dictionary<BufferID, GraphicsBuffer> _buffers = new();
        private readonly HashSet<EventID> _inProgressRequests = new();
        private readonly HashSet<EventID> _failedRequests = new();
        private readonly HashSet<EventID> _successfulRequests = new();
        private uint _nextFreeBufferId;
        private uint _nextFreeEventId;
        private CommandBuffer _cmdBuffer;

        private List<BufferID> _temporaryBuffers = new();

        private void CreateCommandBuffer()
        {
            _cmdBuffer?.Dispose();
            _cmdBuffer = new CommandBuffer();
            _cmdBuffer.name = "UnityComputeDeviceContextCommandBuffer";
        }

        public BufferID CreateBuffer(ulong count, ulong stride)
        {
            Debug.Assert(count != 0, "Buffer element count cannot be zero.");
            Debug.Assert(stride != 0, "Stride cannot be zero.");
            Debug.Assert(stride % 4 == 0, "Stride must be a multiple of 4.");
            Debug.Assert(stride <= 2048, "Stride must be 2048 or less.");
            GraphicsBuffer buffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (int)count, (int)stride);
            Debug.Assert(buffer.IsValid(), "Buffer was not successfully created.");
            var zeros = new NativeArray<byte>((int)(count * stride), Allocator.Temp, NativeArrayOptions.ClearMemory);
            buffer.SetData(zeros);
            zeros.Dispose();

            var idInteger = _nextFreeBufferId++;
            var id = new BufferID(idInteger);
            _buffers[id] = buffer;
            return id;
        }

        public void DestroyBuffer(BufferID id)
        {
            Debug.Assert(_buffers.ContainsKey(id), "Invalid buffer ID given.");

            _buffers[id].Release();
            _buffers.Remove(id);
        }

        public void Dispose()
        {
            ReleaseTemporaryBuffers();
            _cmdBuffer?.Dispose();
        }

        public bool Flush()
        {
            Debug.Assert(_cmdBuffer != null);
            Graphics.ExecuteCommandBuffer(_cmdBuffer);

            // TODO(pema.malling): Don't block here https://jira.unity3d.com/browse/LIGHT-1699
            // Ideally we shouldn't need this, but if we don't do it, read-backs will never finish unless explicitly waited on.
            AsyncGPUReadback.WaitAllRequests();

            ReleaseTemporaryBuffers();

            CreateCommandBuffer();
            return true;
        }

        public bool Initialize()
        {
            CreateCommandBuffer();
            return true;
        }

        public bool IsCompleted(EventID id)
        {
            return _successfulRequests.Contains(id) || _failedRequests.Contains(id);
        }

        public bool Wait(EventID id)
        {
            AsyncGPUReadback.WaitAllRequests();

            if (_failedRequests.Contains(id))
            {
                return false;
            }
            return true;
        }

        public void ReadBuffer<T>(BufferSlice<T> src, NativeArray<T> result) where T : struct
        {
            Debug.Assert(_buffers.ContainsKey(src.Id), "Invalid buffer ID given.");

            int stride = UnsafeUtility.SizeOf<T>();
            int offset = (int)src.Offset * stride;
            int size = result.Length * stride;
            _cmdBuffer.RequestAsyncReadbackIntoNativeArray(ref result, _buffers[src.Id], size, offset, delegate { });
        }

        public void ReadBuffer<T>(BufferSlice<T> src, NativeArray<T> result, EventID id) where T : struct
        {
            Debug.Assert(_buffers.ContainsKey(src.Id), "Invalid buffer ID given.");

            int stride = UnsafeUtility.SizeOf<T>();
            int offset = (int)src.Offset * stride;
            int size = result.Length * stride;
            _cmdBuffer.RequestAsyncReadbackIntoNativeArray(ref result, _buffers[src.Id], size, offset, request =>
            {
                Debug.Assert(request.done);
                // The user may have destroyed the event before the readback was completed, so we check if its still there.
                if (_inProgressRequests.Remove(id))
                {
                    if (request.hasError)
                    {
                        _failedRequests.Add(id);
                    }
                    else
                    {
                        _successfulRequests.Add(id);
                    }
                }
            });
            _inProgressRequests.Add(id);
        }

        public void WriteBuffer<T>(BufferSlice<T> dst, NativeArray<T> src)
            where T : struct
        {
            Debug.Assert(_buffers.ContainsKey(dst.Id), "Invalid buffer ID given.");

            _cmdBuffer.SetBufferData(_buffers[dst.Id], src, 0, (int)dst.Offset, src.Length);
        }

        public void WriteBuffer<T>(BufferSlice<T> dst, NativeArray<T> src, EventID id)
            where T : struct
        {
            Debug.Assert(_buffers.ContainsKey(dst.Id), "Invalid buffer ID given.");

            _cmdBuffer.SetBufferData(_buffers[dst.Id], src, 0, (int)dst.Offset, src.Length);

            _successfulRequests.Add(id);
        }

        public EventID CreateEvent()
        {
            var eventIdInteger = _nextFreeEventId++;
            var eventId = new EventID(eventIdInteger);
            return eventId;
        }

        public void DestroyEvent(EventID id)
        {
            if (_inProgressRequests.Contains(id))
            {
                _inProgressRequests.Remove(id);
            }
            if (_failedRequests.Contains(id))
            {
                _failedRequests.Remove(id);
            }
            if (_successfulRequests.Contains(id))
            {
                _successfulRequests.Remove(id);
            }
        }

        public GraphicsBuffer GetComputeBuffer(BufferID id)
        {
            Debug.Assert(_buffers.ContainsKey(id), "Invalid buffer ID given.");
            return _buffers[id];
        }

        public CommandBuffer GetCommandBuffer()
        {
            return _cmdBuffer;
        }

        // Temporary buffers are valid until the next call to Flush().
        public BufferID GetTemporaryBuffer(ulong count, ulong stride)
        {
            BufferID bufferID = CreateBuffer(count, stride);
            _temporaryBuffers.Add(bufferID);
            return bufferID;
        }

        private void ReleaseTemporaryBuffers()
        {
            foreach (var bufferId in _temporaryBuffers)
            {
                if (_buffers.ContainsKey(bufferId))
                {
                    DestroyBuffer(bufferId);
                }
            }
        }
    }
}