// winfly.cpp
// Small Win32 app that draws a fly (now can use an image) and moves it using a simple
// Cocoa-like behavior. Single-file, uses Win32 + GDI+ for image loading.
// v1.05 That One Guy

#include <windows.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
#include <shellapi.h>
// GDI+ for image loading and drawing
#include <gdiplus.h>
using namespace Gdiplus;

#include <stdio.h>
#include <time.h>

// Ensure M_PI is defined on compilers/platforms where it's not provided
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

// Simulation params
#define TARGET_FPS 60
#define TIMER_ID 1
// Animation speed (frames per wing-flap change)
#define ANIMATION_SPEED 8

typedef struct {
    float x, y;
    float dx, dy; // velocity in pixels/sec
    float angle; // movement angle (radians)
    float display_angle; // movement angle + jitter for drawing
    float jitter_angle; // current jitter offset
    float target_jitter; // target jitter angle
    float jitter_timer; // seconds remaining for jitter
    int animation_frame;
    float frame_counter;
    float pause_timer; // seconds remaining to pause
    int is_paused;
    float stored_dx, stored_dy; // stored velocity while paused
    int renderSize; // per-fly render size in pixels (allows -v variation)
} Fly;

static Fly *flies = NULL;
static int g_flyCount = 1;
static LARGE_INTEGER freq, lastTime;
// GDI+ token and loaded bitmap (optional)
static ULONG_PTR g_gdiplusToken = 0;
static Bitmap *g_flyBitmap = NULL;
// Optional runtime timeout (seconds). Default 15 seconds.
static double g_timeoutSeconds = 15.0;
static double g_runTime = 0.0;
// windowWidth/windowHeight store the full VIRTUAL screen size (for movement bounds).
static int windowWidth = 0;
static int windowHeight = 0;
// virtual screen origin (may be negative in multi-monitor setups)
static int virtualX = 0;
static int virtualY = 0;
// Render size for the fly (pixels) and buffer size for the layered window.
// Base size is 18px. Default is 2.0x = 36px to match GUI default.
// Made runtime-configurable via command-line -s/--size so GUI can control it.
static int g_flyRenderSize = 36;
static int g_flyBufSize = 216;
// Size variation percentage (0 == none). Set by -v/--variation
static int g_sizeVariationPercent = 0;
// Rotation offset (degrees) to make the source image's forward direction match movement
#define ROTATION_OFFSET_DEG 90.0f
// Resource ID for embedded fly image
#define IDR_FLY_PNG 101

static float randf(float a, float b) {
    return a + (float)rand() / (float)RAND_MAX * (b - a);
}

// Minimal Win32-based logger that does not depend on C runtime FILE APIs.
static void append_log_winapi(const char *msg) {
    char buf[1024];
    SYSTEMTIME st;
    GetLocalTime(&st);
    // wsprintfA is a lightweight Win32 formatter (avoid heavy CRT init)
    int n = wsprintfA(buf, "%04d-%02d-%02d %02d:%02d:%02d.%03d - %s\r\n",
        st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, msg);
    HANDLE h = CreateFileA("winfly_start.log", FILE_APPEND_DATA, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (h == INVALID_HANDLE_VALUE) return;
    SetFilePointer(h, 0, NULL, FILE_END);
    DWORD written = 0;
    if (n > 0) WriteFile(h, buf, (DWORD)n, &written, NULL);
    CloseHandle(h);
}

// Try to load an image from the fly/ directory. Tries PNG, JPG, GIF in that order.
static void load_fly_image(void) {
    if (g_flyBitmap) return;
    // Prefer an external PNG in fly\fly.png only. If not present, fall back to embedded resource.
    const char *candidate = "fly\\fly.png";
    WCHAR wpath[MAX_PATH];
    int len = MultiByteToWideChar(CP_ACP, 0, candidate, -1, wpath, MAX_PATH);
    if (len != 0) {
        Bitmap *bmp = Bitmap::FromFile(wpath);
        if (bmp && bmp->GetLastStatus() == Ok) {
            g_flyBitmap = bmp;
        } else {
            if (bmp) delete bmp;
            g_flyBitmap = NULL;
        }
    }

    // If we didn't find an external file, try to load the PNG embedded as a resource
    if (!g_flyBitmap) {
        HRSRC hrs = FindResource(NULL, MAKEINTRESOURCE(IDR_FLY_PNG), RT_RCDATA);
        if (hrs) {
            HGLOBAL hRes = LoadResource(NULL, hrs);
            if (hRes) {
                DWORD resSize = SizeofResource(NULL, hrs);
                void *pResData = LockResource(hRes);
                if (pResData && resSize > 0) {
                    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, resSize);
                    if (hMem) {
                        void *pMem = GlobalLock(hMem);
                        if (pMem) {
                            memcpy(pMem, pResData, resSize);
                            GlobalUnlock(hMem);
                            IStream *pStream = NULL;
                            if (CreateStreamOnHGlobal(hMem, TRUE, &pStream) == S_OK) {
                                Bitmap *bmp = Bitmap::FromStream(pStream);
                                if (bmp && bmp->GetLastStatus() == Ok) {
                                    g_flyBitmap = bmp;
                                } else {
                                    if (bmp) delete bmp;
                                }
                                pStream->Release();
                            }
                        }
                        // if CreateStreamOnHGlobal failed, the hMem is freed by TRUE parameter?
                        // In failure paths we could GlobalFree(hMem), but CreateStreamOnHGlobal with TRUE
                        // transfers ownership when the stream is released; if stream not created we free.
                    }
                }
            }
        }
    }
}



// Rotate local point (lx,ly) by cos/sin and translate by (cx,cy).
// This helper is at file scope.
static void rot_point(float lx, float ly, float cx, float cy, float ca, float sa, POINT *p) {
    float rx = lx * ca - ly * sa;
    float ry = lx * sa + ly * ca;
    p->x = (int)(cx + rx);
    p->y = (int)(cy + ry);
}

// Update fly using behavior inspired by the mac Cocoa implementation
static void update_fly(Fly *fly, double dt) {
    // Parameters tuned to feel similar to the Cocoa behavior
    const float BASE_SPEED = 120.0f; // pixels/sec
    const float DIRECTION_CHANGE_CHANCE = 0.02f; // per frame
    const float PAUSE_CHANCE = 0.008f; // per frame
    const int MIN_PAUSE_FRAMES = 15; // frames
    const int MAX_PAUSE_FRAMES = 30; // frames
    const float ROTATION_JITTER_CHANCE = 0.015f; // per frame
    const float MAX_ROTATION_JITTER = (float)(M_PI / 4.0); // radians

    // Working with frame-based probabilities: convert dt to frames (assuming 60fps baseline)
    float frames = (float)(dt * 60.0);

    // If paused, decrease timer
    if (fly->is_paused) {
        fly->pause_timer -= dt;
        if (fly->pause_timer <= 0.0f) {
            fly->is_paused = 0;
            fly->dx = fly->stored_dx;
            fly->dy = fly->stored_dy;
        } else {
            // advance animation while paused
            fly->frame_counter += frames;
            if (fly->frame_counter >= ANIMATION_SPEED * 2) {
                fly->animation_frame = 1 - fly->animation_frame;
                fly->frame_counter = 0;
            }
            return;
        }
    } else {
        // Possibly start a pause
        if (((float)rand() / RAND_MAX) < PAUSE_CHANCE * frames) {
            fly->is_paused = 1;
                int pauseFrames = MIN_PAUSE_FRAMES + (rand() % (MAX_PAUSE_FRAMES - MIN_PAUSE_FRAMES + 1));
                fly->pause_timer = pauseFrames / 60.0f; // seconds
                fly->stored_dx = fly->dx;
                fly->stored_dy = fly->dy;
                fly->dx = 0;
                fly->dy = 0;
            return;
        }
    }

    // Random direction changes
    if (((float)rand() / RAND_MAX) < DIRECTION_CHANGE_CHANCE * frames) {
        float angle = randf(0.0f, (float)(2.0f * M_PI));
        fly->dx = cosf(angle) * BASE_SPEED;
        fly->dy = sinf(angle) * BASE_SPEED;
        fly->angle = atan2f(fly->dy, fly->dx);
    }

    // Buzzing jitter occasionally
    if (rand() % 10 == 0) {
        fly->dx += randf(-0.5f, 0.5f) * BASE_SPEED * 0.05f;
        fly->dy += randf(-0.5f, 0.5f) * BASE_SPEED * 0.05f;
        // limit speed
        float sp = sqrtf(fly->dx*fly->dx + fly->dy*fly->dy);
        float maxSp = BASE_SPEED * 2.0f;
        if (sp > maxSp) {
            fly->dx = (fly->dx / sp) * maxSp;
            fly->dy = (fly->dy / sp) * maxSp;
        }
    }

    // Update position (dt seconds)
    fly->x += fly->dx * (float)dt;
    fly->y += fly->dy * (float)dt;

    // Update base movement angle
    if (fly->dx != 0.0f || fly->dy != 0.0f) {
        fly->angle = atan2f(fly->dy, fly->dx);
    }

    // Rotation jitter (start occasionally)
    if (!fly->is_paused && ((float)rand() / RAND_MAX) < ROTATION_JITTER_CHANCE * frames) {
        fly->target_jitter = randf(-MAX_ROTATION_JITTER, MAX_ROTATION_JITTER);
        fly->jitter_timer = (15 + (rand() % 20)) / 60.0f; // seconds
    }

    // Update jitter animation
    if (fly->jitter_timer > 0.0f) {
        fly->jitter_timer -= (float)dt;
        if (fly->jitter_timer > 10.0f/60.0f) {
            // approach target
            float diff = fly->target_jitter - fly->jitter_angle;
            fly->jitter_angle += diff * 0.2f;
        } else {
            // return to zero
            fly->jitter_angle *= 0.8f;
        }
    } else {
        fly->jitter_angle = 0.0f;
    }

    fly->display_angle = fly->angle + fly->jitter_angle;

    // Bounce off virtual screen bounds
    float minX = (float)virtualX;
    float minY = (float)virtualY;
    float maxX = (float)(virtualX + windowWidth);
    float maxY = (float)(virtualY + windowHeight);
    // approximate fly size (pixels) - use the per-fly renderSize so larger flies don't clip
    int fsz = fly->renderSize;
    if (fly->x < minX) { fly->x = minX; fly->dx = -fly->dx; }
    if (fly->x > maxX - fsz) { fly->x = maxX - fsz; fly->dx = -fly->dx; }
    if (fly->y < minY) { fly->y = minY; fly->dy = -fly->dy; }
    if (fly->y > maxY - fsz) { fly->y = maxY - fsz; fly->dy = -fly->dy; }

    // Update animation
    fly->frame_counter += frames;
    int animSpeed = (fly->is_paused) ? ANIMATION_SPEED * 2 : ANIMATION_SPEED;
    if (fly->frame_counter >= animSpeed) {
        fly->animation_frame = 1 - fly->animation_frame;
        fly->frame_counter = 0;
    }
}

// Draw the fly silhouette rotated by the provided angle (radians).
// animation_frame controls wing offset for flapping.
// renderSize is the intended pixel size for this fly.
static void draw_fly(HDC hdc, float x, float y, float angle, int animation_frame, int renderSize) {
    // Body size (match Cocoa proportions, scaled by renderSize)
    const int bodyW = (int)(renderSize * 0.25f);
    const int bodyH = (int)(renderSize * 0.5f);
    // Wing size
    const int wingW = (int)(renderSize * 0.166f);
    const int wingH = (int)(renderSize * 0.25f);

    float ca = cosf(angle), sa = sinf(angle);

    // Create a visible solid brush (red) so drawn pixels are non-zero in the DIB
    HBRUSH brush = CreateSolidBrush(RGB(255,0,0));
    HBRUSH oldBrush = (HBRUSH)SelectObject(hdc, brush);

    // Draw body centered at (x,y)
    int bx1 = (int)(x - bodyW/2);
    int by1 = (int)(y - bodyH/2);
    int bx2 = (int)(x + bodyW/2);
    int by2 = (int)(y + bodyH/2);
    Ellipse(hdc, bx1, by1, bx2, by2);

    // Wing centers relative to fly center (scaled by renderSize)
    float fs = (float)renderSize;
    float wingOffsetY = (animation_frame == 0) ? 0.0f : fs * 0.04f;
    // left and right wing centers before rotation (approximately +/-25% of size)
    float lx = -fs * 0.25f, ly = -fs * 0.04f + wingOffsetY;
    float rx = fs * 0.25f,  ry = -fs * 0.04f + wingOffsetY;

    POINT pL, pR;
    rot_point(lx, ly, x, y, ca, sa, &pL);
    rot_point(rx, ry, x, y, ca, sa, &pR);

    // Draw left wing (oval)
    int lw1 = pL.x - wingW/2;
    int lh1 = pL.y - wingH/2;
    int lw2 = pL.x + wingW/2;
    int lh2 = pL.y + wingH/2;
    Ellipse(hdc, lw1, lh1, lw2, lh2);

    // Draw right wing (oval)
    int rw1 = pR.x - wingW/2;
    int rh1 = pR.y - wingH/2;
    int rw2 = pR.x + wingW/2;
    int rh2 = pR.y + wingH/2;
    Ellipse(hdc, rw1, rh1, rw2, rh2);

    // Restore and delete brush
    SelectObject(hdc, oldBrush);
    DeleteObject(brush);
}

// Render into a 32-bit DIB, set alpha for non-empty pixels, and update layered window
static void update_layered_content(HWND hwnd) {
    if (windowWidth <= 0 || windowHeight <= 0) return;

    int bufW = windowWidth;
    int bufH = windowHeight;
    int left = virtualX;
    int top = virtualY;

    BITMAPINFO bmi;
    ZeroMemory(&bmi, sizeof(bmi));
    bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
    bmi.bmiHeader.biWidth = bufW;
    bmi.bmiHeader.biHeight = -bufH; // top-down
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;
    bmi.bmiHeader.biCompression = BI_RGB;

    void *bits = NULL;
    HDC hdcScreen = GetDC(NULL);
    HDC hdcMem = CreateCompatibleDC(hdcScreen);
    HBITMAP hbmp = CreateDIBSection(hdcMem, &bmi, DIB_RGB_COLORS, &bits, NULL, 0);
    HBITMAP oldBmp = (HBITMAP)SelectObject(hdcMem, hbmp);

    // Clear to transparent
    if (bits) {
        SIZE_T total = (SIZE_T)bufW * (SIZE_T)bufH * 4;
        ZeroMemory(bits, total);
    }

    // Draw all flies into the buffer
    if (g_flyBitmap) {
        Graphics graphics(hdcMem);
        for (int i = 0; i < g_flyCount; ++i) {
            Fly *f = &flies[i];
            int destW = f->renderSize;
            int destH = f->renderSize;
            float localX = f->x - (float)left;
            float localY = f->y - (float)top;
            int dxi = (int)(localX - destW / 2);
            int dyi = (int)(localY - destH / 2);
            int cx = dxi + destW/2;
            int cy = dyi + destH/2;
            GraphicsState gs = graphics.Save();
            graphics.TranslateTransform((REAL)cx, (REAL)cy);
            REAL deg = (REAL)(f->display_angle * 180.0 / M_PI + ROTATION_OFFSET_DEG);
            graphics.RotateTransform(deg);
            graphics.TranslateTransform((REAL)(-destW/2), (REAL)(-destH/2));
            graphics.DrawImage(g_flyBitmap, 0, 0, destW, destH);
            graphics.Restore(gs);
        }
    } else {
        // Fallback: draw procedural silhouettes
        for (int i = 0; i < g_flyCount; ++i) {
            Fly *f = &flies[i];
            float localX = f->x - (float)left;
            float localY = f->y - (float)top;
            draw_fly(hdcMem, localX, localY, f->display_angle, f->animation_frame, f->renderSize);
        }
        // Post-process to set drawn pixels opaque black
        if (bits) {
            unsigned char *p = (unsigned char*)bits;
            int pixels = bufW * bufH;
            for (int i = 0; i < pixels; ++i) {
                unsigned char b = p[i*4 + 0];
                unsigned char g = p[i*4 + 1];
                unsigned char r = p[i*4 + 2];
                if (r != 0 || g != 0 || b != 0) {
                    p[i*4 + 0] = 0;
                    p[i*4 + 1] = 0;
                    p[i*4 + 2] = 0;
                    p[i*4 + 3] = 255;
                } else {
                    p[i*4 + 3] = 0;
                }
            }
        }
    }

    // Move the layered window to the virtual screen origin and update content
    SetWindowPos(hwnd, HWND_TOPMOST, left, top, bufW, bufH, SWP_NOACTIVATE);
    SIZE size = {bufW, bufH};
    POINT src = {0,0};
    BLENDFUNCTION bf = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
    UpdateLayeredWindow(hwnd, NULL, NULL, &size, hdcMem, &src, 0, &bf, ULW_ALPHA);

    // Cleanup
    SelectObject(hdcMem, oldBmp);
    DeleteObject(hbmp);
    DeleteDC(hdcMem);
    ReleaseDC(NULL, hdcScreen);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_CREATE:
        QueryPerformanceCounter(&lastTime);
        SetTimer(hwnd, TIMER_ID, 1000 / TARGET_FPS, NULL);
        break;
    // Ignore WM_SIZE for the layered window; we manage window sizing ourselves.
    case WM_TIMER:
        if (wParam == TIMER_ID) {
            LARGE_INTEGER now;
            QueryPerformanceCounter(&now);
            double dt = (double)(now.QuadPart - lastTime.QuadPart) / (double)freq.QuadPart;
            lastTime = now;
            if (dt > 0.1) dt = 0.1; // clamp
            // Track total runtime and auto-quit if timeout is set
            if (g_timeoutSeconds > 0.0) {
                g_runTime += dt;
                if (g_runTime >= g_timeoutSeconds) {
                    PostQuitMessage(0);
                    break;
                }
            }
            // Update all flies then update the single layered window once
            for (int i = 0; i < g_flyCount; ++i) {
                update_fly(&flies[i], dt);
            }
            update_layered_content(hwnd);
        }
        break;
    case WM_DESTROY:
        KillTimer(hwnd, TIMER_ID);
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPSTR lpCmdLine, int nShowCmd) {
    srand((unsigned)time(NULL));
    QueryPerformanceFrequency(&freq);

    // Very-early Win32 log to detect if process actually starts (before C runtime)
    {
        char tmp[1024];
        LPCSTR cl = GetCommandLineA();
        if (cl) wsprintfA(tmp, "START (winapi) cmd=%s", cl);
        else wsprintfA(tmp, "START (winapi) cmd=(none)");
        append_log_winapi(tmp);
    }

    // Simple runtime logging to help diagnose immediate exits/crashes.
    auto append_log = [](const char *msg){
        FILE *f = NULL;
        fopen_s(&f, "winfly_start.log", "a");
        if (!f) return;
        SYSTEMTIME st;
        GetLocalTime(&st);
        fprintf(f, "%04d-%02d-%02d %02d:%02d:%02d.%03d - %s\n",
            st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, msg);
        fclose(f);
    };
    // Log startup and the raw command line
    {
        char buf[1024] = {0};
        LPCSTR cl = GetCommandLineA();
        if (cl) snprintf(buf, sizeof(buf), "START cmd=%s", cl);
        else snprintf(buf, sizeof(buf), "START (cmdline unavailable)");
        append_log(buf);
    }

    WNDCLASS wc = {0};
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszClassName = TEXT("WinFlyClass");

    if (!RegisterClass(&wc)) return -1;

    // Initialize GDI+ so we can load images from disk
    GdiplusStartupInput gdiplusStartupInput;
    if (GdiplusStartup(&g_gdiplusToken, &gdiplusStartupInput, NULL) != Ok) {
        g_gdiplusToken = 0;
    }

    // Try to load a fly image (png/jpg/gif) from the fly/ directory
    load_fly_image();

    // Get virtual screen size and origin (supports multi-monitor setups)
    virtualX = GetSystemMetrics(SM_XVIRTUALSCREEN);
    virtualY = GetSystemMetrics(SM_YVIRTUALSCREEN);
    windowWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN);
    windowHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN);

    // Determine buffer size for rendering (single full-screen layered window used here)
    int bufW = g_flyBufSize;
    int bufH = g_flyBufSize;
    // Add WS_EX_TOOLWINDOW so the window does not create a taskbar button.
    DWORD exStyle = WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_TOOLWINDOW;

    // Parse command line for an optional count: -n N or --count N
    int argc = 0;
    LPWSTR *argvw = CommandLineToArgvW(GetCommandLineW(), &argc);
    if (argvw) {
        for (int i = 1; i < argc; ++i) {
            if (wcscmp(argvw[i], L"-n") == 0 || wcscmp(argvw[i], L"--count") == 0) {
                if (i + 1 < argc) {
                    int v = _wtoi(argvw[i+1]);
                    if (v > 0) g_flyCount = v;
                    i++;
                }
            } else if (wcscmp(argvw[i], L"-t") == 0 || wcscmp(argvw[i], L"--timeout") == 0) {
                if (i + 1 < argc) {
                    double tv = _wtof(argvw[i+1]);
                    if (tv > 0.0) g_timeoutSeconds = tv;
                    i++;
                }
            } else if (wcscmp(argvw[i], L"-s") == 0 || wcscmp(argvw[i], L"--size") == 0) {
                // -s accepts 1..5 (scale levels) where current size == 1. Values >5 are treated as explicit pixels.
                if (i + 1 < argc) {
                    int sv = _wtoi(argvw[i+1]);
                    if (sv >= 1 && sv <= 5) {
                        // treat as scale level: level 1 == current default size
                        g_flyRenderSize = g_flyRenderSize * sv;
                    } else if (sv > 0) {
                        // explicit pixel size
                        g_flyRenderSize = sv;
                    }
                    i++;
                }
            } else if (wcscmp(argvw[i], L"-v") == 0 || wcscmp(argvw[i], L"--variation") == 0) {
                // Variation percent around the base size (0..100). e.g. -v 20 allows +/-20% per-fly size jitter.
                if (i + 1 < argc) {
                    int vv = _wtoi(argvw[i+1]);
                    if (vv < 0) vv = 0;
                    if (vv > 100) vv = 100;
                    g_sizeVariationPercent = vv;
                    i++;
                }
            } else {
                // If a bare number is provided, use it as the count
                int v = _wtoi(argvw[i]);
                if (v > 0) g_flyCount = v;
            }
        }
        // Adjust buffer size if the requested render size is large
        if (g_flyRenderSize * 6 > g_flyBufSize) {
            g_flyBufSize = g_flyRenderSize * 6;
        }
        LocalFree(argvw);
    }

    // Allocate flies and create a single full-screen layered window to render them
    flies = (Fly*)malloc(sizeof(Fly) * g_flyCount);
    if (!flies) {
        append_log("ERROR: malloc(flies) failed");
        return -1;
    }

    // Create a single layered full-screen window
    int fullW = windowWidth;
    int fullH = windowHeight;
    HWND hwnd = CreateWindowEx(exStyle, wc.lpszClassName, TEXT("WinFly"),
        WS_POPUP, virtualX, virtualY, fullW, fullH,
        NULL, NULL, hInstance, NULL);
    if (!hwnd) { append_log("ERROR: CreateWindowEx failed"); free(flies); return -1; }

    // Initialize each fly at a random position inside virtual screen
    for (int i = 0; i < g_flyCount; ++i) {
        flies[i].x = (float)(virtualX + rand() % windowWidth);
        flies[i].y = (float)(virtualY + rand() % windowHeight);
        float initAngle = randf(0.0f, (float)(2.0f * M_PI));
        const float BASE_SPEED = 120.0f; // must match update_fly
        flies[i].dx = cosf(initAngle) * BASE_SPEED;
        flies[i].dy = sinf(initAngle) * BASE_SPEED;
        flies[i].angle = atan2f(flies[i].dy, flies[i].dx);
        flies[i].display_angle = flies[i].angle;
        flies[i].jitter_angle = 0.0f;
        flies[i].target_jitter = 0.0f;
        flies[i].jitter_timer = 0.0f;
        flies[i].animation_frame = rand() % 2;
        flies[i].frame_counter = (float)(rand() % ANIMATION_SPEED);
        flies[i].pause_timer = 0.0f;
        flies[i].is_paused = 0;
        flies[i].stored_dx = 0.0f;
        flies[i].stored_dy = 0.0f;
        // Compute per-fly render size with optional variation
        if (g_sizeVariationPercent > 0) {
            float pct = (float)g_sizeVariationPercent / 100.0f;
            float factor = randf(-pct, pct) + 1.0f; // between (1-pct) .. (1+pct)
            int sz = (int)(g_flyRenderSize * factor);
            if (sz < 4) sz = 4;
            flies[i].renderSize = sz;
        } else {
            flies[i].renderSize = g_flyRenderSize;
        }
    }

    ShowWindow(hwnd, nShowCmd);
    UpdateWindow(hwnd);

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

    // Cleanup GDI+ resources
    if (g_flyBitmap) {
        delete g_flyBitmap;
        g_flyBitmap = NULL;
    }
    if (g_gdiplusToken) GdiplusShutdown(g_gdiplusToken);

    // Free flies array
    if (flies) {
        free(flies);
        flies = NULL;
    }

    // Log normal shutdown
    append_log("SHUTDOWN");

    return (int)msg.wParam;
}
