#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Генератор KiCad-проекта seeed-clicker (формат KiCad 8, открывается в 8/9/10).

Проект содержит два устройства системы:
  * CLICKER  — носимый передатчик (XIAO ESP32C3 + RP2040-Zero + OLED/IMU/RTC/джойстик)
  * DONGLE   — USB-приемник для ПК (RP2040-Zero = USB HID + XIAO ESP32C3 = ESP-NOW)

Всё генерируется из одного источника данных (см. ниже: SYMBOLS, FOOTPRINTS,
CLICKER_CONN, DONGLE_CONN), чтобы схема, сетлист, библиотеки и PCB не расходились.

Запуск:  python3 tools/gen.py
"""

import json
import math
import uuid as UUID
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
NS = UUID.uuid5(UUID.NAMESPACE_URL, "https://github.com/local/seeed-clicker")


def uid(key: str) -> str:
    """Детерминированный UUID (стабильные пересборки)."""
    return str(UUID.uuid5(NS, key))


# ---------------------------------------------------------------------------
# Низкоуровневый вывод s-выражений (стиль KiCad 8: табуляция, отдельная строка)
# ---------------------------------------------------------------------------

class Out:
    def __init__(self):
        self.lines = []
        self.d = 0
        self.stack = []

    def raw(self, s: str):
        self.lines.append("\t" * self.d + s)

    def open(self, s: str):
        self.stack.append((s, len(self.lines)))
        self.raw("(" + s)
        self.d += 1

    def close(self):
        if not self.stack:
            raise RuntimeError("close() без open() в строке %d" % len(self.lines))
        self.stack.pop()
        self.d -= 1
        self.raw(")")

    def check(self, what=""):
        if self.stack:
            raise RuntimeError("незакрытые скобки %s: %s" % (what, self.stack[-5:]))

    def text(self) -> str:
        self.check()
        return "\n".join(self.lines) + "\n"


def esc(s: str) -> str:
    return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'


def num(v) -> str:
    if isinstance(v, float):
        if v == int(v):
            return str(int(v))
        return ("%.6f" % v).rstrip("0").rstrip(".")
    return str(v)


# ---------------------------------------------------------------------------
# СИМВОЛЫ (библиотека seeed-clicker: symbols)
# ---------------------------------------------------------------------------
# pin: (number, name, etype, px, py, angle, length)
#   угол 0   -> вывод с ЛЕВОЙ стороны («смотрит» вправо, к корпусу)
#   угол 180 -> с ПРАВОЙ, 90 -> сНИЗУ, 270 -> СВЕРХУ
#   (px, py) — точка ПОДКЛЮЧЕНИЯ (внешний конец), координаты символа (y вверх).

SYM_GND_GFX = [
    ("polyline", [(0, 0), (0, -1.905)], 0.254),
    ("polyline", [(-1.27, -1.905), (1.27, -1.905)], 0.254),
    ("polyline", [(-0.762, -2.54), (0.762, -2.54)], 0.254),
    ("polyline", [(-0.254, -3.175), (0.254, -3.175)], 0.254),
]
SYM_PWR_GFX = [
    ("polyline", [(0, 0), (0, 2.54)], 0.254),
    ("polyline", [(-1.27, 2.54), (1.27, 2.54)], 0.254),
]
SYM_FLAG_GFX = [
    ("polyline", [(0, 0), (0, 1.27)], 0.254),
    ("polyline", [(0, 1.27), (-1.016, 1.905), (0, 2.54), (1.016, 1.905), (0, 1.27)], 0.254),
]


def make_symbols():
    S = {}

    # --- Seeed Studio XIAO ESP32C3 ----------------------------------------
    # Левый ряд (сверху вниз): D0..D6; правый: 5V, GND, 3V3, D10..D7.
    left = [("1", "D0", 15.24), ("2", "D1", 11.43), ("3", "D2", 7.62),
            ("4", "D3", 3.81), ("5", "D4", 0.0), ("6", "D5", -3.81),
            ("7", "D6", -7.62)]
    right = [("14", "5V", 15.24), ("13", "GND", 11.43), ("12", "3V3", 7.62),
             ("11", "D10", 3.81), ("10", "D9", 0.0), ("9", "D8", -3.81),
             ("8", "D7", -7.62)]
    pins = []
    for n, name, y in left:
        pins.append((n, name, "bidirectional", -17.78, y, 0, 5.08))
    for n, name, y in right:
        etype = {"5V": "power_in", "GND": "power_in", "3V3": "power_out"}.get(name, "bidirectional")
        pins.append((n, name, etype, 17.78, y, 180, 5.08))
    pins.append(("15", "VBAT", "power_in", -2.54, -21.59, 90, 2.54))
    S["XIAO_ESP32C3"] = dict(
        ref="U", value="XIAO ESP32C3",
        fp="seeed-clicker:XIAO_ESP32C3_DIP",
        datasheet="https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/",
        descr="Seeed Studio XIAO ESP32C3, USB-C, LiPo-charger, ESP-NOW",
        keywords="XIAO ESP32C3 Seeed ESP-NOW",
        body=(-12.7, 17.78, 12.7, -19.05), pins=pins,
        pin_names_offset=1.27,
    )

    # --- Waveshare RP2040-Zero (нумерация как в dj505/RP2040-Zero-KiCAD) ---
    lp = [("1", "5V", 10.16, "power_in"), ("2", "GND", 6.35, "power_in"),
          ("3", "3V3", 2.54, "power_out"), ("4", "GP29", -1.27, "bidirectional"),
          ("5", "GP28", -5.08, "bidirectional"), ("6", "GP27", -8.89, "bidirectional"),
          ("7", "GP26", -12.7, "bidirectional"), ("8", "GP15", -16.51, "bidirectional"),
          ("9", "GP14", -20.32, "bidirectional")]
    rp = [("10", "GP0", 10.16), ("11", "GP1", 6.35), ("12", "GP2", 2.54),
          ("13", "GP3", -1.27), ("14", "GP4", -5.08), ("15", "GP5", -8.89),
          ("16", "GP6", -12.7), ("17", "GP7", -16.51), ("18", "GP8", -20.32)]
    bp = [("19", "GP13", -10.16), ("20", "GP12", -5.08), ("21", "GP11", 0.0),
          ("22", "GP10", 5.08), ("23", "GP9", 10.16)]
    pins = []
    for n, name, y, et in lp:
        pins.append((n, name, et, -15.24, y, 0, 5.08))
    for n, name, y in rp:
        pins.append((n, name, "bidirectional", 15.24, y, 180, 5.08))
    for n, name, x in bp:
        pins.append((n, name, "bidirectional", x, -25.4, 90, 2.54))
    S["RP2040_Zero"] = dict(
        ref="U", value="Waveshare RP2040-Zero",
        fp="seeed-clicker:RP2040_Zero_DIP",
        datasheet="https://www.waveshare.com/wiki/RP2040-Zero",
        descr="Waveshare RP2040-Zero (RP2040 + 2MB flash + USB-C, castellated)",
        keywords="RP2040 Waveshare Zero",
        body=(-10.16, 12.7, 10.16, -22.86), pins=pins,
        pin_names_offset=1.27,
    )

    # --- Модульные разъёмы периферии --------------------------------------
    def header_sym(name, pinlist, body, value, descr, keywords, ds="", fp=""):
        pins = [(str(i), pname, et, -12.7, y, 0, 5.08)
                for i, (pname, et, y) in enumerate(pinlist, start=1)]
        S[name] = dict(ref="J", value=value, fp=fp, datasheet=ds, descr=descr,
                       keywords=keywords, body=body, pins=pins,
                       pin_names_offset=0.254)

    header_sym(
        "OLED_SSD1306",
        [("GND", "power_in", 3.81), ("VDD", "power_in", 1.27),
         ("SCK", "bidirectional", -1.27), ("SDA", "bidirectional", -3.81)],
        (-6.35, 6.35, 6.35, -6.35),
        "OLED 0.96\" SSD1306 128x64",
        "Модуль OLED 0.96\" SSD1306, разъём 1x04: GND VDD SCK SDA",
        "OLED SSD1306 display I2C",
        fp="seeed-clicker:PinHeader_1x04_P2.54mm_Vertical")

    header_sym(
        "IMU_MPU9250",
        [("VCC", "power_in", 11.43), ("GND", "power_in", 8.89),
         ("SCL", "bidirectional", 6.35), ("SDA", "bidirectional", 3.81),
         ("EDA", "bidirectional", 1.27), ("ECL", "bidirectional", -1.27),
         ("ADO", "bidirectional", -3.81), ("INT", "bidirectional", -6.35),
         ("NCS", "bidirectional", -8.89), ("FSYNC", "bidirectional", -11.43)],
        (-7.62, 13.97, 7.62, -13.97),
        "IMU MPU-9250/MPU-6500",
        "Модуль IMU MPU-9250/6500, разъём 1x10 (VCC GND SCL SDA ...)",
        "MPU-9250 MPU-6500 IMU 9-axis sensor I2C",
        fp="seeed-clicker:PinHeader_1x10_P2.54mm_Vertical")

    header_sym(
        "RTC_DS1302",
        [("VCC", "power_in", 5.08), ("GND", "power_in", 2.54),
         ("CLK", "bidirectional", 0.0), ("DAT", "bidirectional", -2.54),
         ("RST", "bidirectional", -5.08)],
        (-6.35, 7.62, 6.35, -7.62),
        "RTC DS1302 (MH-RTC)",
        "Модуль часов DS1302, разъём 1x03..1x05 (VCC GND CLK DAT RST), 3-проводной интерфейс (НЕ I2C)",
        "DS1302 RTC clock module",
        fp="seeed-clicker:PinHeader_1x05_P2.54mm_Vertical")

    header_sym(
        "JOYSTICK_KY023",
        [("GND", "power_in", 5.08), ("VCC", "power_in", 2.54),
         ("VRX", "bidirectional", 0.0), ("VRY", "bidirectional", -2.54),
         ("SW", "bidirectional", -5.08)],
        (-6.35, 7.62, 6.35, -7.62),
        "Джойстик KY-023",
        "Модуль аналогового джойстика KY-023, разъём 1x05 (GND +5V VRX VRY SW)",
        "KY-023 joystick analog module",
        fp="seeed-clicker:PinHeader_1x05_P2.54mm_Vertical")

    # --- Разъём аккумулятора (JST-PH 2.0 1x02) -----------------------------
    S["BATTERY_JST_PH"] = dict(
        ref="J", value="LiPo 3.7V (JST-PH 2.0)", fp="seeed-clicker:JST_PH_S2B-PH-K-S_1x02_P2.00mm_Horizontal",
        datasheet="", descr="Разъём аккумулятора JST-PH 2.0mm 1x02 (BAT+ / BAT-)",
        keywords="battery LiPo JST PH",
        body=(-3.81, 3.81, 3.81, -3.81),
        pins=[("1", "BAT+", "power_out", -8.89, 1.27, 0, 5.08),
              ("2", "BAT-", "power_in", -8.89, -1.27, 0, 5.08)],
        pin_names_offset=0.254)

    # --- Пассивные ----------------------------------------------------------
    S["R"] = dict(
        ref="R", value="R", fp="seeed-clicker:R_0603_1608Metric", datasheet="",
        descr="Резистор 0603", keywords="R resistor",
        body=(-1.016, 2.54, 1.016, -2.54),
        pins=[("1", "~", "passive", 0, 3.81, 270, 1.27),
              ("2", "~", "passive", 0, -3.81, 90, 1.27)],
        pin_names_offset=0.254, pin_numbers_hide=True)

    S["C"] = dict(
        ref="C", value="C", fp="seeed-clicker:C_0603_1608Metric", datasheet="",
        descr="Конденсатор 0603", keywords="C capacitor",
        body=(-2.032, 0.508, 2.032, -0.508),
        pins=[("1", "~", "passive", 0, 3.81, 270, 2.54),
              ("2", "~", "passive", 0, -3.81, 90, 2.54)],
        pin_names_offset=0.254, pin_numbers_hide=True)

    S["JP_0R"] = dict(
        ref="JP", value="0R (перемычка)", fp="seeed-clicker:R_0603_1608Metric", datasheet="",
        descr="Паяльная перемычка/0 Ом 0603 — вскрывается при отдельной прошивке RP2040",
        keywords="jumper 0R",
        body=(-1.016, 1.27, 1.016, -1.27),
        pins=[("1", "~", "passive", -5.08, 0, 0, 3.81),
              ("2", "~", "passive", 5.08, 0, 180, 3.81)],
        pin_names_offset=0.254, pin_numbers_hide=True)

    # --- Символы питания ----------------------------------------------------
    S["+3V3"] = dict(ref="#PWR", value="+3V3", fp="", datasheet="", descr="Сила тока питания 3.3 В",
                     keywords="power", body=None, gfx=SYM_PWR_GFX, power=True,
                     pins=[("1", "+3V3", "power_in", 0, 0, 90, 0)],
                     pin_names_offset=0.0, value_at=(0, 3.302))
    S["+5V"] = dict(ref="#PWR", value="+5V", fp="", datasheet="", descr="Сила тока питания 5 В",
                    keywords="power", body=None, gfx=SYM_PWR_GFX, power=True,
                    pins=[("1", "+5V", "power_in", 0, 0, 90, 0)],
                    pin_names_offset=0.0, value_at=(0, 3.302))
    S["GND"] = dict(ref="#PWR", value="GND", fp="", datasheet="", descr="Земля",
                    keywords="power", body=None, gfx=SYM_GND_GFX, power=True,
                    pins=[("1", "GND", "power_in", 0, 0, 270, 0)],
                    pin_names_offset=0.0, value_at=(0, -4.572))
    S["PWR_FLAG"] = dict(ref="#FLG", value="PWR_FLAG", fp="", datasheet="",
                         descr="Флаг: сеть запитана", keywords="power flag", body=None,
                         gfx=SYM_FLAG_GFX, power=True,
                         pins=[("1", "pwr", "power_out", 0, 0, 90, 0)],
                         pin_names_offset=0.0, value_at=(0, 3.81))
    return S


SYMBOLS = make_symbols()


# ---------------------------------------------------------------------------
# FOOTPRINTS (библиотека seeed-clicker.pretty)
# ---------------------------------------------------------------------------
# pad: dict(num, kind='thru_hole'|'smd', shape, x, y, size=(w,h), drill=None,
#           rot=0, layers=..., rrat=None)

SILK = "F.SilkS"
FAB = "F.Fab"
CRTYD = "F.CrtYd"


def _pads_xiao_dip():
    """Официальная геометрия Seeed (XIAO-ESP32-C3-DIP), повернутая USB вверх."""
    pads = []
    xs = [-7.62, -5.08, -2.54, 0.0, 2.54, 5.08, 7.62]
    # левый ряд сверху вниз: 1..7 (D0..D6); правый: 14..8 (5V..D7)
    left_nums = ["1", "2", "3", "4", "5", "6", "7"]
    right_nums = ["14", "13", "12", "11", "10", "9", "8"]
    for i, x in enumerate(xs):
        y = -7.62 + i * 2.54
        for side, n in (("L", left_nums[i]), ("R", right_nums[i])):
            tx = -7.62 if side == "L" else 7.62
            pads.append(dict(num=n, kind="thru_hole", shape="circle", x=tx, y=y,
                             size=(1.524, 1.524), drill=0.889))
            sx = -8.455 if side == "L" else 8.455
            pads.append(dict(num=n, kind="smd", shape="roundrect", x=sx, y=y,
                             size=(2.432, 1.524), rrat=0.25))
    # Аккумуляторные площадки с обратной стороны XIAO (официальные Seeed):
    # VBAT и GND выведены сквозными отверстиями, чтобы припаять провода к
    # площадкам BAT+/BAT- на задней стороне модуля.
    pads.append(dict(num="15", kind="thru_hole", shape="circle", x=-4.445, y=-0.386,
                     size=(1.4, 1.4), drill=0.7))
    pads.append(dict(num="13", kind="thru_hole", shape="circle", x=-4.445, y=-2.291,
                     size=(1.4, 1.4), drill=0.7))
    return pads


def _pads_rp2040():
    """Геометрия dj505/RP2040-Zero-KiCAD, отцентрированная, USB вверх."""
    pads = []
    for i in range(9):  # левый ряд: 1..9 (5V, GND, 3V3, GP29..GP14)
        y = -10.16 + i * 2.54
        n = str(i + 1)
        shape = "rect" if i == 0 else "circle"
        pads.append(dict(num=n, kind="thru_hole", shape=shape, x=-7.62, y=y,
                         size=(1.508, 1.508), drill=1.0))
        pads.append(dict(num=n, kind="smd", shape="roundrect", x=-8.52, y=y,
                         size=(3.5, 1.7), rrat=0.25))
    for i in range(9):  # правый ряд: 10..18 (GP0..GP8)
        y = -10.16 + i * 2.54
        n = str(i + 10)
        pads.append(dict(num=n, kind="thru_hole", shape="circle", x=7.62, y=y,
                         size=(1.508, 1.508), drill=1.0))
        pads.append(dict(num=n, kind="smd", shape="roundrect", x=8.52, y=y,
                         size=(3.5, 1.7), rrat=0.25))
    for i in range(5):  # нижний ряд: 19..23 (GP13..GP9)
        x = -5.08 + i * 2.54
        n = str(i + 19)
        pads.append(dict(num=n, kind="thru_hole", shape="circle", x=x, y=10.16,
                         size=(1.508, 1.508), drill=1.0))
        pads.append(dict(num=n, kind="smd", shape="roundrect", x=x, y=11.06,
                         size=(1.7, 3.5), rrat=0.25))
    return pads


def _header_pads(n, pitch=2.54, drill=1.0, pad=1.7):
    pads = []
    total = (n - 1) * pitch
    for i in range(n):
        y = -total / 2 + i * pitch
        shape = "rect" if i == 0 else "circle"
        pads.append(dict(num=str(i + 1), kind="thru_hole", shape=shape, x=0.0, y=y,
                         size=(pad, pad), drill=drill))
    return pads


def make_footprints():
    F = {}

    # Seeed XIAO ESP32C3 — плюс корпус/шёлк/контур
    F["XIAO_ESP32C3_DIP"] = dict(
        descr="Seeed Studio XIAO ESP32C3: 2x7 пинов 2.54, межрядное 15.24 (0.6\"), THT + SMD (castellated), сквозные отверстия под BAT+/BAT-",
        tags="XIAO ESP32C3 Seeed", attr="through_hole",
        pads=_pads_xiao_dip(),
        texts=[("REF", 0.0, -12.2, SILK, 1.0), ("VALUE", 0.0, 12.2, FAB, 1.0)],
        gfx=[
            # корпус модуля 21 x 17.8 (USB вверх)
            ("fp_rect", (-10.5, -11.2, 10.5, 11.2), FAB, 0.1),
            ("fp_rect", (-10.5, -11.2, 10.5, 11.2), CRTYD, 0.05),
            # USB-C модуля (сверху, выступает за край)
            ("fp_rect", (-4.6, -13.3, 4.6, -11.2), FAB, 0.1),
            ("fp_line", (-4.6, -11.2, 4.6, -11.2), SILK, 0.12),
            # метка пина 1
            ("fp_line", (-10.0, -10.2, -9.0, -10.2), SILK, 0.12),
            ("fp_circle", (-9.5, -9.0), FAB, 0.1, 0.3),
        ])

    F["RP2040_Zero_DIP"] = dict(
        descr="Waveshare RP2040-Zero: 9+9+5 пинов, межрядное 15.24 (0.6\"), THT + SMD (castellated), USB-C сверху",
        tags="RP2040 Waveshare Zero", attr="through_hole",
        pads=_pads_rp2040(),
        texts=[("REF", 0.0, -13.5, SILK, 1.0), ("VALUE", 0.0, 13.5, FAB, 1.0)],
        gfx=[
            ("fp_rect", (-9.0, -11.6, 9.0, 11.6), FAB, 0.1),
            ("fp_rect", (-9.0, -11.6, 9.0, 11.6), CRTYD, 0.05),
            ("fp_rect", (-4.5, -12.9, 4.5, -11.6), FAB, 0.1),
            ("fp_line", (-4.5, -11.6, 4.5, -11.6), SILK, 0.12),
            ("fp_line", (-8.5, -10.66, -7.5, -10.66), SILK, 0.12),  # метка 1
        ])

    for n in (2, 4, 5, 10):
        total = (n - 1) * 2.54
        F["PinHeader_1x%02d_P2.54mm_Vertical" % n] = dict(
            descr="Штырьковый разъём 1x%02d, шаг 2.54 мм, вертикальный" % n,
            tags="pin header 2.54 THT", attr="through_hole",
            pads=_header_pads(n),
            texts=[("REF", -4.5, 0.0, SILK, 1.0), ("VALUE", 4.5, 0.0, FAB, 1.0)],
            gfx=[
                ("fp_rect", (-1.27, -total / 2 - 1.27, 1.27, total / 2 + 1.27), SILK, 0.12),
                ("fp_rect", (-1.27, -total / 2 - 1.27, 1.27, total / 2 + 1.27), FAB, 0.1),
                ("fp_rect", (-1.9, -total / 2 - 1.9, 1.9, total / 2 + 1.9), CRTYD, 0.05),
                ("fp_line", (-1.27, -total / 2, -0.6, -total / 2), SILK, 0.12),
            ])

    F["JST_PH_S2B-PH-K-S_1x02_P2.00mm_Horizontal"] = dict(
        descr="JST PH S2B-PH-K-S, разъём 1x02, шаг 2.00 мм, горизонтальный (аккумулятор LiPo)",
        tags="JST PH battery", attr="through_hole",
        pads=[
            dict(num="1", kind="thru_hole", shape="rect", x=-1.0, y=0.0, size=(1.6, 1.6), drill=0.75),
            dict(num="2", kind="thru_hole", shape="circle", x=1.0, y=0.0, size=(1.6, 1.6), drill=0.75),
        ],
        texts=[("REF", 0.0, 4.2, SILK, 0.8), ("VALUE", 0.0, -4.6, FAB, 0.8)],
        gfx=[
            ("fp_rect", (-4.0, 0.8, 4.0, 6.0), SILK, 0.12),
            ("fp_rect", (-4.0, 0.8, 4.0, 6.0), FAB, 0.1),
            ("fp_rect", (-4.6, 0.2, 4.6, 6.6), CRTYD, 0.05),
            ("fp_line", (-4.0, 0.8, -2.5, 2.3), SILK, 0.12),
        ])

    F["R_0603_1608Metric"] = dict(
        descr="Резистор SMD 0603 (1608 метрик), ручная пайка",
        tags="R resistor 0603", attr="smd",
        pads=[
            dict(num="1", kind="smd", shape="roundrect", x=-0.7875, y=0.0, size=(0.9, 0.95), rrat=0.25),
            dict(num="2", kind="smd", shape="roundrect", x=0.7875, y=0.0, size=(0.9, 0.95), rrat=0.25),
        ],
        texts=[("REF", 0.0, -1.3, SILK, 0.7), ("VALUE", 0.0, 1.3, FAB, 0.7)],
        gfx=[
            ("fp_line", (-0.157, -0.508, 0.157, -0.508), FAB, 0.1),
            ("fp_line", (-0.157, 0.508, 0.157, 0.508), FAB, 0.1),
            ("fp_rect", (-1.6, -0.73, 1.6, 0.73), CRTYD, 0.05),
        ])

    F["C_0603_1608Metric"] = dict(
        descr="Конденсатор SMD 0603 (1608 метрик), ручная пайка",
        tags="C capacitor 0603", attr="smd",
        pads=[
            dict(num="1", kind="smd", shape="roundrect", x=-0.7875, y=0.0, size=(0.9, 0.95), rrat=0.25),
            dict(num="2", kind="smd", shape="roundrect", x=0.7875, y=0.0, size=(0.9, 0.95), rrat=0.25),
        ],
        texts=[("REF", 0.0, -1.3, SILK, 0.7), ("VALUE", 0.0, 1.3, FAB, 0.7)],
        gfx=[
            ("fp_line", (-0.157, -0.508, 0.157, -0.508), FAB, 0.1),
            ("fp_line", (-0.157, 0.508, 0.157, 0.508), FAB, 0.1),
            ("fp_rect", (-1.6, -0.73, 1.6, 0.73), CRTYD, 0.05),
        ])
    return F


FOOTPRINTS = make_footprints()


# ---------------------------------------------------------------------------
# ОПИСАНИЕ ЛИСТОВ СХЕМЫ (компоненты + подключения)
# ---------------------------------------------------------------------------
# comp: dict(ref, sym, x, y, value=None, fp=None)
# conn: (ref, pin_number_or_name, net)  | ('NC', ref, pin)  | ('PWR', net)
#        ('WIRE', (x1,y1),(x2,y2))      | ('NOTE', x, y, text, size)

CLICKER_COMPS = [
    dict(ref="J1", sym="OLED_SSD1306", x=42, y=64),
    dict(ref="J2", sym="IMU_MPU9250", x=42, y=92),
    dict(ref="J3", sym="RTC_DS1302", x=42, y=132),
    dict(ref="J4", sym="JOYSTICK_KY023", x=42, y=162),
    dict(ref="U1", sym="XIAO_ESP32C3", x=118, y=95),
    dict(ref="U2", sym="RP2040_Zero", x=205, y=88),
    dict(ref="J5", sym="BATTERY_JST_PH", x=112, y=158),
    dict(ref="R1", sym="R", x=78, y=150, value="4.7k"),
    dict(ref="R2", sym="R", x=88, y=150, value="4.7k"),
    dict(ref="R3", sym="R", x=98, y=150, value="10k"),
    dict(ref="R4", sym="R", x=108, y=150, value="10k"),
    dict(ref="R5", sym="R", x=130, y=150, value="200k"),
    dict(ref="R6", sym="R", x=130, y=162, value="200k"),
    dict(ref="C1", sym="C", x=145, y=150, value="100nF"),
    dict(ref="C2", sym="C", x=155, y=150, value="10uF"),
    dict(ref="JP1", sym="JP_0R", x=175, y=150),
]

CLICKER_CONN = [
    # ---- U1 XIAO ESP32C3 --------------------------------------------------
    ("C", "U1", "D0", "VBAT_SENSE"),
    ("C", "U1", "D1", "JOY_X"),
    ("C", "U1", "D2", "JOY_Y"),
    ("C", "U1", "D3", "RTC_CLK"),
    ("C", "U1", "D4", "SDA"),
    ("C", "U1", "D5", "SCL"),
    ("C", "U1", "D6", "UART_TX"),
    ("C", "U1", "D7", "UART_RX"),
    ("C", "U1", "D8", "JOY_SW"),
    ("C", "U1", "D9", "RTC_DAT"),
    ("C", "U1", "D10", "RTC_RST"),
    ("P", "U1", "3V3", "+3V3"),
    ("P", "U1", "GND", "GND"),
    ("NC", "U1", "5V", None),
    ("C", "U1", "VBAT", "VBAT"),

    # ---- U2 RP2040-Zero (по UART крест-накрест с XIAO) --------------------
    ("NC", "U2", "5V", None),
    ("P", "U2", "GND", "GND"),
    ("C", "U2", "3V3", "RP2040_3V3"),
    ("C", "U2", "GP0", "UART_RX"),   # TX RP2040 -> RX XIAO
    ("C", "U2", "GP1", "UART_TX"),   # RX RP2040 <- TX XIAO
    ("NC", "U2", "GP2", None), ("NC", "U2", "GP3", None), ("NC", "U2", "GP4", None),
    ("NC", "U2", "GP5", None), ("NC", "U2", "GP6", None), ("NC", "U2", "GP7", None),
    ("NC", "U2", "GP8", None), ("NC", "U2", "GP9", None), ("NC", "U2", "GP10", None),
    ("NC", "U2", "GP11", None), ("NC", "U2", "GP12", None), ("NC", "U2", "GP13", None),
    ("NC", "U2", "GP14", None), ("NC", "U2", "GP15", None), ("NC", "U2", "GP26", None),
    ("NC", "U2", "GP27", None), ("NC", "U2", "GP28", None), ("NC", "U2", "GP29", None),

    # ---- J1 OLED SSD1306 (GND VDD SCK SDA) -------------------------------
    ("P", "J1", "GND", "GND"),
    ("P", "J1", "VDD", "+3V3"),
    ("C", "J1", "SCK", "SCL"),
    ("C", "J1", "SDA", "SDA"),

    # ---- J2 MPU-9250 (VCC GND SCL SDA ...) --------------------------------
    ("P", "J2", "VCC", "+3V3"),
    ("P", "J2", "GND", "GND"),
    ("C", "J2", "SCL", "SCL"),
    ("C", "J2", "SDA", "SDA"),
    ("NC", "J2", "EDA", None), ("NC", "J2", "ECL", None), ("NC", "J2", "ADO", None),
    ("NC", "J2", "INT", None), ("NC", "J2", "NCS", None), ("NC", "J2", "FSYNC", None),

    # ---- J3 DS1302 (VCC GND CLK DAT RST) — 3-проводной интерфейс! ---------
    ("P", "J3", "VCC", "+3V3"),
    ("P", "J3", "GND", "GND"),
    ("C", "J3", "CLK", "RTC_CLK"),
    ("C", "J3", "DAT", "RTC_DAT"),
    ("C", "J3", "RST", "RTC_RST"),

    # ---- J4 KY-023 (GND +5V VRX VRY SW) — питание от 3V3! -----------------
    ("P", "J4", "GND", "GND"),
    ("P", "J4", "VCC", "+3V3"),
    ("C", "J4", "VRX", "JOY_X"),
    ("C", "J4", "VRY", "JOY_Y"),
    ("C", "J4", "SW", "JOY_SW"),

    # ---- J5 аккумулятор ----------------------------------------------------
    ("C", "J5", "BAT+", "VBAT"),
    ("P", "J5", "BAT-", "GND"),

    # ---- обвязка -----------------------------------------------------------
    ("P", "R1", "1", "+3V3"), ("C", "R1", "2", "SDA"),      # pull-up I2C SDA
    ("P", "R2", "1", "+3V3"), ("C", "R2", "2", "SCL"),      # pull-up I2C SCL
    ("P", "R3", "1", "+3V3"), ("C", "R3", "2", "JOY_SW"),   # pull-up SW
    ("P", "R4", "1", "+3V3"), ("C", "R4", "2", "RTC_DAT"),  # pull-up DAT (и boot-strap GPIO9)
    ("C", "R5", "1", "VBAT"), ("C", "R5", "2", "VBAT_SENSE"),
    ("C", "R6", "1", "VBAT_SENSE"), ("P", "R6", "2", "GND"),
    ("P", "C1", "1", "+3V3"), ("P", "C1", "2", "GND"),
    ("P", "C2", "1", "+3V3"), ("P", "C2", "2", "GND"),
    ("P", "JP1", "1", "+3V3"), ("C", "JP1", "2", "RP2040_3V3"),

    # ---- флаг питания (GND запитан через разъёмы/контроллер) ---------------
    ("FLAG", "GND", None, None),
    # ---- вспомогательные проводники/заметки --------------------------------
    ("WIRE", (130.0, 153.81), (130.0, 158.19)),   # делитель VBAT: R5 -> R6
    ("LABEL_AT", (130.0, 156.0), "VBAT_SENSE"),
]

CLICKER_NOTES = [
    (16, 16, "КЛИКЕР (передатчик): ESP-NOW 2.4 ГГц + сопроцессор RP2040 (UART).", 2.0),
    (16, 20, "Питание: LiPo 3.7V -> BAT+/BAT- XIAO (встроенный зарядник) -> шина +3V3.", 1.5),
    (16, 24, "UART: D6(TX)->GP1(RX), D7(RX)<-GP0(TX). Перекрёстное соединение обязательно!", 1.5),
    (16, 28, "I2C (D4/D5): OLED SSD1306 + MPU-9250, подтягивающие R1/R2 = 4.7к к +3V3.", 1.5),
    (16, 32, "DS1302 — 3-проводной интерфейс (CLK/DAT/RST), НЕ подключать к I2C!", 1.5),
    (16, 36, "Джойстик KY-023: VRX/VRY на ADC1 (D1/D2), SW на D8 + подтягивающий R3.", 1.5),
    (16, 40, "ВАЖНО: пин 2 модуля KY-023 подписан «+5V», но питаем строго +3V3 (логика не 5V-толерантна).", 1.5),
    (16, 44, "D9 = GPIO9 (strapping BOOT): подтянут R4 к +3V3 — иначе возможен сбой загрузки.", 1.5),
    (16, 48, "JP1 (0R): питание RP2040 от шины +3V3. Выпаять перед прошивкой RP2040 через свой USB-C.", 1.5),
    (16, 52, "Делитель R5/R6 (200к/200к) с аккумулятора на D0 — контроль заряда батареи (см. wiki Seeed).", 1.5),
]

DONGLE_COMPS = [
    dict(ref="U4", sym="RP2040_Zero", x=190, y=80),
    dict(ref="U3", sym="XIAO_ESP32C3", x=118, y=88),
    dict(ref="C3", sym="C", x=150, y=140, value="100nF"),
    dict(ref="C4", sym="C", x=162, y=140, value="10uF"),
]

DONGLE_CONN = [
    # ---- U1 XIAO: принимает ESP-NOW, отдаёт кадры по UART ------------------
    ("NC", "U3", "D0", None), ("NC", "U3", "D1", None), ("NC", "U3", "D2", None),
    ("NC", "U3", "D3", None), ("NC", "U3", "D4", None), ("NC", "U3", "D5", None),
    ("C", "U3", "D6", "UART_TX"),
    ("C", "U3", "D7", "UART_RX"),
    ("NC", "U3", "D8", None), ("NC", "U3", "D9", None), ("NC", "U3", "D10", None),
    ("NC", "U3", "3V3", None),
    ("P", "U3", "GND", "GND"),
    ("C", "U3", "5V", "+5V"),
    ("NC", "U3", "VBAT", None),

    # ---- U2 RP2040-Zero: USB-HID (клавиатура/мышь) для ПК ------------------
    ("C", "U4", "5V", "+5V"),      # VBUS от USB-C модуля питает и XIAO
    ("P", "U4", "GND", "GND"),
    ("NC", "U4", "3V3", None),
    ("C", "U4", "GP0", "UART_RX"),  # TX RP2040 -> RX XIAO
    ("C", "U4", "GP1", "UART_TX"),  # RX RP2040 <- TX XIAO
    ("NC", "U4", "GP2", None), ("NC", "U4", "GP3", None), ("NC", "U4", "GP4", None),
    ("NC", "U4", "GP5", None), ("NC", "U4", "GP6", None), ("NC", "U4", "GP7", None),
    ("NC", "U4", "GP8", None), ("NC", "U4", "GP9", None), ("NC", "U4", "GP10", None),
    ("NC", "U4", "GP11", None), ("NC", "U4", "GP12", None), ("NC", "U4", "GP13", None),
    ("NC", "U4", "GP14", None), ("NC", "U4", "GP15", None), ("NC", "U4", "GP26", None),
    ("NC", "U4", "GP27", None), ("NC", "U4", "GP28", None), ("NC", "U4", "GP29", None),

    ("P", "C3", "1", "+5V"), ("P", "C3", "2", "GND"),
    ("P", "C4", "1", "+5V"), ("P", "C4", "2", "GND"),

    ("FLAG", "+5V", None, None),
    ("FLAG", "GND", None, None),
]

DONGLE_NOTES = [
    (16, 16, "USB-ДОНГЛ (приемник): RP2040-Zero эмулирует HID-клавиатуру/мышь, XIAO ловит ESP-NOW.", 2.0),
    (16, 20, "Питание: VBUS (USB-C RP2040) -> шина +5V -> 5V XIAO. Шины GND объединены жёстко.", 1.5),
    (16, 24, "UART крест-накрест: D6(TX)->GP1(RX), D7(RX)<-GP0(TX). Скорость на усмотрение прошивки (напр. 921600).", 1.5),
    (16, 28, "USB-C XIAO — только для прошивки/отладки; в рабочем режиме dongle питается через USB-C RP2040.", 1.5),
    (16, 32, "3V3 контроллеров НЕ объединяются (у каждого свой стабилизатор).", 1.5),
]

POWER_NETS = {"GND": "GND", "+3V3": "+3V3", "+5V": "+5V"}


# ---------------------------------------------------------------------------
# Раскладка листа: компоненты, подключения, питание
# ---------------------------------------------------------------------------

class Sheet:
    def __init__(self, name, title, uuid_key, comps, conn, notes, page):
        self.name = name            # имя листа (и префикс локальных сетей)
        self.title = title
        self.uuid = uid("sheet:" + uuid_key)
        self.comps = comps
        self.conn = conn
        self.notes = notes
        self.page = page
        self.symbols = []           # эмитируемые экземпляры
        self.wires = []             # ((x1,y1),(x2,y2))
        self.labels = []            # (text,x,y,rot,justify)
        self.ncs = []               # (x,y)
        self.texts = []             # (text,x,y,size)
        self.pwr_count = 0
        self.flg_count = 0
        self.netmap = []            # (ref, pin_number, netname) для netlist/PCB
        self.ref_by_ref = {}
        self._build()

    # --- служебное ---------------------------------------------------------
    def _pin(self, ref, pin_key):
        c = self.ref_by_ref[ref]
        sym = SYMBOLS[c["sym"]]
        for (num_, name, etype, px, py, ang, ln) in sym["pins"]:
            if pin_key in (num_, name):
                return c, sym, (num_, name, etype, px, py, ang, ln)
        raise KeyError("%s: пин %s не найден в символе %s" % (ref, pin_key, c["sym"]))

    def _abs(self, comp, px, py):
        return (comp["x"] + px, comp["y"] - py)

    def _stub(self, cx, cy, ang, length):
        d = {0: (-1, 0), 180: (1, 0), 90: (0, 1), 270: (0, -1)}[ang]
        ex, ey = cx + d[0] * length, cy + d[1] * length
        self.wires.append(((cx, cy), (ex, ey)))
        return ex, ey, d

    def _add_power(self, net, x, y, updown):
        """Вешает символ питания/флага в точку (x,y). updown: +1 вниз (GND), -1 вверх."""
        self.pwr_count += 1 if net != "PWR_FLAG" else 0
        if net == "PWR_FLAG":
            ref = "#FLG%02d" % self.flg_count
            self.flg_count += 1
            symname = "PWR_FLAG"
        else:
            ref = "#PWR%02d" % self.pwr_count
            symname = net
        comp = dict(ref=ref, sym=symname, x=x, y=y, value=symname)
        self.symbols.append(comp)
        self.ref_by_ref[ref] = comp
        # короткий провод от точки подключения к символу питания
        self.wires.append(((x, y), (x, y + updown * 5.08)))
        return comp

    def _add_label(self, text, x, y, ang):
        if ang == 0:      # штырь ушёл влево — текст влево
            self.labels.append((text, x, y, 180, "right bottom"))
        else:
            self.labels.append((text, x, y, 0, "left bottom"))

    # --- сборка ------------------------------------------------------------
    def _build(self):
        for c in self.comps:
            self.symbols.append(c)
            self.ref_by_ref[c["ref"]] = c

        flag_nets = set()
        for item in self.conn:
            kind = item[0]
            if kind == "WIRE":
                self.wires.append((item[1], item[2]))
                continue
            if kind == "LABEL_AT":
                self.labels.append((item[2], item[1][0], item[1][1], 0, "left bottom"))
                continue
            if kind == "NOTE":
                self.texts.append((item[3], item[1], item[2], item[4]))
                continue
            if kind == "FLAG":
                flag_nets.add(item[1])
                continue

            ref, pin_key, net = item[1], item[2], item[3]
            comp, sym, (num_, name, etype, px, py, ang, ln) = self._pin(ref, pin_key)
            cx, cy = self._abs(comp, px, py)

            if kind == "NC":
                self.ncs.append((cx, cy))
                continue
            if kind == "C":                       # локальная метка
                ex, ey, d = self._stub(cx, cy, ang, 3.81)
                self._add_label(net, ex, ey, ang)
                self.netmap.append((ref, num_, net))
            elif kind == "P":                     # символ питания
                # GND отводим длиннее (5.08), чтобы символы GND/3V3 соседних
                # пинов расходились по X и не накладывались друг на друга
                ex, ey, d = self._stub(cx, cy, ang, 5.08 if net == "GND" else 2.54)
                # GND вниз, источники питания вверх
                updown = 1 if net == "GND" else -1
                # доворачиваем: конечная точка строго вертикально от (ex,ey)
                self._add_power(net, ex, ey, updown)
                self.netmap.append((ref, num_, net))
            else:
                raise ValueError(kind)

        # флаги PWR_FLAG: подключаем отдельным стубом к символу питания той же сети
        for net in sorted(flag_nets):
            base_x, base_y = 250.0, 30.0 + 12.0 * (len(flag_nets) and
                                                   sorted(flag_nets).index(net))
            self._add_power(net, base_x, base_y, 1 if net == "GND" else -1)
            flg = self._add_power("PWR_FLAG", base_x + 12.7, base_y + (5.08 if net == "GND" else -5.08),
                                  1 if net == "GND" else -1)
            # соединяем флаг с сетью питания проводом через стуб
            self.wires.append(((base_x, base_y), (base_x, base_y + (5.08 if net == "GND" else -5.08))))
            self.wires.append(((base_x, base_y + (5.08 if net == "GND" else -5.08)),
                               (base_x + 12.7, base_y + (5.08 if net == "GND" else -5.08))))
            self.netmap.append((flg["ref"], "1", net))

        for t in self.notes:
            self.texts.append((t[2], t[0], t[1], t[3]))

    # --- проверки ----------------------------------------------------------
    def check(self):
        errs = []
        seen = {}
        for c in self.symbols:
            if c["ref"] in seen:
                errs.append("дублирующийся референс %s" % c["ref"])
            seen[c["ref"]] = c
        nets = {}
        for ref, pin, net in self.netmap:
            nets.setdefault(net, []).append((ref, pin))
        for net, pins in nets.items():
            if len(pins) < 2:
                errs.append("сеть '%s' содержит только %s" % (net, pins))
        # все ли пины расписаны (метка/питание/NC)
        for c in self.comps:
            sym = SYMBOLS[c["sym"]]
            mapped = {p for (r, p, _n) in self.netmap if r == c["ref"]}
            ncd = {1 for _ in []}
            nc_pins = set()
            for item in self.conn:
                if item[0] == "NC" and item[1] == c["ref"]:
                    nc_pins.add(item[2])
            for (num_, name, *_r) in sym["pins"]:
                if num_ not in mapped and name not in mapped and \
                        num_ not in nc_pins and name not in nc_pins:
                    errs.append("%s.%s(%s) не подключён и не помечен NC" % (c["ref"], num_, name))
        return errs


CLICKER = Sheet("CLICKER", "Кликер — передатчик", "clicker",
                CLICKER_COMPS, CLICKER_CONN, CLICKER_NOTES, "2")
DONGLE = Sheet("DONGLE", "USB-донгл — приемник HID", "dongle",
               DONGLE_COMPS, DONGLE_CONN, DONGLE_NOTES, "3")

ROOT_UUID = uid("sheet:root")


# ---------------------------------------------------------------------------
# Запись библиотек
# ---------------------------------------------------------------------------

def emit_symbol_lib(out: Out, name, s, prefix=""):
    symid = prefix + name
    out.open("symbol %s" % esc(symid))
    if s.get("power"):
        out.raw("(power)")
    if s.get("pin_numbers_hide"):
        out.open("pin_numbers")
        out.raw("(hide yes)")
        out.close()
    out.open("pin_names")
    out.raw("(offset %s)" % num(s.get("pin_names_offset", 0.254)))
    out.close()
    out.raw("(exclude_from_sim no)")
    out.raw("(in_bom yes)")
    out.raw("(on_board yes)")
    # свойства
    ref_at = (0, (s["body"][1] + 2.54) if s.get("body") else 6.35)
    props = [
        ("Reference", s["ref"], ref_at, False),
        ("Value", s["value"], (0, (s["body"][1] + 1.27) if s.get("body") else 4.572)
         if not s.get("value_at") else s["value_at"], False),
        ("Footprint", s.get("fp", ""), (0, 0), True),
        ("Datasheet", s.get("datasheet", ""), (0, 0), True),
        ("Description", s.get("descr", ""), (0, 0), True),
    ]
    for pname, pval, (px, py), hide in props:
        out.open("property %s %s" % (esc(pname), esc(pval)))
        out.raw("(at %s %s 0)" % (num(px), num(py)))
        out.open("effects")
        out.open("font")
        out.raw("(size 1.27 1.27)")
        out.close()
        if hide:
            out.raw("(hide yes)")
        out.close()
        out.close()
    # графика/пины
    short = name.replace(" ", "_")
    if s.get("body"):
        x1, y1, x2, y2 = s["body"]
        out.open("symbol %s_0_1" % esc(short))
        out.open("rectangle")
        out.raw("(start %s %s)" % (num(x1), num(y1)))
        out.raw("(end %s %s)" % (num(x2), num(y2)))
        out.open("stroke")
        out.raw("(width 0.254)")
        out.raw("(type default)")
        out.close()
        out.open("fill")
        out.raw("(type background)")
        out.close()
        out.close()
        out.close()
    if s.get("gfx"):
        out.open("symbol %s_0_1" % esc(short))
        for kind, pts, w in s["gfx"]:
            out.open("polyline")
            out.open("pts")
            for (px, py) in pts:
                out.raw("(xy %s %s)" % (num(px), num(py)))
            out.close()
            out.open("stroke")
            out.raw("(width %s)" % num(w))
            out.raw("(type default)")
            out.close()
            out.close()
        out.close()
    out.open("symbol %s_1_1" % esc(short))
    for (num_, name_, etype, px, py, ang, ln) in s["pins"]:
        out.open("pin %s line" % etype)
        out.raw("(at %s %s %d)" % (num(px), num(py), ang))
        out.raw("(length %s)" % num(ln))
        out.open("name %s" % esc(name_))
        out.open("effects")
        out.open("font")
        out.raw("(size 1.27 1.27)")
        out.close()
        out.close()
        out.close()
        out.open("number %s" % esc(num_))
        out.open("effects")
        out.open("font")
        out.raw("(size 1.27 1.27)")
        out.close()
        out.close()
        out.close()
        out.close()   # закрывает (pin ...)
    out.close()
    out.close()


def write_symbol_lib():
    out = Out()
    out.open("kicad_symbol_lib")
    out.raw("(version 20231120)")
    out.raw('(generator "seeed-clicker-gen")')
    out.raw('(generator_version "8.0")')
    for name in sorted(SYMBOLS):
        emit_symbol_lib(out, name, SYMBOLS[name])
    out.close()
    path = ROOT / "libs" / "seeed-clicker.kicad_sym"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(out.text(), encoding="utf-8")
    return path


def write_footprint(fpname, fp):
    out = Out()
    out.open("footprint %s" % esc(fpname))
    out.raw("(version 20240108)")
    out.raw('(generator "seeed-clicker-gen")')
    out.raw('(generator_version "8.0")')
    out.raw('(layer "F.Cu")')
    out.open("descr %s" % esc(fp["descr"]))
    out.close()
    out.open("tags %s" % esc(fp["tags"]))
    out.close()
    out.raw('(attr %s)' % fp["attr"])
    # тексты
    for kind, x, y, layer, size in fp["texts"]:
        if kind == "REF":
            out.open('property "Reference" "REF**"')
        else:
            out.open('property "Value" "%s"' % esc(fpname))
        out.raw("(at %s %s 0)" % (num(x), num(y)))
        out.raw('(layer "%s")' % layer)
        out.open("effects")
        out.open("font")
        out.raw("(size %s %s)" % (num(size), num(size)))
        out.raw("(thickness 0.15)")
        out.close()
        out.close()
        out.close()
    # графика
    for g in fp["gfx"]:
        kind = g[0]
        if kind in ("fp_rect",):
            x1, y1, x2, y2 = g[1]
            out.open("fp_rect")
            out.raw("(start %s %s)" % (num(x1), num(y1)))
            out.raw("(end %s %s)" % (num(x2), num(y2)))
            layer, w = g[2], g[3]
        elif kind == "fp_line":
            x1, y1, x2, y2 = g[1]
            out.open("fp_line")
            out.raw("(start %s %s)" % (num(x1), num(y1)))
            out.raw("(end %s %s)" % (num(x2), num(y2)))
            layer, w = g[2], g[3]
        elif kind == "fp_circle":
            cx, cy = g[1]
            layer, w, r = g[2], g[3], g[4]
            out.open("fp_circle")
            out.raw("(center %s %s)" % (num(cx), num(cy)))
            out.raw("(end %s %s)" % (num(cx + r), num(cy)))
        else:
            raise ValueError(kind)
        out.open("stroke")
        out.raw("(width %s)" % num(w))
        out.raw("(type solid)")
        out.close()
        out.raw('(layer "%s")' % layer)
        out.close()
    # pads
    for p in fp["pads"]:
        out.open("pad %s %s %s" % (esc(p["num"]), p["kind"], p["shape"]))
        out.raw("(at %s %s)" % (num(p["x"]), num(p["y"])))
        out.raw("(size %s %s)" % (num(p["size"][0]), num(p["size"][1])))
        if p["kind"] == "thru_hole":
            out.raw("(drill %s)" % num(p["drill"]))
            out.raw('(layers "*.Cu" "*.Mask")')
        else:
            out.raw('(layers "F.Cu" "F.Mask" "F.Paste")')
            if p.get("rrat"):
                out.raw("(roundrect_rratio %s)" % num(p["rrat"]))
        out.close()
    out.close()
    return out.text()


def write_footprint_lib():
    libdir = ROOT / "libs" / "seeed-clicker.pretty"
    libdir.mkdir(parents=True, exist_ok=True)
    for name, fp in FOOTPRINTS.items():
        (libdir / (name + ".kicad_mod")).write_text(write_footprint(name, fp), encoding="utf-8")
    return libdir


# ---------------------------------------------------------------------------
# Запись схем
# ---------------------------------------------------------------------------

def emit_lib_symbols(out: Out, used):
    out.open("lib_symbols")
    for name in used:
        emit_symbol_lib(out, name, SYMBOLS[name], prefix="seeed-clicker:")
    out.close()


def write_sheet(sheet: Sheet, used_syms, project="seeed-clicker", is_root=False):
    out = Out()
    out.open("kicad_sch")
    out.raw("(version 20231120)")
    out.raw('(generator "seeed-clicker-gen")')
    out.raw('(generator_version "8.0")')
    out.raw("(uuid %s)" % esc(sheet.uuid if not is_root else ROOT_UUID))
    out.raw('(paper "A4")')
    out.open("title_block")
    out.raw("(title %s)" % esc("seeed-clicker — " + sheet.title))
    out.raw('(company "seeed-clicker")')
    out.raw('(comment 1 "Сгенерировано tools/gen.py — править генератором, не руками")')
    out.close()
    emit_lib_symbols(out, used_syms)

    # провода
    for ((x1, y1), (x2, y2)) in sheet.wires:
        out.open("wire")
        out.open("pts")
        out.raw("(xy %s %s) (xy %s %s)" % (num(x1), num(y1), num(x2), num(y2)))
        out.close()
        out.open("stroke")
        out.raw("(width 0)")
        out.raw("(type default)")
        out.close()
        out.raw("(uuid %s)" % esc(uid("wire:%s:%s:%s" % (sheet.name, x1, y1) + str(x2) + str(y2))))
        out.close()
    # no-connect
    for (x, y) in sheet.ncs:
        out.open("no_connect")
        out.raw("(at %s %s)" % (num(x), num(y)))
        out.raw("(uuid %s)" % esc(uid("nc:%s:%s:%s" % (sheet.name, x, y))))
        out.close()
    # метки
    for (text, x, y, rot, just) in sheet.labels:
        out.open("label %s" % esc(text))
        out.raw("(at %s %s %d)" % (num(x), num(y), rot))
        out.open("effects")
        out.open("font")
        out.raw("(size 1.27 1.27)")
        out.close()
        out.raw("(justify %s)" % just)
        out.close()
        out.raw("(uuid %s)" % esc(uid("label:%s:%s:%s:%s" % (sheet.name, text, x, y))))
        out.close()
    # текстовые примечания
    for (text, x, y, size) in sheet.texts:
        out.open("text %s" % esc(text))
        out.raw("(at %s %s 0)" % (num(x), num(y)))
        out.open("effects")
        out.open("font")
        out.raw("(size %s %s)" % (num(size), num(size)))
        out.close()
        out.raw("(justify left bottom)")
        out.close()
        out.raw("(uuid %s)" % esc(uid("text:%s:%s:%s" % (sheet.name, x, y))))
        out.close()
    # экземпляры символов
    for c in sheet.symbols:
        sym = SYMBOLS[c["sym"]]
        lib_id = "seeed-clicker:" + c["sym"]
        out.open("symbol")
        out.raw("(lib_id %s)" % esc(lib_id))
        out.raw("(at %s %s 0)" % (num(c["x"]), num(c["y"])))
        out.raw("(unit 1)")
        out.raw("(exclude_from_sim no)")
        out.raw("(in_bom %s)" % ("no" if sym.get("power") else "yes"))
        out.raw("(on_board yes)")
        out.raw("(dnp no)")
        out.raw("(uuid %s)" % esc(uid("sym:%s:%s" % (sheet.name, c["ref"]))))
        # поля
        body = sym.get("body")
        ref_y = -((body[1] + 2.54) if body else -6.35)
        val_y = -((body[1] + 1.27) if body else -4.572)
        if sym.get("value_at"):
            ref_y = -(sym["value_at"][1] + 1.27)
            val_y = -sym["value_at"][1]
        is_power = bool(sym.get("power"))
        props = [
            ("Reference", c["ref"], (0, ref_y), is_power),
            # Value у символов питания показываем (как в родном KiCad),
            # Reference прячем
            ("Value", c.get("value", sym["value"]), (0, val_y), False),
            ("Footprint", c.get("fp", sym.get("fp", "")), (0, 0), True),
            ("Datasheet", sym.get("datasheet", ""), (0, 0), True),
            ("Description", sym.get("descr", ""), (0, 0), True),
        ]
        for pname, pval, (px, py), hide in props:
            out.open("property %s %s" % (esc(pname), esc(pval)))
            out.raw("(at %s %s 0)" % (num(c["x"] + px), num(c["y"] + py)))
            out.open("effects")
            out.open("font")
            out.raw("(size 1.27 1.27)")
            out.close()
            if hide:
                out.raw("(hide yes)")
            out.close()
            out.close()
        for (num_, *_r) in sym["pins"]:
            out.open("pin %s" % esc(num_))
            out.raw("(uuid %s)" % esc(uid("pin:%s:%s:%s" % (sheet.name, c["ref"], num_))))
            out.close()
        out.open("instances")
        out.open("project %s" % esc(project))
        if is_root:
            path = "/" + ROOT_UUID
        else:
            path = "/" + ROOT_UUID + "/" + sheet.uuid
        out.open("path %s" % esc(path))
        out.raw("(reference %s)" % esc(c["ref"]))
        out.raw("(unit 1)")
        out.close()
        out.close()
        out.close()
        out.close()   # закрывает (symbol ...)
    out.close()
    return out.text()


def write_root_schematic():
    out = Out()
    out.open("kicad_sch")
    out.raw("(version 20231120)")
    out.raw('(generator "seeed-clicker-gen")')
    out.raw('(generator_version "8.0")')
    out.raw("(uuid %s)" % esc(ROOT_UUID))
    out.raw('(paper "A2")')
    out.open("title_block")
    out.raw('(title "seeed-clicker — система «кликер + USB-донгл» (ESP-NOW)")')
    out.raw('(company "seeed-clicker")')
    out.raw('(comment 1 "Схема первой версии. Оба листа — отдельные платы, лежат в одном проекте.")')
    out.close()
    emit_lib_symbols(out, [])
    for sheet in (CLICKER, DONGLE):
        x = 25.4 if sheet.name == "CLICKER" else 152.4
        y = 60.96
        out.open("sheet")
        out.raw("(at %s %s)" % (num(x), num(y)))
        out.raw("(size 101.6 50.8)")
        out.open("stroke")
        out.raw("(width 0.1524)")
        out.raw("(type solid)")
        out.close()
        out.open("fill")
        out.raw("(color 0 0 0 0.0000)")
        out.close()
        out.raw("(uuid %s)" % esc(sheet.uuid))
        for pname, pval, py in (("Sheetname", sheet.title + "  [" + sheet.name + "]", -0.762),
                                ("Sheetfile", sheet.name.lower() + ".kicad_sch", 51.562)):
            out.open("property %s %s" % (esc(pname), esc(pval)))
            out.raw("(at %s %s 0)" % (num(x), num(y + py)))
            out.open("effects")
            out.open("font")
            out.raw("(size 1.524 1.524)")
            out.close()
            out.raw("(justify left bottom)")
            out.close()
            out.close()
        out.open("instances")
        out.open('project "seeed-clicker"')
        out.open("path %s" % esc("/" + ROOT_UUID))
        out.raw('(page "%s")' % sheet.page)
        out.close()
        out.close()
        out.close()
        out.close()   # закрывает (sheet ...)
    for t, x, y, size in [
        ("seeed-clicker — два устройства на ESP-NOW", 25.4, 30, 3.0),
        ("1) CLICKER  — носимый передатчик: XIAO ESP32C3 (ESP-NOW) + RP2040-Zero (расширение ввода/вывода, UART),", 25.4, 38, 1.8),
        ("   OLED SSD1306 (I2C), MPU-9250 (I2C), DS1302 (3-проводной), джойстик KY-023 (ADC), LiPo.", 25.4, 42, 1.8),
        ("2) DONGLE   — приемник для ПК: RP2040-Zero (USB HID: клавиатура/мышь) + XIAO ESP32C3 (ESP-NOW -> UART).", 25.4, 46, 1.8),
        ("Связь между платами: только радиоканел 2.4 ГГц (ESP-NOW). UART-перекрестие D6/D7 <-> GP1/GP0 обязательно.", 25.4, 52, 1.5),
    ]:
        out.open("text %s" % esc(t))
        out.raw("(at %s %s 0)" % (num(x), num(y)))
        out.open("effects")
        out.open("font")
        out.raw("(size %s %s)" % (num(size), num(size)))
        out.close()
        out.raw("(justify left bottom)")
        out.close()
        out.raw("(uuid %s)" % esc(uid("roottext:%s" % t)))
        out.close()
    out.open("sheet_instances")
    out.open('path "/"')
    out.raw('(page "1")')
    out.close()
    out.close()
    out.close()
    return out.text()


# ---------------------------------------------------------------------------
# Netlist и PCB
# ---------------------------------------------------------------------------

def build_nets():
    """Возвращает список (netname, [(sheet, ref, pin, pintype, pinname)])."""
    nets = {}
    for sheet in (CLICKER, DONGLE):
        comp_by_ref = sheet.ref_by_ref
        for ref, pin, net in sheet.netmap:
            comp = comp_by_ref[ref]
            sym = SYMBOLS[comp["sym"]]
            pname, ptype = pin, "passive"
            for (n_, nm_, et, *_r) in sym["pins"]:
                if n_ == pin:
                    pname, ptype = nm_, et
            if net in POWER_NETS:
                key = net                       # глобальные сети питания
            else:
                key = "/%s/%s" % (sheet.name.lower(), net)
            nets.setdefault(key, []).append((sheet.name, ref, pin, ptype, pname))
    return nets


def write_pcb():
    nets = build_nets()
    order = [""] + sorted(nets)
    net_id = {name: i for i, name in enumerate(order)}

    out = Out()
    out.open("kicad_pcb")
    out.raw("(version 20240108)")
    out.raw('(generator "seeed-clicker-gen")')
    out.raw('(generator_version "8.0")')
    out.open("general")
    out.raw("(thickness 1.6)")
    out.raw("(legacy_teardrops no)")
    out.close()
    out.raw('(paper "A4")')
    out.open("title_block")
    out.raw('(title "seeed-clicker — CLICKER + DONGLE (две платы в одном файле)")')
    out.raw('(company "seeed-clicker")')
    out.raw('(comment 1 "Начальная расстановка. Трассировка — следующий этап.")')
    out.close()
    out.open("layers")
    for n, nm, typ, *rest in [(0, "F.Cu", "signal"), (31, "B.Cu", "signal"),
                              (32, "B.Adhes", "user", "B.Adhesive"), (33, "F.Adhes", "user", "F.Adhesive"),
                              (34, "B.Paste", "user"), (35, "F.Paste", "user"),
                              (36, "B.SilkS", "user", "B.Silkscreen"), (37, "F.SilkS", "user", "F.Silkscreen"),
                              (38, "B.Mask", "user"), (39, "F.Mask", "user"),
                              (40, "Dwgs.User", "user", "User.Drawings"), (41, "Cmts.User", "user", "User.Comments"),
                              (42, "Eco1.User", "user", "User.Eco1"), (43, "Eco2.User", "user", "User.Eco2"),
                              (44, "Edge.Cuts", "user"), (45, "Margin", "user"),
                              (46, "B.CrtYd", "user", "B.Courtyard"), (47, "F.CrtYd", "user", "F.Courtyard"),
                              (48, "B.Fab", "user"), (49, "F.Fab", "user")]:
        if rest:
            out.raw('(%d %s %s %s)' % (n, esc(nm), typ, esc(rest[0])))
        else:
            out.raw('(%d %s %s)' % (n, esc(nm), typ))
    out.close()
    out.open("setup")
    out.raw("(pad_to_mask_clearance 0)")
    out.raw("(allow_soldermask_bridges_in_footprints no)")
    out.open("pcbplotparams")
    out.raw("(layerselection 0x00000000_00000000_55555555_5755f5ff)")
    out.raw("(plot_on_all_layers_selection 0x00000000_00000000_00000000_00000000)")
    out.raw("(disableapertmacros no)")
    out.raw("(usegerberextensions no)")
    out.raw("(usegerberattributes yes)")
    out.raw("(usegerberadvancedattributes yes)")
    out.raw("(creategerberjobfile yes)")
    out.raw("(dashed_line_dash_ratio 12.000000)")
    out.raw("(dashed_line_gap_ratio 3.000000)")
    out.raw("(svgprecision 4)")
    out.raw("(plotframeref no)")
    out.raw("(viasonmask no)")
    out.raw("(mode 1)")
    out.raw("(useauxorigin no)")
    out.raw("(hpglpennumber 1)")
    out.raw("(hpglpenspeed 20)")
    out.raw("(hpglpendiameter 15.000000)")
    out.raw("(pdf_front_fp_property_popups yes)")
    out.raw("(pdf_back_fp_property_popups yes)")
    out.raw("(dxfpolygonmode yes)")
    out.raw("(dxfimperialunits yes)")
    out.raw("(dxfusepcbnewfont yes)")
    out.raw("(psnegative no)")
    out.raw("(psa4output no)")
    out.raw("(plotreference yes)")
    out.raw("(plotvalue yes)")
    out.raw("(plotfptext yes)")
    out.raw("(plotinvisibletext no)")
    out.raw("(sketchpadsonfab no)")
    out.raw("(subtractmaskfromsilk no)")
    out.raw("(outputformat 1)")
    out.raw("(mirror no)")
    out.raw("(drillshape 1)")
    out.raw("(scaleselection 1)")
    out.raw('(outputdirectory "")')
    out.close()
    out.close()

    for name in order:
        out.raw("(net %d %s)" % (net_id[name], esc(name)))

    # ---- расстановка плат --------------------------------------------------
    # CLICKER: контур 0,0 .. 56,88 ; DONGLE: 66,0 .. 92,54
    placements = {
        "CLICKER": {
            "outline": (0.0, 0.0, 56.0, 88.0),
            "title": "CLICKER v1.0",
            "place": {
                "U1": (14.0, 16.0, 0),      # XIAO (USB вверх)
                "U2": (42.0, 16.0, 0),      # RP2040-Zero (USB вверх)
                # пассивные одним рядом (0603, шаг 4 мм)
                "R1": (10.0, 34.0, 0), "R2": (14.0, 34.0, 0),
                "R3": (18.0, 34.0, 0), "R4": (22.0, 34.0, 0),
                "R5": (26.0, 34.0, 0), "R6": (30.0, 34.0, 0),
                "C1": (34.0, 34.0, 0), "C2": (38.0, 34.0, 0),
                "JP1": (44.0, 34.0, 0),
                # разъёмы периферии — вертикально, в нижней половине
                "J2": (6.0, 58.0, 0),       # IMU 1x10 (самый длинный — у края)
                "J1": (12.0, 52.0, 0),      # OLED 1x04
                "J3": (18.0, 52.0, 0),      # RTC 1x05
                "J4": (24.0, 52.0, 0),      # JOY 1x05
                "J5": (40.0, 78.0, 0),      # аккумулятор (JST-PH)
            },
            "labels": [(12.0, 44.5, "OLED"), (6.0, 73.5, "MPU"), (18.0, 61.5, "RTC"),
                       (24.0, 61.5, "JOY"), (40.0, 75.5, "BAT")],
        },
        "DONGLE": {
            "outline": (66.0, 0.0, 92.0, 54.0),
            "title": "DONGLE v1.0",
            "place": {
                "U4": (79.0, 13.5, 0),      # RP2040-Zero: USB-C у верхнего края
                "U3": (79.0, 40.0, 180),    # XIAO: USB-C у нижнего края
                # конденсаторы слева от контроллерам (зазор 66..70 мм)
                "C3": (67.5, 14.0, 90), "C4": (67.5, 20.0, 90),
            },
            "labels": [(68.0, 4.0, "USB-C"), (68.0, 50.0, "USB-C")],
        },
    }

    sym_path = {}
    for sheet in (CLICKER, DONGLE):
        for c in sheet.symbols:
            sym_path[(sheet.name, c["ref"])] = ("/%s/%s" % (sheet.uuid, uid("sym:%s:%s" % (sheet.name, c["ref"]))))

    for sheet in (CLICKER, DONGLE):
        cfg = placements[sheet.name]
        x1, y1, x2, y2 = cfg["outline"]
        out.open("gr_rect")
        out.raw("(start %s %s)" % (num(x1), num(y1)))
        out.raw("(end %s %s)" % (num(x2), num(y2)))
        out.open("stroke")
        out.raw("(width 0.1)")
        out.raw("(type solid)")
        out.close()
        out.raw("(fill none)")
        out.raw('(layer "Edge.Cuts")')
        out.raw("(uuid %s)" % esc(uid("edge:%s" % sheet.name)))
        out.close()
        out.open("gr_text %s" % esc(cfg["title"]))
        out.raw("(at %s %s 0)" % (num((x1 + x2) / 2), num(y2 - 2.0)))
        out.raw('(layer "F.SilkS")')
        out.raw("(uuid %s)" % esc(uid("btitle:%s" % sheet.name)))
        out.open("effects")
        out.open("font")
        out.raw("(size 1.5 1.5)")
        out.raw("(thickness 0.3)")
        out.close()
        out.close()
        out.close()
        for (lx, ly, txt) in cfg["labels"]:
            out.open("gr_text %s" % esc(txt))
            out.raw("(at %s %s 0)" % (num(lx), num(ly)))
            out.raw('(layer "F.SilkS")')
            out.raw("(uuid %s)" % esc(uid("blabel:%s:%s" % (sheet.name, txt))))
            out.open("effects")
            out.open("font")
            out.raw("(size 1.0 1.0)")
            out.raw("(thickness 0.15)")
            out.close()
            out.close()
            out.close()

    # ---- футпринты ---------------------------------------------------------
    for sheet in (CLICKER, DONGLE):
        cfg = placements[sheet.name]
        comp_by_ref = sheet.ref_by_ref
        pin_net = {}
        for (ref, pin, net) in sheet.netmap:
            if net in POWER_NETS:
                pin_net[(ref, pin)] = net
            else:
                pin_net[(ref, pin)] = "/%s/%s" % (sheet.name.lower(), net)
        for c in sheet.comps:
            sym = SYMBOLS[c["sym"]]
            fpname = c.get("fp") or sym.get("fp")
            if not fpname:
                raise SystemExit("нет футпринта у %s (%s)" % (c["ref"], c["sym"]))
            lib, name = fpname.split(":", 1)
            fp = FOOTPRINTS[name]
            px, py, prot = cfg["place"][c["ref"]]
            out.open("footprint %s" % esc(fpname))
            out.raw('(layer "F.Cu")')
            out.raw("(uuid %s)" % esc(uid("fp:%s:%s" % (sheet.name, c["ref"]))))
            out.raw("(at %s %s%s)" % (num(px), num(py), (" " + str(int(prot))) if prot else ""))
            out.open("descr %s" % esc(fp["descr"]))
            out.close()
            out.open("tags %s" % esc(fp["tags"]))
            out.close()
            for pname, pval, pxy, layer, hide in [
                ("Reference", c["ref"], (0.0, -2.5), "F.SilkS", False),
                ("Value", c.get("value", sym["value"]), (0.0, 2.5), "F.Fab", False),
                ("Footprint", fpname, (0.0, 0.0), "F.Fab", True),
                ("Datasheet", sym.get("datasheet", ""), (0.0, 0.0), "F.Fab", True),
                ("Description", sym.get("descr", ""), (0.0, 0.0), "F.Fab", True),
            ]:
                out.open("property %s %s" % (esc(pname), esc(pval)))
                out.raw("(at %s %s 0)" % (num(pxy[0]), num(pxy[1])))
                out.raw('(layer "%s")' % layer)
                if hide:
                    out.raw("(hide yes)")
                out.open("effects")
                out.open("font")
                out.raw("(size 1 1)")
                out.raw("(thickness 0.15)")
                out.close()
                out.close()
                out.close()
            out.raw("(path %s)" % esc(sym_path[(sheet.name, c["ref"])]))
            out.raw("(sheetname %s)" % esc(sheet.name))
            out.raw("(sheetfile %s)" % esc(sheet.name.lower() + ".kicad_sch"))
            out.raw("(attr %s)" % fp["attr"])
            # KiCad 8 пишет на F.Fab тех.надпись ${REFERENCE}
            out.open("fp_text user %s" % esc("${REFERENCE}"))
            out.raw("(at 0 0 0)")
            out.raw('(layer "F.Fab")')
            out.raw("(uuid %s)" % esc(uid("fptxt:%s:%s" % (sheet.name, c["ref"]))))
            out.open("effects")
            out.open("font")
            out.raw("(size 1 1)")
            out.raw("(thickness 0.15)")
            out.close()
            out.close()
            out.close()
            ca = math.cos(math.radians(prot))
            sa = math.sin(math.radians(prot))

            def tf(gx, gy):
                # поворот футпринта (KiCad: положительный угол — против часовой)
                rx = gx * ca - gy * sa
                ry = gx * sa + gy * ca
                return (px + rx, py + ry)

            for g in fp["gfx"]:
                kind = g[0]
                if kind == "fp_rect":
                    ax, ay, bx, by = g[1]
                    (p1, p2) = tf(ax, ay), tf(bx, by)
                    out.open("fp_rect")
                    out.raw("(start %s %s)" % (num(min(p1[0], p2[0])), num(min(p1[1], p2[1]))))
                    out.raw("(end %s %s)" % (num(max(p1[0], p2[0])), num(max(p1[1], p2[1]))))
                    layer, w = g[2], g[3]
                elif kind == "fp_line":
                    p1, p2 = tf(g[1][0], g[1][1]), tf(g[1][2], g[1][3])
                    out.open("fp_line")
                    out.raw("(start %s %s)" % (num(p1[0]), num(p1[1])))
                    out.raw("(end %s %s)" % (num(p2[0]), num(p2[1])))
                    layer, w = g[2], g[3]
                elif kind == "fp_circle":
                    c0 = tf(g[1][0], g[1][1])
                    c1 = tf(g[1][0] + g[4], g[1][1])
                    out.open("fp_circle")
                    out.raw("(center %s %s)" % (num(c0[0]), num(c0[1])))
                    out.raw("(end %s %s)" % (num(c1[0]), num(c1[1])))
                    layer, w = g[2], g[3]
                out.open("stroke")
                out.raw("(width %s)" % num(w))
                out.raw("(type solid)")
                out.close()
                out.raw('(layer "%s")' % layer)
                out.raw("(uuid %s)" % esc(uid("g:%s:%s:%s" % (sheet.name, c["ref"], g))))
                out.close()
            for p in fp["pads"]:
                # поворот пада: для наших футпринтов важен только ориентация
                rx = p["x"] * ca - p["y"] * sa
                ry = p["x"] * sa + p["y"] * ca
                prot_total = (prot) % 360
                out.open("pad %s %s %s" % (esc(p["num"]), p["kind"], p["shape"]))
                out.raw("(at %s %s%s)" % (num(px + rx), num(py + ry),
                                          (" " + str(int(prot_total))) if prot_total else ""))
                out.raw("(size %s %s)" % (num(p["size"][0]), num(p["size"][1])))
                if p["kind"] == "thru_hole":
                    out.raw("(drill %s)" % num(p["drill"]))
                    out.raw('(layers "*.Cu" "*.Mask")')
                else:
                    out.raw('(layers "F.Cu" "F.Mask" "F.Paste")')
                    if p.get("rrat"):
                        out.raw("(roundrect_rratio %s)" % num(p["rrat"]))
                net = pin_net.get((c["ref"], p["num"]))
                if net:
                    out.raw("(net %d %s)" % (net_id[net], esc(net)))
                    pname, ptype = p["num"], "passive"
                    for (n_, nm_, et, *_rr) in sym["pins"]:
                        if n_ == p["num"]:
                            pname, ptype = nm_, et
                    out.raw("(pinfunction %s)" % esc(pname))
                    out.raw("(pintype %s)" % esc(ptype))
                out.raw("(uuid %s)" % esc(uid("pad:%s:%s:%s" % (sheet.name, c["ref"], p["num"] + str(p["x"]) + str(p["y"])))))
                out.close()
            out.close()
    out.close()
    path = ROOT / "seeed-clicker.kicad_pcb"
    path.write_text(out.text(), encoding="utf-8")
    return path, nets


# ---------------------------------------------------------------------------
# Прочие файлы проекта
# ---------------------------------------------------------------------------

def write_project():
    pro = {
        "board": {
            "3dviewports": [],
            "design_settings": {
                "defaults": {},
                "diff_pair_dimensions": [],
                "drc_exclusions": [],
                "rules": {},
                "track_widths": [0.0, 0.25, 0.4, 0.6],
                "via_dimensions": [{"diameter": 0.0, "drill": 0.0},
                                   {"diameter": 0.6, "drill": 0.3}],
            },
            "layer_presets": [],
            "viewports": [],
        },
        "boards": [],
        "cvpcb": {"equivalence_files": []},
        "erc": {"erc_exclusions": [], "meta": {"version": 0}, "pin_map": [], "rule_severities": {}},
        "libraries": {"pinned_footprint_libs": [], "pinned_symbol_libs": []},
        "meta": {"filename": "seeed-clicker.kicad_pro", "version": 1},
        "net_settings": {
            "classes": [{
                "bus_width": 12.0,
                "clearance": 0.2,
                "diff_pair_gap": 0.25,
                "diff_pair_via_gap": 0.25,
                "diff_pair_width": 0.2,
                "line_style": 0,
                "microvia_diameter": 0.3,
                "microvia_drill": 0.1,
                "name": "Default",
                "pcb_color": "rgba(0, 0, 0, 0.000)",
                "schematic_color": "rgba(0, 0, 0, 0.000)",
                "track_width": 0.25,
                "via_diameter": 0.6,
                "via_drill": 0.3,
                "wire_width": 6.0,
            }],
            "meta": {"version": 3},
            "net_colors": None,
            "netclass_assignments": None,
            "netclass_patterns": [],
        },
        "pcbnew": {"last_paths": {"gencad": "", "idf": "", "netlist": "", "plot": "",
                                  "pos_files": "", "specctra_dsn": "", "step": "",
                                  "svg": "", "vrml": ""},
                   "page_layout_descr_file": ""},
        "schematic": {
            "annotate_start_num": 0,
            "drawing": {"default_line_thickness": 6.0, "default_text_size": 50.0,
                        "field_names": [], "pin_symbol_size": 25.0, "text_offset_ratio": 0.3},
            "legacy_lib_dir": "",
            "legacy_lib_list": [],
            "meta": {"version": 1},
            "net_format_name": "",
            "page_layout_descr_file": "",
            "plot_directory": "",
            "spice_current_sheet_as_root": False,
            "spice_external_command": "\"spice\" \"%I\"",
            "spice_model_current_sheet_as_root": True,
            "spice_save_all_currents": False,
            "spice_save_all_dissipations": False,
            "spice_save_all_voltages": False,
            "subpart_first_id": 65,
            "subpart_id_separator": 0,
        },
        "sheets": [[ROOT_UUID, "Root"]] + [[s.uuid, s.title] for s in (CLICKER, DONGLE)],
        "text_variables": {},
    }
    path = ROOT / "seeed-clicker.kicad_pro"
    path.write_text(json.dumps(pro, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    return path


def write_lib_tables():
    (ROOT / "fp-lib-table").write_text(
        "(fp_lib_table\n  (version 7)\n"
        '  (lib (name "seeed-clicker")(type "KiCad")(uri "${KIPRJMOD}/libs/seeed-clicker.pretty")(options "")(descr "Посадочные места проекта seeed-clicker"))\n)\n',
        encoding="utf-8")
    (ROOT / "sym-lib-table").write_text(
        "(sym_lib_table\n  (version 7)\n"
        '  (lib (name "seeed-clicker")(type "KiCad")(uri "${KIPRJMOD}/libs/seeed-clicker.kicad_sym")(options "")(descr "Символы проекта seeed-clicker"))\n)\n',
        encoding="utf-8")


# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------

def main():
    errs = CLICKER.check() + DONGLE.check()
    if errs:
        print("ПРОВЕРКИ НЕ ПРОЙДЕНЫ:")
        for e in errs:
            print("  -", e)
        raise SystemExit(1)

    write_symbol_lib()
    write_footprint_lib()
    (ROOT / "clicker.kicad_sch").write_text(write_sheet(CLICKER, sorted(
        {c["sym"] for c in CLICKER.symbols})), encoding="utf-8")
    (ROOT / "dongle.kicad_sch").write_text(write_sheet(DONGLE, sorted(
        {c["sym"] for c in DONGLE.symbols})), encoding="utf-8")
    (ROOT / "seeed-clicker.kicad_sch").write_text(write_root_schematic(), encoding="utf-8")
    pcb, nets = write_pcb()
    write_project()
    write_lib_tables()

    print("Сгенерировано:")
    for p in sorted(ROOT.rglob("*")):
        if p.is_file() and "tools" not in p.parts and p.suffix in (
                ".kicad_sch", ".kicad_pcb", ".kicad_pro", ".kicad_sym", ".kicad_mod") \
                or p.name in ("fp-lib-table", "sym-lib-table"):
            print("  ", p.relative_to(ROOT))
    print("\nСетлист (%d сетей):" % len(nets))
    for name in sorted(nets):
        pins = ", ".join("%s.%s(%s)" % (s, r, p) for (s, r, p, *_x) in nets[name])
        print("  %-28s %s" % (name, pins))


if __name__ == "__main__":
    main()
