#include <Core/Common/EnumUtils.h>
#include <Core/HAL/Display/DisplayDevice.h>
#include <Core/HAL/Display/Win32/Win32DisplayEvent.h>
#include <Core/HAL/Display/Win32/Win32Window.h>
#include <Core/HAL/Event/Keyboard_Private.h>
#include <Core/HAL/Event/Mouse_Private.h>
#include <Core/HAL/Event/SystemEvent.h>
#include <Core/HAL/Event/Win32ScanCode.h>
#include <Core/HAL/Event/WindowEvent.h>
#include <Core/HAL/Keyboard/KeyCode.h>
#include <Core/HAL/Win32.h>
#include <Core/Memory/Utils.h>

// For GET_X_LPARAM, GET_Y_LPARAM.
#include <windowsx.h>

#ifdef JULIET_ENABLE_IMGUI
#include <imgui.h> // Need For IMGUI_IMPL_API
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
#endif

namespace Juliet::Win32
{
    namespace
    {
        constexpr uint8 kPeekMessageLimit = 3;

        // Win32 Display Utils
        Window32State* GetWindowStateFromHandle(HWND handle)
        {
            if (auto* displayDevice = GetDisplayDevice())
            {
                for (auto& window : displayDevice->Windows)
                {
                    auto state = static_cast<Window32State*>(window.State);
                    if (state && state->Handle == handle)
                    {
                        return state;
                    }
                }
            }

            return nullptr;
        }

        // Win32 Keyboard Utils
        Key GetScanScodeFromWinScanCode(WPARAM /*wParam*/, LPARAM lParam)
        {
            Key key = {};

            uint16 flags    = HIWORD(lParam);
            uint16 scanCode = LOBYTE(flags);

            if (scanCode != 0)
            {
                // Use extended keyboard
                if ((flags & KF_EXTENDED) == KF_EXTENDED)
                {
                    // Transforms into the range of our scancodes
                    scanCode = MAKEWORD(scanCode, 0xe0);
                }
                else if (scanCode == 0x45)
                {
                    // Pause scancode in win32 is 0xE11D45 (for raw input) or 0x0045 for legacy KeyDown (which is the
                    // one we use) Break is 0x0046 (Ctrl + Pause). We merge it into one because nobody cares.
                    scanCode = 0x46;
                }
            }
            else
            {
                Assert(false, "Virtual Key : Unimplemented");
                // uint16 virtualKey = LOWORD(wParam);
            }

            // Pack the scancode to have everything within one byte.
            // Because scancode low byte are never bigger than 0x7F
            // We can use the most significant bit to store anything that is in the High Byte of the scancode
            uint8 index  = static_cast<uint8>(LOBYTE(scanCode) | (HIBYTE(scanCode) ? 0x80 : 0x00));
            key.ScanCode = Win32ToHIDUsagePageTable[index];
            key.Raw      = scanCode;

            return key;
        }

        static_assert(ArraySize(Win32ToHIDUsagePageTable) > sizeof(uint8));

        void ExtractMouseButtonState(uint64 timestamp, bool buttonPressed, MouseButton buttonFlag, bool swapButtons,
                                     NonNullPtr<Window32State> windowState, MouseButton button, MouseID mouseID)
        {
            if (swapButtons)
            {
                if (button == MouseButton::Left)
                {
                    button = MouseButton::Right;
                }
                else if (button == MouseButton::Right)
                {
                    button = MouseButton::Left;
                }
            }

            if (buttonPressed && ((buttonFlag & button) == MouseButton::None))
            {
                SendMouseButton(timestamp, windowState->Window, mouseID, button, true);
            }
            else if (!buttonPressed && ((buttonFlag & button) != MouseButton::None))
            {
                SendMouseButton(timestamp, windowState->Window, mouseID, button, false);
            }
        }

        WPARAM LastFrameButtonState = static_cast<WPARAM>(-1);
        void ExtractAllMouseButtonState(uint64 timestamp, WPARAM wParam, NonNullPtr<Window32State> windowState, MouseID mouseID)
        {
            if (wParam != LastFrameButtonState)
            {
                MouseButton buttonFlags = GetMouseButtonState();
                ExtractMouseButtonState(timestamp, (wParam & MK_LBUTTON), buttonFlags, false, windowState, MouseButton::Left, mouseID);
                ExtractMouseButtonState(timestamp, (wParam & MK_MBUTTON), buttonFlags, false, windowState,
                                        MouseButton::Middle, mouseID);
                ExtractMouseButtonState(timestamp, (wParam & MK_RBUTTON), buttonFlags, false, windowState, MouseButton::Right, mouseID);
                ExtractMouseButtonState(timestamp, (wParam & MK_XBUTTON1), buttonFlags, false, windowState,
                                        MouseButton::Button1, mouseID);
                ExtractMouseButtonState(timestamp, (wParam & MK_XBUTTON2), buttonFlags, false, windowState,
                                        MouseButton::Button2, mouseID);
                LastFrameButtonState = wParam;
            }
        }
    } // namespace

    void PumpEvents(NonNullPtr<DisplayDevice> /*self*/)
    {
        uint8 peekedMessageCount = 0;
        MSG   message            = {};
        while (PeekMessageA(&message, nullptr, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&message);
            DispatchMessageA(&message);

            // Since we peek at all messages of the program, it's possible that it stall here so we limit the number of peeked messages to an arbitrary limit
            if (++peekedMessageCount > kPeekMessageLimit)
            {
                break;
            }
        }
    }

    LRESULT CALLBACK Win32MainWindowCallback(HWND handle, UINT message, WPARAM wParam, LPARAM lParam)
    {
#ifdef JULIET_ENABLE_IMGUI
        if (ImGui_ImplWin32_WndProcHandler(handle, message, wParam, lParam))
        {
            return true;
        }
#endif

        LRESULT returnCode = -1;

        // Wait until the window state is created before doing anything
        auto* windowState = GetWindowStateFromHandle(handle);
        if (!windowState)
        {
            return CallWindowProcA(DefWindowProcA, handle, message, wParam, lParam);
        }

        switch (message)
        {
        case WM_CLOSE:
            {
                SendWindowEvent(windowState->Window, EventType::Window_Close_Request);
                returnCode = 0;
                break;
            }
        case WM_DESTROY:
            {
                PostQuitMessage(0);
                break;
            }

        case WM_SYSKEYDOWN:
        case WM_KEYDOWN:
            {
                Key key = GetScanScodeFromWinScanCode(wParam, lParam);

                // Catch Alt+F4 and close the window immediately
                if (key.ScanCode == ScanCode::F4 && ((GetKeyModState() & KeyMod::Alt) != KeyMod::None))
                {
                    SendWindowEvent(windowState->Window, EventType::Window_Close_Request);
                }

                SendKeyboardKey(1, kGlobalKeyboardID, key, KeyState::Down);

                returnCode = 0;
                break;
            }
        case WM_SYSKEYUP:
        case WM_KEYUP:
            {
                Key key = GetScanScodeFromWinScanCode(wParam, lParam);
                SendKeyboardKey(1, kGlobalKeyboardID, key, KeyState::Up);

                returnCode = 0;
                break;
            }

        case WM_MOUSEMOVE:
            {
                if (!windowState->IsMouseTracked)
                {
                    TRACKMOUSEEVENT MouseEvent;
                    MouseEvent.cbSize    = sizeof(TRACKMOUSEEVENT);
                    MouseEvent.hwndTrack = windowState->Handle;
                    MouseEvent.dwFlags   = TME_LEAVE;
                    TrackMouseEvent(&MouseEvent);

                    windowState->IsMouseTracked = true;
                }

                SendMouseMotion(1, windowState->Window, kGlobalMouseID, static_cast<float>(GET_X_LPARAM(lParam)),
                                static_cast<float>(GET_Y_LPARAM(lParam)));

                break;
            }

        case WM_LBUTTONUP:
        case WM_RBUTTONUP:
        case WM_MBUTTONUP:
        case WM_XBUTTONUP:
        case WM_LBUTTONDOWN:
        case WM_LBUTTONDBLCLK:
        case WM_RBUTTONDOWN:
        case WM_RBUTTONDBLCLK:
        case WM_MBUTTONDOWN:
        case WM_MBUTTONDBLCLK:
        case WM_XBUTTONDOWN:
        case WM_XBUTTONDBLCLK:
            {
                ExtractAllMouseButtonState(1, wParam, windowState, kGlobalMouseID);
                break;
            }

        case WM_MOUSELEAVE:
            {
                // TODO : Check focus
                const bool isNotMinimized = !IsIconic(handle);
                if (isNotMinimized)
                {
                    POINT cursorPos;
                    GetCursorPos(&cursorPos);
                    ScreenToClient(handle, &cursorPos);
                    SendMouseMotion(1, windowState->Window, kGlobalMouseID, static_cast<float>(cursorPos.x),
                                    static_cast<float>(cursorPos.y));
                }

                windowState->IsMouseTracked = false;

                returnCode = 0;
                break;
            }

        case WM_PAINT:
            {
                PAINTSTRUCT Paint;
                BeginPaint(handle, &Paint);
                EndPaint(handle, &Paint);
                break;
            }
        default: break;
        }

        if (returnCode >= 0)
        {
            return returnCode;
        }

        return CallWindowProcA(DefWindowProcA, handle, message, wParam, lParam);
    }
} // namespace Juliet::Win32
