"""Phosphor Icons(SVG) → @fox/core/icons 컴포넌트 변환기.

입력: $FOX_ICONS_SRC/assets/{weight}/*.svg (phosphor-icons/core 체크아웃, 기본 ~/Downloads/phosphor-core)
출력: @fox/core/icons/glyphs/*.tsx (아이콘 1개당 1파일) + fox-icon.tsx + index.ts

실행: python3 @fox/tools/build-icons.py

왜 Figma가 아니라 Phosphor 원본에서 뽑나
- 시안(`ico/*` 1,512종 × Format 2 × Weight 6)은 Phosphor를 그대로 올린 것이다. `ico/House`
  Regular와 Phosphor `house.svg`는 문 너비 16.67%·문 span 25%·벽 두께 8.33%가 전부 같다.
  Figma를 1,512번 호출하는 대신 같은 결과를 원본에서 얻는다. 이름은 PascalCase ↔ kebab-case로
  1:1 대응한다.
- 시안의 Format=Stroke/Outline은 편집용 구분(획 유지 / 획을 면으로 확장)이라 렌더 결과가 같다.
  Phosphor가 배포하는 SVG가 Outline 쪽이고, 기존 @fox 아이콘도 이 면 데이터를 쓰고 있었다.

규칙
- viewBox는 Phosphor 원본의 `0 0 256 256`을 그대로 쓴다. 기존 8종은 Figma export라 16/24
  박스에 `translate`가 붙어 있었지만, 그건 256 뷰박스를 축소한 것과 같은 그림이다(House 기준
  bbox x=32 → 32/256×16=2 = 기존 translate x와 일치). 즉 이 변경은 픽셀 동일하다.
- path 개수를 가정하지 않는다 — fill 8종이 2~3개, duotone 2종(cell-signal-none·wifi-none)은
  보조 path가 없다.
- 크기도 색도 갖지 않는다. 감싼 쪽이 정한다(@fox 아이콘 규약).
"""

import os
import re
import shutil

SRC = os.environ.get("FOX_ICONS_SRC", os.path.expanduser("~/Downloads/phosphor-core"))
OUT = os.environ.get(
    "FOX_ICONS_OUT",
    os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "core", "icons"),
)

# 시안 `Weight=` 변형과 같은 순서. regular만 파일명에 접미사가 없다.
WEIGHTS = ["thin", "light", "regular", "bold", "fill", "duotone"]

PATH_RE = re.compile(r"<path\b([^>]*)>")
ATTR_RE = re.compile(r'([a-z-]+)="([^"]*)"')

# 예약어·기존 컴포넌트와 부딪히지 않게 접두사를 붙인다. `ico/ArrowSquareUp` → `FoxArrowSquareUpIcon`.
def component_name(kebab):
    return "Fox" + "".join(p[:1].upper() + p[1:] for p in kebab.split("-")) + "Icon"


def read_paths(weight, kebab):
    """한 SVG에서 (d, opacity) 목록을 순서대로 뽑는다."""
    fname = f"{kebab}.svg" if weight == "regular" else f"{kebab}-{weight}.svg"
    full = os.path.join(SRC, "assets", weight, fname)
    if not os.path.exists(full):
        raise SystemExit(f"원본 없음: {full}")

    out = []
    for raw in PATH_RE.findall(open(full, encoding="utf-8").read()):
        attrs = dict(ATTR_RE.findall(raw))
        d = attrs.get("d")
        if not d:
            raise SystemExit(f"d 속성 없는 path: {full}")
        out.append((d, attrs.get("opacity")))
    if not out:
        raise SystemExit(f"path가 없다: {full}")
    return out


def ts_paths(paths):
    """[["M...", 0.2], ["M..."]] 형태의 TS 리터럴."""
    items = []
    for d, opacity in paths:
        # d에는 따옴표가 들어가지 않는다(숫자·문자 명령뿐) — 검증은 아래 assert가 한다.
        assert '"' not in d and "\\" not in d, "d에 이스케이프가 필요한 문자가 있다"
        items.append(f'["{d}"]' if opacity is None else f'["{d}", {opacity}]')
    return "[" + ", ".join(items) + "]"


# ── 수집 ─────────────────────────────────────────────────────────────────────
regular_dir = os.path.join(SRC, "assets", "regular")
if not os.path.isdir(regular_dir):
    raise SystemExit(
        f"원본 디렉터리가 없다: {regular_dir}\n"
        "phosphor-icons/core 를 받아 FOX_ICONS_SRC로 지정하세요."
    )

names = sorted(f[:-4] for f in os.listdir(regular_dir) if f.endswith(".svg"))
if not names:
    raise SystemExit("regular 아이콘을 하나도 못 찾았다.")

glyph_dir = os.path.join(OUT, "glyphs")
# 이름이 사라진 아이콘의 잔재가 남지 않도록 통째로 다시 만든다.
if os.path.isdir(glyph_dir):
    shutil.rmtree(glyph_dir)
os.makedirs(glyph_dir)

GENERATED = "// 이 파일은 `python3 @fox/tools/build-icons.py`가 만든다. 손으로 고치지 않는다."

path_count = 0
for kebab in names:
    comp = component_name(kebab)
    rows = []
    for w in WEIGHTS:
        paths = read_paths(w, kebab)
        path_count += len(paths)
        rows.append(f"  {w}: {ts_paths(paths)},")

    open(os.path.join(glyph_dir, f"{kebab}.tsx"), "w", encoding="utf-8").write(
        f"""{GENERATED}
import {{ FoxIcon, type FoxGlyph, type FoxIconProps }} from "../fox-icon";

const GLYPH: FoxGlyph = {{
{chr(10).join(rows)}
}};

/** 시안 `ico/{comp[3:-4]}`. */
export function {comp}(props: FoxIconProps) {{
  return <FoxIcon {{...props}} glyph={{GLYPH}} />;
}}
"""
    )

# ── 공용 렌더러 ───────────────────────────────────────────────────────────────
open(os.path.join(OUT, "fox-icon.tsx"), "w", encoding="utf-8").write(
    f"""{GENERATED}
//
// 모든 아이콘이 공유하는 껍데기. 아이콘은 **크기도 색도 갖지 않는다** — 감싼 쪽이
// `inline-size`/`block-size`로 정하고 `viewBox`가 따라 늘어나며, 색은 `fill: currentColor`라
// 감싼 쪽의 `color`를 쓴다. 장식 요소라 `aria-hidden`이며, 접근성 이름은 소비처가 책임진다.

/** 시안의 `Weight=` 변형. */
export type FoxIconWeight =
  | "thin"
  | "light"
  | "regular"
  | "bold"
  | "fill"
  | "duotone";

/** `[d, opacity?]` — duotone의 배경 path만 opacity를 갖는다. */
export type FoxGlyphPath = readonly [d: string, opacity?: number];

export type FoxGlyph = Readonly<Record<FoxIconWeight, readonly FoxGlyphPath[]>>;

export interface FoxIconProps {{
  /** 기본 `regular`. 시안이 굵기를 지정한 자리에서만 바꾼다. */
  weight?: FoxIconWeight;
}}

interface FoxIconInternalProps extends FoxIconProps {{
  glyph: FoxGlyph;
}}

export function FoxIcon({{ weight = "regular", glyph }}: FoxIconInternalProps) {{
  return (
    <svg viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
      {{glyph[weight].map(([d, opacity], index) => (
        <path key={{index}} d={{d}} opacity={{opacity}} />
      ))}}
    </svg>
  );
}}
"""
)

# ── 배럴 ─────────────────────────────────────────────────────────────────────
exports = "\n".join(f'export * from "./glyphs/{k}";' for k in names)
open(os.path.join(OUT, "index.ts"), "w", encoding="utf-8").write(
    f"""{GENERATED}
//
// 아이콘 {len(names)}종. 배럴이 크지만 프로덕션 번들에는 실제 쓰는 글리프만 들어간다
// (검증: 안 쓰는 아이콘의 path 문자열이 프로덕션 `.js` 어디에도 없음). dev 빌드는
// 트리셰이킹을 하지 않아 청크가 커지지만 빌드 시간에 눈에 띄는 영향은 없었다.

export {{ FoxIcon }} from "./fox-icon";
export type {{
  FoxGlyph,
  FoxGlyphPath,
  FoxIconProps,
  FoxIconWeight,
}} from "./fox-icon";

{exports}
"""
)

# MIT는 사본에 저작권 고지를 함께 두도록 요구한다. 원본과 같이 갱신되게 생성기가 옮긴다.
license_src = os.path.join(SRC, "LICENSE")
if not os.path.exists(license_src):
    raise SystemExit(f"원본 라이선스가 없다: {license_src}")
shutil.copyfile(license_src, os.path.join(OUT, "PHOSPHOR-LICENSE.txt"))

print(f"아이콘 {len(names)}종 × weight {len(WEIGHTS)} = {len(names) * len(WEIGHTS)}개 변형")
print(f"path {path_count}개")
print(f"출력: {glyph_dir}/*.tsx · {OUT}/fox-icon.tsx · {OUT}/index.ts")
