#!/usr/bin/env python3
"""Connected "panorama" App Store screenshots (modern style).

Composes a single wide canvas spanning N panels with a light modern background,
angled iPhone mockups that overlap and cross panel boundaries, and per-panel
headlines — then slices it into individual App Store slots so that, swiped in the
gallery, they read as one continuous scene (à la NetWorth Pro 2).

Requirements: pip install Pillow

Example:
  python frame_panorama.py \
    --inputs a.png,b.png,c.png \
    --headlines "Watch your net worth grow||Log it once a month||Charts that reveal more" \
    --panel-width 1320 --height 2868 \
    --out-dir out --out-prefix iphone --preview out/_wide.png
"""
from __future__ import annotations

import argparse
import math
from pathlib import Path

from PIL import Image, ImageDraw, ImageFilter, ImageFont

HEADLINE_FONTS = [
    "/System/Library/Fonts/SFNSDisplay.ttf",
    "/System/Library/Fonts/SFNS.ttf",
    "/System/Library/Fonts/Supplemental/HelveticaNeue.ttc",
    "/Library/Fonts/Arial Unicode.ttf",
]
CJK_FONTS = [
    "/System/Library/Fonts/ヒラギノ角ゴシック W6.ttc",
    "/System/Library/Fonts/Hiragino Sans GB.ttc",
    "/System/Library/Fonts/PingFang.ttc",
]


def _cjk(s):
    return any(ord(c) > 0x2E00 for c in s)


def font(size, text):
    for p in (CJK_FONTS if _cjk(text) else []) + HEADLINE_FONTS:
        if Path(p).exists():
            try:
                return ImageFont.truetype(p, size)
            except Exception:
                pass
    return ImageFont.load_default()


def hexrgb(h):
    h = h.lstrip("#")
    return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))


def lerp(a, b, t):
    return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(3))


def rounded_mask(size, radius):
    m = Image.new("L", size, 0)
    ImageDraw.Draw(m).rounded_rectangle([0, 0, size[0], size[1]], radius=radius, fill=255)
    return m


def build_device(shot: Image.Image, target_w: int) -> Image.Image:
    """iPhone mockup (RGBA): metal bezel + rounded screen + Dynamic Island."""
    bezel = max(10, int(target_w * 0.030))
    sw = target_w - bezel * 2
    sh = int(sw * shot.height / shot.width)
    screen = shot.resize((sw, sh), Image.LANCZOS).convert("RGB")
    scr_r = int(target_w * 0.12)
    dev_r = scr_r + bezel
    W, H = target_w, sh + bezel * 2
    dev = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    d = ImageDraw.Draw(dev)
    d.rounded_rectangle([0, 0, W, H], radius=dev_r, fill=(20, 20, 22, 255))
    d.rounded_rectangle([2, 2, W - 2, H - 2], radius=dev_r, outline=(70, 72, 78, 255), width=3)
    dev.paste(screen, (bezel, bezel), rounded_mask((sw, sh), scr_r))
    di_w, di_h = int(sw * 0.30), int(target_w * 0.034)
    dix = bezel + (sw - di_w) // 2
    diy = bezel + int(target_w * 0.024)
    ImageDraw.Draw(dev).rounded_rectangle([dix, diy, dix + di_w, diy + di_h],
                                          radius=di_h // 2, fill=(0, 0, 0, 255))
    return dev


def with_shadow(rgba: Image.Image, blur: int, alpha: float, off=(0, 40)) -> Image.Image:
    pad = blur * 3
    canvas = Image.new("RGBA", (rgba.width + pad * 2, rgba.height + pad * 2), (0, 0, 0, 0))
    sh = Image.new("RGBA", rgba.size, (0, 0, 0, 0))
    sh.putalpha(rgba.split()[3].point(lambda p: int(p * alpha)))
    dark = Image.new("RGBA", rgba.size, (10, 20, 40, 255))
    dark.putalpha(sh.split()[3])
    canvas.alpha_composite(dark, (pad + off[0], pad + off[1]))
    canvas = canvas.filter(ImageFilter.GaussianBlur(blur))
    canvas.alpha_composite(rgba, (pad, pad))
    return canvas


def _blob(W, H, cx, cy, r, a):
    m = Image.new("L", (W, H), 0)
    ImageDraw.Draw(m).ellipse([cx - r, cy - r, cx + r, cy + r], fill=a)
    return m.filter(ImageFilter.GaussianBlur(int(r * 0.6)))


def make_bg(W, H, accent, accent2, n=1):
    """Light, premium background: near-white vertical wash + large soft brand-color
    blobs (blue + green) for depth. `n` = number of panels (for even blob spread)."""
    top, bottom = (251, 252, 254), (238, 242, 248)
    # fast vertical gradient: build a 1px-wide column then stretch to full width
    col = Image.new("RGB", (1, H))
    cp = col.load()
    for y in range(H):
        cp[0, y] = lerp(top, bottom, y / max(1, H - 1))
    base = col.resize((W, H)).convert("RGBA")
    pw = W / max(1, n)
    for i in range(n):
        col = accent if i % 2 == 0 else accent2
        cx = pw * (i + 0.5)
        base = Image.composite(Image.new("RGBA", (W, H), col + (255,)), base,
                               _blob(W, H, cx, H * 0.20, pw * 0.42, 60).point(lambda p: int(p * 0.55)))
        col2 = accent2 if i % 2 == 0 else accent
        base = Image.composite(Image.new("RGBA", (W, H), col2 + (255,)), base,
                               _blob(W, H, cx, H * 0.86, pw * 0.5, 45).point(lambda p: int(p * 0.5)))
    return base


def wrap(draw, text, fnt, maxw):
    words, lines, cur = text.split(), [], ""
    for w in words:
        t = (cur + " " + w).strip()
        if draw.textlength(t, font=fnt) <= maxw or not cur:
            cur = t
        else:
            lines.append(cur); cur = w
    if cur:
        lines.append(cur)
    return lines


def draw_headline(canvas, text, cx, top_y, panel_w, color, max_lines=2):
    """Draw a centered headline, auto-shrinking the font until it fits in
    `max_lines` lines within the panel margins (never clips). Returns the y
    coordinate just below the headline block."""
    d = ImageDraw.Draw(canvas)
    maxw = int(panel_w * 0.82)
    size = int(panel_w * 0.074)
    fnt = font(size, text)
    lines = wrap(d, text, fnt, maxw)
    while size > int(panel_w * 0.042):
        fnt = font(size, text)
        lines = wrap(d, text, fnt, maxw)
        if len(lines) <= max_lines and max(d.textlength(ln, font=fnt) for ln in lines) <= maxw:
            break
        size -= 3
    asc, desc = fnt.getmetrics()
    lh = int((asc + desc) * 1.06)
    for i, ln in enumerate(lines):
        w = d.textlength(ln, font=fnt)
        d.text((cx - w / 2, top_y + i * lh), ln, font=fnt, fill=color)
    return top_y + len(lines) * lh


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--inputs", required=True, help="comma-separated screenshot paths")
    ap.add_argument("--headlines", required=True, help="|| separated, one per panel")
    ap.add_argument("--sublines", default="", help="|| separated, one per panel")
    ap.add_argument("--panel-width", type=int, required=True)
    ap.add_argument("--height", type=int, required=True)
    ap.add_argument("--out-dir", required=True)
    ap.add_argument("--out-prefix", default="iphone")
    ap.add_argument("--accent", default="#3B82F6")
    ap.add_argument("--accent2", default="#10B981")
    ap.add_argument("--device-scale", type=float, default=0.86)
    ap.add_argument("--preview", default="")
    a = ap.parse_args()

    inputs = [s for s in a.inputs.split(",") if s]
    headlines = a.headlines.split("||")
    sublines = a.sublines.split("||") if a.sublines else [""] * len(inputs)
    n = len(inputs)
    assert len(headlines) == n, "headlines count must match inputs"

    W, H = a.panel_width, a.height
    accent, accent2 = hexrgb(a.accent), hexrgb(a.accent2)
    canvas = make_bg(W * n, H, accent, accent2, n=n)

    text_color = (13, 22, 38)
    sub_color = (90, 102, 120)
    dev_w = int(W * a.device_scale)
    angles = [-6, 5, -5, 6, -5, 5]
    y_off = [0.0, 0.05, 0.0, 0.05, 0.0, 0.05]

    for i, (path, head) in enumerate(zip(inputs, headlines)):
        panel_cx = int(W * i + W * 0.5)
        hy = draw_headline(canvas, head, panel_cx, int(H * 0.052), W, text_color)
        sub = sublines[i] if i < len(sublines) else ""
        if sub.strip():
            d = ImageDraw.Draw(canvas)
            sf = font(int(W * 0.034), sub)
            lines = wrap(d, sub, sf, int(W * 0.78))
            asc, desc = sf.getmetrics(); lh = int((asc + desc) * 1.05)
            sy = hy + int(H * 0.012)
            for j, ln in enumerate(lines):
                w = d.textlength(ln, font=sf)
                d.text((panel_cx - w / 2, sy + j * lh), ln, font=sf, fill=sub_color)

        shot = Image.open(path).convert("RGB")
        dev = build_device(shot, dev_w)
        dev = with_shadow(dev, blur=int(W * 0.04), alpha=0.34, off=(0, 46))
        ang = angles[i % len(angles)]
        dev = dev.rotate(ang, expand=True, resample=Image.BICUBIC)
        # nudge horizontally so adjacent devices overlap across the panel border
        nudge = int(W * (0.15 if i % 2 == 0 else -0.15))
        dx = panel_cx + nudge - dev.width // 2
        dy = int(H * (0.315 + y_off[i % len(y_off)]))
        if dy + dev.height > int(H * 1.04):
            dy = int(H * 1.04) - dev.height
        canvas.alpha_composite(dev, (dx, dy))

    out = canvas.convert("RGB")
    outdir = Path(a.out_dir); outdir.mkdir(parents=True, exist_ok=True)
    for i in range(n):
        panel = out.crop((i * W, 0, (i + 1) * W, H))
        panel.save(outdir / f"{a.out_prefix}_{i+1:02d}.png")
    if a.preview:
        Path(a.preview).parent.mkdir(parents=True, exist_ok=True)
        scale = min(1.0, 1600 / out.width)
        out.resize((int(out.width * scale), int(out.height * scale)), Image.LANCZOS).save(a.preview)
    print(f"OK {n} panels -> {outdir} (each {W}x{H})")


if __name__ == "__main__":
    main()
