#!/usr/bin/env python3
"""Genera tutte le icone dell'app da un unico disegno.

Il disegno: tre carte quadrate col contorno, giallo verde rosso (Citta', Fiume,
Foresta), a ventaglio. Tre come le carte che si tengono in mano a Faraway.
Due modalita': fondo beige in chiara, cielo notturno in scura. Piu' la variante
a tinta che chiede iOS 18, in scala di grigi su nero.

Regole rispettate qui, tutte imparate a spese nostre:
  - il master e' QUADRATO. L'arrotondamento lo mettono iOS e Android: inciderlo
    nel file darebbe un angolo dentro l'altro (stesso difetto delle carte).
  - la versione "maskable" tiene tutto dentro la zona sicura, il cerchio
    centrale all'80%, perche' Android ritaglia.
  - apple-touch-icon deve essere un PNG opaco: iOS ignora gli SVG.
  - UNICA eccezione all'angolo quadrato: la FAVICON. Nessun browser la ritaglia,
    quindi se non la arrotondiamo noi resta un quadrato netto in mezzo alle
    schede. Vale solo per icon.svg e favicon-16/32.

I PNG li disegna Pillow, non ImageMagick: il renderer SVG interno di IM sbaglia
le rotazioni e restituiva un'icona sfondata. Gli SVG li scriviamo lo stesso,
perche' il master vettoriale serve come favicon e per gli ingrandimenti futuri.

Genera anche l'immagine di anteprima per la condivisione (Open Graph), quella
che compare quando si incolla il link su WhatsApp, Telegram o iMessage.

Uscita:
  pwa/icons/       solo cio' che serve alla web app (finisce online)
  pwa/anteprima.png  l'immagine di condivisione, 1200x630
  assets/icons/    i master e gli export per gli store (resta in locale)

Uso:  /usr/bin/python3 build-icons.py
"""
import os, math
import numpy as np
from PIL import Image, ImageDraw

ROOT = os.path.dirname(os.path.abspath(__file__))
WEB = os.path.join(ROOT, "pwa", "icons")
STORE = os.path.join(ROOT, "assets", "icons")

# ── il disegno, in coordinate 512 ─────────────────────────────────────────
GIALLO, VERDE, ROSSO = "#cf9a2e", "#3f8f6f", "#c0503a"   # Citta', Fiume, Foresta
BORDO = "#1a1430"
COLORI = [GIALLO, VERDE, ROSSO]                # sinistra, centro, destra
GRIGI = ["#d8d8d8", "#9d9d9d", "#f2f2f2"]      # distanziati a mano: i colori
                                               # convertiti in grigio si somiglierebbero
ANGOLI = [-21, 0, 21]
PERNO_Y, RAGGIO, LATO, CONTORNO = 552, 300, 196, 13
RAGGIO_ANGOLO = round(LATO * 0.145)
STELLE = [(92, 96, 3.5), (418, 80, 3), (356, 132, 2.5),
          (140, 52, 2.5), (466, 176, 2.5), (62, 196, 2)]

CIELO = ((0x2f, 0x25, 0x52), (0x1a, 0x14, 0x30))     # centro -> bordo
CARTA = ((0xff, 0xfd, 0xf7), (0xec, 0xe3, 0xcd))
NERO = ((0, 0, 0), (0, 0, 0))

SS = 4                      # supersampling: disegno grande e riduco
RAGGIO_FAVICON = 0.22       # solo per la favicon: il resto lo arrotonda il sistema


def centro(i):
    t = math.radians(ANGOLI[i])
    return 256 + RAGGIO * math.sin(t), PERNO_Y - RAGGIO * math.cos(t)


def raggio_disegno():
    """distanza dal centro dell'icona all'angolo di carta piu' lontano"""
    m = 0.0
    for i in range(3):
        cx, cy = centro(i)
        a = math.radians(ANGOLI[i])
        mezzo = LATO / 2 + CONTORNO / 2
        for sx in (-1, 1):
            for sy in (-1, 1):
                dx, dy = sx * mezzo, sy * mezzo
                px = cx + dx * math.cos(a) - dy * math.sin(a)
                py = cy + dx * math.sin(a) + dy * math.cos(a)
                m = max(m, math.hypot(px - 256, py - 256))
    return m


def sfondo(estremi):
    """gradiente radiale, centro in alto come nell'app"""
    n = 512 * SS
    cx, cy, r = n * 0.5, n * 0.25, n * 0.94
    y, x = np.ogrid[:n, :n]
    t = np.clip(np.hypot(x - cx, y - cy) / r, 0, 1)[..., None]
    dentro = np.array(estremi[0], float)
    fuori = np.array(estremi[1], float)
    return Image.fromarray((dentro + (fuori - dentro) * t).round().astype(np.uint8)).convert("RGB")


def stelle(img):
    d = ImageDraw.Draw(img, "RGBA")
    for x, y, r in STELLE:
        d.ellipse([(x - r) * SS, (y - r) * SS, (x + r) * SS, (y + r) * SS], fill=(255, 255, 255, 204))


def carta(colore, angolo, scala):
    """una carta quadrata col contorno, gia' ruotata, su sfondo trasparente"""
    lato = LATO * scala
    n = round((lato + CONTORNO) * SS) + 2
    im = Image.new("RGBA", (n, n), (0, 0, 0, 0))
    d = ImageDraw.Draw(im)
    m = (n - lato * SS) / 2
    d.rounded_rectangle([m, m, n - m, n - m], radius=RAGGIO_ANGOLO * scala * SS,
                        fill=colore, outline=BORDO, width=round(CONTORNO * SS))
    # PIL ruota in senso antiorario, SVG in senso orario: il segno va invertito
    return im.rotate(-angolo, resample=Image.BICUBIC, expand=True)


def icona(modo, scala=1.0, con_sfondo=True):
    """modo: chiara | scura | tinta"""
    n = 512 * SS
    estremi = {"chiara": CARTA, "scura": CIELO, "tinta": NERO}[modo]
    if con_sfondo:
        img = sfondo(estremi).convert("RGBA")
        if modo == "scura":
            stelle(img)
    else:
        img = Image.new("RGBA", (n, n), (0, 0, 0, 0))
    colori = GRIGI if modo == "tinta" else COLORI
    for i in (0, 2, 1):                       # dai lati al centro: la centrale sta sopra
        cx, cy = centro(i)
        cx = 256 + (cx - 256) * scala
        cy = 256 + (cy - 256) * scala
        c = carta(colori[i], ANGOLI[i], scala)
        img.alpha_composite(c, (round(cx * SS - c.width / 2), round(cy * SS - c.height / 2)))
    return img


def arrotonda(img):
    """ritaglia gli angoli: serve solo alla favicon"""
    n = img.size[0]
    m = Image.new("L", (n, n), 0)
    ImageDraw.Draw(m).rounded_rectangle([0, 0, n - 1, n - 1], radius=n * RAGGIO_FAVICON, fill=255)
    fuori = img.copy()
    fuori.putalpha(m if img.mode != "RGBA" else
                   Image.fromarray((np.array(img.getchannel("A")) * (np.array(m) / 255)).astype(np.uint8)))
    return fuori


def salva(img, dest, px, opaco_su=None, tonda=False):
    out = img.resize((px, px), Image.LANCZOS)
    if tonda:
        out = arrotonda(out.convert("RGBA"))
    if opaco_su:
        piano = Image.new("RGBA", out.size, opaco_su)
        piano.alpha_composite(out)
        out = piano.convert("RGB")
    out.save(dest, optimize=True)


# ── master vettoriale, per la favicon ─────────────────────────────────────
def carte_svg(colori):
    fuori = []
    for i in (0, 2, 1):
        cx, cy = centro(i)
        fuori.append(
            f'<rect x="{cx - LATO/2:.1f}" y="{cy - LATO/2:.1f}" width="{LATO}" height="{LATO}"'
            f' rx="{RAGGIO_ANGOLO}" fill="{colori[i]}" stroke="{BORDO}" stroke-width="{CONTORNO}"'
            f' stroke-linejoin="round" transform="rotate({ANGOLI[i]} {cx:.1f} {cy:.1f})"/>')
    return "".join(fuori)


STELLINE_SVG = '<g fill="#fff" opacity=".8">' + "".join(
    f'<circle cx="{x}" cy="{y}" r="{r}"/>' for x, y, r in STELLE) + '</g>'


def svg_master(modo):
    grad = {"chiara": '<stop offset="0%" stop-color="#fffdf7"/><stop offset="100%" stop-color="#ece3cd"/>',
            "scura": '<stop offset="0%" stop-color="#2f2552"/><stop offset="100%" stop-color="#1a1430"/>'}
    if modo == "tinta":
        return ('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">'
                '<rect width="512" height="512" fill="#000"/>' + carte_svg(GRIGI) + '</svg>\n')
    return ('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">'
            f'<defs><radialGradient id="f" cx="50%" cy="24%" r="94%">{grad[modo]}</radialGradient></defs>'
            '<rect width="512" height="512" fill="url(#f)"/>'
            + (STELLINE_SVG if modo == "scura" else "")
            + carte_svg(COLORI) + '</svg>\n')


def svg_favicon():
    """Un file solo: il fondo segue il tema del browser."""
    return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
  <style>
    .scuro{{display:none}}
    @media (prefers-color-scheme: dark){{ .chiaro{{display:none}} .scuro{{display:block}} }}
  </style>
  <defs>
    <radialGradient id="c" cx="50%" cy="24%" r="94%">
      <stop offset="0%" stop-color="#fffdf7"/><stop offset="100%" stop-color="#ece3cd"/>
    </radialGradient>
    <radialGradient id="s" cx="50%" cy="26%" r="92%">
      <stop offset="0%" stop-color="#2f2552"/><stop offset="100%" stop-color="#1a1430"/>
    </radialGradient>
  </defs>
  <clipPath id="tondo"><rect width="512" height="512" rx="{512*RAGGIO_FAVICON:.0f}"/></clipPath>
  <g clip-path="url(#tondo)">
    <g class="chiaro"><rect width="512" height="512" fill="url(#c)"/></g>
    <g class="scuro"><rect width="512" height="512" fill="url(#s)"/>{STELLINE_SVG}</g>
    {carte_svg(COLORI)}
  </g>
</svg>
'''


def anteprima():
    """1200x630 per la condivisione: sfondo notturno, ventaglio e due righe di testo"""
    from PIL import ImageFont
    L, A = 1200, 630
    tela = sfondo(CIELO).convert("RGBA").resize((A * 2, A * 2), Image.LANCZOS)
    img = Image.new("RGBA", (L, A))
    img.paste(tela.resize((L, L), Image.LANCZOS).crop((0, (L - A) // 2, L, (L + A) // 2)))
    d = ImageDraw.Draw(img)
    for x, y, r in [(120, 90, 4), (980, 70, 3.5), (1090, 300, 3), (300, 540, 3), (760, 120, 2.5), (60, 380, 2.5)]:
        d.ellipse([x - r, y - r, x + r, y + r], fill=(255, 255, 255, 200))
    # il ventaglio a sinistra, alla stessa scala dell'icona
    ico = icona("scura", con_sfondo=False).resize((380, 380), Image.LANCZOS)
    img.alpha_composite(ico, (74, (A - 380) // 2))
    tit = ImageFont.truetype("/System/Library/Fonts/Supplemental/Georgia Bold.ttf", 76)
    sot = ImageFont.truetype("/System/Library/Fonts/Supplemental/Georgia.ttf", 34)
    d.text((500, 232), "FARAWAY", font=tit, fill=(246, 241, 228, 255))
    d.text((503, 322), "Segnapunti", font=sot, fill=(255, 214, 106, 255))
    d.text((503, 380), "Fotografa le carte, leggi i punti.", font=sot, fill=(196, 190, 214, 255))
    d.text((503, 430), "Gratis, senza pubblicità.", font=sot, fill=(196, 190, 214, 255))
    return img.convert("RGB")


def main():
    negozio = os.path.join(STORE, "store")
    for d in (WEB, STORE, negozio):
        os.makedirs(d, exist_ok=True)

    r = raggio_disegno()
    scala_sicura = (0.40 * 512) / r
    print("raggio del disegno %.0f px -> nella maskable il ventaglio scende al %.0f%%"
          % (r, scala_sicura * 100))

    for modo in ("chiara", "scura", "tinta"):
        with open(os.path.join(STORE, f"master-{modo}.svg"), "w") as f:
            f.write(svg_master(modo))

    scura, chiara, tinta = icona("scura"), icona("chiara"), icona("tinta")

    # ── web app: una sola icona, la scura, che si stacca da qualunque sfondo ──
    salva(scura, os.path.join(WEB, "icon-192.png"), 192)
    salva(scura, os.path.join(WEB, "icon-512.png"), 512)
    salva(icona("scura", scala_sicura), os.path.join(WEB, "icon-maskable.png"), 512)
    salva(scura, os.path.join(WEB, "apple-touch-icon.png"), 180, opaco_su="#1a1430")
    # gli unici due con l'angolo arrotondato inciso: nelle schede del browser
    # nessuno le maschera, e un quadrato netto stona in mezzo alle altre
    salva(scura, os.path.join(WEB, "favicon-32.png"), 32, tonda=True)
    salva(scura, os.path.join(WEB, "favicon-16.png"), 16, tonda=True)
    with open(os.path.join(WEB, "icon.svg"), "w") as f:
        f.write(svg_favicon())
    anteprima().save(os.path.join(ROOT, "pwa", "anteprima.png"), optimize=True)

    # ── store ────────────────────────────────────────────────────────────────
    salva(chiara, os.path.join(negozio, "ios-1024-chiara.png"), 1024, opaco_su="#f6f1e4")
    salva(scura, os.path.join(negozio, "ios-1024-scura.png"), 1024, opaco_su="#1a1430")
    salva(tinta, os.path.join(negozio, "ios-1024-tinta.png"), 1024, opaco_su="#000000")
    # Android tiene fondo e contenuto su due livelli: li anima e li ritaglia
    salva(icona("scura", scala_sicura, con_sfondo=False),
          os.path.join(negozio, "android-carte-432.png"), 432)
    fondo = sfondo(CIELO).convert("RGBA"); stelle(fondo)
    salva(fondo, os.path.join(negozio, "android-fondo-432.png"), 432)
    salva(scura, os.path.join(negozio, "play-512.png"), 512, opaco_su="#1a1430")

    for d in (WEB, negozio):
        print("\n" + os.path.relpath(d, ROOT))
        for f in sorted(os.listdir(d)):
            print("   %-26s %7.1f KB" % (f, os.path.getsize(os.path.join(d, f)) / 1024))


if __name__ == "__main__":
    main()
