#!/usr/bin/env python3
"""Turn a square token logo into a round coin badge, by SVG stacking.

    python3 coin-badge.py SRC.svg OUT.svg --bbox X Y W H [--fit 0.68]
                          [--bg "#000AD2"] [--label "QQQe"]

WHY THIS EXISTS. Every coin icon on Silvana Book is a circle — the asset
strips, the ticker, the Markets widget and the terminal all draw them at one
diameter with no mask of their own, so a square file lands on the page as a
square tile among circles. Issuers publish square files more often than round
ones, and recolouring or redrawing their mark is not an option (see
styles-lib/README.md), so the fix is a wrapper rather than an edit.

THE TECHNIQUE IS AN SVG STACK, which is what coin-cc.svg already does and what
this generalises: an outer `<svg>` holds the circle, and the untouched artwork
goes inside a *nested* `<svg>` whose `viewBox` is the artwork's own bounding
box and whose x/y/width/height place and scale it. Nothing in the original
path data is rewritten — no transforms, no re-coordinating, no re-export — so
the mark that ships is the mark the issuer published, and a newer file from
the brand kit can be dropped in and re-wrapped in one command.

THE BOUNDING BOX HAS TO BE MEASURED, not guessed, because a logo's artwork is
almost never centred in its own canvas: QQQe's mark sits in a 223x139 box
inside a 396 square, and SPYe's spans 363 of 400. Guessing puts the mark off
centre in the circle, which is the one thing that reads as broken on a row of
coins. Measure it in a browser, where getBBox() is exact:

    <svg viewBox="<the file's own viewBox>"><g id="m"> …paths, minus the
    background rect… </g></svg>

    document.querySelector('#m').getBBox()

FIT. `--fit` is the share of the circle's diameter the artwork's *longer* side
takes. 0.68 is the house value and comes from the existing badges: below about
0.6 the mark swims, and above 0.75 a wide mark starts to touch the rim, since
a chord at 0.75 of the diameter leaves only 0.66 of the radius above it.
"""

import argparse
import pathlib
import re


def strip_background(svg: str) -> tuple[str, str]:
    """Return (inner markup without the background rect, the rect's fill).

    The background is the first `<rect>` that covers the whole canvas. Taking
    the first rect blindly would eat a mark's own bar — SPYe has one — so the
    match is on a rect whose width and height are the canvas size.
    """
    vb = re.search(r'viewBox="([^"]+)"', svg)
    if not vb:
        raise SystemExit("source has no viewBox")
    _, _, vw, vh = (float(v) for v in vb.group(1).split())
    fill = None
    out = svg
    for m in re.finditer(r'<rect\b([^>]*?)/?>', svg):
        attrs = m.group(1)
        w = re.search(r'\bwidth="([\d.]+)"', attrs)
        h = re.search(r'\bheight="([\d.]+)"', attrs)
        if w and h and abs(float(w.group(1)) - vw) < 1 and abs(float(h.group(1)) - vh) < 1:
            f = re.search(r'fill="([^"]+)"', attrs)
            fill = f.group(1) if f else None
            out = svg[: m.start()] + svg[m.end():]
            break
    inner = out[out.index(">", out.index("<svg")) + 1: out.rindex("</svg>")]
    return inner.strip(), fill


def badge(src: str, bbox, fit: float, bg: str | None, label: str) -> str:
    inner, found_bg = strip_background(src)
    bg = bg or found_bg or "#0A0A0F"
    x, y, w, h = bbox
    vb = re.search(r'viewBox="([^"]+)"', src).group(1).split()
    d = float(vb[2])                       # the badge keeps the source's canvas size
    long_side = max(w, h)
    scale = fit * d / long_side
    ow, oh = w * scale, h * scale
    ox, oy = (d - ow) / 2, (d - oh) / 2
    return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {d:g} {d:g}" width="{d:g}" height="{d:g}" role="img" aria-label="{label}">
  <circle cx="{d / 2:g}" cy="{d / 2:g}" r="{d / 2:g}" fill="{bg}"/>
  <svg x="{ox:.2f}" y="{oy:.2f}" width="{ow:.2f}" height="{oh:.2f}" viewBox="{x:g} {y:g} {w:g} {h:g}">
{inner}
  </svg>
</svg>
"""


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("src")
    ap.add_argument("out")
    ap.add_argument("--bbox", nargs=4, type=float, required=True,
                    metavar=("X", "Y", "W", "H"))
    ap.add_argument("--fit", type=float, default=0.68)
    ap.add_argument("--bg", default=None, help="circle fill; defaults to the source's own background")
    ap.add_argument("--label", default="", help="aria-label on the badge")
    a = ap.parse_args()
    out = pathlib.Path(a.out)
    out.write_text(badge(pathlib.Path(a.src).read_text(), a.bbox, a.fit, a.bg,
                         a.label or out.stem))
    print("wrote", out, out.stat().st_size, "bytes")
