#!/usr/bin/env python3
"""Generate every Reach logo SVG from source.

The mark is the Hilo knot — Reach is a Hilo product and shares the family mark.
The wordmark is "reach", lowercase, set in Geist SemiBold and shipped as outlines.
Geometry mirrors the production component on reachallcustomers.com:
knot height 1.6em, wordmark 1.25em, gap 0.625em, tracking -0.025em, items centered.

Outputs (this directory):
  reach-logo-light.svg        knot Purple, wordmark Ink            — light surfaces
  reach-logo-dark.svg         knot Lilac,  wordmark White          — Ink / dark surfaces
  reach-logo-mono-ink.svg     single color, Ink
  reach-logo-mono-white.svg   single color, White
  reach-icon.svg              the knot alone, Purple (30 x 28 grid)
  reach-icon-lilac.svg        the knot alone, Lilac (for dark UI)
  reach-app-icon.svg          Ink squircle, Lilac knot, white wordmark  (512)
  reach-avatar.svg            Ink square, full-bleed, for social profiles (1024)
  reach-clearspace.svg        spacing diagram
  reach-line.svg              the thread underline device (gradient stroke)
  reach-thread.svg            the long thread with the teal "reached" dot
  powered-by-hilo-light.svg   endorsement lockup, light
  powered-by-hilo-dark.svg    endorsement lockup, dark
  punch-lockup-dark.svg       demo sub-brand lockup: PUNCH by [knot] reach

Requires: pip install fonttools
"""
import os, re
from fontTools import ttLib
from fontTools.varLib.instancer import instantiateVariableFont
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.transformPen import TransformPen
from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.boundsPen import BoundsPen
from fontTools.misc.transform import Transform

HERE = os.path.dirname(os.path.abspath(__file__))
FONTS = os.path.join(os.path.dirname(HERE), "fonts")

PURPLE = "#6C5CFF"
LILAC = "#8F80FF"
INK = "#0D1133"
WHITE = "#FFFFFF"
BLUE = "#4A90E2"
TEAL = "#00B894"
PUNCH_BLACK = "#07060F"
PUNCH_PURPLE = "#6D4AFF"
PUNCH_LAVENDER = "#C9B8FF"

# ---------------------------------------------------------------- the knot
# Identical to the Hilo mark: 30 x 28 grid, stroke 2.3, round caps and joins.
RX, RY = 4.0, 5.0
DV, DH = 8.8, 5.0
SYM_STROKE = 2.3
xL, xR = 15 - DV / 2, 15 + DV / 2
y1, y2 = 14 - DH / 2, 14 + DH / 2
cT, cB = y1 - RY, y2 + RY
SYM_D = (f"M{xR + RX} {cT - RY}a{RX} {RY} 0 0 0-{RX} {RY}v{cB - cT}"
         f"a{RX} {RY} 0 0 0 {RX} {RY} {RX} {RY} 0 0 0 {RX}-{RY} {RX} {RY} 0 0 0-{RX}-{RY}"
         f"H{xL - RX}a{RX} {RY} 0 0 0-{RX} {RY} {RX} {RY} 0 0 0 {RX} {RY} {RX} {RY} 0 0 0 {RX}-{RY}"
         f"v-{cB - cT}a{RX} {RY} 0 0 0-{RX}-{RY} {RX} {RY} 0 0 0-{RX} {RY} {RX} {RY} 0 0 0 {RX} {RY}"
         f"h{DV + 2 * RX}a{RX} {RY} 0 0 0 {RX}-{RY} {RX} {RY} 0 0 0-{RX}-{RY}z")
SYM_INK = (xL - 2 * RX - SYM_STROKE / 2, cT - RY - SYM_STROKE / 2,
           xR + 2 * RX + SYM_STROKE / 2, cB + RY + SYM_STROKE / 2)   # 0.5..29.5 x 0.5..27.5
SYM_W = SYM_INK[2] - SYM_INK[0]
SYM_H = SYM_INK[3] - SYM_INK[1]

def knot(color, stroke=SYM_STROKE, transform=None):
    t = f' transform="{transform}"' if transform else ""
    return (f'<path{t} d="{SYM_D}" fill="none" stroke="{color}" stroke-width="{stroke}" '
            f'stroke-linecap="round" stroke-linejoin="round"/>')

# ---------------------------------------------------------------- Geist
def load_geist(weight):
    f = ttLib.TTFont(os.path.join(FONTS, "Geist-Variable.ttf"))
    instantiateVariableFont(f, {"wght": weight}, inplace=True)
    return f

class Face:
    def __init__(self, font):
        self.font = font
        self.upm = font["head"].unitsPerEm
        self.gs = font.getGlyphSet()
        self.cmap = font.getBestCmap()
        self.asc = font["hhea"].ascent
        self.desc = font["hhea"].descent
        self.cap = font["OS/2"].sCapHeight
        self.xh = font["OS/2"].sxHeight

    def bounds(self, ch):
        bp = BoundsPen(self.gs)
        self.gs[self.cmap[ord(ch)]].draw(bp)
        return bp.bounds

    def text_paths(self, text, size, tracking=0.0, x0=0.0, y0=0.0, precision=2):
        """Outline `text` at font-size `size` (logo units), baseline at y0, left at x0.
        Returns (list of path d strings, advance width, ink bbox)."""
        s = size / self.upm
        x = x0
        paths, ink = [], None
        for i, ch in enumerate(text):
            gn = self.cmap[ord(ch)]
            g = self.gs[gn]
            sp = SVGPathPen(self.gs, ntos=lambda v: f"{v:.{precision}f}")
            tp = TransformPen(sp, Transform(s, 0, 0, -s, x, y0))
            g.draw(tp)
            d = sp.getCommands()
            if d:
                paths.append(d)
            bp = BoundsPen(self.gs); g.draw(bp)
            if bp.bounds:
                b = (x + bp.bounds[0] * s, y0 - bp.bounds[3] * s, x + bp.bounds[2] * s, y0 - bp.bounds[1] * s)
                ink = b if ink is None else (min(ink[0], b[0]), min(ink[1], b[1]), max(ink[2], b[2]), max(ink[3], b[3]))
            x += g.width * s
            if i < len(text) - 1:
                x += tracking * size
        return paths, x - x0, ink

def svg_open(w, h, label, width=None, extra=""):
    width = width or round(w)
    height = round(width * h / w)
    return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w:.2f} {h:.2f}" '
            f'width="{width}" height="{height}" role="img" aria-label="{label}"{extra}>')

def write(name, content):
    with open(os.path.join(HERE, name), "w") as f:
        f.write(content)
    print("wrote", name)

# ---------------------------------------------------------------- lockup geometry
# Production component: container font-size = 1em. Knot height 1.6em, wordmark
# 1.25em SemiBold with -0.025em tracking, gap 0.625em, flex items-center.
EM = 100.0                       # container em in logo units
KNOT_H = 1.6 * EM                # 160
WORD_SIZE = 1.25 * EM            # 125
GAP = 0.625 * EM                 # 62.5
TRACK = -0.025

semibold = Face(load_geist(600))
regular = Face(load_geist(400))
black = Face(load_geist(900))

k = KNOT_H / SYM_H               # knot scale
knot_w = SYM_W * k

# vertical centering: CSS centers the knot on the wordmark's line box (leading-none:
# line box = 1em of the wordmark, positioned by hhea ascent/descent).
line_h = WORD_SIZE
content_h = (semibold.asc - semibold.desc) / semibold.upm * WORD_SIZE
half_leading = (line_h - content_h) / 2
# baseline y (from top of the line box)
baseline = half_leading + semibold.asc / semibold.upm * WORD_SIZE
line_mid = line_h / 2
knot_top = line_mid - KNOT_H / 2

# lay out: knot at x=0, wordmark after gap; everything relative to a top at min(knot_top, 0)
word_paths, word_adv, word_ink = semibold.text_paths("reach", WORD_SIZE, TRACK, x0=0, y0=0)
# word ink relative to baseline; place baseline at `baseline`
word_x = knot_w + GAP - word_ink[0]        # ink-left aligned to gap
lock_top = min(knot_top, baseline + word_ink[1])
lock_bottom = max(knot_top + KNOT_H, baseline + word_ink[3])
ink_w = word_x + word_ink[2]
ink_h = lock_bottom - lock_top

PAD = 0.09 * ink_h
W, Hgt = ink_w + 2 * PAD, ink_h + 2 * PAD

def knot_transform(x, y, scale):
    """Transform placing the knot's ink box top-left at (x, y) at `scale`."""
    return f"translate({x - SYM_INK[0] * scale:.3f} {y - SYM_INK[1] * scale:.3f}) scale({scale:.4f})"

def lockup_group(mark_color, text_color, ox, oy):
    """The lockup with its ink top-left at (ox, oy)."""
    parts = [f'<g transform="translate({ox:.2f} {oy - lock_top:.2f})">',
             "  " + knot(mark_color, transform=knot_transform(0, knot_top, k))]
    for d in word_paths:
        parts.append(f'  <path transform="translate({word_x:.2f} {baseline:.2f})" d="{d}" fill="{text_color}"/>')
    parts.append("</g>")
    return "\n".join(parts)

def lockup_svg(mark_color, text_color):
    return "\n".join([svg_open(W, Hgt, "Reach", width=round(W / 2)),
                      lockup_group(mark_color, text_color, PAD, PAD), "</svg>"])

write("reach-logo-light.svg", lockup_svg(PURPLE, INK))
write("reach-logo-dark.svg", lockup_svg(LILAC, WHITE))
write("reach-logo-mono-ink.svg", lockup_svg(INK, INK))
write("reach-logo-mono-white.svg", lockup_svg(WHITE, WHITE))

# ---------------------------------------------------------------- icons
icon = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 28" width="96" height="90" role="img" aria-label="Reach mark">
  {knot(PURPLE)}
</svg>'''
write("reach-icon.svg", icon)
write("reach-icon-lilac.svg", icon.replace(PURPLE, LILAC))

# app icon — Ink squircle, Lilac knot, white wordmark under it (mirrors the chat avatar)
TILE = 512
ks = (TILE * 0.44) / SYM_W
kx = (TILE - SYM_W * ks) / 2
ky = TILE * 0.20
wsize = TILE * 0.20
wp, wadv, wink = semibold.text_paths("reach", wsize, TRACK)
wx = (TILE - (wink[2] - wink[0])) / 2 - wink[0]
wy = ky + SYM_H * ks + TILE * 0.06 - wink[1]   # ink top sits 6% below the knot
app = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {TILE} {TILE}" width="{TILE}" height="{TILE}" role="img" aria-label="Reach app icon">',
       f'  <rect width="{TILE}" height="{TILE}" rx="{round(TILE * 0.225)}" fill="{INK}"/>',
       "  " + knot(LILAC, transform=knot_transform(kx, ky, ks))]
app += [f'  <path transform="translate({wx:.2f} {wy:.2f})" d="{d}" fill="{WHITE}"/>' for d in wp]
app.append("</svg>")
write("reach-app-icon.svg", "\n".join(app))

# social avatar — same composition, full-bleed square (platforms apply their own crop)
AV = 1024
sc = AV / TILE
av = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {AV} {AV}" width="{AV}" height="{AV}" role="img" aria-label="Reach avatar">',
      f'  <rect width="{AV}" height="{AV}" fill="{INK}"/>',
      f'  <g transform="scale({sc})">',
      "    " + knot(LILAC, transform=knot_transform(kx, ky, ks))]
av += [f'    <path transform="translate({wx:.2f} {wy:.2f})" d="{d}" fill="{WHITE}"/>' for d in wp]
av += ["  </g>", "</svg>"]
write("reach-avatar.svg", "\n".join(av))

# ---------------------------------------------------------------- clear space
# Module: the loop (ink width of one knot loop) — shared with Hilo so the two
# brands sit on the same grid. Gap between knot and wordmark = 0.625em ≈ 1 loop.
LOOP = (2 * RX + SYM_STROKE) * k
CS = LOOP
GUIDE = PURPLE
gW, gH = ink_w + 2 * CS, ink_h + 2 * CS
m = 6
lbl = 'font-family="Geist, -apple-system, Helvetica, Arial, sans-serif" font-size="14"'
parts = [
    f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{-m} {-m} {gW + 2 * m:.2f} {gH + 2 * m + 24:.2f}" '
    f'width="{round((gW + 2 * m) / 2)}" height="{round((gH + 2 * m + 24) / 2)}" role="img" aria-label="Reach logo clear-space guide">',
    f'  <rect x="0" y="0" width="{gW:.2f}" height="{gH:.2f}" fill="none" stroke="{GUIDE}" stroke-opacity="0.45" stroke-width="1" stroke-dasharray="4 4"/>',
    f'  <rect x="{CS:.2f}" y="{CS:.2f}" width="{ink_w:.2f}" height="{ink_h:.2f}" fill="none" stroke="{GUIDE}" stroke-opacity="0.2" stroke-width="1"/>',
]
for cx, cy in ((CS / 2, CS / 2 + 2), (gW - CS / 2, CS / 2 + 2), (CS / 2, gH - CS / 2 - 2), (gW - CS / 2, gH - CS / 2 - 2)):
    parts.append(f'  <ellipse cx="{cx:.2f}" cy="{cy:.2f}" rx="{RX * k:.2f}" ry="{RY * k:.2f}" fill="none" stroke="{GUIDE}" stroke-opacity="0.3" stroke-width="{SYM_STROKE * k:.2f}"/>')
gap_x = CS + knot_w
parts += [
    f'  <line x1="{CS:.2f}" y1="{CS + baseline - lock_top:.2f}" x2="{CS + ink_w:.2f}" y2="{CS + baseline - lock_top:.2f}" stroke="{GUIDE}" stroke-opacity="0.25" stroke-width="0.8" stroke-dasharray="3 3"/>',
    f'  <rect x="{gap_x:.2f}" y="{CS:.2f}" width="{GAP:.2f}" height="{ink_h:.2f}" fill="{GUIDE}" fill-opacity="0.07"/>',
    f'  <line x1="{gap_x:.2f}" y1="{CS:.2f}" x2="{gap_x:.2f}" y2="{CS + ink_h:.2f}" stroke="{GUIDE}" stroke-opacity="0.35" stroke-width="0.8"/>',
    f'  <line x1="{gap_x + GAP:.2f}" y1="{CS:.2f}" x2="{gap_x + GAP:.2f}" y2="{CS + ink_h:.2f}" stroke="{GUIDE}" stroke-opacity="0.35" stroke-width="0.8"/>',
    lockup_group(PURPLE, INK, CS, CS),
    f'  <text x="{gap_x + GAP / 2:.2f}" y="{gH + 17:.2f}" text-anchor="middle" {lbl} fill="{GUIDE}">gap = 0.625 em ≈ 1 loop</text>',
    f'  <text x="0" y="{gH + 17:.2f}" text-anchor="start" {lbl} fill="{GUIDE}">clear space ≥ 1 loop</text>',
    "</svg>",
]
write("reach-clearspace.svg", "\n".join(parts))

# ---------------------------------------------------------------- the thread devices
# The underline that carries "all" / "everyone" / "todos" in headlines. Same path
# as production: a hand-drawn curve, thread gradient Purple → Blue → Teal.
THREAD_GRAD = (f'<linearGradient id="thread" x1="0" x2="1" y1="0" y2="0">'
               f'<stop offset="0" stop-color="{PURPLE}"/><stop offset="0.6" stop-color="{BLUE}"/>'
               f'<stop offset="1" stop-color="{TEAL}"/></linearGradient>')
write("reach-line.svg",
      f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 18" width="240" height="36" fill="none" '
      f'preserveAspectRatio="none" role="img" aria-label="Reach thread underline">\n'
      f'  <defs>{THREAD_GRAD}</defs>\n'
      f'  <path d="M4 12 C 30 16, 60 4, 116 9" stroke="url(#thread)" stroke-width="5" stroke-linecap="round"/>\n'
      f'</svg>')

# The long thread: travels flat, lifts at the end, and lands on the teal dot —
# the customer, reached. Used as a section divider and on the social card.
write("reach-thread.svg",
      f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 80" width="1200" height="80" fill="none" '
      f'preserveAspectRatio="none" role="img" aria-label="Reach thread">\n'
      f'  <defs>{THREAD_GRAD}</defs>\n'
      f'  <path d="M0 66 C 400 62, 800 58, 1040 52 C 1100 50, 1125 40, 1140 22" stroke="url(#thread)" stroke-width="3" stroke-linecap="round"/>\n'
      f'  <circle cx="1140" cy="22" r="8" fill="{TEAL}"/>\n'
      f'</svg>')

# ---------------------------------------------------------------- Powered by Hilo
def svg_inner(path):
    svg = open(path).read()
    vb = re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg)
    body = svg.split(">", 1)[1].rsplit("</svg>", 1)[0].strip()
    return float(vb.group(1)), float(vb.group(2)), body

def powered_by(hilo_file, text_color, name):
    hw, hh, hbody = svg_inner(os.path.join(HERE, "hilo", hilo_file))
    # "Powered by" in Geist Regular, x-height matched to the lockup: text size = 0.42 × Hilo height
    tsize = hh * 0.62
    tp, tadv, tink = regular.text_paths("Powered by", tsize, 0)
    gap = tsize * 0.45
    # baseline of the text aligned with the Hilo wordmark baseline (≈ 0.795 × hh — measured from the lockup)
    hilo_scale = 1.0
    total_w = (tink[2] - tink[0]) + gap + hw
    H = hh
    base_y = hh * 0.795
    pad = hh * 0.1
    parts = [svg_open(total_w + 2 * pad, H + 2 * pad, "Powered by Hilo", width=round(total_w + 2 * pad))]
    parts += [f'  <path transform="translate({pad - tink[0]:.2f} {pad + base_y:.2f})" d="{d}" fill="{text_color}"/>' for d in tp]
    parts.append(f'  <g transform="translate({pad + (tink[2] - tink[0]) + gap:.2f} {pad:.2f})">{hbody}</g>')
    parts.append("</svg>")
    write(name, "\n".join(parts))

powered_by("hilo-logo-light.svg", "#505570", "powered-by-hilo-light.svg")
powered_by("hilo-logo-dark.svg", "#B9B8D0", "powered-by-hilo-dark.svg")

# ---------------------------------------------------------------- Punch by Reach
# Demo sub-brand lockup: PUNCH (Geist Black, tight) · by · [knot] reach
psize = 120
pp, padv, pink = black.text_paths("PUNCH", psize, -0.03)
bsize = psize * 0.30
bp_, badv, bink = regular.text_paths("by", bsize, 0)
rs = psize * 0.36 / WORD_SIZE          # scale the reach lockup so its wordmark is 36% of PUNCH
g1, g2 = psize * 0.22, psize * 0.16
x = 0
pw = pink[2] - pink[0]
x_by = pw + g1
x_reach = x_by + (bink[2] - bink[0]) + g2
total = x_reach + ink_w * rs
top = min(pink[1], -(baseline - lock_top) * rs + (baseline + word_ink[1]) * rs)
bottom = max(pink[3], 0)
pad = psize * 0.12
Hp = bottom - top
parts = [svg_open(total + 2 * pad, Hp + 2 * pad, "Punch by Reach", width=round((total + 2 * pad) / 2)),
         f'  <rect width="100%" height="100%" fill="{PUNCH_BLACK}"/>',
         f'  <g transform="translate({pad:.2f} {pad - top:.2f})">']
parts += [f'    <path transform="translate({-pink[0]:.2f} 0)" d="{d}" fill="{WHITE}"/>' for d in pp]
parts += [f'    <path transform="translate({x_by - bink[0]:.2f} 0)" d="{d}" fill="{PUNCH_LAVENDER}" fill-opacity="0.8"/>' for d in bp_]
# reach lockup: place its baseline on the PUNCH baseline
parts.append(f'    <g transform="translate({x_reach:.2f} {-(baseline - lock_top) * rs:.2f}) scale({rs:.4f})">')
parts.append("      " + knot(LILAC, transform=knot_transform(0, knot_top, k)))
parts += [f'      <path transform="translate({word_x:.2f} {baseline:.2f})" d="{d}" fill="{WHITE}"/>' for d in word_paths]
parts += ["    </g>", "  </g>", "</svg>"]
write("punch-lockup-dark.svg", "\n".join(parts))

print(f"lockup: knot {knot_w:.1f}x{KNOT_H:.1f}, gap {GAP:.1f}, loop {LOOP:.1f}, ink {ink_w:.1f}x{ink_h:.1f}, viewBox {W:.1f}x{Hgt:.1f}")
