/*
Anytime I edit any C++ Code, in specifically Win32 code i get acute seizures cause of the naming
conventions. I hope that whoever came up with these fuckass names gets fucked by a femboy. idk.

Anyways this is under the AGPL3.0 License so do whatever you want under that license terms.

Quick description of the app:
    - A hyrax picture which bounces around your desktop, like bonzi buddy but not destructive, more of a fun thing which you can open
    when you get bored.
    - Run the exe youll find out.
    - you can click and it will awawa

This is not malware or a virus, its purely for fun and entertainment purpouses.

rattatwinko - 04/05/26
*/

#include <windows.h>
#include <windowsx.h>
#include <gdiplus.h>
#include <objidl.h>
#include <mmsystem.h>
#include <math.h>
#include <cstring>
#include <cstdlib>
#include <ctime>
#include <cstdint>
#include <memory>
#include "resource.h"
#include "sound_handler.h"
#include "physics.h"

#pragma comment(lib, "gdiplus.lib")
#pragma comment(lib, "winmm.lib")

using namespace Gdiplus;

// consts
static constexpr UINT   TIMER_MS = 10;
static constexpr int    IMAGE_RESOURCE = 111;
static constexpr int    RESIZE_BORDER = 12;     // make wider so easier grab
static constexpr int    DRAG_THRESHOLD = 5;     // pixels before its a drag not a click

// globals
static ULONG_PTR     g_gdiplusToken = 0;
static Image* g_image = nullptr;        // normal hyrax
static Image* g_imageScream = nullptr;  // awawa hyrax
static int           g_winW = 640;
static int           g_winH = 480;
static LARGE_INTEGER g_freq = {};

// physics object (initial position 200,50 with zero velocity)
static physics g_physics(200.0, 50.0, 0.0, 0.0);

// resize / drag status
static bool   g_resizing = false;
static bool   g_dragging = false;
static bool   g_didDrag = false;
static int    g_dragOffsetX = 0;
static int    g_dragOffsetY = 0;
static POINT  g_lastMouse = {};
static POINT  g_clickOrigin = {};
static double g_lastMouseTime = 0.0;
static double g_lastTime = 0.0;

static bool   g_screaming = false;
static std::unique_ptr<sound_handler> g_sound;

// helper definitions
static double Now() {
    LARGE_INTEGER t;
    QueryPerformanceCounter(&t);
    return static_cast<double>(t.QuadPart) / static_cast<double>(g_freq.QuadPart);
}

// load any image from any rc resource type
static Image* LoadImageFromResource(HINSTANCE hInst, int resId, LPCWSTR resType) {
    HRSRC hRes = FindResource(hInst, MAKEINTRESOURCE(resId), resType);
    if (!hRes) return nullptr;

    DWORD size = SizeofResource(hInst, hRes);
    if (!size) return nullptr;

    HGLOBAL hData = LoadResource(hInst, hRes);
    if (!hData) return nullptr;

    void* pData = LockResource(hData);
    if (!pData) return nullptr;

    HGLOBAL hBuf = GlobalAlloc(GMEM_MOVEABLE, size);
    if (!hBuf) return nullptr;

    void* pBuf = GlobalLock(hBuf);
    if (!pBuf) { GlobalFree(hBuf); return nullptr; }

    memcpy(pBuf, pData, size);
    GlobalUnlock(hBuf);

    IStream* stream = nullptr;
    if (FAILED(CreateStreamOnHGlobal(hBuf, TRUE, &stream)) || !stream) {
        GlobalFree(hBuf);
        return nullptr;
    }

    Image* img = Image::FromStream(stream);
    stream->Release();

    if (!img || img->GetLastStatus() != Ok) {
        delete img;
        return nullptr;
    }

    return img;
}

// win api window proc
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {

        // init the window
    case WM_CREATE: {
        QueryPerformanceFrequency(&g_freq);
        srand(static_cast<unsigned>(time(nullptr)));

        GdiplusStartupInput si;
        GdiplusStartup(&g_gdiplusToken, &si, nullptr);

        HINSTANCE hInst = reinterpret_cast<CREATESTRUCT*>(lParam)->hInstance;

        g_image = LoadImageFromResource(hInst, IDR_RCDATA1, RT_RCDATA);
        g_imageScream = LoadImageFromResource(hInst, IDB_BITMAP1, L"JPG");

        if (g_image) {
            /*
            g_winW = static_cast<int>(g_image->GetWidth());
            g_winH = static_cast<int>(g_image->GetHeight());
            */
            g_winW = 480;
            g_winH = 480;
            SetWindowPos(hwnd, nullptr, 0, 0, g_winW, g_winH,
                SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
        }

        // sound handler
        g_sound = std::make_unique<sound_handler>(
            hInst,
            std::initializer_list<int>{
            IDR_WAVE1, IDR_WAVE2, IDR_WAVE3,
                IDR_WAVE4, IDR_WAVE5, IDR_WAVE6,
                IDR_WAVE7, IDR_WAVE8, IDR_WAVE9
        }
        );

        g_lastTime = Now();
        g_lastMouseTime = g_lastTime;

        SetTimer(hwnd, 1, TIMER_MS, nullptr);
        break;
    }

    // drag start
    case WM_LBUTTONDOWN: {
        g_dragging = true;
        g_didDrag = false;
        g_dragOffsetX = GET_X_LPARAM(lParam);
        g_dragOffsetY = GET_Y_LPARAM(lParam);
        g_physics.resetVelocity();                 // stop all motion

        GetCursorPos(&g_lastMouse);
        g_clickOrigin = g_lastMouse;
        g_lastMouseTime = Now();

        SetCapture(hwnd);
        break;
    }

    // drag move
    case WM_MOUSEMOVE: {
        if (!g_dragging) break;

        POINT p;
        GetCursorPos(&p);

        if (abs(p.x - g_clickOrigin.x) > DRAG_THRESHOLD ||
            abs(p.y - g_clickOrigin.y) > DRAG_THRESHOLD)
            g_didDrag = true;

        const double now = Now();
        const double dt = now - g_lastMouseTime;

        if (dt > 0.0) {
            double vx = (p.x - g_lastMouse.x) / dt;
            double vy = (p.y - g_lastMouse.y) / dt;
            g_physics.setVelocity(vx, vy);
        }

        g_lastMouse = p;
        g_lastMouseTime = now;

        g_physics.setPos(p.x - g_dragOffsetX, p.y - g_dragOffsetY);

        SetWindowPos(hwnd, nullptr, static_cast<int>(g_physics.x()), static_cast<int>(g_physics.y()),
            0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
        break;
    }

    // drag end or click
    case WM_LBUTTONUP: {
        if (!g_didDrag) {
            // play random sound
            if (g_sound)
                g_sound->PlayRandom();
        }
        g_dragging = false;
        ReleaseCapture();
        break;
    }

    // while dragging nutz accross fohead disable OS from doing shit
    case WM_ENTERSIZEMOVE:
        g_resizing = true;
        break;

    case WM_EXITSIZEMOVE: {
        g_resizing = false;
        g_physics.resetVelocity();

        // sync gx/y with resized cords
        RECT r;
        GetWindowRect(hwnd, &r);
        g_physics.setPos(static_cast<double>(r.left), static_cast<double>(r.top));

        g_lastTime = Now();  // after everything is clean then redraw
        break;
    }

    // weird asf logic
    case WM_NCHITTEST: {
        POINT p = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
        RECT  r;
        GetWindowRect(hwnd, &r);

        const bool left = p.x < r.left + RESIZE_BORDER;
        const bool right = p.x >= r.right - RESIZE_BORDER;
        const bool top = p.y < r.top + RESIZE_BORDER;
        const bool bottom = p.y >= r.bottom - RESIZE_BORDER;

        if (top && left)  return HTTOPLEFT;
        if (top && right) return HTTOPRIGHT;
        if (bottom && left)  return HTBOTTOMLEFT;
        if (bottom && right) return HTBOTTOMRIGHT;
        if (left)            return HTLEFT;
        if (right)           return HTRIGHT;
        if (top)             return HTTOP;
        if (bottom)          return HTBOTTOM;
        return HTCLIENT;
    }

    // sync dimensions after resizing
    case WM_SIZE:
        g_winW = LOWORD(lParam);
        g_winH = HIWORD(lParam);
        InvalidateRect(hwnd, nullptr, FALSE);
        break;

    // cursor, bunch of this dont work
    case WM_SETCURSOR:
        if (LOWORD(lParam) == HTCLIENT) {
            SetCursor(LoadCursor(nullptr, g_dragging ? IDC_SIZEALL : IDC_HAND));
            return TRUE;
        }
        return DefWindowProcW(hwnd, msg, wParam, lParam);

    // phyisks tick and scream face check
    case WM_TIMER: {
        const double now = Now();
        const double dt = now - g_lastTime;
        g_lastTime = now;

        if (!g_dragging && !g_resizing) {
            int screenW = GetSystemMetrics(SM_CXSCREEN);
            int screenH = GetSystemMetrics(SM_CYSCREEN);
            g_physics.update(dt, screenW, screenH, g_winW, g_winH);
            SetWindowPos(hwnd, nullptr, static_cast<int>(g_physics.x()), static_cast<int>(g_physics.y()),
                0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
        }

        // use sound handler instead of doing this in main
        if (g_sound) {
            g_sound->Update(now);
            bool shouldScream = (g_imageScream && g_sound->IsAwAwA(now));
            if (shouldScream != g_screaming) {
                g_screaming = shouldScream;
                InvalidateRect(hwnd, nullptr, FALSE);
            }
        }

        break;
    }

    // pablo picasso
    case WM_PAINT: {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);

        // db to avoid me floppin on the floor like a fish (cause of flashing lights)
        HDC     memDC = CreateCompatibleDC(hdc);
        HBITMAP memBmp = CreateCompatibleBitmap(hdc, g_winW, g_winH);
        HGDIOBJ old = SelectObject(memDC, memBmp);

        Graphics g(memDC);
        g.SetInterpolationMode(InterpolationModeHighQualityBicubic);

        // swap to scream face on peaks
        Image* frame = (g_screaming && g_imageScream) ? g_imageScream : g_image;
        if (frame)
            g.DrawImage(frame, 0, 0, g_winW, g_winH);

        BitBlt(hdc, 0, 0, g_winW, g_winH, memDC, 0, 0, SRCCOPY);

        SelectObject(memDC, old);
        DeleteObject(memBmp);
        DeleteDC(memDC);

        EndPaint(hwnd, &ps);
        break;
    }

    // escape destroy
    case WM_KEYDOWN:
        if (wParam == VK_ESCAPE)
            DestroyWindow(hwnd);
        else if (wParam == VK_SPACE)
            DestroyWindow(hwnd);
        break;


    // consuela
    case WM_DESTROY:
        KillTimer(hwnd, 1);
        g_sound.reset();               // clean up sound handler
        delete g_image;
        delete g_imageScream;
        g_image = g_imageScream = nullptr;
        GdiplusShutdown(g_gdiplusToken);
        PostQuitMessage(0);
        break;

    default:
        return DefWindowProcW(hwnd, msg, wParam, lParam);
    }

    return 0;
}

// right around the entry point (reference to asshair guy)
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
    WNDCLASSW wc = {};
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = L"Hyrax";
    wc.hCursor = LoadCursor(nullptr, IDC_HAND);
    RegisterClassW(&wc);

    HWND hwnd = CreateWindowExW(
        WS_EX_TOPMOST,
        L"Hyrax", L"",
        WS_POPUP,
        static_cast<int>(g_physics.x()), static_cast<int>(g_physics.y()),
        g_winW, g_winH,
        nullptr, nullptr, hInstance, nullptr
    );

    if (!hwnd)
        return 1;

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg;
    while (GetMessageW(&msg, nullptr, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    return static_cast<int>(msg.wParam);
}