#!/usr/bin/env python3
"""
Generate 1200×1600 portrait setup-screen backgrounds for the Waveshare
13.3" Spectra-6 panel.

Layout mirrors the 7.3" gen_screens.py (yellow header + instructions +
QR codes) but adapted for portrait: a single yellow header band on top,
then a two-column body — instructions + manual QR on the left, the two
runtime QR codes on the right. No orientation chooser (13.3" setup is
portrait-only — see project memory).

Run from the firmware/ directory:
    python3 scripts/gen_screens_13e6.py

Exports the QR overlay constants the firmware uses to overlay the
runtime QR images on the static .bin backgrounds; the driver must hold
the same numbers.
"""

from PIL import Image, ImageDraw, ImageFont
import qrcode
import os, sys

MANUAL_URL = "https://wevisto.com/help"

W, H = 1200, 1600

# ── Spectra-6 palette ────────────────────────────────────────────────────────
BLACK  = 0x0;  BK = (26,  26,  26 )
WHITE  = 0x1;  WH = (245, 245, 240)
YELLOW = 0x2;  YL = (240, 208, 0  )
RED    = 0x3;  RD = (192, 48,  32 )
BLUE   = 0x5;  BL = (24,  64,  192)
GREEN  = 0x6;  GR = (16,  160, 64 )

PALETTE_RGB = {BLACK: BK, WHITE: WH, YELLOW: YL, RED: RD, BLUE: BL, GREEN: GR}


def nearest(r, g, b):
    best, best_d = WHITE, float("inf")
    for n, (pr, pg, pb) in PALETTE_RGB.items():
        d = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2
        if d < best_d:
            best, best_d = n, d
    return best


def pack(img):
    """RGB PIL → 4bpp packed bytearray, row-major panel-native order."""
    px = img.load()
    out = bytearray()
    for y in range(H):
        for x in range(0, W, 2):
            hi = nearest(*px[x,     y])
            lo = nearest(*px[x + 1, y])
            out.append((hi << 4) | lo)
    return out


# ── Fonts ────────────────────────────────────────────────────────────────────
FONT_DIR = "/usr/share/fonts/truetype/dejavu"
def ttf(name, size):
    try:
        return ImageFont.truetype(os.path.join(FONT_DIR, name), size)
    except Exception:
        return ImageFont.load_default()


F_BAR    = ttf("DejaVuSans-Bold.ttf", 34)   # header band text
F_CHIP   = ttf("DejaVuSans-Bold.ttf", 30)   # SSID chip
F_HEAD   = ttf("DejaVuSans-Bold.ttf", 78)   # "Connect to WiFi" heading
F_TITLE  = ttf("DejaVuSans-Bold.ttf", 60)   # "Almost ready" on setup screen
F_LABEL  = ttf("DejaVuSans-Bold.ttf", 36)   # column section labels
F_STEPN  = ttf("DejaVuSans-Bold.ttf", 32)   # step number inside black box
F_STEP_B = ttf("DejaVuSans-Bold.ttf", 30)   # bold step body
F_STEP   = ttf("DejaVuSans.ttf",      30)   # step body
F_FOOT   = ttf("DejaVuSans.ttf",      26)   # captions under QR
F_TINY   = ttf("DejaVuSans-Bold.ttf", 22)   # progress-bar labels
F_URL    = ttf("DejaVuSans.ttf",      22)   # URL bar mono


# ── Layout constants ─────────────────────────────────────────────────────────
# Hero treatment: full-bleed harbour photo banner (460 tall) + slim accent
# band (70 tall) carrying the section title. Replaces the prior 130-tall
# single header band — at 110×110 the harbour photo couldn't render
# recognisably under the panel's 6-colour palette, so the brand moment now
# takes the whole top of the screen. 320×320 logo card variant is kept in
# compose_logo() for web/email use.
HERO_BANNER_H = 460
HERO_BAND_H   = 70
HERO_H        = HERO_BANNER_H + HERO_BAND_H    # 530
HEADER_H      = HERO_H                          # alias kept for grep
BODY_Y        = HERO_H
DIV_X     = 600       # vertical divider between left + right columns
LEFT_X    = 0;   LEFT_W  = 600
RIGHT_X   = 602; RIGHT_W = W - RIGHT_X      # 598
LEFT_PAD  = 36
RIGHT_PAD = 36


# ── QR overlay regions — MUST match the panel driver ─────────────────────────
# Dynamic QRs (left as WHITE rectangles in the .bin so firmware can overlay).
# Coords shifted to accommodate the taller HERO_H=530 (was HEADER_H=130);
# epd_driver.cpp epd_draw_*_screen calls must update to match.
AP_QR_MODS    = 37    # version 5, ECC_LOW
AP_QR_CELL    = 14    # 37 × 14 = 518 px
AP_QR_PX      = AP_QR_MODS * AP_QR_CELL
AP_QR_X       = RIGHT_X + (RIGHT_W - AP_QR_PX) // 2   # 642
AP_QR_Y       = BODY_Y + 60                           # 590

# Setup QR shrunk from 16 to 14 cells (656 → 574 px) so the post-hero body
# can still fit the QR + MAC chip + caption + progress bar within the
# remaining 1070 px below the hero.
SETUP_QR_MODS = 41    # version 6, ECC_LOW
SETUP_QR_CELL = 14    # 41 × 14 = 574 px
SETUP_QR_PX   = SETUP_QR_MODS * SETUP_QR_CELL
SETUP_QR_X    = (W - SETUP_QR_PX) // 2                # 313
SETUP_QR_Y    = BODY_Y + 220                          # 750


def text_center(draw, cx, y, text, font, fill):
    bb = draw.textbbox((0, 0), text, font=font)
    tw = bb[2] - bb[0]
    draw.text((cx - tw // 2, y), text, font=font, fill=fill)


def wrap_to_width(draw, text, font, max_w):
    """Greedy word-wrap so each rendered line stays within max_w pixels.
    Doesn't split individual words. Returns a list of lines."""
    lines = []
    current = ""
    for word in text.split():
        candidate = (current + " " + word) if current else word
        bb = draw.textbbox((0, 0), candidate, font=font)
        if bb[2] - bb[0] <= max_w:
            current = candidate
        else:
            if current:
                lines.append(current)
            current = word
    if current:
        lines.append(current)
    return lines


def leave_qr_white(draw, qr_x, qr_y, qr_px):
    draw.rectangle([qr_x, qr_y, qr_x + qr_px - 1, qr_y + qr_px - 1], fill=WH)


def draw_qr_frame(draw, qx, qy, qp, accent):
    """Two-layer decorative border around a QR — accent outer + black inner."""
    draw.rectangle([qx - 12, qy - 12, qx + qp + 11, qy + qp + 11], outline=accent, width=6)
    draw.rectangle([qx - 4,  qy - 4,  qx + qp + 3,  qy + qp + 3],  outline=BK,     width=4)


def draw_hero(img, accent, header_text):
    """Full-bleed photo banner + wordmark + slim accent band carrying the
    section title. The brand moment occupies the top 530 px of the panel."""
    banner = compose_hero_banner(W, HERO_BANNER_H)
    img.paste(banner, (0, 0))

    draw = ImageDraw.Draw(img)
    band_y = HERO_BANNER_H
    draw.rectangle([0, band_y, W - 1, HERO_H - 1], fill=accent)
    # F_BAR is 34pt, ~38 px tall — vertically centred in the band.
    text_y = band_y + (HERO_BAND_H - 38) // 2
    draw.text((LEFT_PAD, text_y), header_text, font=F_BAR, fill=BK)


# ── Brand assets ─────────────────────────────────────────────────────────────
# Source photo (5712×4284 original — gives us latitude for landscape crops
# without having to upscale the 900-square version). The square JPEG is
# used by compose_logo() for the web/email logo card; the hires original
# is used by compose_hero_banner() for the panel hero banner.
LOGO_SRC_SQUARE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               "..", "..", "webApp", "brand",
                               "IMG_2524-square900.jpg")
LOGO_SRC_HIRES  = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                               "..", "..", "webApp", "brand",
                               "IMG_2524.jpg")

def _gradient_overlay(w, h):
    """Black-fade overlay matching webApp/frontend/public/logo.svg: 0%
    opaque at top/bottom, 45% in the middle 42-58% band. Used to anchor
    the wordmark legibly against varying photo content underneath."""
    layer = Image.new("RGBA", (w, h), (0, 0, 0, 0))
    od = ImageDraw.Draw(layer)
    for y in range(h):
        t = y / max(1, h - 1)
        if t < 0.42:
            a = 0.45 * (t / 0.42)
        elif t < 0.58:
            a = 0.45
        else:
            a = 0.45 * ((1 - t) / 0.42)
        od.line([(0, y), (w, y)], fill=(0, 0, 0, int(a * 255)))
    return layer

def _render_wordmark(canvas, cx, cy, font_size, stroke=2):
    """We[white] V[yellow] isto[white] centred at (cx, cy). stroke_width
    fakes font-weight 900 — DejaVuSans-Bold is only weight 700."""
    draw = ImageDraw.Draw(canvas)
    font = ttf("DejaVuSans-Bold.ttf", font_size)
    parts = [("We", WH), ("V", YL), ("isto", WH)]
    widths = [draw.textbbox((0, 0), t, font=font, stroke_width=stroke)[2] for t, _ in parts]
    total_w = sum(widths)
    x = cx - total_w // 2
    y = cy - font_size // 2 - max(2, font_size // 16)
    for (t, fill), w in zip(parts, widths):
        draw.text((x, y), t, font=font, fill=fill,
                  stroke_width=stroke, stroke_fill=fill)
        x += w

def compose_logo(size):
    """Square logo card (photo + gradient + wordmark) at `size`×`size`.
    Composes at SVG-native 320×320 then LANCZOS-downsamples for clean
    anti-aliasing. Kept for any future use that wants the square mark on
    the panel; the active hero uses compose_hero_banner() instead."""
    SRC = 320
    bg = Image.open(LOGO_SRC_SQUARE).convert("RGB").resize((SRC, SRC), Image.LANCZOS)
    bg = Image.alpha_composite(bg.convert("RGBA"), _gradient_overlay(SRC, SRC)).convert("RGB")
    _render_wordmark(bg, SRC // 2, int(0.547 * SRC), 62, stroke=2)
    return bg.resize((size, size), Image.LANCZOS) if size != SRC else bg

def compose_hero_banner(banner_w, banner_h):
    """Wide hero banner: harbour photo cropped from the hi-res original to
    the target aspect, gradient overlay for legibility, Floyd-Steinberg
    dithered to the Spectra-6 palette (so the photo reads as continuous
    tone instead of nearest-neighbour colour blocks), then a big WeVisto
    wordmark composited on top in crisp palette-exact colours."""
    src = Image.open(LOGO_SRC_HIRES).convert("RGB")
    sw, sh = src.size
    src_ar = sw / sh
    tgt_ar = banner_w / banner_h
    if src_ar > tgt_ar:
        new_w = int(sh * tgt_ar)
        x0 = (sw - new_w) // 2
        src = src.crop((x0, 0, x0 + new_w, sh))
    else:
        new_h = int(sw / tgt_ar)
        y0 = int(sh * 0.42 - new_h // 2)
        y0 = max(0, min(y0, sh - new_h))
        src = src.crop((0, y0, sw, y0 + new_h))
    banner = src.resize((banner_w, banner_h), Image.LANCZOS)
    banner = Image.alpha_composite(banner.convert("RGBA"),
                                   _gradient_overlay(banner_w, banner_h)).convert("RGB")

    # FS-dither the photo + gradient against the Spectra-6 palette. Without
    # this, the harbour collapses to blocky nearest-neighbour colour fields
    # when pack()'s nearest() runs over the final panel image.
    pal = Image.new("P", (1, 1))
    pal.putpalette([*BK, *WH, *YL, *RD, *BL, *GR, *([0, 0, 0] * 250)])
    banner = banner.quantize(palette=pal, dither=Image.FLOYDSTEINBERG).convert("RGB")

    # Wordmark rendered AFTER the dither so text edges stay crisp instead
    # of getting stippled. Colours used (WH, YL) are already palette-exact.
    _render_wordmark(banner, banner_w // 2, int(0.55 * banner_h), 200, stroke=4)
    return banner


def draw_divider(draw):
    """Vertical divider between the two body columns."""
    draw.rectangle([DIV_X, BODY_Y + 20, DIV_X + 1, H - 30], fill=BK)


def up_arrow(draw, cx, cy, half_w=18, h=34, color=BK):
    """Solid filled triangle pointing up, centered on (cx, cy)."""
    draw.polygon([
        (cx,            cy - h // 2),
        (cx - half_w,   cy + h // 2),
        (cx + half_w,   cy + h // 2),
    ], fill=color)


def left_arrow(draw, cx, cy, half_h=18, w=34, color=BK):
    """Solid filled triangle pointing left, centered on (cx, cy)."""
    draw.polygon([
        (cx - w // 2,   cy),
        (cx + w // 2,   cy - half_h),
        (cx + w // 2,   cy + half_h),
    ], fill=color)


def paste_rotated_text(img, text, font, fill, anchor_xy, ccw_degrees=90):
    """
    Render `text` horizontally onto a transparent overlay, rotate ccw, and
    paste it onto `img` at `anchor_xy` (top-left of the rotated bounding
    box). Used for vertical labels on orientation diagrams — PIL's
    ImageDraw.text() can't rotate, so we render-then-rotate.
    """
    bb = font.getbbox(text)
    tw, th = bb[2] - bb[0], bb[3] - bb[1]
    pad = 4
    layer = Image.new("RGBA", (tw + pad * 2, th + pad * 2), (0, 0, 0, 0))
    ImageDraw.Draw(layer).text((pad - bb[0], pad - bb[1]), text, font=font, fill=fill)
    rotated = layer.rotate(ccw_degrees, expand=True, resample=Image.BILINEAR)
    img.paste(rotated, anchor_xy, rotated)


def orientation_diagrams(img, cx, top_y, label_color=None, compact=False):
    """
    Side-by-side PORTRAIT / LANDSCAPE diagrams illustrating the two ways
    the user can hang the frame. Drawn in current portrait-view coords:

      PORTRAIT  = upright tall rect, ribbon along the bottom short edge,
                  up-arrow inside.
      LANDSCAPE = pre-rotated 90° CCW from upright landscape. The frame
                  rotation portrait→landscape is 90° CW (ribbon moves
                  bottom→left as viewed by the user); the CCW pre-rotation
                  cancels that, so when the user picks the frame up and
                  rotates it 90° CW into landscape the diagram lands
                  upright (wide rect, ribbon-left, up-arrow).
                  In the portrait rendering that means: tall rect, ribbon
                  along bottom edge (was the LEFT edge upright), LEFT-
                  pointing arrow (was UP upright), and the "LANDSCAPE"
                  label rotated 90° CCW so it runs up the long edge —
                  reads horizontally once the frame is mounted landscape.
    """
    if label_color is None:
        label_color = BK
    draw = ImageDraw.Draw(img)

    # Section heading
    text_center(draw, cx, top_y,      "FRAME",       F_TINY, label_color)
    text_center(draw, cx, top_y + 28, "ORIENTATION", F_TINY, label_color)

    # Same external dimensions for both diagrams (the LANDSCAPE is a 90°-CCW
    # rotation of an upright wide rect — its bounding box is square'd to
    # match portrait so the pair sits in a clean two-up grid). `compact`
    # shrinks the diagrams ~35% so they fit alongside the manual QR in the
    # AP screen's tighter post-hero body.
    if compact:
        diag_w, diag_h = 90, 140
        ribbon_thick   = 10
        pair_gap       = 70
    else:
        diag_w, diag_h = 130, 200
        ribbon_thick   = 14
        pair_gap       = 100
    pair_w         = diag_w * 2 + pair_gap
    base_y         = top_y + 60 if compact else top_y + 100

    pt_x  = cx - pair_w // 2
    ls_x  = pt_x + diag_w + pair_gap
    pt_y  = base_y
    ls_y  = base_y
    diag_bottom = base_y + diag_h

    # PORTRAIT — upright tall rect, ribbon along bottom short edge.
    draw.rectangle([pt_x, pt_y, pt_x + diag_w - 1, pt_y + diag_h - 1],
                   outline=BK, width=3)
    draw.rectangle([pt_x, pt_y + diag_h - ribbon_thick,
                    pt_x + diag_w - 1, pt_y + diag_h - 1], fill=BK)
    up_arrow(draw, pt_x + diag_w // 2,
             pt_y + (diag_h - ribbon_thick) // 2)
    text_center(draw, pt_x + diag_w // 2, diag_bottom + 14, "PORTRAIT", F_TINY, BK)

    # LANDSCAPE — pre-rotated 90° CCW from upright.
    # Upright landscape: wide rect, ribbon along LEFT long edge, UP arrow.
    # After 90° CCW (in portrait rendering): tall rect, ribbon along BOTTOM
    # short edge, LEFT-pointing arrow. Label runs up the LEFT long edge,
    # rotated 90° CCW so it reads L→R once the frame is rotated to landscape.
    draw.rectangle([ls_x, ls_y, ls_x + diag_w - 1, ls_y + diag_h - 1],
                   outline=BK, width=3)
    draw.rectangle([ls_x, ls_y + diag_h - ribbon_thick,
                    ls_x + diag_w - 1, ls_y + diag_h - 1], fill=BK)
    left_arrow(draw, ls_x + diag_w // 2,
               ls_y + (diag_h - ribbon_thick) // 2)
    # Rotated label, anchored just left of the diagram's left long edge.
    label_text = "LANDSCAPE"
    bb = F_TINY.getbbox(label_text)
    label_w = bb[2] - bb[0]
    label_h = bb[3] - bb[1]
    # Rotated label is `label_w` tall, `label_h` wide. Centred vertically
    # against the rect, sitting just to its left.
    rotated_x = ls_x - label_h - 16
    rotated_y = ls_y + (diag_h - label_w) // 2
    paste_rotated_text(img, label_text, F_TINY, BK, (rotated_x, rotated_y),
                       ccw_degrees=90)


# ═══════════════════════════════════════════════════════════════════════════════
# AP SCREEN — yellow (or red retry) accent, Step 1 of 2
# ═══════════════════════════════════════════════════════════════════════════════
def gen_ap(accent=YL,
           header_text="SETUP MODE  —  STEP 1 OF 2",
           qr_label="SCAN TO CONNECT"):
    img = Image.new("RGB", (W, H), WH)
    draw = ImageDraw.Draw(img)

    draw_hero(img, accent, header_text)
    draw_divider(draw)

    # ── LEFT COLUMN ──────────────────────────────────────────────────────────
    # Single-line "Connect to WiFi" heading — was a two-line block under the
    # old shallow header; the new 530-tall hero swallowed the vertical budget
    # so the body collapses to one line at a smaller F_TITLE size.
    head_y = BODY_Y + 30
    f_head = ttf("DejaVuSans-Bold.ttf", 60)
    draw.text((LEFT_PAD, head_y), "Connect to WiFi", font=f_head, fill=BK)
    bb = draw.textbbox((0, 0), "Connect to WiFi", font=f_head)
    underline_y = head_y + bb[3] + 6
    draw.rectangle([LEFT_PAD, underline_y,
                    LEFT_PAD + bb[2] + 4, underline_y + 6], fill=accent)

    # 4 numbered steps. step_pitch tightened from 112 → 108 so the steps +
    # orientation diagram + manual QR all fit in the post-hero 1070-px body.
    steps = [
        ("Turn on your WeVisto", ""),
        ("Unlock your phone",    ""),
        ("Scan QR 1",            "This will connect your phone to the WeVisto"),
        ("Scan QR 2",            "This will open the WeVisto setup page"),
    ]
    step_y0    = underline_y + 60
    step_pitch = 108
    box        = 50  # numbered black box size
    text_x     = LEFT_PAD + box + 22
    text_max_w = DIV_X - text_x - 18    # don't cross the column divider
    line_h     = 34                      # ~F_STEP line height
    for i, (l1, l2) in enumerate(steps):
        by = step_y0 + i * step_pitch
        draw.rectangle([LEFT_PAD, by, LEFT_PAD + box, by + box], fill=BK)
        text_center(draw, LEFT_PAD + box // 2, by + 8,
                    str(i + 1), F_STEPN, accent)
        draw.text((text_x, by - 4), l1, font=F_STEP_B, fill=BK)
        if l2:
            for j, line in enumerate(wrap_to_width(draw, l2, F_STEP, text_max_w)):
                draw.text((text_x, by + 32 + j * line_h), line, font=F_STEP, fill=BK)

    # Orientation diagrams — between the steps and the manual QR. Compact
    # variant (diag_h=130) so the LEFT column fits inside the smaller body.
    orientation_diagrams(img, LEFT_X + LEFT_W // 2,
                         step_y0 + len(steps) * step_pitch + 30,
                         compact=True)

    # Manual QR bottom-left — covers "captive portal didn't open" + general
    # troubleshooting. Shrunk to box_size=5 (~205 px) to share the column
    # with the orientation diagrams above it.
    manual_qr = qrcode.QRCode(
        version=None,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=5,
        border=2,
    )
    manual_qr.add_data(MANUAL_URL)
    manual_qr.make(fit=True)
    manual_img = manual_qr.make_image(fill_color=BK, back_color=WH).convert("RGB")
    mw, mh = manual_img.size
    manual_x = LEFT_PAD
    manual_y = H - mh - 30
    img.paste(manual_img, (manual_x, manual_y))

    label_x = manual_x + mw + 24
    label_y = manual_y + (mh - 64) // 2
    draw.text((label_x, label_y),       "Need help?",     font=F_STEP_B, fill=BK)
    draw.text((label_x, label_y + 32),  "Scan for setup", font=F_STEP,   fill=BK)
    draw.text((label_x, label_y + 58),  "& troubleshoot", font=F_STEP,   fill=BK)

    # ── RIGHT COLUMN ─────────────────────────────────────────────────────────
    rcx = RIGHT_X + RIGHT_W // 2

    # Step 1 — WiFi join QR (dynamic, firmware overlay)
    text_center(draw, rcx, AP_QR_Y - 56, "STEP 1  —  JOIN WIFI", F_LABEL, BK)
    draw_qr_frame(draw, AP_QR_X, AP_QR_Y, AP_QR_PX, accent)
    leave_qr_white(draw, AP_QR_X, AP_QR_Y, AP_QR_PX)
    text_center(draw, rcx, AP_QR_Y + AP_QR_PX + 16, qr_label, F_FOOT, BK)

    # Step 2 — URL QR (static "http://192.168.4.1/", baked into bg). Scanning
    # this in iOS Safari forces the captive portal to render — works around
    # iOS's reluctance to auto-launch CNA from a QR-scan WiFi join.
    url_qr = qrcode.QRCode(
        version=None,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=10,
        border=2,
    )
    url_qr.add_data("http://192.168.4.1/")
    url_qr.make(fit=True)
    url_img = url_qr.make_image(fill_color=BK, back_color=WH).convert("RGB")
    uw, uh = url_img.size
    url_x = rcx - uw // 2
    url_y = H - uh - 100

    text_center(draw, rcx, url_y - 56, "STEP 2  —  OPEN PAGE", F_LABEL, BK)
    draw.rectangle([url_x - 12, url_y - 12, url_x + uw + 11, url_y + uh + 11],
                   outline=accent, width=6)
    draw.rectangle([url_x - 4,  url_y - 4,  url_x + uw + 3,  url_y + uh + 3],
                   outline=BK, width=4)
    img.paste(url_img, (url_x, url_y))
    text_center(draw, rcx, url_y + uh + 12, "http://192.168.4.1/", F_FOOT, BK)

    return img


def gen_ap_retry():
    """Red-accented retry screen, served after a failed WiFi-join attempt."""
    return gen_ap(
        accent=RD,
        header_text="CONNECTION FAILED  —  TRY AGAIN",
        qr_label="Connection failed — try again",
    )


# ═══════════════════════════════════════════════════════════════════════════════
# SETUP SCREEN — green accent, post-WiFi setup-claim QR
# Single-column centred layout: heading + numbered steps + big setup QR + MAC.
# No two-column split here because the QR is much bigger (16-px cells × 41
# modules = 656 px) and a side column would crowd it.
# ═══════════════════════════════════════════════════════════════════════════════
def gen_setup():
    img = Image.new("RGB", (W, H), WH)
    draw = ImageDraw.Draw(img)

    draw_hero(img, GR, "WIFI CONNECTED  —  STEP 2 OF 2")

    # Centered heading. Two-line instructions that used to sit beneath the
    # underline were dropped in the redesign — the hero already establishes
    # WeVisto, the "SCAN TO FINISH" label sits right above the QR, and the
    # MAC chip below identifies the device. Three labels for one QR was
    # redundant under the tighter post-hero body height.
    text_center(draw, W // 2, BODY_Y + 30, "Almost ready", F_HEAD, BK)
    bb = draw.textbbox((0, 0), "Almost ready", font=F_HEAD)
    text_w      = bb[2] - bb[0]
    underline_x = (W - text_w) // 2
    underline_y = BODY_Y + 30 + bb[3] + 6
    draw.rectangle([underline_x, underline_y,
                    underline_x + text_w, underline_y + 6], fill=GR)

    # Setup QR with decorative border + green/black double frame
    draw_qr_frame(draw, SETUP_QR_X, SETUP_QR_Y, SETUP_QR_PX, GR)
    leave_qr_white(draw, SETUP_QR_X, SETUP_QR_Y, SETUP_QR_PX)
    text_center(draw, W // 2, SETUP_QR_Y - 56,
                "Scan the QR to link your wevisto.com account",
                F_STEP_B, BK)

    # MAC chip below QR — frame's identifier so the user knows which device
    # they're claiming. Static placeholder until firmware writes text here.
    mac = "1C:C3:AB:D1:91:F8"
    bb = draw.textbbox((0, 0), mac, font=F_CHIP)
    chip_w = bb[2] - bb[0] + 32
    chip_x = (W - chip_w) // 2
    chip_y = SETUP_QR_Y + SETUP_QR_PX + 30
    draw.rectangle([chip_x, chip_y, chip_x + chip_w, chip_y + 50], fill=BK)
    text_center(draw, W // 2, chip_y + 14, mac, F_CHIP, WH)

    text_center(draw, W // 2, chip_y + 80,
                "Your frame's MAC — handy for support",
                F_FOOT, BK)

    # Progress track at the bottom
    track_y    = H - 90
    track_h    = 14
    seg_pad    = 20
    segs       = [("WiFi", GR), ("Account", BK), ("Frame ready", (200, 200, 195))]
    seg_w      = (W - LEFT_PAD * 2 - seg_pad * 2) // 3
    text_center(draw, W // 2, track_y - 36, "SETUP PROGRESS", F_TINY, BK)
    for i, (label, color) in enumerate(segs):
        sx = LEFT_PAD + i * (seg_w + seg_pad)
        draw.rectangle([sx, track_y, sx + seg_w, track_y + track_h], fill=color)
        text_center(draw, sx + seg_w // 2, track_y + track_h + 10, label, F_FOOT, BK)

    return img


# ── Save ─────────────────────────────────────────────────────────────────────
def save_bin(img, path, preview_path):
    data = pack(img)
    with open(path, "wb") as f:
        f.write(data)
    print(f"Written {len(data):,} bytes → {os.path.abspath(path)}")

    prev = Image.new("RGB", (W, H))
    px = prev.load()
    for y in range(H):
        for x in range(0, W, 2):
            byte = data[y * (W // 2) + x // 2]
            px[x,     y] = PALETTE_RGB.get(byte >> 4,  (128, 128, 128))
            px[x + 1, y] = PALETTE_RGB.get(byte & 0xF, (128, 128, 128))
    prev.save(preview_path)
    print(f"Preview → {os.path.abspath(preview_path)}")


if __name__ == "__main__":
    out_dir = os.path.join(os.path.dirname(__file__), "../data/waveshare13e6-v1")
    os.makedirs(out_dir, exist_ok=True)

    print("Generating AP screen…")
    save_bin(gen_ap(),       f"{out_dir}/ap_bg.bin",       f"{out_dir}/ap_bg_preview.png")
    print()
    print("Generating AP retry screen…")
    save_bin(gen_ap_retry(), f"{out_dir}/ap_bg_retry.bin", f"{out_dir}/ap_bg_retry_preview.png")
    print()
    print("Generating setup screen…")
    save_bin(gen_setup(),    f"{out_dir}/setup_bg.bin",    f"{out_dir}/setup_bg_preview.png")
    print()
    print("QR overlay constants — keep these in sync with epd_driver.cpp:")
    print(f"  AP_QR_X={AP_QR_X}, AP_QR_Y={AP_QR_Y}, AP_QR_CELL={AP_QR_CELL}, AP_QR_PX={AP_QR_PX}")
    print(f"  SETUP_QR_X={SETUP_QR_X}, SETUP_QR_Y={SETUP_QR_Y}, "
          f"SETUP_QR_CELL={SETUP_QR_CELL}, SETUP_QR_PX={SETUP_QR_PX}")
