using System;
using System.Runtime.InteropServices;
using RefreshCS;

namespace Nerfed.Runtime.Graphics;

/// <summary>
/// Render passes are begun in command buffers and are used to apply render state and issue draw calls.
/// Render passes are pooled and should not be referenced after calling EndRenderPass.
/// </summary>
public class RenderPass
{
    public nint Handle { get; private set; }

#if DEBUG
    internal uint colorAttachmentCount;
    internal SampleCount colorAttachmentSampleCount;
    internal TextureFormat colorFormatOne;
    internal TextureFormat colorFormatTwo;
    internal TextureFormat colorFormatThree;
    internal TextureFormat colorFormatFour;
    internal bool hasDepthStencilAttachment;
    internal SampleCount depthStencilAttachmentSampleCount;
    internal TextureFormat depthStencilFormat;

    GraphicsPipeline currentGraphicsPipeline;
#endif

    internal void SetHandle(nint handle)
    {
        Handle = handle;
    }

    /// <summary>
    /// Binds a graphics pipeline so that rendering work may be performed.
    /// </summary>
    /// <param name="graphicsPipeline">The graphics pipeline to bind.</param>
    public void BindGraphicsPipeline(GraphicsPipeline graphicsPipeline)
    {
#if DEBUG
        AssertRenderPassPipelineFormatMatch(graphicsPipeline);

        if (colorAttachmentCount > 0)
        {
            if (graphicsPipeline.SampleCount != colorAttachmentSampleCount)
            {
                throw new System.InvalidOperationException(
                    $"Sample count mismatch! Graphics pipeline sample count: {graphicsPipeline.SampleCount}, Color attachment sample count: {colorAttachmentSampleCount}"
                );
            }
        }

        if (hasDepthStencilAttachment)
        {
            if (graphicsPipeline.SampleCount != depthStencilAttachmentSampleCount)
            {
                throw new System.InvalidOperationException(
                    $"Sample count mismatch! Graphics pipeline sample count: {graphicsPipeline.SampleCount}, Depth stencil attachment sample count: {depthStencilAttachmentSampleCount}"
                );
            }
        }
#endif

        Refresh.Refresh_BindGraphicsPipeline(
            Handle,
            graphicsPipeline.Handle
        );

#if DEBUG
        currentGraphicsPipeline = graphicsPipeline;
#endif
    }

    /// <summary>
    /// Sets the viewport.
    /// </summary>
    public void SetViewport(in Viewport viewport)
    {
        Refresh.Refresh_SetViewport(
            Handle,
            viewport.ToRefresh()
        );
    }

    /// <summary>
    /// Sets the scissor area.
    /// </summary>
    public void SetScissor(in Rect scissor)
    {
#if DEBUG
        if (scissor.X < 0 || scissor.Y < 0 || scissor.W <= 0 || scissor.H <= 0)
        {
            throw new System.ArgumentOutOfRangeException("Scissor position cannot be negative and dimensions must be positive!");
        }
#endif

        Refresh.Refresh_SetScissor(
            Handle,
            scissor.ToRefresh()
        );
    }

    /// <summary>
    /// Binds vertex buffers to be used by subsequent draw calls.
    /// </summary>
    /// <param name="bufferBinding">Buffer to bind and associated offset.</param>
    /// <param name="firstBinding">The index of the first vertex input binding whose state is updated by the command.</param>
    public unsafe void BindVertexBuffer(
        in BufferBinding bufferBinding,
        uint firstBinding = 0
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
#endif

        Refresh.BufferBinding refreshBufferBinding = bufferBinding.ToRefresh();

        Refresh.Refresh_BindVertexBuffers(
            Handle,
            firstBinding,
            &refreshBufferBinding,
            1
        );
    }

    /// <summary>
    /// Binds an index buffer to be used by subsequent draw calls.
    /// </summary>
    /// <param name="indexBuffer">The index buffer to bind.</param>
    /// <param name="indexElementSize">The size in bytes of the index buffer elements.</param>
    /// <param name="offset">The offset index for the buffer.</param>
    public void BindIndexBuffer(
        BufferBinding bufferBinding,
        IndexElementSize indexElementSize
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
#endif

        Refresh.Refresh_BindIndexBuffer(
            Handle,
            bufferBinding.ToRefresh(),
            (Refresh.IndexElementSize)indexElementSize
        );
    }

    /// <summary>
    /// Binds samplers to be used by the vertex shader.
    /// </summary>
    /// <param name="textureSamplerBindings">The texture-sampler to bind.</param>
    public unsafe void BindVertexSampler(
        in TextureSamplerBinding textureSamplerBinding,
        uint slot = 0
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
        AssertTextureSamplerBindingNonNull(textureSamplerBinding);
        AssertTextureHasSamplerFlag(textureSamplerBinding.Texture);
#endif

        Refresh.TextureSamplerBinding refreshTextureSamplerBinding = textureSamplerBinding.ToRefresh();

        Refresh.Refresh_BindVertexSamplers(
            Handle,
            slot,
            &refreshTextureSamplerBinding,
            1
        );
    }

    public unsafe void BindVertexStorageTexture(
        in TextureSlice textureSlice,
        uint slot = 0
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
        AssertTextureNonNull(textureSlice.Texture);
        AssertTextureHasGraphicsStorageFlag(textureSlice.Texture);
#endif

        Refresh.TextureSlice refreshTextureSlice = textureSlice.ToRefresh();

        Refresh.Refresh_BindVertexStorageTextures(
            Handle,
            slot,
            &refreshTextureSlice,
            1
        );
    }

    public unsafe void BindVertexStorageBuffer(
        Buffer buffer,
        uint slot = 0
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
        AssertBufferNonNull(buffer);
        AssertBufferHasGraphicsStorageFlag(buffer);
#endif

        IntPtr bufferHandle = buffer.Handle;

        Refresh.Refresh_BindVertexStorageBuffers(
            Handle,
            slot,
            &bufferHandle,
            1
        );
    }

    public unsafe void BindFragmentSampler(
        in TextureSamplerBinding textureSamplerBinding,
        uint slot = 0
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
        AssertTextureSamplerBindingNonNull(textureSamplerBinding);
        AssertTextureHasSamplerFlag(textureSamplerBinding.Texture);
#endif

        Refresh.TextureSamplerBinding refreshTextureSamplerBinding = textureSamplerBinding.ToRefresh();

        Refresh.Refresh_BindFragmentSamplers(
            Handle,
            slot,
            &refreshTextureSamplerBinding,
            1
        );
    }

    public unsafe void BindFragmentStorageTexture(
        in TextureSlice textureSlice,
        uint slot = 0
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
        AssertTextureNonNull(textureSlice.Texture);
        AssertTextureHasGraphicsStorageFlag(textureSlice.Texture);
#endif

        Refresh.TextureSlice refreshTextureSlice = textureSlice.ToRefresh();

        Refresh.Refresh_BindFragmentStorageTextures(
            Handle,
            slot,
            &refreshTextureSlice,
            1
        );
    }

    public unsafe void BindFragmentStorageBuffer(
        Buffer buffer,
        uint slot = 0
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
        AssertBufferNonNull(buffer);
        AssertBufferHasGraphicsStorageFlag(buffer);
#endif

        IntPtr bufferHandle = buffer.Handle;

        Refresh.Refresh_BindFragmentStorageBuffers(
            Handle,
            slot,
            &bufferHandle,
            1
        );
    }

    /// <summary>
    /// Draws using a vertex buffer and an index buffer, and an optional instance count.
    /// </summary>
    /// <param name="baseVertex">The starting index offset for the vertex buffer.</param>
    /// <param name="startIndex">The starting index offset for the index buffer.</param>
    /// <param name="primitiveCount">The number of primitives to draw.</param>
    /// <param name="instanceCount">The number of instances to draw.</param>
    public void DrawIndexedPrimitives(
        uint baseVertex,
        uint startIndex,
        uint primitiveCount,
        uint instanceCount = 1
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
#endif

        Refresh.Refresh_DrawIndexedPrimitives(
            Handle,
            baseVertex,
            startIndex,
            primitiveCount,
            instanceCount
        );
    }

    /// <summary>
    /// Draws using a vertex buffer and an index buffer.
    /// </summary>
    /// <param name="baseVertex">The starting index offset for the vertex buffer.</param>
    /// <param name="startIndex">The starting index offset for the index buffer.</param>
    /// <param name="primitiveCount">The number of primitives to draw.</param>
    public void DrawPrimitives(
        uint vertexStart,
        uint primitiveCount
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
#endif

        Refresh.Refresh_DrawPrimitives(
            Handle,
            vertexStart,
            primitiveCount
        );
    }

    /// <summary>
    /// Similar to DrawPrimitives, but parameters are set from a buffer.
    /// The buffer must have the Indirect usage flag set.
    /// </summary>
    /// <param name="buffer">The draw parameters buffer.</param>
    /// <param name="offsetInBytes">The offset to start reading from the draw parameters buffer.</param>
    /// <param name="drawCount">The number of draw parameter sets that should be read from the buffer.</param>
    /// <param name="stride">The byte stride between sets of draw parameters.</param>
    public void DrawPrimitivesIndirect(
        Buffer buffer,
        uint offsetInBytes,
        uint drawCount,
        uint stride
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
#endif

        Refresh.Refresh_DrawPrimitivesIndirect(
            Handle,
            buffer.Handle,
            offsetInBytes,
            drawCount,
            stride
        );
    }

    /// <summary>
    /// Similar to DrawIndexedPrimitives, but parameters are set from a buffer.
    /// The buffer must have the Indirect usage flag set.
    /// </summary>
    /// <param name="buffer">The draw parameters buffer.</param>
    /// <param name="offsetInBytes">The offset to start reading from the draw parameters buffer.</param>
    /// <param name="drawCount">The number of draw parameter sets that should be read from the buffer.</param>
    /// <param name="stride">The byte stride between sets of draw parameters.</param>
    public void DrawIndexedPrimitivesIndirect(
        Buffer buffer,
        uint offsetInBytes,
        uint drawCount,
        uint stride
    )
    {
#if DEBUG
        AssertGraphicsPipelineBound();
#endif

        Refresh.Refresh_DrawIndexedPrimitivesIndirect(
            Handle,
            buffer.Handle,
            offsetInBytes,
            drawCount,
            stride
        );
    }

#if DEBUG
    private void AssertRenderPassPipelineFormatMatch(GraphicsPipeline graphicsPipeline)
    {
        for (int i = 0; i < graphicsPipeline.AttachmentInfo.ColorAttachmentDescriptions.Length; i += 1)
        {
            TextureFormat format;
            if (i == 0)
            {
                format = colorFormatOne;
            }
            else if (i == 1)
            {
                format = colorFormatTwo;
            }
            else if (i == 2)
            {
                format = colorFormatThree;
            }
            else
            {
                format = colorFormatFour;
            }

            TextureFormat pipelineFormat = graphicsPipeline.AttachmentInfo.ColorAttachmentDescriptions[i].Format;
            if (pipelineFormat != format)
            {
                throw new System.InvalidOperationException(
                    $"Color texture format mismatch! Pipeline expects {pipelineFormat}, render pass attachment is {format}"
                );
            }
        }

        if (graphicsPipeline.AttachmentInfo.HasDepthStencilAttachment)
        {
            TextureFormat pipelineDepthFormat = graphicsPipeline.AttachmentInfo.DepthStencilFormat;

            if (!hasDepthStencilAttachment)
            {
                throw new System.InvalidOperationException("Pipeline expects depth attachment!");
            }

            if (pipelineDepthFormat != depthStencilFormat)
            {
                throw new System.InvalidOperationException(
                    $"Depth texture format mismatch! Pipeline expects {pipelineDepthFormat}, render pass attachment is {depthStencilFormat}"
                );
            }
        }
    }

    private void AssertGraphicsPipelineBound(string message = "No graphics pipeline is bound!")
    {
        if (currentGraphicsPipeline == null)
        {
            throw new System.InvalidOperationException(message);
        }
    }

    private void AssertTextureNonNull(in TextureSlice textureSlice)
    {
        if (textureSlice.Texture == null)
        {
            throw new NullReferenceException("Texture must not be null!");
        }
    }

    private void AssertTextureSamplerBindingNonNull(in TextureSamplerBinding binding)
    {
        if (binding.Texture == null || binding.Texture.Handle == IntPtr.Zero)
        {
            throw new NullReferenceException("Texture binding must not be null!");
        }

        if (binding.Sampler == null || binding.Sampler.Handle == IntPtr.Zero)
        {
            throw new NullReferenceException("Sampler binding must not be null!");
        }
    }

    private void AssertTextureHasSamplerFlag(Texture texture)
    {
        if ((texture.UsageFlags & TextureUsageFlags.Sampler) == 0)
        {
            throw new System.ArgumentException("The bound Texture's UsageFlags must include TextureUsageFlags.Sampler!");
        }
    }

    private void AssertTextureHasGraphicsStorageFlag(Texture texture)
    {
        if ((texture.UsageFlags & TextureUsageFlags.GraphicsStorage) == 0)
        {
            throw new System.ArgumentException("The bound Texture's UsageFlags must include TextureUsageFlags.GraphicsStorage!");
        }
    }

    private void AssertBufferNonNull(Buffer buffer)
    {
        if (buffer == null || buffer.Handle == IntPtr.Zero)
        {
            throw new System.NullReferenceException("Buffer must not be null!");
        }
    }

    private void AssertBufferHasGraphicsStorageFlag(Buffer buffer)
    {
        if ((buffer.UsageFlags & BufferUsageFlags.GraphicsStorage) == 0)
        {
            throw new System.ArgumentException("The bound Buffer's UsageFlags must include BufferUsageFlag.GraphicsStorage!");
        }
    }
#endif
}
