#include <Core/HAL/Filesystem/Filesystem.h>
#include <Core/HAL/IO/IOStream.h>
#include <Core/ImGui/ImGuiTests.h>
#include <Core/Logging/LogManager.h>
#include <Core/Logging/LogTypes.h>
#include <Graphics/Graphics.h>
#include <Graphics/GraphicsDevice.h>

namespace Juliet
{
    namespace Internal::Graphics
    {
        // TODO : IfDef new factories that are not compatible
        // Low chance of porting on something else than windows though. May be linux but will use vulkan that works on windows
        constexpr GraphicsDeviceFactory* Factories[] = { &DX12DeviceFactory };

        const GraphicsDeviceFactory* ChooseFactory(GraphicsConfig config)
        {
            // First try to check the preferred renderer from the config if any.
            if (config.PreferredDriver != DriverType::Any)
            {
                for (const GraphicsDeviceFactory* factory : Factories)
                {
                    // If the config has a preferred renderer, immediately pick it up.
                    if (factory->Type == config.PreferredDriver)
                    {
                        if (factory->CheckDriver())
                        {
                            return factory;
                        }
                    }
                }
            }

            // If not preferred renderer was set, use the first one that works in the list
            for (const GraphicsDeviceFactory* factory : Factories)
            {
                if (factory->Type == config.PreferredDriver)
                {
                    continue;
                }

                // TODO : Make sure it's supported by querying it first. If supported -> Lets go
                if (factory->CheckDriver())
                {
                    return factory;
                }
            }

            Log(LogLevel::Error, LogCategory::Graphics, "CreateGraphicsDevice::ChooseFactory: No valid driver found");
            return nullptr;
        }
    } // namespace Internal::Graphics

    GraphicsDevice* CreateGraphicsDevice(GraphicsConfig config)
    {
        if (const GraphicsDeviceFactory* chosenFactory = Internal::Graphics::ChooseFactory(config))
        {
            if (GraphicsDevice* newDevice = chosenFactory->CreateGraphicsDevice(config.EnableDebug))
            {

                newDevice->Name = chosenFactory->Name;
                return newDevice;
            }
        }
        return nullptr;
    }

    void DestroyGraphicsDevice(NonNullPtr<GraphicsDevice> device)
    {
        device->DestroyDevice(device);
    }

    bool AttachToWindow(NonNullPtr<GraphicsDevice> device, NonNullPtr<Window> window)
    {
        GPUDriver* driver = device->Driver;
        return device->AttachToWindow(driver, window);
    }

    void DetachFromWindow(NonNullPtr<GraphicsDevice> device, NonNullPtr<Window> window)
    {
        GPUDriver* driver = device->Driver;
        device->DetachFromWindow(driver, window);
    }

    bool AcquireSwapChainTexture(NonNullPtr<CommandList> commandList, NonNullPtr<Window> window, Texture** swapChainTexture)
    {
        auto header = reinterpret_cast<CommandListHeader*>(commandList.Get());

        if (header->Device->DebugEnabled)
        {
            bool error = false;
            if (header->Submitted)
            {
                error = true;
                Assert(false, "Cannot submit command list twice");
            }

            if (header->RenderPass.IsInProgress)
            {
                error = true;
                Assert(false, "Cannot submit command list twice");
            }

            if (error)
            {
                return false;
            }
        }

        header->Device->AcquireSwapChainTexture(commandList, window, swapChainTexture);
        if (swapChainTexture)
        {
            header->AcquiredSwapChain = true;
        }

        return true;
    }

    bool WaitAndAcquireSwapChainTexture(NonNullPtr<CommandList> commandList, NonNullPtr<Window> window, Texture** swapChainTexture)
    {
        auto header = reinterpret_cast<CommandListHeader*>(commandList.Get());

        if (header->Device->DebugEnabled)
        {
            bool error = false;
            if (header->Submitted)
            {
                error = true;
                Assert(false, "Cannot submit command list twice");
            }

            if (header->RenderPass.IsInProgress)
            {
                error = true;
                Assert(false, "Cannot submit command list twice");
            }

            if (error)
            {
                return false;
            }
        }

        header->Device->WaitAndAcquireSwapChainTexture(commandList, window, swapChainTexture);
        if (swapChainTexture)
        {
            header->AcquiredSwapChain = true;
        }

        return true;
    }

    bool WaitForSwapchain(NonNullPtr<GraphicsDevice> device, NonNullPtr<Window> window)
    {
        return device->WaitForSwapchain(device->Driver, window);
    }

    TextureFormat GetSwapChainTextureFormat(NonNullPtr<GraphicsDevice> device, NonNullPtr<Window> window)
    {
        return device->GetSwapChainTextureFormat(device->Driver, window);
    }

    Texture* CreateTexture(NonNullPtr<GraphicsDevice> device, const TextureCreateInfo& createInfo)
    {
        return device->CreateTexture(device->Driver, createInfo);
    }

    void DestroyTexture(NonNullPtr<GraphicsDevice> device, NonNullPtr<Texture> texture)
    {
        device->DestroyTexture(device->Driver, texture);
    }

    CommandList* AcquireCommandList(NonNullPtr<GraphicsDevice> device, QueueType queueType /* = QueueType::Graphics */)
    {
        GPUDriver*   driver  = device->Driver;
        CommandList* cmdList = device->AcquireCommandList(driver, queueType);
        if (!cmdList)
        {
            return nullptr;
        }

        auto header                    = reinterpret_cast<CommandListHeader*>(cmdList);
        header->Device                 = device.Get();
        header->RenderPass.CommandList = cmdList;
        header->Submitted              = false;

        return cmdList;
    }

    void SubmitCommandLists(NonNullPtr<CommandList> commandList)
    {
        auto* commandListHeader = reinterpret_cast<CommandListHeader*>(commandList.Get());

        commandListHeader->Submitted = true;

        commandListHeader->Device->SubmitCommandLists(commandList);
    }

    RenderPass* BeginRenderPass(NonNullPtr<CommandList> commandList, ColorTargetInfo& colorTargetInfo,
                                DepthStencilTargetInfo* depthStencilTargetInfo)
    {
        return BeginRenderPass(commandList, &colorTargetInfo, 1, depthStencilTargetInfo);
    }

    RenderPass* BeginRenderPass(NonNullPtr<CommandList> commandList, NonNullPtr<const ColorTargetInfo> colorTargetInfos,
                                uint32 colorTargetInfoCount, DepthStencilTargetInfo* depthStencilTargetInfo)
    {
        if (colorTargetInfoCount > GPUDriver::kMaxColorTargetInfo)
        {
            Log(LogLevel::Error, LogCategory::Graphics, "BeginRenderPass: ColorTargetInfoCount is > than kMaxColorTargetInfo");
            return nullptr;
        }

        auto* header = reinterpret_cast<CommandListHeader*>(commandList.Get());
        header->Device->BeginRenderPass(commandList, colorTargetInfos, colorTargetInfoCount, depthStencilTargetInfo);

        header->RenderPass.IsInProgress = true;
        return reinterpret_cast<RenderPass*>(&header->RenderPass);
    }

    void EndRenderPass(NonNullPtr<RenderPass> renderPass)
    {
        auto* commandList       = reinterpret_cast<GPUPass*>(renderPass.Get())->CommandList;
        auto* commandListHeader = reinterpret_cast<CommandListHeader*>(commandList);

        commandListHeader->Device->EndRenderPass(commandList);

        commandListHeader->RenderPass.IsInProgress = false;
    }

    void SetGraphicsViewPort(NonNullPtr<RenderPass> renderPass, const GraphicsViewPort& viewPort)
    {
        auto* commandList       = reinterpret_cast<GPUPass*>(renderPass.Get())->CommandList;
        auto* commandListHeader = reinterpret_cast<CommandListHeader*>(commandList);

        commandListHeader->Device->SetViewPort(commandList, viewPort);
    }

    void SetScissorRect(NonNullPtr<RenderPass> renderPass, const Rectangle& rectangle)
    {
        auto* commandList       = reinterpret_cast<GPUPass*>(renderPass.Get())->CommandList;
        auto* commandListHeader = reinterpret_cast<CommandListHeader*>(commandList);

        commandListHeader->Device->SetScissorRect(commandList, rectangle);
    }

    void SetBlendConstants(NonNullPtr<RenderPass> renderPass, FColor blendConstants)
    {
        auto* commandList       = reinterpret_cast<GPUPass*>(renderPass.Get())->CommandList;
        auto* commandListHeader = reinterpret_cast<CommandListHeader*>(commandList);

        commandListHeader->Device->SetBlendConstants(commandList, blendConstants);
    }

    void SetStencilReference(NonNullPtr<RenderPass> renderPass, uint8 reference)
    {
        auto* commandList       = reinterpret_cast<GPUPass*>(renderPass.Get())->CommandList;
        auto* commandListHeader = reinterpret_cast<CommandListHeader*>(commandList);

        commandListHeader->Device->SetStencilReference(commandList, reference);
    }

    void SetIndexBuffer(NonNullPtr<CommandList> commandList, NonNullPtr<GraphicsBuffer> buffer, IndexFormat format,
                        size_t indexCount, index_t offset)
    {
        auto* commandListHeader = reinterpret_cast<CommandListHeader*>(commandList.Get());

        if (commandListHeader->Device->SetIndexBuffer)
        {
            commandListHeader->Device->SetIndexBuffer(commandList, buffer, format, indexCount, offset);
        }
    }

    void BindGraphicsPipeline(NonNullPtr<RenderPass> renderPass, NonNullPtr<GraphicsPipeline> graphicsPipeline)
    {
        auto* commandList       = reinterpret_cast<GPUPass*>(renderPass.Get())->CommandList;
        auto* commandListHeader = reinterpret_cast<CommandListHeader*>(commandList);

        commandListHeader->Device->BindGraphicsPipeline(commandList, graphicsPipeline);
    }

    void DrawPrimitives(NonNullPtr<RenderPass> renderPass, uint32 numVertices, uint32 numInstances, uint32 firstVertex, uint32 firstInstance)
    {
        auto* commandList       = reinterpret_cast<GPUPass*>(renderPass.Get())->CommandList;
        auto* commandListHeader = reinterpret_cast<CommandListHeader*>(commandList);

        commandListHeader->Device->DrawPrimitives(commandList, numVertices, numInstances, firstVertex, firstInstance);
    }

    void DrawIndexedPrimitives(NonNullPtr<RenderPass> renderPass, uint32 numIndices, uint32 numInstances,
                               uint32 firstIndex, uint32 vertexOffset, uint32 firstInstance)
    {
        auto* commandList       = reinterpret_cast<GPUPass*>(renderPass.Get())->CommandList;
        auto* commandListHeader = reinterpret_cast<CommandListHeader*>(commandList);

        if (commandListHeader->Device->DrawIndexedPrimitives)
        {
            commandListHeader->Device->DrawIndexedPrimitives(commandList, numIndices, numInstances, firstIndex,
                                                             vertexOffset, firstInstance);
        }
    }

    void SetPushConstants(NonNullPtr<CommandList> commandList, ShaderStage stage, uint32 rootParameterIndex,
                          uint32 numConstants, const void* constants)
    {
        auto* header = reinterpret_cast<CommandListHeader*>(commandList.Get());
        header->Device->SetPushConstants(commandList, stage, rootParameterIndex, numConstants, constants);
    }

    // Fences
    bool WaitUntilGPUIsIdle(NonNullPtr<GraphicsDevice> device)
    {
        return device->WaitUntilGPUIsIdle(device->Driver);
    }

    // Shaders
    Shader* CreateShader(NonNullPtr<GraphicsDevice> device, String filename, ShaderCreateInfo& shaderCreateInfo)
    {
        ByteBuffer shaderByteCode = {};

        // Create path from filename
        if (IsAbsolutePath(filename))
        {
            shaderByteCode = LoadFile(filename);
        }
        else
        {
            // TODO: Add path builder in the lib
            String base = GetBasePath();
            char   inplaceBuffer[256];
            snprintf(inplaceBuffer, sizeof(inplaceBuffer), "%s%s", base.Data, filename.Data);
            String absolutePath = WrapString(inplaceBuffer);
            shaderByteCode      = LoadFile(absolutePath);
        }

        if (!IsValid(shaderByteCode))
        {
            return nullptr;
        }

        Shader* shader = device->CreateShader(device->Driver, shaderByteCode, shaderCreateInfo);

        Free(shaderByteCode);

        return shader;
    }

    void DestroyShader(NonNullPtr<GraphicsDevice> device, NonNullPtr<Shader> shader)
    {
        device->DestroyShader(device->Driver, shader);
    }

    GraphicsPipeline* CreateGraphicsPipeline(NonNullPtr<GraphicsDevice> device, const GraphicsPipelineCreateInfo& createInfo)
    {
        return device->CreateGraphicsPipeline(device->Driver, createInfo);
    }

    void DestroyGraphicsPipeline(NonNullPtr<GraphicsDevice> device, NonNullPtr<GraphicsPipeline> graphicsPipeline)
    {
        device->DestroyGraphicsPipeline(device->Driver, graphicsPipeline);
    }

#if ALLOW_SHADER_HOT_RELOAD
    bool UpdateGraphicsPipelineShaders(NonNullPtr<GraphicsDevice> device, NonNullPtr<GraphicsPipeline> graphicsPipeline,
                                       Shader* optional_vertexShader, Shader* optional_fragmentShader)
    {
        return device->UpdateGraphicsPipelineShaders(device->Driver, graphicsPipeline, optional_vertexShader, optional_fragmentShader);
    }
#endif

    GraphicsBuffer* CreateGraphicsBuffer(NonNullPtr<GraphicsDevice> device, const BufferCreateInfo& createInfo)
    {
        return device->CreateGraphicsBuffer(device->Driver, createInfo.Size, createInfo.Stride, createInfo.Usage, createInfo.IsDynamic);
    }

    GraphicsTransferBuffer* CreateGraphicsTransferBuffer(NonNullPtr<GraphicsDevice> device, const TransferBufferCreateInfo& createInfo)
    {
        return device->CreateGraphicsTransferBuffer(device->Driver, createInfo.Size, createInfo.Usage);
    }

    void* MapGraphicsBuffer(NonNullPtr<GraphicsDevice> device, NonNullPtr<GraphicsBuffer> buffer)
    {
        return device->MapGraphicsBuffer(device->Driver, buffer);
    }

    void UnmapGraphicsBuffer(NonNullPtr<GraphicsDevice> device, NonNullPtr<GraphicsBuffer> buffer)
    {
        device->UnmapGraphicsBuffer(device->Driver, buffer);
    }

    void* MapGraphicsTransferBuffer(NonNullPtr<GraphicsDevice> device, NonNullPtr<GraphicsTransferBuffer> buffer)
    {
        return device->MapGraphicsTransferBuffer(device->Driver, buffer);
    }

    void UnmapGraphicsTransferBuffer(NonNullPtr<GraphicsDevice> device, NonNullPtr<GraphicsTransferBuffer> buffer)
    {
        device->UnmapGraphicsTransferBuffer(device->Driver, buffer);
    }

    void CopyBuffer(NonNullPtr<CommandList> commandList, NonNullPtr<GraphicsBuffer> dst,
                    NonNullPtr<GraphicsTransferBuffer> src, size_t size, size_t dstOffset, size_t srcOffset)
    {
        auto* headers = reinterpret_cast<CommandListHeader*>(commandList.Get());
        headers->Device->CopyBuffer(commandList, dst, src, size, dstOffset, srcOffset);
    }

    void CopyBufferToTexture(NonNullPtr<CommandList> commandList, NonNullPtr<Texture> dst, NonNullPtr<GraphicsTransferBuffer> src)
    {
        auto* headers = reinterpret_cast<CommandListHeader*>(commandList.Get());
        if (headers->Device->CopyBufferToTexture)
        {
            headers->Device->CopyBufferToTexture(commandList, dst, src);
        }
    }

    void TransitionBufferToReadable(NonNullPtr<CommandList> commandList, NonNullPtr<GraphicsBuffer> buffer)
    {
        auto* header = reinterpret_cast<CommandListHeader*>(commandList.Get());
        header->Device->TransitionBufferToReadable(commandList, buffer);
    }

    uint32 GetDescriptorIndex(NonNullPtr<GraphicsDevice> device, NonNullPtr<GraphicsBuffer> buffer)
    {
        return device->GetDescriptorIndex(device, buffer);
    }

    uint32 GetDescriptorIndex(NonNullPtr<GraphicsDevice> device, NonNullPtr<Texture> texture)
    {
        return device->GetDescriptorIndexTexture(device, texture);
    }

    void DestroyGraphicsBuffer(NonNullPtr<GraphicsDevice> device, NonNullPtr<GraphicsBuffer> buffer)
    {
        device->DestroyGraphicsBuffer(buffer);
    }

    void DestroyGraphicsTransferBuffer(NonNullPtr<GraphicsDevice> device, NonNullPtr<GraphicsTransferBuffer> buffer)
    {
        device->DestroyGraphicsTransferBuffer(buffer);
    }
} // namespace Juliet
