#!/usr/bin/env python3
"""
build_logos.py: Aster Chai logo geometry, written as clean SVG.

Sources
  * Wordmark outlines: set by PhotoCraft's type engine (Linux Libertine Display G,
    SIL OFL 1.1), converted to shape there, saved to PSD, then read back from the
    PSD's vector-mask records (tools/psd_paths.py). PhotoCraft's own kerning is kept.
  * Ring text for the seal: glyph outlines from Linux Libertine G (OFL) via fontTools.
  * The aster star: one analytic outline (8 lancet rays, V notches, optional eye),
    the most geometric level of the Phase 0 bloom system.

  python build_logos.py <wordmark_source.psd> <out_dir>
"""
from __future__ import annotations

import math
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "tools"))
from psd_paths import read_vector_masks, bbox  # noqa: E402

INK = "#2B211B"      # dark roast
CREAM = "#F2EADB"
VIOLET = "#7B6A92"   # dusty violet
GOLD = "#D3A13B"     # golden center
BROWN = "#6B4733"


def f(v, nd=2):
    s = f"{v:.{nd}f}".rstrip("0").rstrip(".")
    return "0" if s in ("-0", "") else s


# --------------------------------------------------------------------------
# the aster star
# --------------------------------------------------------------------------
def star_segments(rays=8, notch=0.33, bulge=0.20, rot=-90.0):
    """Closed outline as cubic segments in unit coords (tip radius 1).
    Lancet rays: each flank leaves the tip along the ray, swells (bulge) and runs
    into a crisp V notch along the notch-to-tip line, so rays never get a neck.
    Returns [(p0, c1, c2, p1), ...] running tip -> notch -> tip ..."""
    segs = []
    step = 2 * math.pi / rays
    a0 = math.radians(rot)
    for k in range(rays):
        ta = a0 + k * step
        na = ta + step / 2
        nb = ta + step
        tip = (math.cos(ta), math.sin(ta))
        n1 = (notch * math.cos(na), notch * math.sin(na))
        tip2 = (math.cos(nb), math.sin(nb))

        def flank(p_tip, p_notch, ray_a, side):
            ux, uy = math.cos(ray_a), math.sin(ray_a)
            px, py = -uy * side, ux * side  # perpendicular, toward the notch
            c1 = (p_tip[0] - ux * 0.30 + px * bulge * 0.55, p_tip[1] - uy * 0.30 + py * bulge * 0.55)
            dx, dy = p_tip[0] - p_notch[0], p_tip[1] - p_notch[1]
            c2 = (p_notch[0] + dx * 0.22 + px * bulge * 0.05, p_notch[1] + dy * 0.22 + py * bulge * 0.05)
            return c1, c2

        c1, c2 = flank(tip, n1, ta, +1)
        segs.append((tip, c1, c2, n1))
        d1, d2 = flank(tip2, n1, nb, -1)
        segs.append((n1, d2, d1, tip2))
    return segs


def segs_to_d(segs, xf, close=True):
    p = xf(*segs[0][0])
    out = [f"M{f(p[0])} {f(p[1])}"]
    for _p0, c1, c2, p1 in segs:
        a, b, c = xf(*c1), xf(*c2), xf(*p1)
        out.append(f"C{f(a[0])} {f(a[1])} {f(b[0])} {f(b[1])} {f(c[0])} {f(c[1])}")
    if close:
        out.append("Z")
    return "".join(out)


def circle_d(cx, cy, r, ccw=False):
    k = 0.5522847498 * r
    if not ccw:
        return (f"M{f(cx + r)} {f(cy)}C{f(cx + r)} {f(cy + k)} {f(cx + k)} {f(cy + r)} {f(cx)} {f(cy + r)}"
                f"C{f(cx - k)} {f(cy + r)} {f(cx - r)} {f(cy + k)} {f(cx - r)} {f(cy)}"
                f"C{f(cx - r)} {f(cy - k)} {f(cx - k)} {f(cy - r)} {f(cx)} {f(cy - r)}"
                f"C{f(cx + k)} {f(cy - r)} {f(cx + r)} {f(cy - k)} {f(cx + r)} {f(cy)}Z")
    return (f"M{f(cx + r)} {f(cy)}C{f(cx + r)} {f(cy - k)} {f(cx + k)} {f(cy - r)} {f(cx)} {f(cy - r)}"
            f"C{f(cx - k)} {f(cy - r)} {f(cx - r)} {f(cy - k)} {f(cx - r)} {f(cy)}"
            f"C{f(cx - r)} {f(cy + k)} {f(cx - k)} {f(cy + r)} {f(cx)} {f(cy + r)}"
            f"C{f(cx + k)} {f(cy + r)} {f(cx + r)} {f(cy + k)} {f(cx + r)} {f(cy)}Z")


def star_d(cx, cy, r, notch=0.33, bulge=0.20, rays=8, rot=-90.0):
    return segs_to_d(star_segments(rays, notch, bulge, rot), lambda x, y: (cx + x * r, cy + y * r))


def star_area(notch=0.33, bulge=0.20, n=4000):
    """Numeric area of the unit star (shoelace on a dense polyline)."""
    pts = []
    for p0, c1, c2, p1 in star_segments(8, notch, bulge):
        for i in range(60):
            t = i / 60
            mt = 1 - t
            pts.append(tuple(mt**3 * p0[j] + 3 * mt * mt * t * c1[j] + 3 * mt * t * t * c2[j] + t**3 * p1[j] for j in range(2)))
    a = 0
    for i in range(len(pts)):
        x0, y0 = pts[i]
        x1, y1 = pts[(i + 1) % len(pts)]
        a += x0 * y1 - x1 * y0
    return abs(a) / 2


def half_star_d(cx, cy, r, notch=0.33, bulge=0.20):
    """Upper half of the star (rays at 180, 135, 90, 45, 0 degrees), base on y=cy."""
    segs = star_segments(8, notch, bulge, rot=180.0)
    # tips run 180, 225(=-135 up in screen coords is y<0)... pick the 9 segments from 180 to 360 going through y<0
    # with rot=180 and y down, angles 180->225->270->315->360 are the upper half on screen
    upper = segs[:8]
    xf = lambda x, y: (cx + x * r, cy + y * r)  # noqa: E731
    d = segs_to_d(upper, xf, close=False)
    return d + "Z"


# --------------------------------------------------------------------------
# wordmark from PSD
# --------------------------------------------------------------------------
def load_wordmark(psd):
    masks, _size = read_vector_masks(psd)
    wm = masks["wordmark-outlines"]
    caps = masks.get("caps-outlines")
    return wm, caps


def subpath_d(sp, xf):
    ks = sp["knots"]
    p = xf(*ks[0][1])
    d = [f"M{f(p[0])} {f(p[1])}"]
    n = len(ks)
    for j in range(1, n + 1):
        a, b = ks[j - 1], ks[j % n]
        c1, c2, q = xf(*a[2]), xf(*b[0]), xf(*b[1])
        d.append(f"C{f(c1[0])} {f(c1[1])} {f(c2[0])} {f(c2[1])} {f(q[0])} {f(q[1])}")
    d.append("Z")
    return "".join(d)


class Wordmark:
    """Normalised wordmark: baseline y=0, left edge x=0, cap height 1 unit."""
    BASELINE = 900.0

    def __init__(self, subs):
        boxes = [bbox(s) for s in subs]
        self.x0 = min(b[0] for b in boxes)
        self.cap = self.BASELINE - min(b[1] for b in boxes[:1])  # 'A' apex
        self.subs = subs
        self.boxes = boxes
        # the i tittle: small, closed, high, last glyph
        cand = [i for i, b in enumerate(boxes) if (b[2] - b[0]) < 0.25 * self.cap and b[3] < self.BASELINE - 0.6 * self.cap]
        self.tittle = cand[-1] if cand else None
        self.width = (max(b[2] for b in boxes) - self.x0) / self.cap

    def u(self, x, y):
        return ((x - self.x0) / self.cap, (y - self.BASELINE) / self.cap)

    def d(self, ox, oy, s, drop=()):
        xf = lambda x, y: (ox + (x - self.x0) / self.cap * s, oy + (y - self.BASELINE) / self.cap * s)  # noqa: E731
        return "".join(subpath_d(sp, xf) for i, sp in enumerate(self.subs) if i not in drop)

    def tittle_geom(self):
        b = self.boxes[self.tittle]
        cx, cy = (b[0] + b[2]) / 2, (b[1] + b[3]) / 2
        return self.u(cx, cy), ((b[2] - b[0]) / 2) / self.cap


# --------------------------------------------------------------------------
# ring text for the seal (fontTools)
# --------------------------------------------------------------------------
def ring_text_d(text, cx, cy, radius, size, font_path, start_deg, tracking=0.12, outward=True, center=True):
    from fontTools.ttLib import TTFont
    from fontTools.pens.recordingPen import RecordingPen
    font = TTFont(font_path)
    gs = font.getGlyphSet()
    cmap = font.getBestCmap()
    upm = font["head"].unitsPerEm
    hmtx = font["hmtx"]
    sc = size / upm
    names = [cmap.get(ord(ch)) for ch in text]
    advs = [hmtx[n][0] * sc + (tracking * size if i < len(text) - 1 else 0) for i, n in enumerate(names)]
    total = sum(advs)
    ang_total = total / radius
    # outward text (top arc) runs clockwise; inward (bottom arc) runs counter-clockwise
    a = math.radians(start_deg) - (ang_total / 2 if center else 0) * (1 if outward else -1)
    parts = []
    for n, adv in zip(names, advs):
        w = hmtx[n][0] * sc
        mid = a + (w / 2 / radius) * (1 if outward else -1)
        pen = RecordingPen()
        gs[n].draw(pen)
        # glyph local: x centred on advance midline, baseline on circle
        rot = mid + (math.pi / 2 if outward else -math.pi / 2)
        cr, sr = math.cos(rot), math.sin(rot)
        bx, by = cx + radius * math.cos(mid), cy + radius * math.sin(mid)

        def T(pt):
            gx = pt[0] * sc - w / 2
            gy = -pt[1] * sc  # font y up -> svg y down
            if not outward:
                gy = gy  # inward text reads upright at the bottom of the ring
            return (bx + gx * cr - gy * sr, by + gx * sr + gy * cr)

        d = []
        cur = None
        for op, args in pen.value:
            if op == "moveTo":
                cur = T(args[0]); d.append(f"M{f(cur[0])} {f(cur[1])}")
            elif op == "lineTo":
                cur = T(args[0]); d.append(f"L{f(cur[0])} {f(cur[1])}")
            elif op == "qCurveTo":
                pts = list(args)
                # expand implied on-curve points, then quad -> cubic
                on_end = pts[-1]
                offs = pts[:-1]
                seq = []
                for i, o in enumerate(offs):
                    seq.append(("off", o))
                    if i < len(offs) - 1:
                        seq.append(("on", ((o[0] + offs[i + 1][0]) / 2, (o[1] + offs[i + 1][1]) / 2)))
                seq.append(("on", on_end))
                p0 = cur
                i = 0
                while i < len(seq):
                    q = T(seq[i][1]); p1 = T(seq[i + 1][1])
                    c1 = (p0[0] + 2 / 3 * (q[0] - p0[0]), p0[1] + 2 / 3 * (q[1] - p0[1]))
                    c2 = (p1[0] + 2 / 3 * (q[0] - p1[0]), p1[1] + 2 / 3 * (q[1] - p1[1]))
                    d.append(f"C{f(c1[0])} {f(c1[1])} {f(c2[0])} {f(c2[1])} {f(p1[0])} {f(p1[1])}")
                    p0 = p1
                    i += 2
                cur = p0
            elif op == "curveTo":
                c1, c2, p = (T(x) for x in args)
                d.append(f"C{f(c1[0])} {f(c1[1])} {f(c2[0])} {f(c2[1])} {f(p[0])} {f(p[1])}")
                cur = p
            elif op in ("closePath", "endPath"):
                d.append("Z")
        parts.append("".join(d))
        a += (adv / radius) * (1 if outward else -1)
    return "".join(parts)


# --------------------------------------------------------------------------
# SVG
# --------------------------------------------------------------------------
def svg_doc(w, h, body, title):
    return (f'<svg xmlns="http://www.w3.org/2000/svg" width="{f(w)}" height="{f(h)}" viewBox="0 0 {f(w)} {f(h)}">\n'
            f"<title>{title}</title>\n{body}\n</svg>\n")


def p(id_, d, fill, rule="nonzero"):
    fr = ' fill-rule="evenodd"' if rule == "evenodd" else ""
    return f'<path id="{id_}" d="{d}" fill="{fill}"{fr}/>'


class Logos:
    def __init__(self, psd):
        subs, caps = load_wordmark(psd)
        self.wm = Wordmark(subs)
        self.caps = caps
        (tx, ty), tr = self.wm.tittle_geom()
        self.tittle_c = (tx, ty)
        self.tittle_r = tr
        # match the star's ink to ~1.2x the dot it replaces: reads as a star, not a blot
        dot_area = math.pi * tr * tr
        self.tittle_star_r = math.sqrt(1.2 * dot_area / star_area(0.36, 0.20))
        self.font_regular = r"C:\Windows\Fonts\LinLibertine_R_G.ttf"

    # ---- pieces (unit = wordmark cap height) -------------------------------
    def wordmark_paths(self, ox, oy, s, star=True, color=INK, star_color=None, pid="wordmark"):
        drop = (self.wm.tittle,) if star else ()
        out = [p(f"{pid}-letters", self.wm.d(ox, oy, s, drop), color, "evenodd")]
        if star:
            tx, ty = self.tittle_c
            # sit the star a hair higher than the dot: same optical gap over the stem
            r = self.tittle_star_r * s
            out.append(p(f"{pid}-tittle-star", star_d(ox + tx * s, oy + ty * s - 0.03 * s, r, 0.36, 0.20),
                         star_color or color))
        return out

    def symbol_paths(self, cx, cy, r, color=VIOLET, eye=GOLD, mono=False, pid="symbol", eye_r=0.17):
        """Aster star with an eye. mono: the eye is a hole (one colour)."""
        if mono or eye is None:
            return [p(pid, star_d(cx, cy, r) + circle_d(cx, cy, r * eye_r, ccw=True), color, "evenodd")]
        return [p(f"{pid}-rays", star_d(cx, cy, r) + circle_d(cx, cy, r * eye_r, ccw=True), color, "evenodd"),
                p(f"{pid}-eye", circle_d(cx, cy, r * eye_r * 0.999), eye)]

    def symbol16_paths(self, cx, cy, r, color, eye=None, pid="symbol-16"):
        """Optical 16 px cut: medium rays and a larger eye (3-4 px), so the open
        centre survives at 16 px instead of closing into a blot."""
        rays = star_d(cx, cy, r, notch=0.42, bulge=0.14)
        hole = circle_d(cx, cy, r * 0.25, ccw=True)
        out = [p(pid, rays + hole, color, "evenodd")]
        if eye:
            out.append(p(f"{pid}-eye", circle_d(cx, cy, r * 0.25), eye))
        return out

    def late_sun_paths(self, cx, cy, r, color=VIOLET, eye=GOLD, line=INK, mono=False, pid="late-sun"):
        star = half_star_d(cx, cy, r)
        hole = ""
        er = r * 0.2
        k = 0.5522847498 * er
        half_disk = (f"M{f(cx - er)} {f(cy)}C{f(cx - er)} {f(cy - k)} {f(cx - k)} {f(cy - er)} {f(cx)} {f(cy - er)}"
                     f"C{f(cx + k)} {f(cy - er)} {f(cx + er)} {f(cy - k)} {f(cx + er)} {f(cy)}Z")
        gap, lw, ll = r * 0.06, r * 0.045, r * 1.30
        horizon = f"M{f(cx - ll)} {f(cy + gap)}H{f(cx + ll)}V{f(cy + gap + lw)}H{f(cx - ll)}Z"
        if mono:
            return [p(pid, star + half_disk + horizon, color, "evenodd")]
        return [p(f"{pid}-rays", star + half_disk, color, "evenodd"), p(f"{pid}-eye", half_disk, eye),
                p(f"{pid}-horizon", horizon, line)]

    def seal_paths(self, cx, cy, R, ring=INK, star=VIOLET, eye=GOLD, mono=False, pid="seal"):
        out = []
        ring_d = circle_d(cx, cy, R) + circle_d(cx, cy, R * 0.966, ccw=True)
        inner_d = circle_d(cx, cy, R * 0.70) + circle_d(cx, cy, R * 0.688, ccw=True)
        size = R * 0.135
        top = ring_text_d("ASTER CHAI", cx, cy, R * 0.775, size, self.font_regular, -90, tracking=0.22, outward=True)
        bot = ring_text_d("REAL CHAI FOR REAL MOMENTS", cx, cy, R * 0.775 + size * 0.56, size * 0.72, self.font_regular, 90,
                          tracking=0.16, outward=False)
        dots = star_d(cx - R * 0.83, cy, R * 0.05, 0.42, 0.14) + star_d(cx + R * 0.83, cy, R * 0.05, 0.42, 0.14)
        col = ring if not mono else ring
        out.append(p(f"{pid}-rings", ring_d + inner_d, col, "evenodd"))
        out.append(p(f"{pid}-text-top", top, col))
        out.append(p(f"{pid}-text-bottom", bot, col))
        out.append(p(f"{pid}-dots", dots, col))
        out += self.symbol_paths(cx, cy, R * 0.43, star if not mono else ring, eye, mono=mono, pid=f"{pid}-star")
        return out

    # ---- lockups -----------------------------------------------------------
    def lockup_stacked(self, s=200, pad=0.6, **kw):
        """Symbol centred over wordmark. s = cap height px."""
        wm_w = self.wm.width * s
        sym_r = 0.78 * s
        gap = 0.55 * s
        W = wm_w + 2 * pad * s
        H = 2 * sym_r + gap + 1.0 * s + 2 * pad * s
        cx = W / 2
        cy = pad * s + sym_r
        body = self.symbol_paths(cx, cy, sym_r, **kw.get("sym", {}))
        oy = cy + sym_r + gap + s
        body += self.wordmark_paths((W - wm_w) / 2, oy, s, **kw.get("wm", {}))
        return W, H, body

    def lockup_horizontal(self, s=200, pad=0.5, mark="symbol", **kw):
        gap = 0.42 * s
        wm_w = self.wm.width * s
        if mark == "symbol":
            sym_r = 0.72 * s
            mw, mh = 2 * sym_r, 2 * sym_r
        else:  # the half sun is wide and low
            sym_r = 0.80 * s
            mw, mh = 2 * 1.55 * sym_r, 1.14 * sym_r
            gap = 0.30 * s
        W = 2 * pad * s + mw + gap + wm_w
        H = 2 * pad * s + max(mh, 1.25 * s)
        cy_mid = H / 2
        oy = cy_mid + 0.5 * s  # wordmark baseline; cap height centred
        if mark == "symbol":
            body = self.symbol_paths(pad * s + sym_r, cy_mid, sym_r, **kw.get("sym", {}))
        else:
            # the horizon line sits exactly on the wordmark's baseline
            body = self.late_sun_paths(pad * s + mw / 2, oy - 0.135 * sym_r, sym_r, **kw.get("sym", {}))
        body += self.wordmark_paths(pad * s + mw + gap, oy, s, **kw.get("wm", {}))
        return W, H, body


COLORWAYS = {
    "color": dict(sym=dict(color=VIOLET, eye=GOLD), wm=dict(color=INK)),
    "mono": dict(sym=dict(color=INK, mono=True), wm=dict(color=INK)),
    "reversed": dict(sym=dict(color=CREAM, mono=True), wm=dict(color=CREAM)),
}


def write(path, w, h, body, title):
    with open(path, "w", encoding="utf-8") as fh:
        fh.write(svg_doc(w, h, "\n".join(body), title))
    return path


def build(psd, out):
    os.makedirs(out, exist_ok=True)
    L = Logos(psd)
    files = []
    s = 200
    # ---------------- Direction A: Tittle Star ----------------
    for cw, kw in COLORWAYS.items():
        wm_w = L.wm.width * s
        pad = 0.45 * s
        W, H = wm_w + 2 * pad, 1.62 * s + 2 * pad
        star_col = {"color": VIOLET, "mono": INK, "reversed": CREAM}[cw]
        body = L.wordmark_paths(pad, pad + 1.32 * s, s, color=kw["wm"]["color"], star_color=star_col)
        files.append(write(f"{out}/A_tittle-star_wordmark_{cw}.svg", W, H, body, "Aster Chai wordmark"))
        R = 300
        body = L.symbol_paths(R * 1.15, R * 1.15, R, **kw["sym"])
        files.append(write(f"{out}/A_tittle-star_symbol_{cw}.svg", R * 2.3, R * 2.3, body, "Aster Chai symbol"))
        W, H, body = L.lockup_stacked(s, sym=kw["sym"], wm=dict(color=kw["wm"]["color"], star_color=star_col))
        files.append(write(f"{out}/A_tittle-star_lockup-stacked_{cw}.svg", W, H, body, "Aster Chai lockup"))
        W, H, body = L.lockup_horizontal(s, sym=kw["sym"], wm=dict(color=kw["wm"]["color"], star_color=star_col))
        files.append(write(f"{out}/A_tittle-star_lockup-horizontal_{cw}.svg", W, H, body, "Aster Chai lockup"))
    # 16 px optical cut + true-size test files (PhotoCraft rasterises at the SVG's width/height)
    for cw, colr, eye in (("color", VIOLET, GOLD), ("mono", INK, None), ("reversed", CREAM, None)):
        files.append(write(f"{out}/A_tittle-star_symbol-16_{cw}.svg", 16, 16,
                           L.symbol16_paths(8, 8, 7.7, colr, eye), "Aster Chai symbol, 16 px cut"))
    # ---------------- Direction B: Late Sun ----------------
    for cw, kw in COLORWAYS.items():
        R = 300
        line = {"color": INK, "mono": INK, "reversed": CREAM}[cw]
        sym = dict(color=kw["sym"]["color"], eye=kw["sym"].get("eye", GOLD), line=line, mono=kw["sym"].get("mono", False))
        body = L.late_sun_paths(R * 1.7, R * 1.25, R, **sym)
        files.append(write(f"{out}/B_late-sun_symbol_{cw}.svg", R * 3.4, R * 1.75, body, "Aster Chai Late Sun mark"))
        W, H, body = L.lockup_horizontal(s, mark="late-sun", sym=sym, wm=dict(color=kw["wm"]["color"], star=False))
        files.append(write(f"{out}/B_late-sun_lockup-horizontal_{cw}.svg", W, H, body, "Aster Chai Late Sun lockup"))
    # ---------------- Direction C: Prairie Seal ----------------
    for cw, kw in COLORWAYS.items():
        R = 420
        ring = {"color": INK, "mono": INK, "reversed": CREAM}[cw]
        body = L.seal_paths(R * 1.08, R * 1.08, R, ring=ring, star=kw["sym"]["color"], eye=kw["sym"].get("eye", GOLD),
                            mono=kw["sym"].get("mono", False))
        files.append(write(f"{out}/C_prairie-seal_{cw}.svg", R * 2.16, R * 2.16, body, "Aster Chai seal"))
    return files, L


if __name__ == "__main__" and "--explorations" not in sys.argv:
    files, L = build(sys.argv[1], sys.argv[2])
    print(len(files), "files; wordmark width", round(L.wm.width, 3), "cap units; tittle", L.wm.tittle,
          "r", round(L.tittle_r, 4), "-> star r", round(L.tittle_star_r, 4))


# --------------------------------------------------------------------------
# explorations sheet (5 directions) and the petal-bar A
# --------------------------------------------------------------------------
def petal_bar_A(L, ox, oy, s, color=INK, petal=VIOLET):
    """Remove the A's crossbar by splicing the counter into the outer contour
    (knots 36 -> 37 run along the bar's underside), then float a petal there."""
    import copy
    subs = copy.deepcopy(L.wm.subs)
    A, cnt = subs[0], subs[1]
    ks = A["knots"]
    i36, i37 = 36, 37
    corner = lambda k: (k[1], k[1], k[1])  # noqa: E731
    c = cnt["knots"]
    splice = [corner(c[1]), c[2], c[3], c[4], c[5], (c[0][0], c[0][1], c[0][1])]
    new = ks[:i36] + [(ks[i36][0], ks[i36][1], ks[i36][1])] + splice + [(ks[i37][1], ks[i37][1], ks[i37][2])] + ks[i37 + 1:]
    A["knots"] = new
    xf = lambda x, y: (ox + (x - L.wm.x0) / L.wm.cap * s, oy + (y - L.wm.BASELINE) / L.wm.cap * s)  # noqa: E731
    letters = "".join(subpath_d(sp, xf) for i, sp in enumerate(subs) if i != 1)
    # petal: horizontal lancet between the legs at the old bar height
    (x0, y0), (x1, _y1) = xf(356, 742.5), xf(462, 742.5)
    cx, cy = (x0 + x1) / 2, y0
    hl, hw = (x1 - x0) / 2, 0.03 * s
    pd = (f"M{f(cx - hl)} {f(cy)}C{f(cx - hl * 0.45)} {f(cy - hw * 1.6)} {f(cx + hl * 0.45)} {f(cy - hw * 1.6)} {f(cx + hl)} {f(cy)}"
          f"C{f(cx + hl * 0.45)} {f(cy + hw * 1.6)} {f(cx - hl * 0.45)} {f(cy + hw * 1.6)} {f(cx - hl)} {f(cy)}Z")
    return [p("petal-a-letters", letters, color, "evenodd"), p("petal-a-petal", pd, petal)]


def explorations_svg(L, path):
    sys.path.insert(0, os.path.join(HERE, "..", "00_aster_system"))
    from aster import Bloom, bloom_xf, PathBuilder  # noqa: F401
    W, H = 3600, 2400
    body = [f'<rect id="bg" width="{W}" height="{H}" fill="{CREAM}"/>']
    tiles = [(150 + i * 1125, 380 + j * 900) for j in range(2) for i in range(3)]
    tw, th = 1050, 820
    for n, (tx, ty) in enumerate(tiles):
        body.append(f'<rect id="tile-{n + 1}" x="{tx}" y="{ty}" width="{tw}" height="{th}" fill="#EAE0CD"/>')
    s = 120
    # 1 tittle star
    tx, ty = tiles[0]
    ww = L.wm.width * s
    body += L.wordmark_paths(tx + (tw - ww) / 2, ty + 400, s, color=INK, star_color=VIOLET, pid="e1-wordmark")
    # 2 eight-ray marks: open bloom (Phase 0 level 1) and fused aster star
    tx, ty = tiles[1]
    b = Bloom(petals=8, abstraction=1, center=0.22)
    for name, (fill, pb) in b.layers(bloom_xf(tx + 300, ty + 330, 190)).items():
        if pb:
            body.append(p(f"e2-open-{name}", pb.d(), fill))
    body += L.symbol_paths(tx + 750, ty + 330, 190, pid="e2-star")
    # 3 petal-bar A
    tx, ty = tiles[2]
    body += petal_bar_A(L, tx + (tw - ww) / 2, ty + 400, s)
    # 4 late sun
    tx, ty = tiles[3]
    r = 150
    body += L.late_sun_paths(tx + tw / 2, ty + 380, r, pid="e4-late-sun")
    # 5 seal
    tx, ty = tiles[4]
    body += L.seal_paths(tx + tw / 2, ty + 330, 250, pid="e5-seal")
    with open(path, "w", encoding="utf-8") as fh:
        fh.write(svg_doc(W, H, "\n".join(body), "Aster Chai logo explorations"))
    return tiles


if __name__ == "__main__" and len(sys.argv) > 3 and sys.argv[3] == "--explorations":
    L = Logos(sys.argv[1])
    print(explorations_svg(L, sys.argv[2]))


# --------------------------------------------------------------------------
# Late Sun as the primary mark (approved direction): compact cut + small sizes
# --------------------------------------------------------------------------
def late_sun_compact_paths(L, cx, cy, r, color=VIOLET, eye=GOLD, line=INK, mono=False, pid="late-sun-compact"):
    """Tight Late Sun for small squares: horizon only as wide as the star, heavier line."""
    star = half_star_d(cx, cy, r, notch=0.38, bulge=0.18)
    er = r * 0.24
    k = 0.5522847498 * er
    half_disk = (f"M{f(cx - er)} {f(cy)}C{f(cx - er)} {f(cy - k)} {f(cx - k)} {f(cy - er)} {f(cx)} {f(cy - er)}"
                 f"C{f(cx + k)} {f(cy - er)} {f(cx + er)} {f(cy - k)} {f(cx + er)} {f(cy)}Z")
    gap, lw, ll = r * 0.10, r * 0.13, r * 1.08
    horizon = f"M{f(cx - ll)} {f(cy + gap)}H{f(cx + ll)}V{f(cy + gap + lw)}H{f(cx - ll)}Z"
    if mono:
        return [p(pid, star + half_disk + horizon, color, "evenodd")]
    return [p(f"{pid}-rays", star + half_disk, color, "evenodd"), p(f"{pid}-eye", half_disk, eye),
            p(f"{pid}-horizon", horizon, line)]


def build_late_sun_system(psd, out):
    L = Logos(psd)
    files = []
    for cw, colr, eye, line, mono in (("color", VIOLET, GOLD, INK, False), ("mono", INK, None, INK, True),
                                      ("reversed", CREAM, None, CREAM, True)):
        # square 16 px test cut: star r chosen so rays + line fill the square
        r = 7.3
        files.append(write(f"{out}/late-sun_compact-16_{cw}.svg", 16, 16,
                           late_sun_compact_paths(L, 8, 8 + r * 0.42, r, colr, eye or GOLD, line, mono), "Late Sun, 16 px cut"))
        R = 300
        files.append(write(f"{out}/late-sun_compact_{cw}.svg", R * 2.5, R * 1.7,
                           late_sun_compact_paths(L, R * 1.25, R * 1.2, R, colr, eye or GOLD, line, mono), "Late Sun, compact"))
    return files
