/*
 * Horse - a desktop prank for Windows (XP / Vista and up).
 *
 * A horse (horse.bmp, magenta = transparent) spawns on screen, falls with
 * gravity, and can be dragged and thrown around. Throw it fast enough and it
 * neighs (horse.wav). Drop files onto it and it grows while it munches
 * (chew.wav). Lives in the taskbar AND the tray (right-click -> Exit).
 * Multiple instances allowed.
 *
 * Pure Win32 + GDI + winmm. No third-party libraries.
 *
 * The bitmap and both sounds are embedded as resources (horse.rc), so the
 * built horse.exe is fully self-contained -- no loose files needed.
 *
 * -static* keeps the GCC/C++ runtime in the exe, so no libgcc/libstdc++
 * DLLs are needed on the target machine.
 *
 * Build (64-bit):
 *   windres horse.rc -O coff -o horse_res.o
 *   g++ -O2 -mwindows -static -static-libgcc -static-libstdc++ horse.cpp horse_res.o -o horse.exe -lwinmm -lgdi32 -lshell32
 *
 * Build (32-bit, for 32-bit XP/Vista) with the i686 toolchain on PATH:
 *   windres horse.rc -O coff -o horse_res32.o
 *   g++ -O2 -mwindows -static -static-libgcc -static-libstdc++ horse.cpp horse_res32.o -o horse_x86.exe -lwinmm -lgdi32 -lshell32
 */

#include <windows.h>
#include <shellapi.h>
#include <mmsystem.h>
#include <cstdio>
#include <cmath>
#include "resource.h"

static HINSTANCE g_hInst = NULL;   /* module that owns the embedded resources */

#define WM_TRAYICON   (WM_APP + 1)
#define IDM_EXIT      1001
#define IDM_NEIGH     1002
#define TIMER_PHYSICS 1
#define TRANSPARENT_KEY RGB(255, 0, 255)   /* magenta */

/* ---- tunable physics constants ---- */
static const double GRAVITY      = 1.20;
static const double RESTITUTION  = 0.55;   /* vertical bounce energy kept   */
static const double WALL_DAMP    = 0.60;   /* horizontal bounce energy kept */
static const double GROUND_FRIC  = 0.80;   /* slowdown while sliding        */
static const double AIR_FRIC     = 0.995;  /* horizontal air drag           */
static const double THROW_NEIGH  = 34.0;   /* throw speed that triggers neigh */
static const double SCALE_STEP   = 1.18;   /* growth per dropped file        */
static const double SCALE_MAX    = 8.0;
static const double SCALE_MIN    = 0.40;
static const int    BASE_HEIGHT  = 40;     /* small horse height at scale 1  */

struct Horse {
    HBITMAP bmp;
    int     srcW, srcH;        /* native bitmap pixel size       */
    int     baseW, baseH;      /* small display size at scale 1  */
    int     curW, curH;        /* current scaled window size     */
    double  scale;
    double  x, y;              /* window top-left (screen coords)*/
    double  vx, vy;            /* velocity in px/tick            */
    bool    dragging;
    int     grabX, grabY;      /* cursor offset within window    */
    double  smVx, smVy;        /* smoothed drag velocity         */
    DWORD   lastMoveT;         /* tick of last drag sample       */
    NOTIFYICONDATAA nid;
};

static void playSoundRes(int resId)
{
    PlaySoundA(MAKEINTRESOURCEA(resId), g_hInst,
               SND_RESOURCE | SND_ASYNC | SND_NODEFAULT);
}

/* Build a 32x32 HICON from the horse bitmap, using magenta as transparent. */
static HICON makeHorseIcon(HBITMAP src, int srcW, int srcH)
{
    const int S = 32;
    HDC screen  = GetDC(NULL);
    HDC srcDC   = CreateCompatibleDC(screen);
    HDC colorDC = CreateCompatibleDC(screen);
    HDC maskDC  = CreateCompatibleDC(screen);

    HBITMAP color = CreateCompatibleBitmap(screen, S, S);
    HBITMAP mask  = CreateBitmap(S, S, 1, 1, NULL);   /* monochrome */

    HBITMAP oldSrc   = (HBITMAP)SelectObject(srcDC, src);
    HBITMAP oldColor = (HBITMAP)SelectObject(colorDC, color);
    HBITMAP oldMask  = (HBITMAP)SelectObject(maskDC, mask);

    SetStretchBltMode(colorDC, HALFTONE);
    StretchBlt(colorDC, 0, 0, S, S, srcDC, 0, 0, srcW, srcH, SRCCOPY);

    /* mask: white(1) where the color image is magenta (transparent) */
    SetBkColor(colorDC, TRANSPARENT_KEY);
    BitBlt(maskDC, 0, 0, S, S, colorDC, 0, 0, SRCCOPY);

    /* black out the transparent pixels in the color image */
    SetBkColor(colorDC, RGB(0, 0, 0));
    SetTextColor(colorDC, RGB(255, 255, 255));
    BitBlt(colorDC, 0, 0, S, S, maskDC, 0, 0, SRCAND);

    SelectObject(srcDC, oldSrc);
    SelectObject(colorDC, oldColor);
    SelectObject(maskDC, oldMask);

    ICONINFO ii;
    ii.fIcon    = TRUE;
    ii.xHotspot = 0;
    ii.yHotspot = 0;
    ii.hbmMask  = mask;
    ii.hbmColor = color;
    HICON icon = CreateIconIndirect(&ii);

    DeleteObject(color);
    DeleteObject(mask);
    DeleteDC(srcDC);
    DeleteDC(colorDC);
    DeleteDC(maskDC);
    ReleaseDC(NULL, screen);
    return icon;
}

static void getWorkArea(RECT *r)
{
    if (!SystemParametersInfo(SPI_GETWORKAREA, 0, r, 0)) {
        r->left = r->top = 0;
        r->right  = GetSystemMetrics(SM_CXSCREEN);
        r->bottom = GetSystemMetrics(SM_CYSCREEN);
    }
}

static void applyScale(HWND hwnd, Horse *h, double newScale)
{
    if (newScale > SCALE_MAX) newScale = SCALE_MAX;
    if (newScale < SCALE_MIN) newScale = SCALE_MIN;

    /* keep the horse centred on its current centre */
    double cx = h->x + h->curW / 2.0;
    double cy = h->y + h->curH / 2.0;

    h->scale = newScale;
    h->curW  = (int)(h->baseW * h->scale);
    h->curH  = (int)(h->baseH * h->scale);

    h->x = cx - h->curW / 2.0;
    h->y = cy - h->curH / 2.0;

    SetWindowPos(hwnd, NULL, (int)h->x, (int)h->y, h->curW, h->curH,
                 SWP_NOZORDER | SWP_NOACTIVATE);
    InvalidateRect(hwnd, NULL, FALSE);
}

static void stepPhysics(HWND hwnd, Horse *h)
{
    RECT wa; getWorkArea(&wa);

    h->vy += GRAVITY;
    h->vx *= AIR_FRIC;

    h->x += h->vx;
    h->y += h->vy;

    double floorY = wa.bottom - h->curH;
    double ceilY  = wa.top;
    double leftX  = wa.left;
    double rightX = wa.right - h->curW;

    /* floor */
    if (h->y > floorY) {
        h->y  = floorY;
        h->vy = -h->vy * RESTITUTION;
        h->vx *= GROUND_FRIC;
        if (fabs(h->vy) < GRAVITY * 1.5) h->vy = 0.0;
        if (fabs(h->vx) < 0.4)           h->vx = 0.0;
    }
    /* ceiling */
    if (h->y < ceilY) {
        h->y  = ceilY;
        h->vy = -h->vy * RESTITUTION;
    }
    /* walls */
    if (h->x < leftX) {
        h->x  = leftX;
        h->vx = -h->vx * WALL_DAMP;
    } else if (h->x > rightX) {
        h->x  = rightX;
        h->vx = -h->vx * WALL_DAMP;
    }

    MoveWindow(hwnd, (int)h->x, (int)h->y, h->curW, h->curH, FALSE);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    Horse *h = (Horse *)GetWindowLongPtr(hwnd, GWLP_USERDATA);

    switch (msg) {
    case WM_CREATE: {
        CREATESTRUCT *cs = (CREATESTRUCT *)lParam;
        h = (Horse *)cs->lpCreateParams;
        SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)h);
        DragAcceptFiles(hwnd, TRUE);
        SetTimer(hwnd, TIMER_PHYSICS, 16, NULL);
        return 0;
    }

    case WM_TIMER:
        if (h && !h->dragging) stepPhysics(hwnd, h);
        return 0;

    case WM_PAINT: {
        PAINTSTRUCT ps;
        HDC dc = BeginPaint(hwnd, &ps);
        if (h) {
            HDC mem = CreateCompatibleDC(dc);
            HBITMAP old = (HBITMAP)SelectObject(mem, h->bmp);
            SetStretchBltMode(dc, COLORONCOLOR);
            StretchBlt(dc, 0, 0, h->curW, h->curH,
                       mem, 0, 0, h->srcW, h->srcH, SRCCOPY);
            SelectObject(mem, old);
            DeleteDC(mem);
        }
        EndPaint(hwnd, &ps);
        return 0;
    }

    case WM_LBUTTONDOWN: {
        if (!h) return 0;
        SetCapture(hwnd);
        h->dragging = true;
        POINT p; GetCursorPos(&p);
        h->grabX = p.x - (int)h->x;
        h->grabY = p.y - (int)h->y;
        h->smVx = h->smVy = 0.0;
        h->vx = h->vy = 0.0;
        h->lastMoveT = GetTickCount();
        return 0;
    }

    case WM_MOUSEMOVE: {
        if (!h || !h->dragging) return 0;
        POINT p; GetCursorPos(&p);
        double nx = p.x - h->grabX;
        double ny = p.y - h->grabY;
        /* time-normalized velocity: pixels per 16 ms tick, so a fast flick
           reads its true speed regardless of how often the OS fires moves. */
        DWORD now = GetTickCount();
        double dt = (double)(now - h->lastMoveT);
        if (dt < 1.0) dt = 1.0;
        double instVx = (nx - h->x) / dt * 16.0;
        double instVy = (ny - h->y) / dt * 16.0;
        h->lastMoveT = now;
        /* light smoothing keeps jitter out without killing the peak */
        h->smVx = h->smVx * 0.3 + instVx * 0.7;
        h->smVy = h->smVy * 0.3 + instVy * 0.7;
        h->x = nx;
        h->y = ny;
        MoveWindow(hwnd, (int)h->x, (int)h->y, h->curW, h->curH, FALSE);
        return 0;
    }

    case WM_LBUTTONUP: {
        if (!h || !h->dragging) return 0;
        h->dragging = false;
        ReleaseCapture();
        h->vx = h->smVx;
        h->vy = h->smVy;
        double speed = sqrt(h->vx * h->vx + h->vy * h->vy);
        if (speed > THROW_NEIGH)
            playSoundRes(IDW_NEIGH);
        return 0;
    }

    case WM_LBUTTONDBLCLK:
        playSoundRes(IDW_NEIGH);
        return 0;

    case WM_DROPFILES: {
        if (!h) return 0;
        HDROP drop = (HDROP)wParam;
        UINT n = DragQueryFileA(drop, 0xFFFFFFFF, NULL, 0);
        DragFinish(drop);
        if (n > 0) {
            applyScale(hwnd, h, h->scale * SCALE_STEP);
            playSoundRes(IDW_CHEW);
        }
        return 0;
    }

    case WM_TRAYICON:
        if (LOWORD(lParam) == WM_RBUTTONUP ||
            LOWORD(lParam) == WM_CONTEXTMENU) {
            POINT p; GetCursorPos(&p);
            HMENU menu = CreatePopupMenu();
            AppendMenuA(menu, MF_STRING, IDM_NEIGH, "Neigh!");
            AppendMenuA(menu, MF_SEPARATOR, 0, NULL);
            AppendMenuA(menu, MF_STRING, IDM_EXIT, "Exit");
            SetForegroundWindow(hwnd);   /* so the menu dismisses correctly */
            TrackPopupMenu(menu, TPM_RIGHTBUTTON | TPM_BOTTOMALIGN,
                           p.x, p.y, 0, hwnd, NULL);
            PostMessage(hwnd, WM_NULL, 0, 0);
            DestroyMenu(menu);
        } else if (LOWORD(lParam) == WM_LBUTTONDBLCLK) {
            playSoundRes(IDW_NEIGH);
        }
        return 0;

    case WM_COMMAND:
        switch (LOWORD(wParam)) {
        case IDM_EXIT:
            DestroyWindow(hwnd);
            return 0;
        case IDM_NEIGH:
            playSoundRes(IDW_NEIGH);
            return 0;
        }
        return 0;

    case WM_DESTROY:
        if (h) {
            KillTimer(hwnd, TIMER_PHYSICS);
            Shell_NotifyIconA(NIM_DELETE, &h->nid);
        }
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR lpCmd, int nShow)
{
    (void)hPrev; (void)lpCmd;

    g_hInst = hInst;

    static Horse horse;
    ZeroMemory(&horse, sizeof(horse));
    horse.scale = 1.0;
    horse.dragging = false;

    horse.bmp = (HBITMAP)LoadImageA(hInst, MAKEINTRESOURCEA(IDB_HORSE),
                                    IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
    if (!horse.bmp) {
        MessageBoxA(NULL, "Could not load the embedded horse bitmap.",
                    "Horse", MB_ICONERROR);
        return 1;
    }

    BITMAP bm;
    GetObject(horse.bmp, sizeof(bm), &bm);
    horse.srcW  = bm.bmWidth;
    horse.srcH  = bm.bmHeight;
    /* small display size at scale 1.0, aspect-ratio preserved */
    horse.baseH = BASE_HEIGHT;
    horse.baseW = (int)((double)bm.bmWidth / bm.bmHeight * BASE_HEIGHT + 0.5);
    horse.curW  = horse.baseW;
    horse.curH  = horse.baseH;

    HICON icon = makeHorseIcon(horse.bmp, horse.srcW, horse.srcH);
    if (!icon) icon = LoadIcon(NULL, IDI_APPLICATION);

    WNDCLASSEXA wc;
    ZeroMemory(&wc, sizeof(wc));
    wc.cbSize        = sizeof(wc);
    wc.lpfnWndProc   = WndProc;
    wc.hInstance     = hInst;
    wc.hCursor       = LoadCursor(NULL, IDC_HAND);
    wc.hbrBackground = (HBRUSH)CreateSolidBrush(TRANSPARENT_KEY);
    wc.lpszClassName = "HorseWindowClass";
    wc.hIcon         = icon;
    wc.hIconSm       = icon;
    RegisterClassExA(&wc);

    /* start centred horizontally, near the top, so it falls in */
    RECT wa; getWorkArea(&wa);
    horse.x = wa.left + (wa.right - wa.left - horse.curW) / 2.0;
    horse.y = wa.top + 10;

    HWND hwnd = CreateWindowExA(
        WS_EX_LAYERED | WS_EX_APPWINDOW,
        "HorseWindowClass", "Horse",
        WS_POPUP,
        (int)horse.x, (int)horse.y, horse.curW, horse.curH,
        NULL, NULL, hInst, &horse);

    if (!hwnd) {
        MessageBoxA(NULL, "Could not create the horse window.", "Horse", MB_ICONERROR);
        return 1;
    }

    SetLayeredWindowAttributes(hwnd, TRANSPARENT_KEY, 0, LWA_COLORKEY);
    SendMessage(hwnd, WM_SETICON, ICON_BIG,   (LPARAM)icon);
    SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)icon);

    /* tray icon */
    ZeroMemory(&horse.nid, sizeof(horse.nid));
    horse.nid.cbSize           = sizeof(horse.nid);
    horse.nid.hWnd             = hwnd;
    horse.nid.uID              = 1;
    horse.nid.uFlags           = NIF_ICON | NIF_MESSAGE | NIF_TIP;
    horse.nid.uCallbackMessage = WM_TRAYICON;
    horse.nid.hIcon            = icon;
    lstrcpyA(horse.nid.szTip, "Horse - right-click for Exit");
    Shell_NotifyIconA(NIM_ADD, &horse.nid);

    ShowWindow(hwnd, nShow);
    UpdateWindow(hwnd);

    /* spawn announcement: a hearty neigh */
    playSoundRes(IDW_NEIGH);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0) > 0) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    DeleteObject(horse.bmp);
    DestroyIcon(icon);
    return (int)msg.wParam;
}
