#include "mac.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "m68k.h"

enum {
    RAM_SIZE      = 16 * 1024 * 1024,
    RAM_MASK      = RAM_SIZE - 1,
    STACK_TOP     = 0x0f0000,
    CODE_BASE     = 0x100000,
    HEAP_BASE     = 0x200000,
    SENTINEL      = 0x000400,
    A5_WORLD      = 0x000900,

    LM_SCREENBYTES = 0x0106,
    LM_RAND_SEED   = 0x0156,
    LM_TICKS       = 0x016a,
    LM_SCRNBASE    = 0x0824,
    LM_CURRENT_A5  = 0x0904,
    LM_THE_PORT    = 0x0d66,

    AD_INITIALIZE = 0,
    AD_BLANK      = 2,
    AD_DRAW_FRAME = 3
};

typedef struct { int16_t top, left, bottom, right; } Rect;
typedef struct { uint8_t r, g, b; } Color;

static uint8_t *ram;
static uint8_t rgba[SCREEN_WIDTH * SCREEN_HEIGHT * 4];
static uint32_t heap_next;
static uint32_t code_size;
static uint8_t code_prefix[4];
static uint32_t screen_bits, port, qd_globals;
static uint32_t storage_var, blank_rgn, param_block;
static Color foreground = {255, 255, 255};
static Rect clip_rect;
static int32_t random_seed = 1;
static bool frame_active, drawing_yield, guest_fault;
static uint32_t completed_frames;
static char error_text[192];

static uint16_t host_be16(const uint8_t *p) {
    return (uint16_t)((uint16_t)p[0] << 8 | p[1]);
}

static uint32_t host_be32(const uint8_t *p) {
    return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 |
           (uint32_t)p[2] << 8 | p[3];
}

static uint8_t r8(uint32_t address) {
    return ram[address & RAM_MASK];
}

static uint16_t r16(uint32_t address) {
    return (uint16_t)((uint16_t)r8(address) << 8 | r8(address + 1));
}

static uint32_t r32(uint32_t address) {
    return (uint32_t)r8(address) << 24 | (uint32_t)r8(address + 1) << 16 |
           (uint32_t)r8(address + 2) << 8 | r8(address + 3);
}

static void w8(uint32_t address, uint8_t value) {
    ram[address & RAM_MASK] = value;
}

static void w16(uint32_t address, uint16_t value) {
    w8(address, (uint8_t)(value >> 8));
    w8(address + 1, (uint8_t)value);
}

static void w32(uint32_t address, uint32_t value) {
    w8(address, (uint8_t)(value >> 24));
    w8(address + 1, (uint8_t)(value >> 16));
    w8(address + 2, (uint8_t)(value >> 8));
    w8(address + 3, (uint8_t)value);
}

static void write_rect(uint32_t address, Rect r) {
    w16(address + 0, (uint16_t)r.top);
    w16(address + 2, (uint16_t)r.left);
    w16(address + 4, (uint16_t)r.bottom);
    w16(address + 6, (uint16_t)r.right);
}

static Rect read_rect(uint32_t address) {
    Rect r = {(int16_t)r16(address), (int16_t)r16(address + 2),
              (int16_t)r16(address + 4), (int16_t)r16(address + 6)};
    return r;
}

static uint32_t allocate(uint32_t size) {
    uint32_t address = (heap_next + 3u) & ~3u;
    size = (size + 3u) & ~3u;
    if (address < HEAP_BASE || address >= RAM_SIZE || size > RAM_SIZE - address)
        return 0;
    heap_next = address + size;
    memset(ram + address, 0, size);
    return address;
}

static uint32_t new_handle(uint32_t size) {
    uint32_t cell = allocate(4);
    uint32_t data = allocate(size ? size : 4);
    if (!cell || !data) return 0;
    w32(cell, data);
    return cell;
}

static uint32_t new_region(Rect bounds) {
    uint32_t handle = new_handle(10);
    uint32_t data = r32(handle);
    w16(data, 10);
    write_rect(data + 2, bounds);
    return handle;
}

static void clear_pixels(Color color) {
    for (size_t i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; i++) {
        rgba[i * 4 + 0] = color.r;
        rgba[i * 4 + 1] = color.g;
        rgba[i * 4 + 2] = color.b;
        rgba[i * 4 + 3] = 255;
    }
}

static void put_pixel(int x, int y, Color color) {
    if ((unsigned)x >= SCREEN_WIDTH || (unsigned)y >= SCREEN_HEIGHT) return;
    size_t offset = ((size_t)y * SCREEN_WIDTH + (size_t)x) * 4;
    rgba[offset + 0] = color.r;
    rgba[offset + 1] = color.g;
    rgba[offset + 2] = color.b;
    rgba[offset + 3] = 255;
}

static void fill_rect(Rect r, Color color) {
    int top = r.top < clip_rect.top ? clip_rect.top : r.top;
    int left = r.left < clip_rect.left ? clip_rect.left : r.left;
    int bottom = r.bottom > clip_rect.bottom ? clip_rect.bottom : r.bottom;
    int right = r.right > clip_rect.right ? clip_rect.right : r.right;
    if (top < 0) top = 0;
    if (left < 0) left = 0;
    if (bottom > SCREEN_HEIGHT) bottom = SCREEN_HEIGHT;
    if (right > SCREEN_WIDTH) right = SCREEN_WIDTH;
    for (int y = top; y < bottom; y++)
        for (int x = left; x < right; x++) put_pixel(x, y, color);
}

static bool inside_oval(int x, int y, Rect r) {
    double rx = (r.right - r.left) / 2.0;
    double ry = (r.bottom - r.top) / 2.0;
    if (rx <= 0.0 || ry <= 0.0) return false;
    double dx = (x + 0.5 - (r.left + rx)) / rx;
    double dy = (y + 0.5 - (r.top + ry)) / ry;
    return dx * dx + dy * dy <= 1.0;
}

static void fill_oval(Rect r, Color color) {
    int top = r.top < clip_rect.top ? clip_rect.top : r.top;
    int left = r.left < clip_rect.left ? clip_rect.left : r.left;
    int bottom = r.bottom > clip_rect.bottom ? clip_rect.bottom : r.bottom;
    int right = r.right > clip_rect.right ? clip_rect.right : r.right;
    if (top < 0) top = 0;
    if (left < 0) left = 0;
    if (bottom > SCREEN_HEIGHT) bottom = SCREEN_HEIGHT;
    if (right > SCREEN_WIDTH) right = SCREEN_WIDTH;
    for (int y = top; y < bottom; y++)
        for (int x = left; x < right; x++)
            if (inside_oval(x, y, r)) put_pixel(x, y, color);
}

static void draw_line(int dh, int dv) {
    int x0 = (int16_t)r16(port + 50), y0 = (int16_t)r16(port + 48);
    int x1 = x0 + dh, y1 = y0 + dv;
    int width = (int16_t)r16(port + 54), height = (int16_t)r16(port + 52);
    int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
    int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
    int error = dx + dy;
    if (width < 1) width = 1;
    if (height < 1) height = 1;
    for (;;) {
        fill_rect((Rect){(int16_t)y0, (int16_t)x0,
                         (int16_t)(y0 + height), (int16_t)(x0 + width)},
                  foreground);
        if (x0 == x1 && y0 == y1) break;
        int twice = error * 2;
        if (twice >= dy) { error += dy; x0 += sx; }
        if (twice <= dx) { error += dx; y0 += sy; }
    }
    w16(port + 48, (uint16_t)y1);
    w16(port + 50, (uint16_t)x1);
}

static Color classic_color(int32_t value) {
    switch (value) {
        case 30:  return (Color){255, 255, 255}; /* whiteColor */
        case 33:  return (Color){0, 0, 0};       /* blackColor */
        case 69:  return (Color){255, 255, 0};
        case 137: return (Color){255, 0, 255};
        case 205: return (Color){255, 0, 0};
        case 273: return (Color){0, 255, 255};
        case 341: return (Color){0, 255, 0};
        case 409: return (Color){0, 0, 255};
        default:  return (Color){255, 255, 255};
    }
}

static void push16(uint16_t value) {
    uint32_t sp = m68k_get_reg(NULL, M68K_REG_SP) - 2;
    m68k_set_reg(M68K_REG_SP, sp);
    w16(sp, value);
}

static void push32(uint32_t value) {
    uint32_t sp = m68k_get_reg(NULL, M68K_REG_SP) - 4;
    m68k_set_reg(M68K_REG_SP, sp);
    w32(sp, value);
}

static void pop_bytes(uint32_t count) {
    m68k_set_reg(M68K_REG_SP,
                 m68k_get_reg(NULL, M68K_REG_SP) + count);
}

static void request_drawing_yield(void) {
    drawing_yield = true;
    m68k_end_timeslice();
}

static void fail_trap(uint16_t opcode) {
    snprintf(error_text, sizeof error_text,
             "unimplemented Macintosh trap $%04X at guest PC $%06X",
             opcode, m68k_get_reg(NULL, M68K_REG_PPC) & 0x00ffffff);
    guest_fault = true;
    m68k_end_timeslice();
}

/* Musashi calls these six functions for every guest memory access. */
unsigned int m68k_read_memory_8(unsigned int address) { return r8(address); }
unsigned int m68k_read_memory_16(unsigned int address) { return r16(address); }
unsigned int m68k_read_memory_32(unsigned int address) { return r32(address); }
void m68k_write_memory_8(unsigned int a, unsigned int v) { w8(a, (uint8_t)v); }
void m68k_write_memory_16(unsigned int a, unsigned int v) { w16(a, (uint16_t)v); }
void m68k_write_memory_32(unsigned int a, unsigned int v) { w32(a, v); }

int mac_trap(int raw_opcode) {
    uint16_t opcode = (uint16_t)raw_opcode;
    uint32_t sp = m68k_get_reg(NULL, M68K_REG_SP);

    if ((opcode & 0xf000) != 0xa000) return 0;
#ifdef STARRY_TRACE
    fprintf(stderr, "trap $%04x pc=$%06x sp=$%06x\n", opcode,
            m68k_get_reg(NULL, M68K_REG_PPC) & 0x00ffffff, sp);
#endif

    switch (opcode) {
        case 0xa322: { /* NewHandleClear: size in D0, Handle in A0 */
            uint32_t handle = new_handle(m68k_get_reg(NULL, M68K_REG_D0));
            m68k_set_reg(M68K_REG_A0, handle);
            m68k_set_reg(M68K_REG_D0, handle ? 0 : (uint32_t)-108);
            break;
        }
        case 0xa029: /* HLock */
        case 0xa02a: /* HUnlock */
            break;

        case 0xa861: { /* Random: function result is already reserved at SP */
            random_seed = (int32_t)((int64_t)random_seed * 16807 % 2147483647);
            if (random_seed <= 0) random_seed += 2147483646;
            w32(LM_RAND_SEED, (uint32_t)random_seed);
            w16(sp, (uint16_t)((random_seed >> 16) ^ (random_seed & 0xffff)));
            break;
        }
        case 0xa862: { /* ForeColor */
#ifdef STARRY_TRACE
            fprintf(stderr, "  ForeColor(%d)\n", (int32_t)r32(sp));
#endif
            foreground = classic_color((int32_t)r32(sp));
            w32(port + 80, r32(sp));
            pop_bytes(4);
            break;
        }
        case 0xa863: { /* BackColor */
            w32(port + 84, r32(sp));
            pop_bytes(4);
            break;
        }
        case 0xa87b: { /* ClipRect */
            clip_rect = read_rect(r32(sp));
            uint32_t data = r32(r32(port + 28));
            if (data) write_rect(data + 2, clip_rect);
            pop_bytes(4);
            break;
        }
        case 0xa892: { /* Line(deltaHorizontal, deltaVertical) */
            int dv = (int16_t)r16(sp), dh = (int16_t)r16(sp + 2);
            pop_bytes(4);
            draw_line(dh, dv);
            request_drawing_yield();
            break;
        }
        case 0xa893: /* MoveTo(h, v) */
            w16(port + 48, r16(sp));     /* vertical */
            w16(port + 50, r16(sp + 2)); /* horizontal */
            pop_bytes(4);
            break;
        case 0xa898: { /* GetPenState */
            uint32_t state = r32(sp);
            /* pnLoc, pnSize, pnMode, and pnPat are contiguous in GrafPort. */
            for (uint32_t i = 0; i < 18; i++) w8(state + i, r8(port + 48 + i));
            pop_bytes(4);
            break;
        }
        case 0xa899: { /* SetPenState */
            uint32_t state = r32(sp);
            for (uint32_t i = 0; i < 18; i++) w8(port + 48 + i, r8(state + i));
            pop_bytes(4);
            break;
        }
        case 0xa89a: { /* GetPen */
            uint32_t point = r32(sp);
            w16(point, r16(port + 48));
            w16(point + 2, r16(port + 50));
            pop_bytes(4);
            break;
        }
        case 0xa89b: /* PenSize(width, height) */
            w16(port + 52, r16(sp));     /* height */
            w16(port + 54, r16(sp + 2)); /* width */
            pop_bytes(4);
            break;
        case 0xa89c: /* PenMode */
            w16(port + 56, r16(sp));
            pop_bytes(2);
            break;
        case 0xa89d: { /* PenPat */
            uint32_t pattern = r32(sp);
            for (uint32_t i = 0; i < 8; i++) w8(port + 58 + i, r8(pattern + i));
            pop_bytes(4);
            break;
        }
        case 0xaa14: { /* RGBForeColor */
            uint32_t rgb = r32(sp);
#ifdef STARRY_TRACE
            fprintf(stderr, "  RGBForeColor(%04x,%04x,%04x)\n",
                    r16(rgb), r16(rgb + 2), r16(rgb + 4));
#endif
            foreground = (Color){(uint8_t)(r16(rgb) >> 8),
                                 (uint8_t)(r16(rgb + 2) >> 8),
                                 (uint8_t)(r16(rgb + 4) >> 8)};
            pop_bytes(4);
            break;
        }
        case 0xa8d4: { /* EraseRgn */
            uint32_t handle = r32(sp);
            uint32_t data = r32(handle);
            pop_bytes(4);
            /* After Dark's screen port carries a solid-black erase pattern. */
            if (data) fill_rect(read_rect(data + 2), (Color){0, 0, 0});
            request_drawing_yield();
            break;
        }
        case 0xa8a2: { /* PaintRect */
#ifdef STARRY_TRACE
            Rect trace_rect = read_rect(r32(sp));
            fprintf(stderr, "  PaintRect(%d,%d,%d,%d) rgb=%u,%u,%u\n",
                    trace_rect.top, trace_rect.left, trace_rect.bottom,
                    trace_rect.right, foreground.r, foreground.g, foreground.b);
#endif
            fill_rect(read_rect(r32(sp)), foreground);
            pop_bytes(4);
            request_drawing_yield();
            break;
        }
        case 0xa8b8: { /* PaintOval */
            fill_oval(read_rect(r32(sp)), foreground);
            pop_bytes(4);
            request_drawing_yield();
            break;
        }
        case 0xa8f9: { /* MapPt(pt, sourceRect, destinationRect) */
            uint32_t dst_ptr = r32(sp);
            uint32_t src_ptr = r32(sp + 4);
            uint32_t pt_ptr = r32(sp + 8);
            Rect src = read_rect(src_ptr), dst = read_rect(dst_ptr);
            int32_t v = (int16_t)r16(pt_ptr), h = (int16_t)r16(pt_ptr + 2);
            int32_t sw = src.right - src.left, sh = src.bottom - src.top;
            int32_t dw = dst.right - dst.left, dh = dst.bottom - dst.top;
            w16(pt_ptr, (uint16_t)(sh ? dst.top + (v - src.top) * dh / sh : dst.top));
            w16(pt_ptr + 2,
                (uint16_t)(sw ? dst.left + (h - src.left) * dw / sw : dst.left));
            pop_bytes(12);
            break;
        }
        default:
            fail_trap(opcode);
            break;
    }
    return 1; /* suppress Musashi's illegal-instruction exception */
}

static uint8_t *read_adgm(const char *path, uint32_t *out_size) {
    FILE *file = fopen(path, "rb");
    if (!file) {
        snprintf(error_text, sizeof error_text, "cannot open %s", path);
        return NULL;
    }
    fseek(file, 0, SEEK_END);
    long file_size_long = ftell(file);
    rewind(file);
    if (file_size_long < 16 || file_size_long > 16 * 1024 * 1024) {
        fclose(file);
        snprintf(error_text, sizeof error_text, "resource fork has an invalid size");
        return NULL;
    }
    size_t file_size = (size_t)file_size_long;
    uint8_t *fork = malloc(file_size);
    if (!fork || fread(fork, 1, file_size, file) != file_size) {
        fclose(file);
        free(fork);
        snprintf(error_text, sizeof error_text, "cannot read resource fork");
        return NULL;
    }
    fclose(file);

    uint32_t data_offset = host_be32(fork + 0);
    uint32_t map_offset = host_be32(fork + 4);
    uint32_t data_length = host_be32(fork + 8);
    uint32_t map_length = host_be32(fork + 12);
    if (data_offset > file_size || data_length > file_size - data_offset ||
        map_offset > file_size || map_length > file_size - map_offset ||
        map_length < 30) goto malformed;

    uint32_t types = map_offset + host_be16(fork + map_offset + 24);
    if (types + 2 > file_size) goto malformed;
    uint32_t type_count = (uint32_t)host_be16(fork + types) + 1;
    if (type_count > 256 || types + 2 + type_count * 8 > file_size) goto malformed;

    for (uint32_t i = 0; i < type_count; i++) {
        uint32_t type = types + 2 + i * 8;
        if (memcmp(fork + type, "ADgm", 4) != 0) continue;
        uint32_t refs = types + host_be16(fork + type + 6);
        uint32_t count = (uint32_t)host_be16(fork + type + 4) + 1;
        if (count > 4096 || refs + count * 12 > file_size) goto malformed;
        for (uint32_t j = 0; j < count; j++) {
            uint32_t ref = refs + j * 12;
            if ((int16_t)host_be16(fork + ref) != 0) continue;
            uint32_t relative = (uint32_t)fork[ref + 5] << 16 |
                                (uint32_t)fork[ref + 6] << 8 | fork[ref + 7];
            uint32_t block = data_offset + relative;
            if (block + 4 > file_size) goto malformed;
            uint32_t size = host_be32(fork + block);
            if (size > file_size - block - 4) goto malformed;
            uint8_t *copy = malloc(size);
            if (!copy) goto malformed;
            memcpy(copy, fork + block + 4, size);
            free(fork);
            *out_size = size;
            return copy;
        }
    }
    free(fork);
    snprintf(error_text, sizeof error_text,
             "no ADgm 0 resource; use a projected Starry Night module");
    return NULL;

malformed:
    free(fork);
    snprintf(error_text, sizeof error_text, "malformed Macintosh resource fork");
    return NULL;
}

static void initialize_quickdraw(void) {
    Rect screen = {0, 0, SCREEN_HEIGHT, SCREEN_WIDTH};
    clip_rect = screen;
    screen_bits = allocate(SCREEN_WIDTH * SCREEN_HEIGHT);
    memset(ram + screen_bits, 0xff, SCREEN_WIDTH * SCREEN_HEIGHT);
    port = allocate(156);
    qd_globals = allocate(206);

    w32(port + 2, screen_bits);
    w16(port + 6, SCREEN_WIDTH);
    write_rect(port + 8, screen);
    write_rect(port + 16, screen);
    w32(port + 24, new_region(screen));
    w32(port + 28, new_region(screen));
    w16(port + 52, 1);
    w16(port + 54, 1);
    for (int i = 0; i < 8; i++) w8(port + 58 + (uint32_t)i, 0xff);
    w32(port + 80, 33); /* blackColor */
    w32(port + 84, 30); /* whiteColor */

    w32(qd_globals, port);
    for (int i = 0; i < 8; i++) {
        w8(qd_globals + 12 + (uint32_t)i, 0xff);
        w8(qd_globals + 20 + (uint32_t)i, (i & 1) ? 0x55 : 0xaa);
        w8(qd_globals + 28 + (uint32_t)i, (i & 1) ? 0x11 : 0x88);
        w8(qd_globals + 36 + (uint32_t)i, (i & 1) ? 0x77 : 0xdd);
    }
    w32(qd_globals + 80, screen_bits);
    w16(qd_globals + 84, SCREEN_WIDTH);
    write_rect(qd_globals + 86, screen);
    w32(qd_globals + 112, screen_bits);
    w16(qd_globals + 116, SCREEN_WIDTH);
    write_rect(qd_globals + 118, screen);

    /* The application A5 world exposes the same QuickDraw globals. */
    w32(A5_WORLD, A5_WORLD);
    w32(A5_WORLD - 4, port);
    w32(A5_WORLD - 122, screen_bits);
    w16(A5_WORLD - 118, SCREEN_WIDTH);
    write_rect(A5_WORLD - 116, screen);
    for (int i = 0; i < 8; i++) {
        w8(A5_WORLD - 16 + (uint32_t)i, 0xff);
        w8(A5_WORLD - 24 + (uint32_t)i, (i & 1) ? 0x55 : 0xaa);
        w8(A5_WORLD - 32 + (uint32_t)i, (i & 1) ? 0x11 : 0x88);
        w8(A5_WORLD - 40 + (uint32_t)i, (i & 1) ? 0x77 : 0xdd);
    }

    w32(LM_SCRNBASE, screen_bits);
    w16(LM_SCREENBYTES, SCREEN_WIDTH);
    w32(LM_THE_PORT, port);
}

static void initialize_after_dark(void) {
    Rect screen = {0, 0, SCREEN_HEIGHT, SCREEN_WIDTH};
    storage_var = allocate(4);
    blank_rgn = new_region(screen);

    uint32_t monitors = allocate(18);
    w16(monitors, 1);
    write_rect(monitors + 2, screen);
    w8(monitors + 10, 0);  /* synchFlag */
    w8(monitors + 11, 8);  /* curDepth */
    w32(monitors + 14, port);

    param_block = allocate(44);
    w32(param_block + 8, monitors);
    w8(param_block + 12, 1);       /* Color QuickDraw is available */
    w16(param_block + 14, 0xc1af); /* capabilities */
    w32(param_block + 16, qd_globals);
    w16(param_block + 20, 255);    /* brightness */
    w32(param_block + 30, allocate(256));
    w16(param_block + 38, 0x0300); /* host protocol version */
}

static void begin_message(int16_t message) {
    memcpy(ram + CODE_BASE, code_prefix, sizeof code_prefix);
    m68k_set_reg(M68K_REG_SP, STACK_TOP);
    push16(0);             /* OSErr result slot */
    push32(storage_var);   /* VAR storage: address of Handle variable */
    push32(blank_rgn);
    push16((uint16_t)message);
    push32(param_block);
    push32(SENTINEL);
    m68k_set_reg(M68K_REG_A5, A5_WORLD);
    w32(LM_CURRENT_A5, A5_WORLD);
    m68k_set_reg(M68K_REG_PC, CODE_BASE);
#ifdef STARRY_TRACE
    fprintf(stderr, "begin message %d code=%02x%02x%02x%02x sp=$%06x\n",
            message, r8(CODE_BASE), r8(CODE_BASE + 1), r8(CODE_BASE + 2),
            r8(CODE_BASE + 3), m68k_get_reg(NULL, M68K_REG_SP));
#endif
}

static bool finish_message(int16_t message) {
    begin_message(message);
    drawing_yield = false;
    for (uint32_t cycles = 0; cycles < 500000000; cycles += 200000) {
        m68k_execute(200000);
        if (guest_fault) return false;
        if ((m68k_get_reg(NULL, M68K_REG_PC) & 0x00ffffff) == SENTINEL) {
            int16_t result = (int16_t)r16(STACK_TOP - 2);
            if (result != 0) {
                snprintf(error_text, sizeof error_text,
                         "After Dark message %d returned %d", message, result);
                return false;
            }
            return true;
        }
        drawing_yield = false; /* boot-time drawing never waits for the browser */
    }
    snprintf(error_text, sizeof error_text, "guest did not return from message %d", message);
    return false;
}

bool starry_boot(const char *resource_path) {
    uint8_t *code = read_adgm(resource_path, &code_size);
    if (!code) return false;
#ifdef STARRY_TRACE
    fprintf(stderr, "ADgm size=%u prefix=%02x%02x%02x%02x\n", code_size,
            code[0], code[1], code[2], code[3]);
#endif
    if (code_size < 4 || code_size > HEAP_BASE - CODE_BASE) {
        free(code);
        snprintf(error_text, sizeof error_text, "ADgm code has an invalid size");
        return false;
    }

    ram = calloc(1, RAM_SIZE);
    if (!ram) {
        free(code);
        snprintf(error_text, sizeof error_text, "cannot allocate 16 MiB guest RAM");
        return false;
    }
    clear_pixels((Color){0, 0, 0});
    heap_next = HEAP_BASE;
    random_seed = 1;
    w32(LM_RAND_SEED, 1);
    w32(0, STACK_TOP);
    w32(4, SENTINEL);
    for (int vector = 2; vector < 64; vector++) w32((uint32_t)vector * 4, SENTINEL);
    w16(SENTINEL, 0x60fe); /* BRA.S * */
    memcpy(ram + CODE_BASE, code, code_size);
    memcpy(code_prefix, code, sizeof code_prefix);
    free(code);

    initialize_quickdraw();
    initialize_after_dark();

    m68k_init();
    m68k_set_cpu_type(M68K_CPU_TYPE_68EC020);
    m68k_pulse_reset();

    if (!finish_message(AD_INITIALIZE) || !finish_message(AD_BLANK)) return false;
    frame_active = false;
    completed_frames = 0;
    return true;
}

StepResult starry_step(void) {
    if (guest_fault || !ram) return STARRY_FAULT;
    if (!frame_active) {
        begin_message(AD_DRAW_FRAME);
        frame_active = true;
    }
    drawing_yield = false;
    int executed = m68k_execute(200000);
    (void)executed;
#ifdef STARRY_TRACE
    fprintf(stderr, "  execute=%d pc=$%06x\n", executed,
            m68k_get_reg(NULL, M68K_REG_PC) & 0x00ffffff);
#endif
    if (guest_fault) return STARRY_FAULT;
    if ((m68k_get_reg(NULL, M68K_REG_PC) & 0x00ffffff) == SENTINEL) {
        frame_active = false;
        completed_frames++;
        w32(LM_TICKS, r32(LM_TICKS) + 2); /* 30 frames/s, 60 classic ticks/s */
        return STARRY_COMPLETE;
    }
    return drawing_yield ? STARRY_YIELDED : STARRY_RUNNING;
}

const uint8_t *starry_rgba(void) { return rgba; }
const char *starry_error(void) { return error_text; }
uint32_t starry_completed_frames(void) { return completed_frames; }

size_t starry_lit_pixels(void) {
    size_t count = 0;
    for (size_t i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; i++)
        if (rgba[i * 4] || rgba[i * 4 + 1] || rgba[i * 4 + 2]) count++;
    return count;
}

void starry_shutdown(void) {
    free(ram);
    ram = NULL;
}
