#!/usr/bin/env python3
"""Build RT-4D app image with narrow single-byte CP1251 Cyrillic support.

Mechanism (all in the recoverable MCU app region, no SPI writes):
  1. dispatch patch @0x08008A6A: cmp #0x80 -> #0xFF  (bytes 0x80..0xFE take the
     narrow 7px single-byte path instead of the 14px CJK double-byte path)
  2. blitter redirect @0x08007FDA: bl SPI_read -> bl CAVE
  3. CAVE routine (appended): for glyph src addr >= 0x0019C540 (font index >=96,
     i.e. byte>=0x80) memcpy the 14-byte glyph from our in-flash Cyrillic table;
     otherwise tail-call the original SPI_read (ASCII unchanged).
  4. Cyrillic glyph table (appended): 128 cells (byte 0x80..0xFF) x 14 bytes,
     column-major 7x14 (2 bytes/col LE, bit0=top) — same format as the ASCII font.
This test only fills the few glyphs needed to render one CP1251 word.
"""
import sys, struct
from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB

BASE = 0x08002800
CAVE_ADDR = 0x08028860          # file 0x26060
CYR_BASE  = 0x08028900          # file 0x26100  (glyphs for byte 0x80..0xFF)
SPI_READ  = 0x08021828          # original glyph-load routine (thumb entry+1)
IMG_END   = 0x08029000          # extend image to cover the font table

# ---- glyph editor: 14 rows x 7 cols, '#'=on ; encode column-major 2B/col LE --
# Glyphs are drawn <=6 columns wide (col 6 stays blank) so the fixed 7px advance
# always leaves >=1px between letters.
def enc(rows):
    assert len(rows) == 14
    out = bytearray()
    for c in range(7):
        v = 0
        for r in range(14):
            if c < len(rows[r]) and rows[r][c] == '#':
                v |= 1 << r
        out += struct.pack('<H', v)
    assert struct.unpack('<H', out[12:14])[0] == 0, "col 6 must stay blank (<=6px wide)"
    return bytes(out)

BL = "      "   # blank row (6 wide)
def G(*body, top=3):
    rows = [BL]*14
    for i, r in enumerate(body):
        rows[top+i] = r
    return enc(rows)

GLYPHS = {  # CP1251 code -> 6px-wide 14-tall bitmap (col 6 blank => 1px gap)
 0xC7: G(".####.","#....#",".....#","..###.",".....#","#....#",".####."),   # З
 0xEE: G(".####.","#....#","#....#","#....#",".####.", top=5),              # о
 0xED: G("#....#","#....#","######","#....#","#....#", top=5),              # н
 0xFB: G("#....#","#....#","##...#","#.#..#","##...#", top=5),              # ы
}

def main():
    inp  = sys.argv[1] if len(sys.argv) > 1 else "rt4d_stock_v3.25_abs_0x08002800.bin"
    outp = sys.argv[2] if len(sys.argv) > 2 else "rt4d_ru_font_test.bin"
    stock = open(inp, "rb").read()
    img = bytearray(b"\xff" * (IMG_END - BASE))
    img[:len(stock)] = stock

    def w(va, data):
        img[va-BASE:va-BASE+len(data)] = data

    ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)

    # --- CAVE ---
    cave_asm = f"""
        movw r3, #0xC540
        movt r3, #0x0019
        cmp  r1, r3
        blo  do_spi
        subs r1, r1, r3
        movw r3, #{CYR_BASE & 0xFFFF}
        movt r3, #{CYR_BASE >> 16}
        adds r1, r1, r3
      copy:
        ldrb r3, [r1]
        strb r3, [r0]
        adds r1, r1, #1
        adds r0, r0, #1
        subs r2, r2, #1
        bne  copy
        bx   lr
      do_spi:
        movw r3, #{SPI_READ & 0xFFFF}
        movt r3, #{SPI_READ >> 16}
        orr  r3, r3, #1
        bx   r3
    """
    cave, _ = ks.asm(cave_asm, CAVE_ADDR)
    cave = bytes(cave)
    assert len(cave) <= (CYR_BASE - CAVE_ADDR), "cave too big"
    w(CAVE_ADDR, cave)

    # --- blitter redirect: bl CAVE at 0x08007FDA ---
    bl, _ = ks.asm(f"bl #{CAVE_ADDR}", 0x08007FDA)
    assert len(bl) == 4
    orig = bytes(img[0x08007FDA-BASE:0x08007FDA-BASE+4])
    w(0x08007FDA, bytes(bl))

    # --- dispatch patch: cmp r0,#0x80 -> #0xFF at 0x08008A6A ---
    dpo = 0x08008A6A - BASE
    assert img[dpo] == 0x80 and img[dpo+1] == 0x28, f"unexpected {img[dpo]:02x} {img[dpo+1]:02x}"
    img[dpo] = 0xFF

    # --- Cyrillic glyph table ---
    for code, g in GLYPHS.items():
        w(CYR_BASE + (code-0x80)*14, g)

    # --- menu string: "Zone Set      06" -> CP1251 "Зоны"+pad+"06" ---
    rec = 0x0802543D
    old = b"Zone Set      06"
    assert bytes(img[rec-BASE:rec-BASE+16]) == old
    new = "Зоны".encode("cp1251") + b" "*10 + b"06"
    assert len(new) == 16, len(new)
    w(rec, new)

    # sanity: vectors intact
    sp, rst = struct.unpack_from("<II", img, 0)
    assert (sp, rst) == struct.unpack_from("<II", stock, 0)

    open(outp, "wb").write(img)
    print(f"cave {len(cave)}B @0x{CAVE_ADDR:08X}; bl {orig.hex()}->{bytes(bl).hex()}")
    print(f'"Зоны" cp1251 = {new[:8].hex(" ")}')
    print(f"image {len(img)} bytes -> {outp}")

if __name__ == "__main__":
    main()
