#!/usr/bin/env python3
"""Slice the Figma `assets/icons.svg` export into per-glyph SVGs.

The source layout is a single canvas where every icon lives in a 71x39 dashed
purple cell (`<rect stroke="#9747FF">`). Each cell holds two glyphs side by
side: Line on the left, Filled on the right. Sections are font-size 20
headings ("System", "Energy", "Safety", ...) and cell labels are font-size 12
text just below each cell.

Output:
    assets/icons/line/<section>/<icon>.svg
    assets/icons/filled/<section>/<icon>.svg

Each output keeps the original absolute path data wrapped in a translating
<g> so a normalised viewBox covers just the glyph + a small padding.
"""

import re
import shutil
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

ET.register_namespace("", "http://www.w3.org/2000/svg")
NS = "{http://www.w3.org/2000/svg}"
PATH_NUM = re.compile(r"-?\d+\.?\d*(?:[eE][+-]?\d+)?")
PATH_TOKEN = re.compile(r"[A-Za-z]|-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?")

SECTIONS_TO_SKIP = {"icono-base", "tamanos", "tamaos"}

# Restrict extraction to a subset for the proof run. Empty = all sections.
ONLY_SECTIONS = set()


def slugify(s):
	s = s.lower().replace(" ", " ").replace("\n", " ").replace("/", " ")
	s = s.replace("á", "a").replace("é", "e").replace("í", "i").replace("ó", "o").replace("ú", "u").replace("ñ", "n")
	s = re.sub(r"[^a-z0-9]+", "-", s).strip("-")
	return s


def path_bbox(d):
	"""Compute bbox by walking SVG path commands. Handles M/L/H/V/C/S/Q/T/A
	and Z, both absolute and relative, as Figma symbolic exports use V/H
	freely and our naive odd/even pairing breaks on them."""
	tokens = PATH_TOKEN.findall(d)
	xs = []
	ys = []
	cx = cy = 0.0
	sx = sy = 0.0
	cmd = None
	i = 0
	def take(n):
		nonlocal i
		out = tokens[i : i + n]
		i += n
		return [float(t) for t in out]
	while i < len(tokens):
		tok = tokens[i]
		if tok.isalpha():
			cmd = tok
			i += 1
			if cmd in ("Z", "z"):
				cx, cy = sx, sy
				continue
		if cmd is None:
			i += 1
			continue
		upper = cmd.upper()
		rel = cmd.islower()
		if upper == "M":
			x, y = take(2)
			if rel:
				x += cx
				y += cy
			cx = sx = x
			cy = sy = y
			xs.append(x)
			ys.append(y)
			cmd = "l" if rel else "L"
		elif upper == "L" or upper == "T":
			x, y = take(2)
			if rel:
				x += cx
				y += cy
			cx, cy = x, y
			xs.append(x)
			ys.append(y)
		elif upper == "H":
			(x,) = take(1)
			if rel:
				x += cx
			cx = x
			xs.append(x)
			ys.append(cy)
		elif upper == "V":
			(y,) = take(1)
			if rel:
				y += cy
			cy = y
			xs.append(cx)
			ys.append(y)
		elif upper == "C":
			x1, y1, x2, y2, x, y = take(6)
			if rel:
				x1 += cx; y1 += cy
				x2 += cx; y2 += cy
				x += cx;  y += cy
			xs.extend([x1, x2, x])
			ys.extend([y1, y2, y])
			cx, cy = x, y
		elif upper == "S" or upper == "Q":
			x2, y2, x, y = take(4)
			if rel:
				x2 += cx; y2 += cy
				x += cx;  y += cy
			xs.extend([x2, x])
			ys.extend([y2, y])
			cx, cy = x, y
		elif upper == "A":
			rx, ry, rot, large, sweep, x, y = take(7)
			if rel:
				x += cx
				y += cy
			cx, cy = x, y
			xs.append(x)
			ys.append(y)
		else:
			i += 1
	if not xs:
		return None
	return (min(xs), min(ys), max(xs), max(ys))


def text_at(t):
	first = t.find(f"{NS}tspan")
	if first is None:
		return None
	try:
		x = float(first.get("x", 0))
		y = float(first.get("y", 0))
	except ValueError:
		return None
	parts = []
	for ts in t.iter(f"{NS}tspan"):
		if ts.text:
			parts.append(ts.text.strip())
	return (x, y, " ".join(parts))


def collect_paths_with_clip(root):
	"""Return [(path_elem, innermost_clip_id_or_None)]."""
	out = []
	def walk(node, clip):
		for child in node:
			if child.tag == f"{NS}defs":
				continue
			if child.tag == f"{NS}g":
				cp = child.get("clip-path")
				new_clip = clip
				if cp:
					m = re.match(r"url\(#([^)]+)\)", cp)
					if m:
						new_clip = m.group(1)
				walk(child, new_clip)
			elif child.tag == f"{NS}path":
				out.append((child, clip))
	walk(root, None)
	return out


def main():
	src = Path("assets/icons.svg")
	out_root = Path("assets/icons")
	if not src.exists():
		print(f"ERROR: {src} not found (run from repo root)", file=sys.stderr)
		sys.exit(1)

	tree = ET.parse(src)
	root = tree.getroot()

	# Sections (font-size 20, in document order by y).
	sections_raw = []
	for t in root.iter(f"{NS}text"):
		if t.get("font-size") != "20":
			continue
		info = text_at(t)
		if info is None:
			continue
		name = slugify(info[2])
		if name in SECTIONS_TO_SKIP:
			continue
		sections_raw.append((info[0], info[1], name))
	sections_raw.sort(key=lambda i: i[1])

	def section_for(y):
		cur = None
		for sx, sy, name in sections_raw:
			# Section heading is above its cells; allow tiny tolerance.
			if sy <= y + 5:
				cur = name
			else:
				break
		return cur

	# Cells: dashed purple rects.
	cells = []
	for r in root.iter(f"{NS}rect"):
		if r.get("stroke") != "#9747FF":
			continue
		try:
			cx = float(r.get("x", 0))
			cy = float(r.get("y", 0))
			cw = float(r.get("width", 0))
			ch = float(r.get("height", 0))
		except ValueError:
			continue
		section = section_for(cy)
		if section is None:
			continue
		if ONLY_SECTIONS and section not in ONLY_SECTIONS:
			continue
		cells.append({"x": cx, "y": cy, "w": cw, "h": ch, "section": section})

	# Cell labels (font-size 12).
	labels_raw = []
	for t in root.iter(f"{NS}text"):
		if t.get("font-size") != "12":
			continue
		info = text_at(t)
		if info is None:
			continue
		labels_raw.append(info)

	def label_for_cell(cell):
		# Prefer a label whose x ≈ cell.x and y just below cell bottom.
		bottom = cell["y"] + cell["h"]
		best = None
		best_dy = 1e9
		for lx, ly, txt in labels_raw:
			if abs(lx - cell["x"]) > 6:
				continue
			dy = ly - bottom
			if 0 < dy < 80 and dy < best_dy:
				best_dy = dy
				best = txt
		return best

	# Paths.
	raw_paths = collect_paths_with_clip(root)

	# Reset target subtree we own.
	if ONLY_SECTIONS:
		for variant in ("line", "filled"):
			for sec in ONLY_SECTIONS:
				d = out_root / variant / sec
				if d.exists():
					shutil.rmtree(d)
	else:
		for variant in ("line", "filled"):
			d = out_root / variant
			if d.exists():
				shutil.rmtree(d)

	seen = {}
	written = 0
	skipped_no_label = 0
	skipped_no_paths = 0

	for cell in cells:
		label = label_for_cell(cell)
		if not label:
			skipped_no_label += 1
			continue

		# Pad cell window slightly so glyphs that bleed a touch over the
		# dashed border still get captured.
		cx_min = cell["x"] - 8
		cx_max = cell["x"] + cell["w"] + 8
		cy_min = cell["y"] - 8
		cy_max = cell["y"] + cell["h"] + 8

		cell_paths = []
		for p, clip in raw_paths:
			d_attr = p.get("d", "")
			bb = path_bbox(d_attr)
			if bb is None:
				continue
			mx = (bb[0] + bb[2]) / 2
			my = (bb[1] + bb[3]) / 2
			if cx_min <= mx <= cx_max and cy_min <= my <= cy_max:
				cell_paths.append((p, bb, clip))

		if not cell_paths:
			skipped_no_paths += 1
			continue

		midx = cell["x"] + cell["w"] / 2
		line_paths = [t for t in cell_paths if (t[1][0] + t[1][2]) / 2 < midx]
		filled_paths = [t for t in cell_paths if (t[1][0] + t[1][2]) / 2 >= midx]

		for variant, group in (("line", line_paths), ("filled", filled_paths)):
			if not group:
				continue
			x0 = min(g[1][0] for g in group)
			y0 = min(g[1][1] for g in group)
			x1 = max(g[1][2] for g in group)
			y1 = max(g[1][3] for g in group)
			pad = 0.5
			vbx = x0 - pad
			vby = y0 - pad
			vbw = (x1 - x0) + 2 * pad
			vbh = (y1 - y0) + 2 * pad

			slug = slugify(label)
			key = (cell["section"], variant, slug)
			n = seen.get(key, 0) + 1
			seen[key] = n
			final_slug = slug if n == 1 else f"{slug}-{n}"

			out_dir = out_root / variant / cell["section"]
			out_dir.mkdir(parents=True, exist_ok=True)
			out_path = out_dir / f"{final_slug}.svg"

			body = [
				f'<svg xmlns="http://www.w3.org/2000/svg" '
				f'viewBox="0 0 {vbw:.3f} {vbh:.3f}" fill="none">',
				f'<g transform="translate({-vbx:.3f} {-vby:.3f})" fill="#000000">',
			]
			for p, bb, clip in group:
				d_attr = p.get("d", "")
				attrs = ""
				fr = p.get("fill-rule")
				cr = p.get("clip-rule")
				if fr:
					attrs += f' fill-rule="{fr}"'
				if cr:
					attrs += f' clip-rule="{cr}"'
				body.append(f'<path{attrs} d="{d_attr}"/>')
			body.append("</g>")
			body.append("</svg>")
			out_path.write_text("\n".join(body) + "\n")
			written += 1

	print(f"Wrote {written} icon files")
	print(f"Skipped: {skipped_no_label} no-label, {skipped_no_paths} no-paths")


if __name__ == "__main__":
	main()
