#!/usr/bin/env python3
"""Bobby Directory brand asset generator. Bracket Index A2."""
import os, subprocess, math, json
from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.transformPen import TransformPen

ROOT = os.environ.get("BD_OUT", "/tmp/bd/out")
FONTS = "/tmp/bd/fonts"
BOLD = os.path.join(FONTS, "SpaceGrotesk-2.0.0/ttf/static/SpaceGrotesk-Bold.ttf")
SEMI = os.path.join(FONTS, "SpaceGrotesk-SemiBold.ttf")

# ---------------------------------------------------------------- typography
_cache = {}
def font(path):
    if path not in _cache:
        _cache[path] = TTFont(path)
    return _cache[path]

def _glyphset(path):
    f = font(path)
    return f.getGlyphSet(), f["cmap"].getBestCmap(), f["head"].unitsPerEm, f["hmtx"]

def text_path(s, path, size, x, y, tracking=0.0, anchor="start"):
    """Return (svg path data, advance width). y is the baseline."""
    gs, cmap, upem, hmtx = _glyphset(path)
    scale = size / upem
    widths = []
    names = []
    for ch in s:
        gname = cmap.get(ord(ch))
        names.append(gname)
        widths.append((hmtx[gname][0] * scale if gname else size * 0.4))
    total = sum(widths) + tracking * (len(s) - 1)
    if anchor == "middle":
        x -= total / 2
    elif anchor == "end":
        x -= total
    d = []
    cx = x
    for gname, w in zip(names, widths):
        if gname:
            pen = SVGPathPen(gs)
            tpen = TransformPen(pen, (scale, 0, 0, -scale, cx, y))
            gs[gname].draw(tpen)
            seg = pen.getCommands()
            if seg:
                d.append(seg)
        cx += w + tracking
    return " ".join(d), total

def text_width(s, path, size, tracking=0.0):
    return text_path(s, path, size, 0, 0, tracking)[1]

# ---------------------------------------------------------------- palettes
PAL = {
    "color-light":  {"ink": "#101828", "accent": "#C2440A"},
    "color-dark":   {"ink": "#F2F4F7", "accent": "#FFB066"},
    "black":        {"ink": "#101828", "accent": "#101828"},
    "white":        {"ink": "#FFFFFF", "accent": "#FFFFFF"},
    "grayscale":    {"ink": "#1F2937", "accent": "#5A6472"},
    "pure-black":   {"ink": "#000000", "accent": "#000000"},
    "pure-white":   {"ink": "#FFFFFF", "accent": "#FFFFFF"},
}
GROUND_LIGHT = "#FFFFFF"
GROUND_DARK = "#0D1117"

# ---------------------------------------------------------------- symbol geometry
def symbol_full(p, tx=0.0, ty=0.0, s=1.0):
    """64x64 design frame. Returns svg fragment."""
    g = (f'<g transform="translate({tx},{ty}) scale({s})">'
         f'<path d="M26 12H15a3 3 0 0 0-3 3v34a3 3 0 0 0 3 3h11" fill="none" '
         f'stroke="{p["ink"]}" stroke-width="6" stroke-linecap="round"/>'
         f'<rect x="32" y="11.5" width="21" height="18.5" rx="5" fill="{p["ink"]}"/>'
         f'<rect x="32" y="33.5" width="21" height="18.5" rx="5" fill="{p["accent"]}"/>'
         f'</g>')
    return g

def symbol_simplified(p, tx=0.0, ty=0.0, s=1.0):
    """Heavier stroke, tighter radii, larger counters. For 20 to 48 px."""
    g = (f'<g transform="translate({tx},{ty}) scale({s})">'
         f'<path d="M27 11H15.5a3.5 3.5 0 0 0-3.5 3.5v35a3.5 3.5 0 0 0 3.5 3.5H27" fill="none" '
         f'stroke="{p["ink"]}" stroke-width="7" stroke-linecap="butt"/>'
         f'<rect x="33" y="11" width="21" height="19" rx="4" fill="{p["ink"]}"/>'
         f'<rect x="33" y="34" width="21" height="19" rx="4" fill="{p["accent"]}"/>'
         f'</g>')
    return g

def symbol_micro(p, tx=0.0, ty=0.0, s=1.0):
    """Pixel snapped on a 16 unit grid. Every edge on an integer boundary."""
    g = (f'<g transform="translate({tx},{ty}) scale({s})" shape-rendering="crispEdges">'
         f'<rect x="2" y="2" width="2" height="12" fill="{p["ink"]}"/>'
         f'<rect x="4" y="2" width="3" height="2" fill="{p["ink"]}"/>'
         f'<rect x="4" y="12" width="3" height="2" fill="{p["ink"]}"/>'
         f'<rect x="9" y="3" width="5" height="4" fill="{p["ink"]}"/>'
         f'<rect x="9" y="9" width="5" height="4" fill="{p["accent"]}"/>'
         f'</g>')
    return g

SYMBOL = {"full": symbol_full, "simplified": symbol_simplified, "micro": symbol_micro}
SYMBOL_UNITS = {"full": 64, "simplified": 64, "micro": 16}

def svg(w, h, body, bg=None):
    b = f'<rect width="{w}" height="{h}" fill="{bg}"/>' if bg else ""
    return (f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" '
            f'viewBox="0 0 {w} {h}" role="img">{b}{body}</svg>')

# ---------------------------------------------------------------- wordmark
def wordmark(p, x, y, size, case="mixed", anchor="start", two_line=False, gap=None):
    """Bobby over DIRECTORY (two_line) or Bobby DIRECTORY inline (one line)."""
    name = "BOBBY" if case == "upper" else "Bobby"
    n_track = size * 0.02 if case == "upper" else -size * 0.022
    d_size = size * 0.70
    d_track = d_size * 0.121   # 2.3 at 19
    if two_line:
        gap = gap if gap is not None else size * 0.93
        d1, w1 = text_path(name, BOLD, size, x, y, n_track, anchor)
        d2, w2 = text_path("DIRECTORY", SEMI, d_size, x, y + gap, d_track, anchor)
        return (f'<path d="{d1}" fill="{p["ink"]}"/><path d="{d2}" fill="{p["accent"]}"/>',
                max(w1, w2), gap)
    else:
        d1, w1 = text_path(name, BOLD, size, x, y, n_track, "start")
        d2, w2 = text_path("DIRECTORY", SEMI, d_size, x + w1 + size * 0.34, y, d_track, "start")
        return (f'<path d="{d1}" fill="{p["ink"]}"/><path d="{d2}" fill="{p["accent"]}"/>',
                w1 + size * 0.34 + w2, 0)

def wordmark_block_width(p, size, case, two_line):
    name = "BOBBY" if case == "upper" else "Bobby"
    n_track = size * 0.02 if case == "upper" else -size * 0.022
    d_size = size * 0.70
    d_track = d_size * 0.121
    w1 = text_width(name, BOLD, size, n_track)
    w2 = text_width("DIRECTORY", SEMI, d_size, d_track)
    return max(w1, w2) if two_line else w1 + size * 0.34 + w2

# ---------------------------------------------------------------- compositions
def horizontal(scheme, case="mixed", style="full", bg=None, pad=None, sym=52):
    """Horizontal lockup: symbol left, two-line wordmark right."""
    p = PAL[scheme]
    u = SYMBOL_UNITS[style]
    s = sym / u
    pad = pad if pad is not None else sym * 0.34
    tsize = sym * 0.52
    tw = wordmark_block_width(p, tsize, case, True)
    gapx = sym * 0.42
    w = pad * 2 + sym + gapx + tw
    h = pad * 2 + sym
    cy = pad + sym / 2
    body = SYMBOL[style](p, pad, pad, s)
    wm, _, _ = wordmark(p, pad + sym + gapx, cy - tsize * 0.06, tsize, case, "start", True,
                        gap=tsize * 0.90)
    return svg(round(w, 2), round(h, 2), body + wm, bg)

def stacked(scheme, case="mixed", style="full", bg=None, sym=64):
    p = PAL[scheme]
    u = SYMBOL_UNITS[style]
    s = sym / u
    pad = sym * 0.28
    tsize = sym * 0.40
    tw = wordmark_block_width(p, tsize, case, True)
    w = max(sym, tw) + pad * 2
    gapy = sym * 0.40
    h = pad * 2 + sym + gapy + tsize * 1.75
    body = SYMBOL[style](p, (w - sym) / 2, pad, s)
    wm, _, _ = wordmark(p, w / 2, pad + sym + gapy + tsize * 0.8, tsize, case, "middle", True,
                        gap=tsize * 0.98)
    return svg(round(w, 2), round(h, 2), body + wm, bg)

def wordmark_only(scheme, case="mixed", bg=None, inline=False, size=48):
    p = PAL[scheme]
    pad = size * 0.34
    if inline:
        wm, tw, _ = wordmark(p, pad, pad + size * 0.78, size, case, "start", False)
        return svg(round(tw + pad * 2, 2), round(size * 1.05 + pad * 2, 2), wm, bg)
    tw = wordmark_block_width(p, size, case, True)
    gap = size * 0.90
    wm, _, _ = wordmark(p, pad, pad + size * 0.78, size, case, "start", True, gap=gap)
    return svg(round(tw + pad * 2, 2), round(pad * 2 + size * 0.78 + gap + size * 0.22, 2), wm, bg)

def icon(scheme, style="full", field=None, size=1024, radius_ratio=0.2195, safe=0.66, bleed=False):
    """Square app icon. safe = symbol size as a fraction of the canvas."""
    p = PAL[scheme]
    u = SYMBOL_UNITS[style]
    sym = size * safe
    s = sym / u
    off = (size - sym) / 2
    bgel = ""
    if field:
        if bleed:
            bgel = f'<rect width="{size}" height="{size}" fill="{field}"/>'
        else:
            r = size * radius_ratio
            bgel = f'<rect width="{size}" height="{size}" rx="{r}" fill="{field}"/>'
    return svg(size, size, bgel + SYMBOL[style](p, off, off, s))

def avatar(scheme, field=None, size=1024, circle=False, style="full"):
    p = PAL[scheme]
    u = SYMBOL_UNITS[style]
    safe = 0.56 if circle else 0.64
    sym = size * safe
    s = sym / u
    off = (size - sym) / 2
    bgel = ""
    if field:
        bgel = (f'<circle cx="{size/2}" cy="{size/2}" r="{size/2}" fill="{field}"/>' if circle
                else f'<rect width="{size}" height="{size}" fill="{field}"/>')
    return svg(size, size, bgel + SYMBOL[style](p, off, off, s))

def banner_h(w, h, scheme, field, sym_frac=0.42, cx=None, cy=None, grid=True):
    """Wide short banner: symbol plus two line wordmark on one row, optically centred."""
    p = PAL[scheme]
    sym = h * sym_frac
    s = sym / 64
    tsize = sym * 0.52
    tw = wordmark_block_width(p, tsize, "mixed", True)
    gapx = sym * 0.42
    total = sym + gapx + tw
    cx = cx if cx is not None else w / 2
    cy = cy if cy is not None else h / 2
    x0 = cx - total / 2
    parts = [f'<rect width="{w}" height="{h}" fill="{field}"/>']
    if grid:
        step = h / 6
        rows = []
        for i in range(int(h / step) + 2):
            y = i * step
            rows.append(f'<line x1="0" y1="{y:.1f}" x2="{w}" y2="{y:.1f}"/>')
        for i in range(int(w / step) + 2):
            x = i * step
            rows.append(f'<line x1="{x:.1f}" y1="0" x2="{x:.1f}" y2="{h}"/>')
        parts.append(f'<g stroke="{p["ink"]}" stroke-width="1" opacity="0.07">{"".join(rows)}</g>')
    parts.append(symbol_full(p, x0, cy - sym / 2, s))
    wm, _, _ = wordmark(p, x0 + sym + gapx, cy - tsize * 0.06, tsize, "mixed", "start", True,
                        gap=tsize * 0.90)
    parts.append(wm)
    return svg(w, h, "".join(parts))


def banner(w, h, scheme, field, sub=None, sym_frac=0.20, cx=None, cy=None, grid=True):
    """Cover / OG composition: centred stacked lockup on a field with a record grid motif."""
    p = PAL[scheme]
    sym = min(w, h) * sym_frac
    s = sym / 64
    cx = cx if cx is not None else w / 2
    cy = cy if cy is not None else h / 2
    tsize = sym * 0.52
    parts = [f'<rect width="{w}" height="{h}" fill="{field}"/>']
    if grid:
        step = max(w, h) / 26
        rows = []
        for i in range(int(h / step) + 2):
            y = i * step
            rows.append(f'<line x1="0" y1="{y:.1f}" x2="{w}" y2="{y:.1f}"/>')
        for i in range(int(w / step) + 2):
            x = i * step
            rows.append(f'<line x1="{x:.1f}" y1="0" x2="{x:.1f}" y2="{h}"/>')
        parts.append(f'<g stroke="{p["ink"]}" stroke-width="1" opacity="0.07">{"".join(rows)}</g>')
    top = cy - (sym + sym * 0.46 + tsize * 1.7) / 2
    parts.append(symbol_full(p, cx - sym / 2, top, s))
    wm, _, _ = wordmark(p, cx, top + sym + sym * 0.46 + tsize * 0.78, tsize, "mixed", "middle",
                        True, gap=tsize * 0.98)
    parts.append(wm)
    if sub:
        d, _ = text_path(sub, SEMI, tsize * 0.40,
                         cx, top + sym + sym * 0.46 + tsize * 0.78 + tsize * 0.98 + tsize * 1.15,
                         tsize * 0.40 * 0.14, "middle")
        parts.append(f'<path d="{d}" fill="{p["ink"]}" opacity="0.72"/>')
    return svg(w, h, "".join(parts))

def email_header(scheme, field=None, w=1200, h=280):
    """Wide, generous padding, horizontal lockup left aligned."""
    p = PAL[scheme]
    sym = h * 0.44
    s = sym / 64
    pad = h * 0.28
    tsize = sym * 0.52
    body = symbol_full(p, pad, (h - sym) / 2, s)
    wm, _, _ = wordmark(p, pad + sym + sym * 0.42, h / 2 - tsize * 0.06, tsize, "mixed",
                        "start", True, gap=tsize * 0.90)
    return svg(w, h, (f'<rect width="{w}" height="{h}" fill="{field}"/>' if field else "") + body + wm)

# ---------------------------------------------------------------- output helpers
INDEX = []
def out(rel):
    p = os.path.join(ROOT, rel)
    os.makedirs(os.path.dirname(p), exist_ok=True)
    return p

def write_svg(rel, content, note=""):
    p = out(rel)
    with open(p, "w") as f:
        f.write(content)
    INDEX.append({"file": rel, "type": "svg", "note": note})
    return p

def png(src_rel, dst_rel, width=None, height=None, note=""):
    src = os.path.join(ROOT, src_rel)
    dst = out(dst_rel)
    cmd = ["rsvg-convert", src, "-o", dst]
    if width:
        cmd += ["-w", str(width)]
    if height:
        cmd += ["-h", str(height)]
    subprocess.run(cmd, check=True)
    INDEX.append({"file": dst_rel, "type": "png", "note": note})
    return dst
