/* ---------------------------------------------- */
/* - 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, like it's 1989.  - */
/* -                                      :)    - */
/* - 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;      /* Horizontal position in 16.16 fixed-point space */
    long y;      /* Vertical position in 16.16 fixed-point space */
    long vx;     /* Horizontal velocity (sub-pixels per frame) in 16.16 space */
    long vy;     /* Vertical velocity (sub-pixels per frame) in 16.16 space */
    short layer; /* 0 = Far/Slow (Background), 1 = Mid, 2 = Close/Fast (Foreground) */
} Star;

WindowPtr gWindow = NULL;       /* Pointer to the onscreen display window */
GWorldPtr gOffscreen = NULL;    /* Pointer to our offscreen graphics environment (prevents flicker) */
Star stars[STAR_COUNT];         /* Array holding all active star data */

/* 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;

    for (i = 0; i < STAR_COUNT; i++) {
        /* Apply a speed multiplier using Fixed-Point Multiplication. */
        /* Multiplying two 16.16 numbers gives you a 32.32 result. */
        /* Pre-shifting both components right by 8 bits allows us to safely-multiply */
        /* and final-shift down by 8, keeping the final value safely in 16.16 space without overflow. */
        actualVx = (stars[i].vx * (gSpeedMultiplier >> 8)) >> 8;
        actualVy = (stars[i].vy * (gSpeedMultiplier >> 8)) >> 8;

        /* Add velocity to current positions */
        stars[i].x += actualVx;
        stars[i].y += actualVy;

        /* Convert back to integer space for boundary checking and screen rendering */
        screenX = FROM_FIX(stars[i].x);
        screenY = FROM_FIX(stars[i].y);

        /* If a star scrolls off the left side, wrap it around to the right side */
        if (screenX < 0) {
            stars[i].x = TO_FIX(WIN_WIDTH - 1);
            stars[i].y = TO_FIX(myRandom() % WIN_HEIGHT); /* Randomize Y to vary the field */
        }

        /* Handle vertical wrapping (in case you want to add vertical drift later) */
        if (screenY < 0) {
            stars[i].y = TO_FIX(WIN_HEIGHT - 1);
        } else if (screenY >= WIN_HEIGHT) {
            stars[i].y = 0;
        }
    }
}

/* ----------- 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);

    /* 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; 
        }
        rowPtr += rowBytes; /* Jump to the next scanline byte offset */
    }

    /* Plot Stars... PEW PEW! */
    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 check */
        if (x < 0 || x >= WIN_WIDTH) continue;
        if (y < 0 || y >= WIN_HEIGHT) continue;

        /* 1-Bit Buffer Pixel Mapping: */
        /* `y * rowBytes` finds the vertical row start. */
        /* `x >> 3` divides x by 8 to locate the specific byte holding our pixel. */
        pixelByte = screen + (y * rowBytes) + (x >> 3);

        /* `x & 7` finds the bit offset (0-7). Clears that specific bit to 0 (Black star pixel) */
        *pixelByte &= ~gBitMaskTable[x & 7];

        /* 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, "\pHorizontal Parallax Star Field", 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;

    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);
}