#include "main.h"

#include <Core/Application/ApplicationManager.h>
#include <Core/Common/EnumUtils.h>
#include <Core/Common/String.h>
#include <Core/HAL/Display/Display.h>
#include <Core/HAL/Event/SystemEvent.h>
#include <Core/HAL/Filesystem/Filesystem.h>
#include <Core/JulietInit.h>
#include <Core/Logging/LogManager.h>
#include <Core/Logging/LogTypes.h>
#include <Core/Math/Matrix.h>
#include <Core/Memory/MemoryArena.h>
#include <Core/Memory/Utils.h>
#include <cstdlib>
#include <Engine/Debug/MemoryDebugger.h>
#include <Graphics/Camera.h>
#include <Graphics/DebugDisplay.h>
#include <Graphics/Graphics.h>
#include <Graphics/GraphicsConfig.h>
#include <Graphics/GraphicsPipeline.h>
#include <Graphics/RenderPass.h>
#include <Juliet.h>

#ifdef JULIET_ENABLE_IMGUI
#include <Graphics/ImGuiRenderer.h>
#include <imgui.h>
#endif

static bool ShowMemoryDebugger = false;

// TODO : Replace with message box from framework + call main and not winmain + subsystem
// TODO : Think how to do the draw pipeline.
// Ex: Expose a Draw method ?
// Store a graphics context ?
// For now. Put everything in Update down below.
// Should split update from draw, update should have a != timestep than graphics (60fps or more)

// TODO : Remove main.h. Useless
// May be remove that Application class, useless too.

using namespace Juliet;

extern "C" {
__declspec(dllexport) extern const unsigned int D3D12SDKVersion = 615;
}
extern "C" {
__declspec(dllexport) extern const char* D3D12SDKPath = ".\\";
}

namespace
{
    using GameInit_t     = void (*)(GameInitParams*);
    using GameShutdown_t = void (*)(void);
    using GameUpdate_t   = void (*)(float deltaTime);
    struct GameFunctionTable
    {
        GameInit_t     Init     = nullptr;
        GameShutdown_t Shutdown = nullptr;
        GameUpdate_t   Update   = nullptr;
    } Game;

    const char* GameFunctionTable[] = { "GameInit", "GameShutdown", "GameUpdate" };
} // namespace

struct Vertex
{
    float Position[2];
    float Color[4];
};

void JulietApplication::Init()
{
    Log(LogLevel::Message, LogCategory::Tool, "Initializing Juliet Application...");

    Log(LogLevel::Message, LogCategory::Tool, "%s", CStr(GetBasePath()));

    GraphicsConfig config;
#if JULIET_DEBUG
    config.EnableDebug = true;
#endif
    GraphicsDevice = CreateGraphicsDevice(config);

    MainWindow = CreatePlatformWindow("Juliet Editor", 1280, 720);

    Running = MainWindow != nullptr && GraphicsDevice != nullptr;

    if (Running)
    {
        AttachToWindow(GraphicsDevice, MainWindow);

        {
            // Create graphics pipeline
            String           entryPoint = WrapString("main");
            ShaderCreateInfo shaderCI   = {};
            shaderCI.EntryPoint         = entryPoint;

            // TODO: Assets management that handles path to assets or something.
            String shaderPath    = WrapString("../../Assets/compiled/Triangle.vert.dxil");
            shaderCI.Stage       = ShaderStage::Vertex;
            Shader* vertexShader = CreateShader(GraphicsDevice, shaderPath, shaderCI);

            shaderPath             = WrapString("../../Assets/compiled/SolidColor.frag.dxil");
            shaderCI.Stage         = ShaderStage::Fragment;
            Shader* fragmentShader = CreateShader(GraphicsDevice, shaderPath, shaderCI);

            ColorTargetDescription colorTargetDescription = {};
            colorTargetDescription.Format                 = GetSwapChainTextureFormat(GraphicsDevice, MainWindow);

            GraphicsPipelineCreateInfo pipelineCI         = {};
            pipelineCI.VertexShader                       = vertexShader;
            pipelineCI.FragmentShader                     = fragmentShader;
            pipelineCI.PrimitiveType                      = PrimitiveType::TriangleList;
            pipelineCI.TargetInfo                         = { .ColorTargetDescriptions = &colorTargetDescription,
                                                              .NumColorTargets         = 1,
                                                              .DepthStencilFormat      = TextureFormat::D32_FLOAT,
                                                              .HasDepthStencilTarget   = true };
            pipelineCI.RasterizerState.FillMode           = FillMode::Solid;
            pipelineCI.DepthStencilState.EnableDepthTest  = true;
            pipelineCI.DepthStencilState.EnableDepthWrite = true;
            pipelineCI.DepthStencilState.CompareOperation = CompareOperation::Less;

            GraphicsPipeline = CreateGraphicsPipeline(GraphicsDevice, pipelineCI);
            if (GraphicsPipeline == nullptr)
            {
                LogError(LogCategory::Game, "Failed to create graphics pipeline!");
                Running = false;
            }

            // Create Depth Buffer
            TextureCreateInfo depthCI = {};
            depthCI.Type              = TextureType::Texture_2D;
            depthCI.Width             = 1280;
            depthCI.Height            = 720;
            depthCI.Format            = TextureFormat::D32_FLOAT;
            depthCI.Flags             = TextureUsageFlag::DepthStencilTarget;
            depthCI.LayerCount        = 1;
            depthCI.MipLevelCount     = 1;
            depthCI.SampleCount       = TextureSampleCount::One;
            DepthBuffer               = CreateTexture(GraphicsDevice, depthCI);
            if (DepthBuffer == nullptr)
            {
                LogError(LogCategory::Game, "Failed to create depth buffer!");
                Running = false;
            }

            // Create Buffers - Using StructuredBuffer for bindless SRV access in shader
            BufferCreateInfo bufferCI = {};
            bufferCI.Size             = 256;
            bufferCI.Usage            = BufferUsage::StructuredBuffer; // SRV for ResourceDescriptorHeap access
            ConstantBuffer            = CreateGraphicsBuffer(GraphicsDevice, bufferCI);

            TransferBufferCreateInfo transferCI = {};
            transferCI.Size                     = 256;
            transferCI.Usage                    = TransferBufferUsage::Upload;
            TransferBuffer                      = CreateGraphicsTransferBuffer(GraphicsDevice, transferCI);

            // Upload Static Data for Test
            if (TransferBuffer && ConstantBuffer)
            {
                void* data = MapGraphicsTransferBuffer(GraphicsDevice, TransferBuffer);
                if (data)
                {
                    Matrix projection = PerspectiveFov(60.0f * (3.14159f / 180.0f), 1200.0f / 800.0f, 0.1f, 1000.0f);
                    Matrix view       = LookAt({ 30.0f, 0.0f, 10.0f }, { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f });
                    Matrix model      = Matrix::Identity();
                    Matrix mvp        = projection * view * model;

                    MemCopy(data, &mvp, sizeof(Matrix));
                    UnmapGraphicsTransferBuffer(GraphicsDevice, TransferBuffer);

                    CommandList* initCmd = AcquireCommandList(GraphicsDevice);
                    CopyBuffer(initCmd, ConstantBuffer, TransferBuffer, 256);
                    TransitionBufferToReadable(initCmd, ConstantBuffer);
                    SubmitCommandLists(initCmd);
                    WaitUntilGPUIsIdle(GraphicsDevice);
                }
            }

            if (vertexShader)
            {
                DestroyShader(GraphicsDevice, vertexShader);
            }
            if (fragmentShader)
            {
                DestroyShader(GraphicsDevice, fragmentShader);
            }

            if (Running == false)
            {
                return;
            }
        }

        GameCode.Functions     = reinterpret_cast<void**>(&Game);
        GameCode.FunctionCount = ArraySize(GameFunctionTable);
        GameCode.FunctionNames = GameFunctionTable;
        InitHotReloadCode(GameCode, ConstString("Game.dll"), ConstString("Game_Temp.dll"), ConstString("lock.tmp"));
        if ((Running = GameCode.IsValid))
        {
            GameInitParams params;
            params.GameArena    = GetGameArena();
            params.ScratchArena = GetScratchArena();
            Game.Init(&params);
        }
    }
}

void JulietApplication::Shutdown()
{
    Log(LogLevel::Message, LogCategory::Tool, "Shutting down Juliet Application...");

    if (GameCode.IsValid)
    {
        Game.Shutdown();
        ShutdownHotReloadCode(GameCode);
    }

    if (GraphicsPipeline)
    {
        DestroyGraphicsPipeline(GraphicsDevice, GraphicsPipeline);
    }
    if (ConstantBuffer)
    {
        DestroyGraphicsBuffer(GraphicsDevice, ConstantBuffer);
    }
    if (TransferBuffer)
    {
        DestroyGraphicsTransferBuffer(GraphicsDevice, TransferBuffer);
    }
    if (DepthBuffer)
    {
        DestroyTexture(GraphicsDevice, DepthBuffer);
    }

    if (MainWindow && GraphicsDevice)
    {
        DetachFromWindow(GraphicsDevice, MainWindow);
    }

    if (MainWindow)
    {
        DestroyPlatformWindow(MainWindow);
    }

    if (GraphicsDevice)
    {
        DestroyGraphicsDevice(GraphicsDevice);
    }

    Log(LogLevel::Message, LogCategory::Tool, "Juliet App shutdown Completed");
}

void JulietApplication::Update()
{
    bool        reloadShaders         = false;
    static bool reloadShadersDebounce = false;

    SystemEvent evt;
    while (GetEvent(evt))
    {
        if (evt.Type == EventType::Window_Close_Request)
        {
            if (evt.Data.Window.AssociatedWindowID == GetWindowID(MainWindow))
            {
                Running = false;
            }
        }

        // Shader hot reload using keyboard.
        if (!reloadShadersDebounce && ((GetKeyModState() & KeyMod::Alt) != KeyMod::None) && IsKeyDown(ScanCode::R))
        {
            reloadShaders         = true;
            reloadShadersDebounce = true;
        }

        if (reloadShadersDebounce && !IsKeyDown(ScanCode::R))
        {
            reloadShadersDebounce = false;
        }
    }

#ifdef JULIET_ENABLE_IMGUI
    ImGui::ShowDemoWindow();
#endif

    // Debug display shapes - can be called from anywhere before engine flush
    DebugDisplay_DrawLine({ 0.0f, 0.0f, 0.0f }, { 10.0f, 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f }, false);
    DebugDisplay_DrawLine({ 0.0f, 0.0f, 0.0f }, { 0.0f, 10.0f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f }, true);
    DebugDisplay_DrawLine({ 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 10.0f }, { 0.0f, 0.0f, 1.0f, 1.0f }, true);
    DebugDisplay_DrawSphere({ 0.0f, 0.0f, 0.0f }, 5.0f, { 1.0f, 1.0f, 0.0f, 1.0f }, true);

    Game.Update(0.0f);

    if (ShouldReloadCode(GameCode))
    {
        ReloadCode(GameCode);
    }

    if (reloadShaders)
    {
        WaitUntilGPUIsIdle(GraphicsDevice);

#if ALLOW_SHADER_HOT_RELOAD
        String           entryPoint = WrapString("main");
        ShaderCreateInfo shaderCI   = {};
        shaderCI.EntryPoint         = entryPoint;
        String shaderPath           = WrapString("../../Assets/compiled/Triangle.vert.dxil");
        shaderCI.Stage              = ShaderStage::Vertex;
        Shader* vertexShader        = CreateShader(GraphicsDevice, shaderPath, shaderCI);

        shaderPath             = WrapString("../../Assets/compiled/SolidColor.frag.dxil");
        shaderCI.Stage         = ShaderStage::Fragment;
        Shader* fragmentShader = CreateShader(GraphicsDevice, shaderPath, shaderCI);

        UpdateGraphicsPipelineShaders(GraphicsDevice, GraphicsPipeline, vertexShader, fragmentShader);

        if (vertexShader)
        {
            DestroyShader(GraphicsDevice, vertexShader);
        }
        if (fragmentShader)
        {
            DestroyShader(GraphicsDevice, fragmentShader);
        }
#endif
    }

    // Memory debugger toggle
    static bool toggleDebounce = false;
    if (IsKeyDown(Juliet::ScanCode::Home))
    {
        if (!toggleDebounce)
        {
            ShowMemoryDebugger = !ShowMemoryDebugger;
            toggleDebounce     = true;
        }
    }
    else
    {
        toggleDebounce = false;
    }

    // Auto-close logic
    if (AutoCloseFrameCount > 0)
    {
        AutoCloseFrameCount--;
        if (AutoCloseFrameCount == 0)
        {
            Log(LogLevel::Message, LogCategory::Tool, "Auto-closing application as requested.");
            Running = false;
        }
    }

    if (ShowMemoryDebugger)
    {
        Debug::DebugDrawMemoryArena();
    }
}

void JulietApplication::OnPreRender(CommandList* cmd)
{
    // Buffer uploads
    if (ConstantBuffer && TransferBuffer)
    {
        void* ptr = MapGraphicsTransferBuffer(GraphicsDevice, TransferBuffer);
        if (ptr)
        {
            auto vertices = static_cast<Vertex*>(ptr);

            // Triangle 1
            vertices[0] = { { -0.5f, -0.5f }, { 1.0f, 0.0f, 0.0f, 1.0f } }; // Red
            vertices[1] = { { 0.0f, 0.5f }, { 0.0f, 1.0f, 0.0f, 1.0f } };   // Green
            vertices[2] = { { 0.5f, -0.5f }, { 0.0f, 0.0f, 1.0f, 1.0f } };  // Blue

            // Triangle 2
            vertices[3] = { { -0.5f, 0.5f }, { 1.0f, 1.0f, 0.0f, 1.0f } }; // Yellow
            vertices[4] = { { 0.0f, 0.8f }, { 0.0f, 1.0f, 1.0f, 1.0f } };  // Cyan
            vertices[5] = { { 0.5f, 0.5f }, { 1.0f, 0.0f, 1.0f, 1.0f } };  // Magenta

            UnmapGraphicsTransferBuffer(GraphicsDevice, TransferBuffer);
        }

        CopyBuffer(cmd, ConstantBuffer, TransferBuffer, 256);
        TransitionBufferToReadable(cmd, ConstantBuffer);
    }
}

void JulietApplication::OnRender(RenderPass* pass, CommandList* cmd)
{
    BindGraphicsPipeline(pass, GraphicsPipeline);

    uint32 descriptorIndex = GetDescriptorIndex(GraphicsDevice, ConstantBuffer);

    struct PushData
    {
        float  ViewProjection[16];
        uint32 BufferIndex;
    } pushData           = {};
    pushData.BufferIndex = descriptorIndex;

    SetPushConstants(cmd, ShaderStage::Vertex, 0, sizeof(pushData) / 4, &pushData);

    DrawPrimitives(pass, 6, 1, 0, 0);
}

ColorTargetInfo JulietApplication::GetColorTargetInfo(Texture* swapchainTexture)
{
    ColorTargetInfo info = {};
    info.TargetTexture   = swapchainTexture;
    info.ClearColor      = { .R = 0.0f, .G = 0.0f, .B = 0.0f, .A = 1.0f };
    info.LoadOperation   = LoadOperation::Clear;
    info.StoreOperation  = StoreOperation::Store;
    return info;
}

DepthStencilTargetInfo* JulietApplication::GetDepthTargetInfo()
{
    static DepthStencilTargetInfo info = {};
    info.TargetTexture                 = DepthBuffer;
    info.ClearDepth                    = 1.0f;
    info.LoadOperation                 = LoadOperation::Clear;
    info.StoreOperation                = StoreOperation::Store;
    return &info;
}

Camera JulietApplication::GetDebugCamera()
{
    static float orbitTime = 0.0f;
    orbitTime += 0.016f;

    float  radius   = 30.0f;
    Camera cam      = {};
    cam.Position    = { cosf(orbitTime) * radius, sinf(orbitTime) * radius, 10.0f };
    cam.Target      = { 0.0f, 0.0f, 0.0f };
    cam.Up          = { 0.0f, 0.0f, 1.0f };
    cam.FOV         = 1.047f;
    cam.AspectRatio = 1200.0f / 800.0f;
    cam.NearPlane   = 0.1f;
    cam.FarPlane    = 1000.0f;
    return cam;
}

bool JulietApplication::IsRunning()
{
    return Running;
}

namespace
{
    JulietApplication EditorApplication;
}

JulietApplication& GetEditorApplication()
{
    return EditorApplication;
}

int main(int /*argc*/, char** /*argv*/)
{
    // Allow only one instance to be launched.
    // Need to create Mutex code in the lib because i dont want to include windows.h in this file anymore
    // CreateMutex(0, false, L"Local\\Juliet.App");
    // if (GetLastError() == ERROR_ALREADY_EXISTS)
    // {
    //     MessageBox(nullptr, L"An instance of Juliet is already running.", L"Juliet", MB_OK | MB_ICONEXCLAMATION);
    //     return EXIT_FAILURE;
    // }

    setvbuf(stdout, nullptr, _IONBF, 0);

    if (__argc > 1)
    {
        for (int i = 1; i < __argc; ++i)
        {
            if (strcmp(__argv[i], "-autoclose") == 0 && (i + 1 < __argc))
            {
                int frames = atoi(__argv[i + 1]);
                EditorApplication.SetAutoCloseFrameCount(frames);
            }
        }
    }

    StartApplication(EditorApplication, JulietInit_Flags::Display);

    // Pause here to not close the console window immediatly on stop
    // Only pause if not in auto-close mode
    if (EditorApplication.GetAutoCloseFrameCount() == -1)
    {
        system("PAUSE");
    }

    return EXIT_SUCCESS;
}
