#!/usr/bin/env python3
# Generate the anti-aliased bitmap fonts baked into main.py.
#
# Each font is one base64 blob: a small metrics table + 4-bit-alpha glyph pixels.
# main.py decodes it at boot (binascii.a2b_base64) and renders text by blending the
# foreground over the background per pixel via a 16-entry LUT (smooth, no upscaling).
#
# Re-run after changing sizes/charset:  python3 pico/gen_font.py
# It writes pico/_font_m.b64 + pico/_font_l.b64 and /tmp/font_verify.png (eyeball it). Then
# inject the two base64 strings into main.py's FONT_M_B64 / FONT_L_B64 (replace the existing
# values, or re-add the @@FONT_M@@ / @@FONT_L@@ placeholders first and substitute them in).

import base64, pathlib
from PIL import Image, ImageDraw, ImageFont

FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
HERE = pathlib.Path(__file__).parent

ASCII = "".join(chr(c) for c in range(0x20, 0x7F))
SYMBOLS = "◀▶■"            # ◀ ▶ ■  (prev / play / stop, anti-aliased)
M_CHARS = ASCII + SYMBOLS
L_CHARS = "0123456789 "
S_CHARS = ASCII             # small font for the pad-grid lane labels (CircuitPython)

def build(size, chars):
    font = ImageFont.truetype(FONT, size)
    recs = []          # (cp, w, h, xoff, top, adv)
    pixels = bytearray()
    for ch in chars:
        cp = ord(ch)
        adv = round(font.getlength(ch))
        bbox = font.getbbox(ch)
        if not bbox or bbox[2] - bbox[0] <= 0 or bbox[3] - bbox[1] <= 0:
            recs.append((cp, 0, 0, 0, 0, adv)); continue
        l, t, r, b = bbox
        w, h = r - l, b - t
        img = Image.new("L", (w, h), 0)
        ImageDraw.Draw(img).text((-l, -t), ch, fill=255, font=font)
        px = img.load()
        # pack 4-bit alpha, row-major, two pixels per byte (first = high nibble)
        nib = []
        for y in range(h):
            for x in range(w):
                nib.append(px[x, y] >> 4)
        if len(nib) % 2: nib.append(0)
        for i in range(0, len(nib), 2):
            pixels.append((nib[i] << 4) | nib[i + 1])
        recs.append((cp, w, h, l & 0xFF, t & 0xFF, adv & 0xFF))
    header = bytearray([len(recs)])
    for cp, w, h, xoff, top, adv in recs:
        header += bytes([cp >> 8, cp & 0xFF, w, h, xoff, top, adv])
    return bytes(header) + bytes(pixels)

# ---- reference decoder + renderer (must match main.py exactly) ----
def load_font(blob):
    count = blob[0]; p = 1; pixoff = 1 + count * 7; glyphs = {}
    for _ in range(count):
        cp = (blob[p] << 8) | blob[p + 1]; w = blob[p + 2]; h = blob[p + 3]
        xoff = blob[p + 4]; xoff = xoff - 256 if xoff > 127 else xoff
        top = blob[p + 5]; adv = blob[p + 6]; p += 7
        glyphs[cp] = (w, h, xoff, top, adv, pixoff)
        pixoff += (w * h + 1) // 2
    return glyphs, blob

def lut(fg, bg):
    def unp(c): return ((c >> 11) & 0x1F, (c >> 5) & 0x3F, c & 0x1F)
    fr, fgc, fb = unp(fg); br, bgc, bb = unp(bg); out = []
    for a in range(16):
        t = a * 17
        r = (br * (255 - t) + fr * t) // 255
        g = (bgc * (255 - t) + fgc * t) // 255
        b = (bb * (255 - t) + fb * t) // 255
        out.append((r << 11) | (g << 5) | b)
    return out

def render(draw_img, font, s, x, y, fg, bg):
    glyphs, blob = font; L = lut(fg, bg); pen = x
    for ch in s:
        g = glyphs.get(ord(ch))
        if not g: continue
        w, h, xoff, top, adv, off = g
        for j in range(h):
            for i in range(w):
                k = j * w + i; byte = blob[off + (k >> 1)]
                nibv = (byte >> 4) if (k & 1) == 0 else (byte & 0xF)
                col = L[nibv]
                r = (col >> 11) & 0x1F; gg = (col >> 5) & 0x3F; b = col & 0x1F
                draw_img.putpixel((pen + xoff + i, y + top + j), (r << 3, gg << 2, b << 3))
        pen += adv
    return pen

blob_s = build(12, S_CHARS)
blob_m = build(22, M_CHARS)
blob_l = build(78, L_CHARS)
# base64 (baked into the MicroPython firmware, pico/main.py — FONT_M / FONT_L)
(HERE / "_font_m.b64").write_text(base64.b64encode(blob_m).decode())
(HERE / "_font_l.b64").write_text(base64.b64encode(blob_l).decode())
print("FONT_M %d bytes -> %d b64" % (len(blob_m), len(base64.b64encode(blob_m))))
print("FONT_L %d bytes -> %d b64" % (len(blob_l), len(base64.b64encode(blob_l))))
# binary blobs (read at boot by the CircuitPython firmware, pico-cp/code.py)
CP = HERE.parent / "pico-cp"
(CP / "font_s.bin").write_bytes(blob_s)
(CP / "font_m.bin").write_bytes(blob_m)
(CP / "font_l.bin").write_bytes(blob_l)
print("wrote pico-cp/font_{s,m,l}.bin  (%d / %d / %d bytes)" % (len(blob_s), len(blob_m), len(blob_l)))

# verification image on a dark bg (565 colours like the firmware)
def c565(r, g, b): return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
BG = c565(6, 9, 14); CYAN = c565(10, 179, 247); TXT = c565(199, 208, 219); GREEN = c565(47, 224, 122)
img = Image.new("RGB", (320, 220), (6, 9, 14))
fm = load_font(blob_m); fl = load_font(blob_l)
render(img, fm, "PM_K-1 KIT", 12, 10, CYAN, BG)
render(img, fl, "120", 150, 40, TXT, BG)
render(img, fm, "BPM", 12, 70, TXT, BG)
render(img, fm, "▶ RUN", 12, 120, GREEN, BG)
render(img, fm, "Four-on-the-floor", 12, 150, TXT, BG)
render(img, fm, "◀◀   ▶   ▶▶    -  TAP  +", 12, 185, TXT, BG)
img.resize((640, 440), Image.NEAREST).save("/tmp/font_verify.png")
print("wrote /tmp/font_verify.png")
