/* ---------------------------------------------- */
/* -  Mouse Following, Horizontally Scrolling   - */
/* -             Parallax Star Field            - */
/* -                                      v1.0  - */
/* - Made with CodeWarrior PRO 4 for 68K MACs   - */
/* - as an example for anyone wishing to        - */
/* - include a horizontally scrolling parallax  - */
/* - star field in their game that can follow,  - */
/* - or be repelled by, the mouse position.     - */
/* -                                            - */
/* - Feel free to modify or use this code       - */
/* - however you wish.                          - */
/* -                                            - */
/* - I hope you find it useful! :)              - */
/* -                                            - */
/* -   www.dsbbs.ca                             - */
/* -   demozoo.org/sceners/66957                - */
/* -                                            - */
/* -                     By DW/Dark Systems BBS - */
/* ---------------------------------------------- */
/* - 'q' or mouse click quits                   - */
/* - '+' or '-' changes star scroll speed       - */
/* ---------------------------------------------- */

#include <MacTypes.h>
#include <Quickdraw.h>
#include <Windows.h>
#include <OSUtils.h>
#include <Events.h>
#include <QDOffscreen.h>

#define WIN_WIDTH   512 /* Horizontal window resolution */
#define WIN_HEIGHT  342 /* Vertical window resolution */
#define STAR_COUNT  180 /* Total number of stars on screen */

/* Most 68k Mac CPUs lack a Floating Point Unit. */
/* To get smooth sub-pixel movement without costly float math, we use a 32-bit signed long. */
/* The upper 16 bits hold the whole integer, and the lower 16 bits hold the fraction. */
#define TO_FIX(x)   ((long)(x) << 16)   /* Convert integer to 16.16 fixed point */
#define FROM_FIX(x) ((short)((x) >> 16)) /* Truncate 16.16 fixed point back to an integer */

typedef struct {
    long x;      /* 16.16 Fixed-point horizontal position */
    long y;      /* 16.16 Fixed-point vertical position */
    long vx;     /* Base horizontal velocity (16.16) */
    long vy;     /* Base vertical velocity (16.16) */
    short layer; /* 0 = Far/Slow (Background), 1 = Mid, 2 = Close/Fast (Foreground) */
} Star;

/* Global Pointers for Mac Memory & Toolbox objects */
WindowPtr gWindow = NULL;       /* Pointer to our Mac window structure allocated in the System Heap */
GWorldPtr gOffscreen = NULL;   /* Pointer to our offscreen graphics world used for double-buffering */
Star stars[STAR_COUNT];

/* Global Speed Multiplier (Fixed-point 16.16 value) */
/* Used to dynamically scale the stars speed. */
/* 0.00x = 0 (stopped) */
/* 0.25x = 16384 */
/* 0.50x = 32768 (default) */
/* 1.00x = 65536 (fastest) */
long gSpeedMultiplier = 32768;

/* Bitmask lookup array used to quickly isolate/manipulate single bits inside a byte for 1-bit video */
static const unsigned char gBitMaskTable[8] = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };

/* Caches the row stride of the framebuffer to avoid dereferencing structures inside inner loops */
static long gRowBytesCache = 0;

/* --- Function Prototypes --- */
short myRandom(void);
void InitStars(void);
void UpdateStars(void);
void DrawStars(void);
void HandleKeyDown(EventRecord *e);
void main(void);

/* ------ Random Number Generator ------ */
static unsigned long seed = 1;

/* Custom randomizer. We avoid QuickDraw's `Random()` */
/* because it's slow and our version yields predictably fast results in loop cycles. */
short myRandom(void)
{
    seed = seed * 1103515245 + 12345;
    return (short)((seed >> 16) & 0x7FFF);
}

/* ----------- Init Stars ----------- */
void InitStars(void)
{
    short i;

    for (i = 0; i < STAR_COUNT; i++) {
        /* Distribute the stars randomly among 3 parallax depth layers (0, 1, 2) */
        stars[i].layer = myRandom() % 3;

        /* Assign random spawn coordinates scaled directly into 16.16 space */
        stars[i].x = TO_FIX(myRandom() % WIN_WIDTH);
        stars[i].y = TO_FIX(myRandom() % WIN_HEIGHT);

        /* * Setup the parallax layering speeds. */
        /* Deeper stars move slower, creating the magical illusion of 3D depth. */
        if (stars[i].layer == 0) { 
            stars[i].vx = -16384;  /* Background: move -0.25 pixels per frame */
            stars[i].vy = 0;       
        }
        if (stars[i].layer == 1) { 
            stars[i].vx = -49152;  /* Midground: move -0.75 pixels per frame */
            stars[i].vy = 0;       
        }
        if (stars[i].layer == 2) { 
            stars[i].vx = -114688; /* Foreground: move -1.75 pixels per frame */
            stars[i].vy = 0;       
        }
    }
}

/* ---------------- Update Stars ---------------- */
void UpdateStars(void)
{
    short i;
    short screenX, screenY;
    long actualVx, actualVy;
    Point mousePt;
    long globalHorizontalTilt;
    long globalVerticalTilt;
    short xDistanceFromCenter;
    short yDistanceFromCenter;

    /* Grab the cursor position. */
    /* The `GetMouse` Toolbox trap populates the Point struct with local coordinates */
    /* relative to the currently active port (`gWindow`). */
    GetMouse(&mousePt);

    /* Find distances from the exact centre of the 512x342 window */
    xDistanceFromCenter = mousePt.h - 256;
    yDistanceFromCenter = mousePt.v - 171;

    /* Clamp limits to prevent values from overflowing fixed math calculations if mouse leaves bounds */
    if (xDistanceFromCenter > 256)  xDistanceFromCenter = 256;
    if (xDistanceFromCenter < -256) xDistanceFromCenter = -256;
    if (yDistanceFromCenter > 171)  yDistanceFromCenter = 171;
    if (yDistanceFromCenter < -171) yDistanceFromCenter = -171;

    /* Convert distances into 16.16 global speed modifiers. */
    /* The `512` multiplier scales tracking sensitivity. */
    globalHorizontalTilt = (long)xDistanceFromCenter * 512;
    globalVerticalTilt   = (long)yDistanceFromCenter * 512;

    /* Update coordinates for all stars in our data array */
    for (i = 0; i < STAR_COUNT; i++) {
        
        /* Parallax scaling calculation */
         /* Vector scales linearly by the depth layer, eg: `(layer + 1)`. */
         /* Shifting right by 1 acts as a quick division by 2 for speed. */
        actualVx = (globalHorizontalTilt * (stars[i].layer + 1)) >> 1;
        actualVy = (globalVerticalTilt   * (stars[i].layer + 1)) >> 1;

        /* Combine the mouse vectors with the keyboard controlled speed multiplier */
        actualVx = (actualVx * (gSpeedMultiplier >> 8)) >> 8;
        actualVy = (actualVy * (gSpeedMultiplier >> 8)) >> 8;

        /* Apply the final values to the star position in 16.16 memory space */
        stars[i].x += actualVx;
        stars[i].y += actualVy;

        /* Convert back to integer values for bounds testing and drawing */
        screenX = FROM_FIX(stars[i].x);
        screenY = FROM_FIX(stars[i].y);

        /* 4-way edge wrapping boundaries */
        /* If a star moves past any edge, wrap it around to the opposite side and */
        /* randomize its axis position to break patterns and keep distribution random. */
        if (screenX < 0) {
            stars[i].x = TO_FIX(WIN_WIDTH - 1);
            stars[i].y = TO_FIX(myRandom() % WIN_HEIGHT);
        }
        else if (screenX >= WIN_WIDTH) {
            stars[i].x = 0;
            stars[i].y = TO_FIX(myRandom() % WIN_HEIGHT);
        }

        if (screenY < 0) {
            stars[i].y = TO_FIX(WIN_HEIGHT - 1);
            stars[i].x = TO_FIX(myRandom() % WIN_WIDTH);
        } 
        else if (screenY >= WIN_HEIGHT) {
            stars[i].y = 0;
            stars[i].x = TO_FIX(myRandom() % WIN_WIDTH);
        }
    }
}

/* ----------- Draw Stars ----------- */
void DrawStars(void)
{
    PixMapHandle pix;
    unsigned char *screen;
    register long rowBytes = gRowBytesCache;
    register short i;
    register unsigned char *rowPtr;
    register unsigned char *pixelByte;
    short yCounter;

    /* Lock the GWorld memory handle before we mess with it so the OS doesn't move it on us */
    pix = GetGWorldPixMap(gOffscreen);
    LockPixels(pix);
    screen = (unsigned char *)GetPixBaseAddr(pix); /* Raw base memory pointer to top-left of screen */

    /* VRAM buffer clear (fast whiteout) */
    /* In 1-bit QuickDraw, a bit state of '0' means white, and '1' means black. */
    /* We flood-fill the screen with 0xFF bytes to wipe the screen completely white. */
    rowPtr = screen;
    for (yCounter = 0; yCounter < WIN_HEIGHT; yCounter++) {
        register unsigned char *bytePtr = rowPtr;
        short byteCount = rowBytes;
        while (byteCount--) {
            *bytePtr++ = 0xFF; /* Write 8 white pixels simultaneously */
        }
        rowPtr += rowBytes; /* Jump to the next scanline byte offset */
    }

    /* Plot Stars... PEW PEW! */
    /* To bypass slow QuickDraw `SetPt`/`LineTo` commands, we write directly to VRAM. */
    for (i = 0; i < STAR_COUNT; i++) {
        register short x = FROM_FIX(stars[i].x);
        register short y = FROM_FIX(stars[i].y);

        /* Boundary clipping checks */
        if (x < 0 || x >= WIN_WIDTH) continue;
        if (y < 0 || y >= WIN_HEIGHT) continue;

        /* `x & 7` finds the bit offset (0-7). Clears that specific bit to 0 (Black star pixel) */
        pixelByte = screen + (y * rowBytes) + (x >> 3);

        /* * Draw black pixel */
        /* We invert the mask (`~`) and use bitwise AND (`&=`) to turn the target bit into a 1 (black) */
        /* without altering the other 7 neighbouring pixels packed inside that same byte. */
        *pixelByte &= ~gBitMaskTable[x & 7]; /* `x & 7` acts as modulo 8 to grab the bit-index */

        /* Give the foreground star layer (2) a double vertical pixel height since they are closer */
        if (stars[i].layer == 2 && (y + 1) < WIN_HEIGHT) {
            *(pixelByte + rowBytes) &= ~gBitMaskTable[x & 7]; /* Target pixel directly one line below */
        }
    }

    /* Safely unlock the graphics memory handle now that drawing is finished */
    UnlockPixels(pix);
}

/* ----------- Need more INPUT.. handling :) ----------- */
void HandleKeyDown(EventRecord *e)
{
    /* Use charCodeMask to pull out the exact ASCII key value from the event message longword */
    char key = (char)(e->message & charCodeMask);
    
    /* '+' or '=' or 'k' increases star speed */
    if (key == '+' || key == '=' || key == 'k') {
        gSpeedMultiplier += 16384; /* Add 0.25 to the speed multiplier each key press */
        if (gSpeedMultiplier > TO_FIX(5)) gSpeedMultiplier = TO_FIX(5); /* MAX speed at 5.0x */
    }
        /* '-' or '_' or 'm' decreases star speed */
    else if (key == '-' || key == '_' || key == 'm') {
        gSpeedMultiplier -= 16384; /* Drop speed multiplier by 0.25 each key press */
        if (gSpeedMultiplier < 0) gSpeedMultiplier = 0; /* Lower limit (complete stop) */
    }
}

/* ---------------- Main ---------------- */
void main(void)
{
    Rect r;
    EventRecord e;
    long lastTick;
    PixMapHandle pix;

    /* Initialize the standard Mac Toolbox managers of fun */
    InitGraf(&qd.thePort); /* Fire up QuickDraw Graphics Engine */
    InitWindows();         /* Window Manager initialization */
    InitCursor();          /* Show active cursor arrow on screen */

    /* Setup the window dimensions */
    SetRect(&r, 80, 80, 80 + WIN_WIDTH, 80 + WIN_HEIGHT);

    /* Create the actual window, a standard borderless dialog-style frame box */
    gWindow = NewWindow(NULL, &r, "\pStarfield Fixed", true,
                        plainDBox, (WindowPtr)-1L, false, 0);

    /* Point Quickdraw rendering routines to draw inside this specific window */
    SetPort(gWindow);

    /* Create the 1-bit Offscreen Graphics World buffer */
    NewGWorld(&gOffscreen, 1, &gWindow->portRect, NULL, NULL, 0);
    
    /* Fetch row width configuration directly out of the OS handle headers, filtering out system flags */
    pix = GetGWorldPixMap(gOffscreen);
    gRowBytesCache = (*pix)->rowBytes & 0x3FFF; /* Flag mask clears high state flag bit definitions */

    InitStars();

    /* Initialize frame timing loop using system Ticks (1 Tick = ~1/60th of a second in NTSC land) */
    lastTick = TickCount();

    /* Main running Loop: Runs until the user clicks the physical mouse button or presses 'q' */
    while (!Button()) {
        
        /* Check the OS Event Queue for keyboard inputs */
        if (GetNextEvent(keyDownMask, &e)) {
            if (e.what == keyDown) {
                char key = (char)(e.message & charCodeMask);
                /* Quit program immediately if Escape key or 'q' is pressed */
                if (key == 27 || key == 'q') {
                    goto quit;
                }
                HandleKeyDown(&e);
            }
        }

        UpdateStars();
        DrawStars();

        /* High-speed copy, moving the finished offscreen state to the visible on-screen window. */
        CopyBits((BitMap*)*GetGWorldPixMap(gOffscreen),
                 &((GrafPtr)gWindow)->portBits,
                 &gOffscreen->portRect,
                 &gWindow->portRect,
                 srcCopy,
                 NULL);

        /* V-Sync/Framerate lock: */
        /* Wait until 1 system tick passes to maintain a consistent 60 FPS */
        while (TickCount() == lastTick) {
        }
        lastTick = TickCount();
    }

quit:
    /* Clean up allocated heap memory structures to prevent memory leaks */
    DisposeGWorld(gOffscreen);
    DisposeWindow(gWindow);
}