#!/usr/bin/env python3
"""Build the original Star Catch NES demo without external tools or assets.
MIT licensed; see local-app/demo/LICENSE.txt. All tile art is generated here.
6502 instructions are emitted explicitly so the complete ROM is reproducible.
"""
from pathlib import Path
import hashlib
import json
import struct

SOURCE_DIR = Path(__file__).resolve().parent
OUT = SOURCE_DIR if SOURCE_DIR.name == 'demo' else SOURCE_DIR.parent / 'local-app/demo'
code = bytearray()
labels = {}
fixups = []

def label(name): labels[name] = 0xC000 + len(code)
def emit(*values): code.extend(values)
def absolute(op, value):
    emit(op)
    if isinstance(value, str):
        fixups.append((len(code), value, False)); emit(0, 0)
    else: code.extend(struct.pack('<H', value))
def branch(op, target):
    emit(op); fixups.append((len(code), target, True)); emit(0)
def lda(value): emit(0xA9, value)
def sta(addr): absolute(0x8D, addr)
def load(addr): absolute(0xAD, addr)
def jump(name): absolute(0x4C, name)
def call(name): absolute(0x20, name)

# Zero page: frame=$00, x=$01, star x=$02, star y=$03, buttons=$04,
# score units=$05, score tens=$06, PRNG=$07, misses=$08.
label('reset')
emit(0x78, 0xD8)                 # SEI, CLD
emit(0xA2, 0xFF, 0x9A, 0xE8)     # LDX #$ff, TXS, INX
emit(0x8E, 0x00, 0x20, 0x8E, 0x01, 0x20)  # STX PPUCTRL/PPUMASK
lda(0x40); sta(0x4017)
lda(0); sta(0x4010); sta(0x4015)
label('vblank1'); absolute(0x2C, 0x2002); branch(0x10, 'vblank1')
lda(0); emit(0xA2, 0)
label('clear')
for page in (0x0000, 0x0100, 0x0300, 0x0400, 0x0500, 0x0600, 0x0700): absolute(0x9D, page)
emit(0xE8); branch(0xD0, 'clear')
lda(0xFF); emit(0xA2, 0)
label('hide'); absolute(0x9D, 0x0200); emit(0xE8); branch(0xD0, 'hide')
label('vblank2'); absolute(0x2C, 0x2002); branch(0x10, 'vblank2')
# Clear name table and attributes.
lda(0x20); sta(0x2006); lda(0); sta(0x2006)
emit(0xA2, 4, 0xA0, 0)
label('clear_nt'); sta(0x2007); emit(0xC8); branch(0xD0, 'clear_nt'); emit(0xCA); branch(0xD0, 'clear_nt')
# All background palettes pale cyan; sprite palettes cyan and gold.
lda(0x3F); sta(0x2006); lda(0); sta(0x2006); emit(0xA2, 0)
label('palette'); absolute(0xBD, 'palettes'); sta(0x2007); emit(0xE8, 0xE0, 32); branch(0xD0, 'palette')
# Original title and instruction tile strings on static background.
for addr, text in [(0x208B, 'STAR CATCH'), (0x2327, 'LEFT RIGHT TO CATCH'), (0x20C2, 'SCORE'), (0x20D6, 'MISS')]:
    lda(addr >> 8); sta(0x2006); lda(addr & 255); sta(0x2006)
    for char in text:
        lda(0 if char == ' ' else 16 + ord(char) - 65); sta(0x2007)
lda(120); sta(1); lda(80); sta(2); lda(48); sta(3); lda(0xA7); sta(7)
lda(0); sta(0x2005); sta(0x2005)
lda(0x80); sta(0x2000); lda(0x1E); sta(0x2001)
label('main')
load(0); branch(0xF0, 'main'); lda(0); sta(0)
# Strobe/read controller.
lda(1); sta(0x4016); lda(0); sta(0x4016); sta(4); emit(0xA2, 8)
label('read_pad'); load(0x4016); emit(0x4A, 0x26, 4, 0xCA); branch(0xD0, 'read_pad')
load(4); emit(0x29, 2); branch(0xF0, 'right')
load(1); emit(0xC9, 12); branch(0x90, 'right'); emit(0x38, 0xE9, 3); sta(1)
label('right'); load(4); emit(0x29, 1); branch(0xF0, 'fall')
load(1); emit(0xC9, 230); branch(0xB0, 'fall'); emit(0x18, 0x69, 3); sta(1)
label('fall'); emit(0xE6, 3, 0xE6, 3)
load(3); emit(0xC9, 199); branch(0x90, 'draw'); emit(0xC9, 203); branch(0xB0, 'miss_check')
# Ball can overlap either of the two paddle tiles; carry-aware signed distance.
load(2); emit(0x38, 0xE5, 1, 0x18, 0x69, 7, 0xC9, 23); branch(0xB0, 'miss_check')
emit(0xE6, 5); load(5); emit(0xC9, 10); branch(0x90, 'respawn')
lda(0); sta(5); emit(0xE6, 6); load(6); emit(0xC9, 10); branch(0x90, 'respawn'); lda(0); sta(6); jump('respawn')
label('miss_check'); load(3); emit(0xC9, 224); branch(0x90, 'draw')
emit(0xE6, 8); load(8); emit(0xC9, 10); branch(0x90, 'respawn'); lda(0); sta(8)
label('respawn')
lda(48); sta(3)
load(7); emit(0x0A); branch(0x90, 'seed'); emit(0x49, 0x1D)
label('seed'); sta(7); emit(0x29, 0xBF, 0x18, 0x69, 24); sta(2)
label('draw')
# PPU sprites: two paddle tiles, falling star, score tens/units, miss counter.
for base, y, tile, attr in [(0x200, 207, 1, 0), (0x204, 207, 1, 0), (0x208, None, 2, 1), (0x20C, 47, None, 0), (0x210, 47, None, 0), (0x214, 47, None, 1)]:
    if y is not None: lda(y); sta(base)
    if tile is not None: lda(tile); sta(base + 1)
    lda(attr); sta(base + 2)
load(1); sta(0x203); emit(0x18, 0x69, 8); sta(0x207)
load(3); sta(0x208); load(2); sta(0x20B)
for var, base, x in [(6, 0x20C, 66), (5, 0x210, 74), (8, 0x214, 216)]:
    load(var); emit(0x18, 0x69, 3); sta(base + 1); lda(x); sta(base + 3)
jump('main')
label('nmi')
emit(0x48, 0x8A, 0x48, 0x98, 0x48)
lda(0); sta(0x2003); lda(2); sta(0x4014)
lda(0); sta(0x2005); sta(0x2005); lda(1); sta(0)
emit(0x68, 0xA8, 0x68, 0xAA, 0x68, 0x40)
label('irq'); emit(0x40)
label('palettes')
emit(*([0x0F, 0x21, 0x31, 0x30] * 4 + [0x0F, 0x21, 0x31, 0x30, 0x0F, 0x28, 0x38, 0x30] * 2))
for offset, name, relative in fixups:
    address = labels[name]
    if relative:
        distance = address - (0xC000 + offset + 1)
        assert -128 <= distance <= 127, (name, distance)
        code[offset] = distance & 255
    else: code[offset:offset + 2] = struct.pack('<H', address)
assert len(code) < 16378
prg = code + bytes(16378 - len(code)) + struct.pack('<HHH', labels['nmi'], labels['reset'], labels['irq'])
chr_data = bytearray(8192)
def tile(index, rows):
    for y, row in enumerate(rows):
        value = sum(1 << (7 - x) for x, c in enumerate(row) if c != '.')
        chr_data[index * 16 + y] = value
# Original geometric pixel artwork and hand-entered 3x5 block glyphs.
tile(1, ['........','........','########','########','.#.##.#.','........','........','........'])
tile(2, ['...#....','...#....','.#####..','..###...','.#####..','..#.#...','........','........'])
glyphs = {
'0':['###','#.#','#.#','#.#','###'], '1':['.#.','##.','.#.','.#.','###'],
'2':['###','..#','###','#..','###'], '3':['###','..#','###','..#','###'],
'4':['#.#','#.#','###','..#','..#'], '5':['###','#..','###','..#','###'],
'6':['###','#..','###','#.#','###'], '7':['###','..#','..#','.#.','.#.'],
'8':['###','#.#','###','#.#','###'], '9':['###','#.#','###','..#','###'],
'A':['.#.','#.#','###','#.#','#.#'], 'C':['###','#..','#..','#..','###'],
'E':['###','#..','##.','#..','###'], 'F':['###','#..','##.','#..','#..'],
'G':['###','#..','#.#','#.#','###'], 'H':['#.#','#.#','###','#.#','#.#'],
'I':['###','.#.','.#.','.#.','###'], 'L':['#..','#..','#..','#..','###'],
'M':['#.#','###','###','#.#','#.#'], 'O':['###','#.#','#.#','#.#','###'],
'R':['##.','#.#','##.','#.#','#.#'], 'S':['###','#..','###','..#','###'],
'T':['###','.#.','.#.','.#.','.#.']}
for char, rows in glyphs.items():
    index = 3 + int(char) if char.isdigit() else 16 + ord(char) - 65
    tile(index, ['........'] + ['..' + row + '...' for row in rows])
rom = b'NES\x1a' + bytes([1, 1, 0, 0]) + bytes(8) + prg + chr_data
OUT.mkdir(parents=True, exist_ok=True)
path = OUT / 'star-catch.nes'
path.write_bytes(rom)
print(json.dumps({'file': str(path), 'bytes': len(rom), 'sha256': hashlib.sha256(rom).hexdigest(), 'program_bytes': len(code)}))
