#include "main.h"

#ifdef global
#undef global
#endif
#include <algorithm>
#include <ios>
#include <iosfwd>
#define global static

#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <Windows.h>
#endif

#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/Main.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/Mesh.h>
#include <Graphics/MeshRenderer.h>
#include <Graphics/RenderPass.h>
#include <Graphics/SkyboxRenderer.h>
#include <Graphics/VertexData.h>

#ifdef JULIET_ENABLE_IMGUI
#include <imgui.h>
#endif

static bool ShowMemoryDebugger = false;
static bool animateCubes       = true;
static bool animateLights      = true;
static bool animateCamera      = true;

static bool            freeCameraMode = false;
static float           camYaw         = 0.0f;
static float           camPitch       = 0.0f;
static Juliet::Vector3 camPos         = { 25.0f, 0.0f, 12.5f };

static float animateCubesTime  = 0.0f;
static float animateLightsTime = 0.0f;
static float animateCameraTime = 0.0f;

static float redLightRadius        = 10.0f;
static float redLightIntensity     = 5.0f;
static float redLightColor[3]      = { 1.0f, 0.2f, 0.2f };
static bool  redLightFollowsCamera = false;

static bool  enableGlobalLight      = true;
static float globalLightDir[3]      = { 0.5f, -1.0f, -0.5f };
static float globalLightColor[3]    = { 1.0f, 0.95f, 0.8f };
static float globalAmbientIntensity = 0.2f;

static float blueLightRadius    = 15.0f;
static float blueLightIntensity = 8.0f;
static float blueLightColor[3]  = { 0.2f, 0.2f, 1.0f };

// 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;

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" };

    LightID RedLightID  = 0;
    LightID BlueLightID = 0;
} // namespace

void JulietApplication::Init(NonNullPtr<Arena>)
{
    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", 1920, 1080);

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

    if (Running)
    {
        AttachToWindow(GraphicsDevice, MainWindow);
        {
            // Create Depth Buffer
            TextureCreateInfo depthCI = {};
            depthCI.Type              = TextureType::Texture_2D;
            depthCI.Width             = 1920;
            depthCI.Height            = 1080;
            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;
            }

            constexpr int   kGridSize = 10;
            constexpr float kSpacing  = 2.5f;
            constexpr float kOffset   = (kGridSize - 1) * kSpacing * 0.5f;

            for (int row = 0; row < kGridSize; ++row)
            {
                for (int col = 0; col < kGridSize; ++col)
                {
                    MeshID cube = AddCube();
                    float  x    = static_cast<float>(col) * kSpacing - kOffset;
                    float  y    = static_cast<float>(row) * kSpacing - kOffset;

                    float  seed     = static_cast<float>(row * kGridSize + col);
                    Matrix rotation = MatrixRotation(seed * 0.73f, seed * 1.17f, seed * 0.53f);
                    SetMeshTransform(cube, MatrixTranslation(x, y, 0.0f) * rotation);
                }
            }

            // Start with some default test lights
            PointLight redLight = {};
            redLight.Position   = { 5.0f, 5.0f, 2.0f };
            redLight.Radius     = redLightRadius;
            redLight.Color      = { redLightColor[0], redLightColor[1], redLightColor[2] };
            redLight.Intensity  = redLightIntensity;
            RedLightID          = AddPointLight(redLight);

            PointLight blueLight = {};
            blueLight.Position   = { -5.0f, 0.0f, 2.0f };
            blueLight.Radius     = blueLightRadius;
            blueLight.Color      = { blueLightColor[0], blueLightColor[1], blueLightColor[2] };
            blueLight.Intensity  = blueLightIntensity;
            BlueLightID          = AddPointLight(blueLight);
        }

        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 = GameArena = ArenaAllocate({} JULIET_DEBUG_ONLY(, "Game Arena"));
            params.ScratchArena = GameScratchArena = ArenaAllocate({} JULIET_DEBUG_ONLY(, "Scratch Arena"));
            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 (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()
{
    static LARGE_INTEGER frequency = {};
    static LARGE_INTEGER lastTime  = {};
    if (frequency.QuadPart == 0)
    {
        QueryPerformanceFrequency(&frequency);
        QueryPerformanceCounter(&lastTime);
    }

    LARGE_INTEGER currentTime;
    QueryPerformanceCounter(&currentTime);
    float deltaTime = static_cast<float>(currentTime.QuadPart - lastTime.QuadPart) / static_cast<float>(frequency.QuadPart);
    lastTime = currentTime;

    CameraTime += deltaTime;

    static float fpsTimer  = 0.0f;
    static int   fpsFrames = 0;
    fpsTimer += deltaTime;
    fpsFrames++;
    if (fpsTimer >= 0.5f)
    {
        float fps = static_cast<float>(fpsFrames) / fpsTimer;
        float ms  = (fpsTimer / static_cast<float>(fpsFrames)) * 1000.0f;
        char  title[64];
        snprintf(title, sizeof(title), "Juliet | %.1f FPS | %.2f ms", static_cast<double>(fps), static_cast<double>(ms));
        SetWindowTitle(MainWindow, WrapString(title));
        fpsTimer  = 0.0f;
        fpsFrames = 0;
    }

    bool        reloadShaders         = false;
    static bool reloadShadersDebounce = false;
    static bool f1Debounce            = 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;
        }
    }

    static bool firstFreeFrame = false;

    if (IsKeyDown(ScanCode::F1))
    {
        if (!f1Debounce)
        {
            freeCameraMode = !freeCameraMode;
            if (freeCameraMode)
            {
                firstFreeFrame = true;
            }
            f1Debounce = true;
        }
    }
    else
    {
        f1Debounce = false;
    }

    // Confine and hide the mouse for Free Camera mode
    if (freeCameraMode)
    {
#ifdef JULIET_ENABLE_IMGUI
        ImGui::SetMouseCursor(ImGuiMouseCursor_None);
#endif

#ifdef _WIN32
        HWND hwnd = GetForegroundWindow();
        if (hwnd)
        {
            RECT rect;
            GetClientRect(hwnd, &rect);
            POINT ptCenterClient = { (rect.right - rect.left) / 2, (rect.bottom - rect.top) / 2 };

            if (!firstFreeFrame)
            {
                POINT currentPos;
                GetCursorPos(&currentPos);
                ScreenToClient(hwnd, &currentPos);

                float deltaX = static_cast<float>(currentPos.x - ptCenterClient.x);
                float deltaY = static_cast<float>(currentPos.y - ptCenterClient.y);

                float sensitivity = 0.005f;
                // Plus because the mouse is inverted inherently by windows to screen coordinate
                camYaw += deltaX * sensitivity;
                camPitch -= deltaY * sensitivity;

                camPitch = std::min(camPitch, 1.5f);
                camPitch = std::max(camPitch, -1.5f);
            }
            firstFreeFrame = false;

            POINT ptCenterScreen = ptCenterClient;
            ClientToScreen(hwnd, &ptCenterScreen);
            SetCursorPos(ptCenterScreen.x, ptCenterScreen.y);
        }
#endif
    }

    if (freeCameraMode)
    {
        float speed = 10.0f * deltaTime;
        if ((GetKeyModState() & KeyMod::Shift) != KeyMod::None)
        {
            speed *= 3.0f;
        }

        Vector3 forward = { cosf(camYaw) * cosf(camPitch), sinf(camYaw) * cosf(camPitch), sinf(camPitch) };
        Vector3 right   = { cosf(camYaw + 1.5708f), sinf(camYaw + 1.5708f), 0.0f };
        Vector3 up      = { 0.0f, 0.0f, 1.0f };

        if (IsKeyDown(ScanCode::W))
        {
            camPos = camPos + forward * speed;
        }
        if (IsKeyDown(ScanCode::S))
        {
            camPos = camPos - forward * speed;
        }
        if (IsKeyDown(ScanCode::D))
        {
            camPos = camPos + right * speed;
        }
        if (IsKeyDown(ScanCode::A))
        {
            camPos = camPos - right * speed;
        }
        if (IsKeyDown(ScanCode::E))
        {
            camPos = camPos + up * speed;
        }
        if (IsKeyDown(ScanCode::Q))
        {
            camPos = camPos - up * speed;
        }
    }

    if (animateCubes)
    {
        animateCubesTime += deltaTime;
    }
    if (animateLights)
    {
        animateLightsTime += deltaTime;
    }
    if (animateCamera)
    {
        animateCameraTime += deltaTime;
    }

#ifdef JULIET_ENABLE_IMGUI
    ImGui::Begin("Debug Controls");
    ImGui::Checkbox("Animate Cubes", &animateCubes);
    ImGui::Checkbox("Animate Lights", &animateLights);
    ImGui::Checkbox("Animate Camera", &animateCamera);

    ImGui::Separator();
    ImGui::Text("Global Light");
    ImGui::Checkbox("Enable Global Light", &enableGlobalLight);
    if (enableGlobalLight)
    {
        ImGui::SliderFloat3("Direction", globalLightDir, -1.0f, 1.0f);
        ImGui::ColorEdit3("Color", globalLightColor);
        ImGui::SliderFloat("Ambient Intensity", &globalAmbientIntensity, 0.0f, 1.0f);
    }

    ImGui::Separator();
    ImGui::Text("Red Point Light");
    ImGui::ColorEdit3("Red Color", redLightColor);
    ImGui::SliderFloat("Red Radius", &redLightRadius, 1.0f, 50.0f);
    ImGui::SliderFloat("Red Intensity", &redLightIntensity, 0.0f, 50.0f);
    ImGui::Checkbox("Red Light Follows Camera", &redLightFollowsCamera);

    ImGui::Separator();
    ImGui::Text("Blue Point Light");
    ImGui::ColorEdit3("Blue Color", blueLightColor);
    ImGui::SliderFloat("Blue Radius", &blueLightRadius, 1.0f, 50.0f);
    ImGui::SliderFloat("Blue Intensity", &blueLightIntensity, 0.0f, 50.0f);

    ImGui::End();
#endif

    ArenaClear(GameScratchArena);

    Vector3 redLightPos  = { 5.0f, 5.0f, 2.0f };
    Vector3 blueLightPos = { -5.0f, 0.0f, 2.0f };

    if (animateLights || animateLightsTime > 0.0f)
    {
        redLightPos  = { cosf(animateLightsTime * 2.0f) * 5.0f, sinf(animateLightsTime * 2.0f) * 5.0f, 2.0f };
        blueLightPos = { -5.0f, cosf(animateLightsTime) * 3.0f, 2.0f };
    }

    if (redLightFollowsCamera)
    {
        Camera cam  = GetDebugCamera();
        redLightPos = cam.Position;
    }

    SetPointLightPosition(RedLightID, redLightPos);
    SetPointLightPosition(BlueLightID, blueLightPos);

    SetPointLightColor(RedLightID, { redLightColor[0], redLightColor[1], redLightColor[2] });
    SetPointLightRadius(RedLightID, redLightRadius);
    SetPointLightIntensity(RedLightID, redLightIntensity);

    SetPointLightColor(BlueLightID, { blueLightColor[0], blueLightColor[1], blueLightColor[2] });
    SetPointLightRadius(BlueLightID, blueLightRadius);
    SetPointLightIntensity(BlueLightID, blueLightIntensity);

    // Animate the 100 cubes (10x10 grid)
    constexpr int   kGridSize = 10;
    constexpr float kSpacing  = 2.5f;
    constexpr float kOffset   = (kGridSize - 1) * kSpacing * 0.5f;

    for (int row = 0; row < kGridSize; ++row)
    {
        for (int col = 0; col < kGridSize; ++col)
        {
            MeshID cube = static_cast<MeshID>(row * kGridSize + col); // Assuming they were added first
            float  x    = static_cast<float>(col) * kSpacing - kOffset;
            float  y    = static_cast<float>(row) * kSpacing - kOffset;

            float seed  = static_cast<float>(cube);
            float timeZ = animateCubesTime * 2.0f + seed * 0.5f;
            float z     = 0.0f;

            float  scaleF   = 1.0f;
            Matrix rotation = MatrixIdentity();

            if (animateCubes || animateCubesTime > 0.0f)
            {
                z      = sinf(timeZ) * 1.5f;                                 // Oscillate up and down
                scaleF = 1.0f + sinf(animateCubesTime * 1.5f + seed) * 0.3f; // Pulse scale
                rotation = MatrixRotation(animateCubesTime * 1.2f + seed * 0.73f, animateCubesTime * 0.8f + seed * 1.17f,
                                          animateCubesTime * 0.5f + seed * 0.53f);
            }
            Matrix scale = MatrixScale(scaleF, scaleF, scaleF);

            SetMeshTransform(cube, MatrixTranslation(x, y, z) * rotation * scale);
        }
    }

    DebugDisplay_DrawLine({ 0.0f, 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f }, false);
    DebugDisplay_DrawLine({ 0.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f }, true);
    DebugDisplay_DrawLine({ 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f, 1.0f, 1.0f }, true);
    DebugDisplay_DrawSphere({ 0.0f, 0.0f, 0.0f }, 0.5f, { 1.0f, 1.0f, 0.0f, 1.0f }, true);
    DebugDisplay_DrawSphere(blueLightPos, 0.5f, { 0.0f, 0.0f, 1.0f, 1.0f }, true);
    DebugDisplay_DrawSphere(redLightPos, 0.5f, { 1.0f, 0.0f, 0.0f, 1.0f }, true);

    Game.Update(0.0f);

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

    if (reloadShaders)
    {
        WaitUntilGPUIsIdle(GraphicsDevice);

#if ALLOW_SHADER_HOT_RELOAD
        ReloadMeshRendererShaders();
#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)
    {
#if JULIET_DEBUG
        Debug::DebugDrawMemoryArena();
#endif
    }

    ArenaClear(GameScratchArena);
}

void JulietApplication::OnPreRender(CommandList* /*cmd*/) {}

void JulietApplication::OnRender(RenderPass* pass, CommandList* cmd)
{
    PushData pushData       = {};
    pushData.ViewProjection = Camera_GetViewProjectionMatrix(GetDebugCamera());

    if (enableGlobalLight)
    {
        pushData.GlobalLightDirection   = Normalize({ globalLightDir[0], globalLightDir[1], globalLightDir[2] });
        pushData.GlobalLightColor       = { globalLightColor[0], globalLightColor[1], globalLightColor[2] };
        pushData.GlobalAmbientIntensity = globalAmbientIntensity;
    }
    else
    {
        pushData.GlobalLightDirection   = { 0.0f, -1.0f, 0.0f };
        pushData.GlobalLightColor       = { 0.0f, 0.0f, 0.0f };
        pushData.GlobalAmbientIntensity = 0.0f;
    }

    RenderSkybox(pass, cmd, pushData.ViewProjection);
    RenderMeshes(pass, cmd, pushData);
}

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()
{
    if (freeCameraMode)
    {
        Camera cam   = {};
        cam.Position = camPos;
        cam.Target   = camPos + Vector3{ cosf(camYaw) * cosf(camPitch), sinf(camYaw) * cosf(camPitch), sinf(camPitch) };
        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;
    }

    // --- Adjusted for 1-Meter Scale ---
    float baseRadius = 25.0f; // Increased to see 10x10 cube grid

    float radius = baseRadius;

    if (animateCamera || animateCameraTime > 0.0f)
    {
        float orbitSpeed       = 0.5f;
        float currentOrbitTime = animateCameraTime * orbitSpeed;

        float zoomAmplitude = 15.0f;
        float zoomSpeed     = 0.5f;
        radius              = baseRadius + (sinf(animateCameraTime * zoomSpeed) * zoomAmplitude);

        float zHeight = radius * 0.5f; // Keep a nice downward viewing angle

        Camera cam      = {};
        cam.Position    = { cosf(currentOrbitTime) * radius, sinf(currentOrbitTime) * radius, zHeight };
        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;
    }

    float zHeight = radius * 0.5f; // Keep a nice downward viewing angle

    Camera cam      = {};
    cam.Position    = { radius, 0.0f, zHeight };
    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 JulietMain(int argc, wchar_t** argv)
{
    if (argc > 1)
    {
        for (int i = 1; i < argc; ++i)
        {
            if (wcscmp(argv[i], L"-autoclose") == 0 && (i + 1 < argc))
            {
                int frames = _wtoi(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 && !Debug::IsDebuggerPresent())
    {
        system("PAUSE");
    }

    return EXIT_SUCCESS;
}
