#!/usr/bin/env python3
"""
polyglot_screen.py — builds a ZX Spectrum SCREEN$ that is also a program.
From the article at https://thespeccy.com/articles/polyglot-screen

The same 6912 bytes work two ways:

    Picture:   LOAD "" CODE            (header autoloads it to 16384)
    Program:   CLEAR 39999
               LOAD "" CODE 40000
               RANDOMIZE USR 46144

The trick: a SCREEN$ is 6144 bytes of bitmap followed by 768 bytes of
attributes (ink/paper colours). ANY byte is a legal attribute — so we put
Z80 machine code in the attribute area, where it appears on screen as a
mosaic of coloured cells. Entry point = load address + 6144.

Outputs: polyglot.scr (raw 6912 bytes) and polyglot.tap (tape image).
No dependencies — pure Python 3.
"""

# ----------------------------------------------------------------------
# The payload, hand-assembled. ORG 46144 (0xB440) = 40000 + 6144.
# It cycles the border through the rainbow, then prints a message
# via the ROM's own print routine, and returns cleanly to BASIC.
# ----------------------------------------------------------------------

ORG = 46144
MSG_TEXT = b"\rHELLO! I WAS HIDING IN THE COLOURS.\r"

PAYLOAD = bytes([
    0x06, 0x40,             # 0000  LD   B,64        ; 64 border changes
                            # outer:
    0x78,                   # 0002  LD   A,B
    0xE6, 0x07,             # 0003  AND  7           ; colour 0-7
    0xD3, 0xFE,             # 0005  OUT  (254),A     ; set border
    0x11, 0x00, 0x0C,       # 0007  LD   DE,3072     ; delay counter
                            # delay:
    0x1B,                   # 000A  DEC  DE          ; (DEC rr sets no flags,
    0x7A,                   # 000B  LD   A,D         ;  hence the OR test)
    0xB3,                   # 000C  OR   E
    0x20, 0xFB,             # 000D  JR   NZ,delay
    0x10, 0xF1,             # 000F  DJNZ outer
    0x3E, 0x07,             # 0011  LD   A,7
    0xD3, 0xFE,             # 0013  OUT  (254),A     ; border white again
    0x3E, 0x02,             # 0015  LD   A,2
    0xCD, 0x01, 0x16,       # 0017  CALL 0x1601      ; ROM CHAN-OPEN, stream 2
    0x21, 0x64, 0xB4,       # 001A  LD   HL,msg      ; 0xB464 = ORG+36
                            # print:
    0x7E,                   # 001D  LD   A,(HL)
    0xB7,                   # 001E  OR   A
    0xC8,                   # 001F  RET  Z           ; zero terminator -> BASIC
    0xD7,                   # 0020  RST  0x10        ; ROM print A
    0x23,                   # 0021  INC  HL
    0x18, 0xF9,             # 0022  JR   print
]) + MSG_TEXT + b"\x00"     # 0024  msg: bytes 13,"HELLO!...",13,0

assert ORG + 36 == 0xB464, "message address baked into LD HL above"
assert len(PAYLOAD) <= 768, "payload must fit in the attribute area"

# ----------------------------------------------------------------------
# Public-domain 8x8 font (Marcel Sondaar / Daniel Hepper, font8x8),
# just the glyphs we need. Rows top-to-bottom, bit 0 = leftmost pixel.
# ----------------------------------------------------------------------

FONT = {  # public-domain 8x8 font (Marcel Sondaar / Daniel Hepper)
    ' ': [0, 0, 0, 0, 0, 0, 0, 0],
    '!': [24, 60, 60, 24, 24, 0, 24, 0],
    '"': [54, 54, 0, 0, 0, 0, 0, 0],
    '$': [12, 62, 3, 30, 48, 31, 12, 0],
    '.': [0, 0, 0, 0, 0, 12, 12, 0],
    '0': [62, 99, 115, 123, 111, 103, 62, 0],
    '1': [12, 14, 12, 12, 12, 12, 63, 0],
    '2': [30, 51, 48, 28, 6, 51, 63, 0],
    '3': [30, 51, 48, 28, 48, 51, 30, 0],
    '4': [56, 60, 54, 51, 127, 48, 120, 0],
    '5': [63, 3, 31, 48, 48, 51, 30, 0],
    '6': [28, 6, 3, 31, 51, 51, 30, 0],
    '7': [63, 51, 48, 24, 12, 12, 12, 0],
    '8': [30, 51, 51, 30, 51, 51, 30, 0],
    '9': [30, 51, 51, 62, 48, 24, 14, 0],
    ':': [0, 12, 12, 0, 0, 12, 12, 0],
    'A': [12, 30, 51, 51, 63, 51, 51, 0],
    'B': [63, 102, 102, 62, 102, 102, 63, 0],
    'C': [60, 102, 3, 3, 3, 102, 60, 0],
    'D': [31, 54, 102, 102, 102, 54, 31, 0],
    'E': [127, 70, 22, 30, 22, 70, 127, 0],
    'F': [127, 70, 22, 30, 22, 6, 15, 0],
    'G': [60, 102, 3, 3, 115, 102, 124, 0],
    'H': [51, 51, 51, 63, 51, 51, 51, 0],
    'I': [30, 12, 12, 12, 12, 12, 30, 0],
    'J': [120, 48, 48, 48, 51, 51, 30, 0],
    'K': [103, 102, 54, 30, 54, 102, 103, 0],
    'L': [15, 6, 6, 6, 70, 102, 127, 0],
    'M': [99, 119, 127, 127, 107, 99, 99, 0],
    'N': [99, 103, 111, 123, 115, 99, 99, 0],
    'O': [28, 54, 99, 99, 99, 54, 28, 0],
    'P': [63, 102, 102, 62, 6, 6, 15, 0],
    'Q': [30, 51, 51, 51, 59, 30, 56, 0],
    'R': [63, 102, 102, 62, 54, 102, 103, 0],
    'S': [30, 51, 7, 14, 56, 51, 30, 0],
    'T': [63, 45, 12, 12, 12, 12, 30, 0],
    'U': [51, 51, 51, 51, 51, 51, 63, 0],
    'V': [51, 51, 51, 51, 51, 30, 12, 0],
    'W': [99, 99, 99, 107, 127, 119, 99, 0],
    'X': [99, 99, 54, 28, 28, 54, 99, 0],
    'Y': [51, 51, 51, 30, 12, 12, 30, 0],
    'Z': [127, 99, 49, 24, 76, 102, 127, 0],
}

# ----------------------------------------------------------------------
# Screen text: (character row, text, attribute byte for that row's cells)
# Attribute byte = FLASH*128 + BRIGHT*64 + PAPER*8 + INK
# ----------------------------------------------------------------------

WHITE_BRIGHT = 0x47   # bright white ink on black
WHITE = 0x07          # white ink on black
GREEN_BRIGHT = 0x44   # bright green ink on black
CYAN = 0x05           # cyan ink on black

TEXT_LINES = [
    (4,  "THIS PICTURE IS A PROGRAM", WHITE_BRIGHT),
    (6,  "THE FLICKERING MOSAIC UP TOP", WHITE),
    (7,  "IS REAL Z80 MACHINE CODE", WHITE),
    (8,  "SEEN AS INK AND PAPER", WHITE),
    (11, "TO RUN THE PICTURE:", CYAN),
    (13, "CLEAR 39999", GREEN_BRIGHT),
    (14, 'LOAD "" CODE 40000', GREEN_BRIGHT),
    (15, "RANDOMIZE USR 46144", GREEN_BRIGHT),
    (18, "THESPECCY.COM", WHITE_BRIGHT),
]

# ----------------------------------------------------------------------
# Build the bitmap (6144 bytes) and attributes (768 bytes)
# ----------------------------------------------------------------------

def screen_offset(y: int, x_cell: int) -> int:
    """The Spectrum's interleaved bitmap layout, as a file offset."""
    return ((y & 0xC0) << 5) | ((y & 0x07) << 8) | ((y & 0x38) << 2) | x_cell


bitmap = bytearray(6144)
attrs = bytearray(768)

# Character rows 0-2 (pixel rows 0-23): solid ink, so the payload bytes
# show through as their INK colours. This is where the code lives.
for y in range(0, 24):
    for cx in range(32):
        bitmap[screen_offset(y, cx)] = 0xFF

# The payload IS the first attribute bytes. The rest of its band is black.
attrs[0:len(PAYLOAD)] = PAYLOAD
for i in range(len(PAYLOAD), 96):
    attrs[i] = 0x00

# Text rows: render glyphs into the bitmap, set the row's attributes.
for row, text, attr in TEXT_LINES:
    col = (32 - len(text)) // 2          # centred
    for ci, ch in enumerate(text.upper()):
        glyph = FONT[ch]
        for gy in range(8):
            byte = 0
            for gx in range(8):          # font bit0 = left, screen bit7 = left
                if glyph[gy] & (1 << gx):
                    byte |= 0x80 >> gx
            bitmap[screen_offset(row * 8 + gy, col + ci)] = byte
    for cx in range(32):
        attrs[row * 32 + cx] = attr

# Bottom four rows: the Sinclair rainbow, as solid paper stripes.
for row, paper in ((20, 2), (21, 6), (22, 4), (23, 5)):   # red yellow green cyan
    for cx in range(32):
        attrs[row * 32 + cx] = 0x40 | (paper << 3)          # bright, paper colour

screen = bytes(bitmap + attrs)
assert len(screen) == 6912

# ----------------------------------------------------------------------
# Write .scr (raw) and .tap (tape image)
# ----------------------------------------------------------------------

def tap_block(flag: int, data: bytes) -> bytes:
    body = bytes([flag]) + data
    checksum = 0
    for b in body:
        checksum ^= b
    body += bytes([checksum])
    return len(body).to_bytes(2, "little") + body


header = bytes([3])                      # type 3 = CODE
header += b"polyglot  "                  # 10-char filename
header += len(screen).to_bytes(2, "little")
header += (16384).to_bytes(2, "little")  # default load address: the screen!
header += (32768).to_bytes(2, "little")  # unused for CODE blocks

with open("polyglot.scr", "wb") as f:
    f.write(screen)
with open("polyglot.tap", "wb") as f:
    f.write(tap_block(0x00, header))
    f.write(tap_block(0xFF, screen))

print(f"payload: {len(PAYLOAD)} bytes of the 768 attribute bytes")
print("wrote polyglot.scr (6912 bytes) and polyglot.tap")
