#!/usr/bin/env python3
"""
X4 briefing-card renderer — 800x480 PNGs for the Xteink X4 (CrossInk cards).

WHITE background, black text, one dark-gray accent. E-ink safe: solid colors
only, no gradients, big type. DejaVu fonts (emoji are DRAWN with PIL
primitives — DejaVu has no emoji glyphs).

Usage:
    python3 render.py nightstand|morning [--weekend] [--data /path/card.json]
                                         [--out /path/out.png]

Default outputs (served by x4-cards.service on :8080):
    cards/nightstand.png, cards/morning.png,
    cards/nightstand_weekend.png, cards/morning_weekend.png

--data JSON schema (all keys optional; built-in placeholder defaults apply):
{
  "date": "Monday, October 5",              # header date string
  "schedule": ["8:30a  MSP309 Atelier 4"],  # schedule lines (max 4 night / 3 morning shown)
  "schedule_weekend": ["9:00a  Gym"],       # used instead of schedule with --weekend
  "countdowns": ["Exams in 36 days"],       # else computed from MILESTONES vs render date
  "weather": {"hours": ["8h","12h","18h","22h"], "temps": [12,17,15,13]},
  "wear": "tee + light jacket",             # "Wear: ..." line under the graph
  "mot": {"word": "flâner", "pron": "fla-nay", "gloss": "to stroll, aimlessly"},
  "liberty": ["first liberty line", "second liberty line"],
  "presence": "Aren't the trees outside beautiful?"
}

Tuesday data-wiring: a scheduled job generates this JSON from calendar /
weather / health data, then runs render.py over SSH to refresh the cards.
"""

import argparse
import datetime as dt
import json
import os
import sys

from PIL import Image, ImageDraw, ImageFont

W, H = 800, 480
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (70, 70, 70)          # the one accent: labels, footers, secondary text

FONT_DIR = "/usr/share/fonts/truetype/dejavu"
F_BOLD = os.path.join(FONT_DIR, "DejaVuSans-Bold.ttf")
F_REG = os.path.join(FONT_DIR, "DejaVuSans.ttf")

# Milestones for the smart countdown footer (nearest-first, max two shown).
MILESTONES = [
    ("Exams", dt.date(2026, 11, 9)),
    ("Externat", dt.date(2026, 11, 23)),
]

# --- Content pools (picked by date; explicit --data values override) ---

# Presence lines: thought-provoking or world-connection nudges, October/fall.
# Justin vetoes cheesy ones — keep them grounded and specific.
PRESENCE_LINES = [
    "Aren't the trees outside beautiful?",
    "The maples on Belvédère are showing off this week.",
    "Crisp air, warm coffee — fall is doing its best work.",
    "What did today teach you that yesterday didn't?",
    "Text someone you haven't seen in a while. Thirty seconds.",
    "The light at 6pm is golden now. Go look.",
    "A walk counts as thinking.",
    "The library at noon on a Sunday — your kind of church.",
    "First snow is coming. Enjoy the in-between.",
    "You know more today than you did in September.",
    "The best study spot is the one you actually go to.",
    "Small steps, every day. That's the whole trick.",
    "Call your parents tomorrow. They'd like that.",
    "Somewhere out there, it's somebody's first day of something.",
    "Look up. The sky is doing something interesting.",
]

# Mot du jour: French word + rough pronunciation + English gloss.
MOTS = [
    ("flâner", "fla-nay", "to stroll, aimlessly"),
    ("lueur", "lyoo-EUR", "glimmer, faint light"),
    ("terroir", "teh-RWAHR", "local soil; sense of place"),
    ("dépaysement", "day-payz-MAHN", "feeling of being somewhere new"),
    ("bricoler", "bree-ko-LAY", "to tinker, to DIY"),
    ("frisson", "free-SOHN", "shiver; thrill"),
    ("musarder", "myoo-zar-DAY", "to dawdle, to loiter"),
    ("chuchoter", "shoo-sho-TAY", "to whisper"),
    ("éphémère", "ay-fay-MEHR", "ephemeral, fleeting"),
    ("savourer", "sa-voo-RAY", "to savor"),
    ("crépuscule", "kray-poos-KOOL", "twilight, dusk"),
    ("aube", "ohb", "dawn"),
    ("bruine", "broo-EEN", "drizzle"),
    ("feuille", "FOY", "leaf"),
    ("récolte", "ray-KOLT", "harvest"),
    ("chemin", "shuh-MAN", "path, way"),
    ("rencontre", "rahn-KOHNTR", "encounter, meeting"),
    ("souvenir", "soo-vuh-NEER", "memory; keepsake"),
    ("espoir", "ehs-PWAHR", "hope"),
    ("courage", "koo-RAHJ", "courage"),
    ("douceur", "doo-SUHR", "gentleness; mildness"),
    ("lenteur", "lahn-TUHR", "slowness"),
    ("silence", "see-LAHNSS", "silence"),
    ("regard", "ruh-GAR", "gaze, look"),
    ("écoute", "ay-KOOT", "listening"),
    ("partage", "par-TAHZH", "sharing"),
    ("découverte", "day-koo-VEHRT", "discovery"),
    ("aventure", "a-vahn-TOOR", "adventure"),
    ("merveille", "mehr-VAY", "marvel, wonder"),
    ("quotidien", "ko-tee-DYAN", "daily life, the everyday"),
]

# Placeholder content (used when --data omits a key).
PLACEHOLDER = {
    "schedule": ["No classes", "8:00p  MSP305 remise (REDCap + RPM)"],
    "schedule_morning": ["No classes", "8:00p  MSP305 remise — deadline!"],
    "schedule_weekend": ["9:00a  Gym — push day", "12:00p  Library (till 5)",
                         "7:30p  Off the couch — go out!"],
    "weather": {"hours": ["8h", "12h", "18h", "22h"], "temps": [12, 17, 15, 13]},
    "wear": "tee + light jacket",
    "liberty": ["Strøm Wednesday — pack swimsuit, sandals",
                "Pharmacy CE tonight 8pm, join from home"],
}

# Avatar box for Finch's daily outfit render (Tuesday: paste a ~140x140 PNG
# here). Coordinates: (x0, y0, x1, y1) on the 800x480 card.
AVATAR_BOX = (630, 202, 770, 342)


def font(size, bold=False):
    return ImageFont.truetype(F_BOLD if bold else F_REG, size)


def fit_text(draw, text, fnt, max_w):
    """Truncate with ellipsis to fit max_w pixels."""
    if draw.textlength(text, font=fnt) <= max_w:
        return text
    while text and draw.textlength(text + "…", font=fnt) > max_w:
        text = text[:-1]
    return text + "…"


def draw_moon(draw, cx, cy, r=20):
    draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=BLACK)
    draw.ellipse([cx - r + 9, cy - r - 4, cx + r + 9, cy + r - 4], fill=WHITE)


def draw_sun(draw, cx, cy, r=16):
    draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=BLACK, width=3)
    import math
    for deg in range(0, 360, 45):
        a = math.radians(deg)
        x1, y1 = cx + (r + 6) * math.cos(a), cy + (r + 6) * math.sin(a)
        x2, y2 = cx + (r + 14) * math.cos(a), cy + (r + 14) * math.sin(a)
        draw.line([x1, y1, x2, y2], fill=BLACK, width=3)


def draw_diamond(draw, x, y, r=8):
    draw.polygon([(x, y - r), (x + r, y), (x, y + r), (x - r, y)], fill=BLACK)


def draw_temp_graph(draw, x0, y0, x1, y1, hours, temps):
    """Minimalist temperature curve: black polyline, dots, labels."""
    n = len(hours)
    tmin, tmax = min(temps), max(temps)
    span = max(tmax - tmin, 2)
    xs = [x0 + i * (x1 - x0) / (n - 1) for i in range(n)]
    ys = [y1 - (t - tmin) / span * (y1 - y0) for t in temps]
    pts = list(zip(xs, ys))
    draw.line(pts, fill=BLACK, width=3, joint="curve")
    f_sm = font(17)
    f_hr = font(17)
    for (x, y), t, h in zip(pts, temps, hours):
        draw.ellipse([x - 5, y - 5, x + 5, y + 5], fill=BLACK)
        label = f"{t}°"
        draw.text((x - draw.textlength(label, font=f_sm) / 2, y - 30),
                  label, font=f_sm, fill=BLACK)
        draw.text((x - draw.textlength(h, font=f_hr) / 2, y1 + 8),
                  h, font=f_hr, fill=GRAY)


def countdowns_for(today):
    out = []
    for name, day in sorted(MILESTONES, key=lambda m: m[1]):
        n = (day - today).days
        if n < 0:
            continue
        out.append(f"{name} in {n} days" if n != 1 else f"{name} in 1 day")
        if n == 0:
            out[-1] = f"{name} today!"
    return out[:2]


def render_night(data, today):
    img = Image.new("RGB", (W, H), WHITE)
    d = ImageDraw.Draw(img)

    date_str = data.get("date", today.strftime("%A, %B %-d"))
    draw_moon(d, 60, 38)
    d.text((100, 18), date_str, font=font(32, bold=True), fill=BLACK)

    d.line([40, 78, W - 40, 78], fill=GRAY, width=2)

    d.text((40, 92), "TOMORROW", font=font(20, bold=True), fill=GRAY)
    sched = data.get("schedule", PLACEHOLDER["schedule"])[:4]
    y = 126
    for line in sched:
        d.text((40, y), fit_text(d, line, font(24), W - 80),
               font=font(24), fill=BLACK)
        y += 34

    d.line([40, 380, W - 40, 380], fill=GRAY, width=2)
    cd = data.get("countdowns") or countdowns_for(today)
    footer = " · ".join(cd) if cd else ""
    if footer:
        fw = d.textlength(footer, font=font(22))
        d.text(((W - fw) / 2, 392), footer, font=font(22), fill=GRAY)
    return img


def render_morning(data, today):
    img = Image.new("RGB", (W, H), WHITE)
    d = ImageDraw.Draw(img)

    date_str = data.get("date", today.strftime("%A, %B %-d"))
    draw_sun(d, 60, 36)
    d.text((100, 16), date_str, font=font(30, bold=True), fill=BLACK)
    d.line([40, 62, W - 40, 62], fill=GRAY, width=2)

    # TODAY
    d.text((40, 70), "TODAY", font=font(20, bold=True), fill=GRAY)
    sched = data.get("schedule", PLACEHOLDER["schedule_morning"])[:3]
    y = 96
    for line in sched:
        d.text((40, y), fit_text(d, line, font(24), W - 80),
               font=font(24), fill=BLACK)
        y += 24

    d.line([40, 176, W - 40, 176], fill=GRAY, width=2)

    # WEATHER + avatar box
    d.text((40, 184), "WEATHER", font=font(20, bold=True), fill=GRAY)
    w = data.get("weather", PLACEHOLDER["weather"])
    draw_temp_graph(d, 50, 210, 600, 280, w["hours"], w["temps"])
    wear = data.get("wear", PLACEHOLDER["wear"])
    d.text((40, 314), fit_text(d, f"Wear: {wear}", font(22), 560),
           font=font(22), fill=BLACK)
    # Reserved avatar slot — Tuesday: paste Finch's outfit PNG into AVATAR_BOX.
    x0, y0, x1, y1 = AVATAR_BOX
    d.rectangle([x0, y0, x1, y1], outline=GRAY, width=2)
    lbl = "avatar"
    d.text((x0 + (x1 - x0 - d.textlength(lbl, font=font(18))) / 2,
            (y0 + y1) / 2 - 12), lbl, font=font(18), fill=GRAY)

    d.line([40, 350, W - 40, 350], fill=GRAY, width=2)

    # MOT DU JOUR
    mot = data.get("mot")
    if mot is None:
        w_, pron, gloss = MOTS[today.toordinal() % len(MOTS)]
        mot = {"word": w_, "pron": pron, "gloss": gloss}
    mot_line = f"MOT DU JOUR  ·  {mot['word']} ({mot['pron']}) — {mot['gloss']}"
    d.text((40, 360), fit_text(d, mot_line, font(20), W - 80),
           font=font(20), fill=BLACK)

    # Liberty slots
    libs = data.get("liberty", PLACEHOLDER["liberty"])[:2]
    y = 388
    for line in libs:
        draw_diamond(d, 52, y + 10, r=7)
        d.text((68, y), fit_text(d, line, font(20), W - 110),
               font=font(20), fill=BLACK)
        y += 22

    # Presence footer (no divider — the gray text sets it apart)
    pres = data.get("presence",
                    PRESENCE_LINES[today.toordinal() % len(PRESENCE_LINES)])
    pres = fit_text(d, pres, font(20), W - 80)
    pw = d.textlength(pres, font=font(20))
    d.text(((W - pw) / 2, 440), pres, font=font(20), fill=GRAY)
    return img


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("card", choices=["nightstand", "morning"])
    ap.add_argument("--weekend", action="store_true")
    ap.add_argument("--data", default=None, help="JSON file with card content")
    ap.add_argument("--out", default=None)
    ap.add_argument("--date", default=None,
                    help="YYYY-MM-DD (default: today); drives countdowns/pools")
    args = ap.parse_args()

    today = dt.date.fromisoformat(args.date) if args.date else dt.date.today()

    data = {}
    if args.data:
        with open(args.data) as f:
            data = json.load(f)
    if args.weekend:
        data.setdefault("schedule",
                        data.get("schedule_weekend",
                                 PLACEHOLDER["schedule_weekend"]))

    img = render_night(data, today) if args.card == "nightstand" \
        else render_morning(data, today)

    name = args.card + ("_weekend" if args.weekend else "")
    out = args.out or f"/opt/x4-cards/cards/{name}.png"
    img.save(out)
    bmp_out = out[:-4] + ".bmp" if out.endswith(".png") else out + ".bmp"
    img.convert("1").save(bmp_out)
    print(f"wrote {out} + {bmp_out} ({img.size[0]}x{img.size[1]})")


if __name__ == "__main__":
    main()
