#!/usr/bin/env python3
"""Assemble the fully-russified RT-4D app image (bold font v2).
  - narrow single-byte CP1251 render (dispatch patch + blitter code-cave)
  - bold uppercase Cyrillic font: 13 letters reuse stock ASCII glyphs, 20 drawn
  - all 485 menu/option records translated to uppercase Russian
All patches in the recoverable MCU app region; no SPI writes.
(Selection-highlight patch intentionally omitted pending correct RE.)
"""
import struct, json
from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB
import glyphs_ru2 as GR

BASE      = 0x08002800
CAVE_ADDR = 0x08028860
CYR_BASE  = 0x08028900
SPI_READ  = 0x08021828
IMG_END   = 0x08029000
ROOT      = "C:/Users/vikto/Documents/Claude/rt-4d"
DUMP      = f"{ROOT}/radio-spi-dump.bin"
ASCII_FONT = 0x19C000

def enc_glyph(rows):
    out = bytearray()
    for c in range(7):
        v = sum((1 << r) for r in range(14) if c < len(rows[r]) and rows[r][c] == '#')
        out += struct.pack('<H', v)
    return bytes(out)

def main():
    stock = open(f"{ROOT}/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin", "rb").read()
    dump  = open(DUMP, "rb").read()
    ru    = json.load(open(f"{ROOT}/menu_ru.json", encoding="utf-8"))
    img = bytearray(b"\xff" * (IMG_END - BASE)); img[:len(stock)] = stock
    def w(va, data): img[va-BASE:va-BASE+len(data)] = data
    def ascii_glyph(ch):
        a = ASCII_FONT + (ord(ch)-0x20)*14
        return dump[a:a+14]

    ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
    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 = bytes(ks.asm(cave_asm, CAVE_ADDR)[0]); w(CAVE_ADDR, cave)
    w(0x08007FDA, bytes(ks.asm(f"bl #{CAVE_ADDR}", 0x08007FDA)[0]))
    assert img[0x08008A6A-BASE] == 0x80; img[0x08008A6A-BASE] = 0xFF

    # bold Cyrillic font (reuse stock ASCII bytes where identical) --------------
    for ch in GR.LETTERS:
        b = ch.encode("cp1251")[0]
        g = ascii_glyph(GR.REUSE[ch]) if ch in GR.REUSE else enc_glyph(GR.unique_rows(ch))
        w(CYR_BASE + (b-0x80)*14, g)

    # translate menu records ---------------------------------------------------
    START = 0x080253ED - BASE; END = 0x0802723D - BASE
    changed = kept = 0
    for i in range((END-START)//16):
        off = START + i*16; r = bytes(img[off:off+16])
        if r[14:16].isdigit():
            label = r[:14].rstrip(b' \x00').decode('latin1'); idd = r[14:16]; maxc = 14
        else:
            label = r.rstrip(b' \x00').decode('latin1'); idd = b''; maxc = 16
        if not label or label not in ru: kept += 1; continue
        rus = ru[label].encode('cp1251'); assert len(rus) <= maxc
        rec = rus + b' '*(maxc-len(rus)) + idd; assert len(rec) == 16
        w(BASE+off, rec); changed += 1

    # translate scattered rodata display strings — ALL occurrences (in place,
    # keep NUL terminator). Lookup covers both menu and rodata translations.
    rod   = json.load(open(f"{ROOT}/rodata_ru.json", encoding="utf-8"))
    allx  = json.load(open(f"{ROOT}/rodata_all.json", encoding="utf-8"))
    lut   = {**ru, **rod}
    rchg = rskip = 0
    for it in allx:
        va, text, maxc = it["va"], it["text"], it["maxc"]
        rus = lut.get(text)
        if not rus or rus == text:
            rskip += 1; continue
        b = rus.encode("cp1251")
        if len(b) > maxc or bytes(img[va-BASE:va-BASE+maxc]) != text.encode("latin1"):
            rskip += 1; continue
        w(va, b + b" "*(maxc-len(b)))
        rchg += 1

    # inline fixed-length strings (not NUL-terminated) — exact-length patch
    INLINE = {
        0x0800C767: (" Remote Stun: ",  " УДАЛ.БЛОК: "),
        0x0800C77C: ("Remote Stun: ",   "УДАЛ.БЛОК: "),
        0x0800C790: ("Remote Kill: ",   "УДАЛ.ОТКЛ: "),
        0x0800C7B8: ("Wake Up: ",       "БУДИЛ: "),
        0x08028705: ("Freq Mode",       "ЧАСТОТА"),
        0x0802871E: ("DTMF Input: _",   "DTMF ВВОД: _"),
        0x0802872E: ("Dial Number",     "НАБОР НОМ."),
        0x08028767: ("VFO MODE",        "VFO РЕЖ"),
        0x08028815: ("Unknown",         "НЕИЗВ."),
        0x0801FB44: ("Unread SMS :    ","НЕПРОЧ.СМС :"),
        0x08017AB8: ("Sending",         "ОТПРАВ."),
    }
    ichg = 0
    for va, (en, rus) in INLINE.items():
        b = rus.encode("cp1251"); assert len(b) <= len(en), (en, rus)
        if bytes(img[va-BASE:va-BASE+len(en)]) != en.encode("latin1"):
            continue
        w(va, b + b" "*(len(en)-len(b)))
        ichg += 1

    # version rebrand: "RT-4D V3.25" -> "Ru-4D V3.25"
    vva = 0x0800BA88
    if bytes(img[vva-BASE:vva-BASE+16]) == b"VER :RT-4D V3.25":
        w(vva, b"VER :Ru-4D V3.25")

    assert struct.unpack_from("<II", img, 0) == struct.unpack_from("<II", stock, 0)
    out = f"{ROOT}/stock-fw/rt4d_ru_full.bin"
    open(out, "wb").write(img)
    print(f"cave {len(cave)}B | font {len(GR.LETTERS)} glyphs | menu {changed} | "
          f"rodata occ {rchg} (skip {rskip}) | inline {ichg}")
    print(f"image {len(img)}B -> {out}")

if __name__ == "__main__":
    main()
