#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Рендер схем .kicad_sch в PNG (без kicad-cli — рисуем сами через PIL).

Что рисуем: рамка листа, провода, символы (корпус/пины/имена/номера),
Reference/Value, локальные метки, текстовые примечания, NC-крестики,
иерархические листы (корневой схема).

Запуск:  python3 tools/render_sch.py [лист ...]
Выход:   docs/*.png
"""

import math
import sys
from pathlib import Path

import sexpdata
from PIL import Image, ImageDraw, ImageFont

ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "docs"

# масштаб: px на 1 мм (суперсэмплинг x2, затем даунскейл)
SCALE = 4
SS = 2

FONT_REG = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"

C_WIRE = (0, 0, 0)
C_BODY_FILL = (255, 255, 220)
C_BODY_LINE = (60, 60, 60)
C_PIN = (0, 0, 0)
C_FIELD = (0, 70, 160)
C_LABEL = (150, 0, 0)
C_TEXT = (0, 0, 0)
C_NC = (0, 0, 255)
C_FRAME = (120, 120, 120)
C_SHEET = (90, 90, 90)


# --------------------------------------------------------------------------
def head(n):
    if isinstance(n, list) and n:
        h = n[0]
        return h.value() if hasattr(h, "value") else h
    return None


def kids(n, key):
    return [c for c in n if isinstance(c, list) and head(c) == key]


def kid(n, key):
    k = kids(n, key)
    return k[0] if k else None


def load(path):
    return sexpdata.loads(Path(path).read_text(encoding="utf-8"))


# --------------------------------------------------------------------------
class FontCache:
    def __init__(self):
        self._c = {}

    def get(self, px, bold=False):
        px = max(6, int(px))
        key = (px, bold)
        if key not in self._c:
            self._c[key] = ImageFont.truetype(FONT_BOLD if bold else FONT_REG, px)
        return self._c[key]


FONTS = FontCache()


class Sheet:
    """Один лист схемы: собирает объекты и рисует."""

    def __init__(self, path):
        self.path = Path(path)
        self.tree = load(self.path)
        self.libs = self._parse_lib_symbols()
        self.wires = []
        self.labels = []      # (text, x, y, rot, justify, size)
        self.texts = []       # (text, x, y, size)
        self.ncs = []
        self.syms = []        # dict(lib, x, y, rot, props, pins_visible...)
        self.sheets = []      # иерархические листы (корень)
        self.paper = (297.0, 210.0)   # A4 landscape по умолчанию
        self._collect()

    # --- разбор -----------------------------------------------------------
    def _parse_lib_symbols(self):
        libs = {}
        ls = kid(self.tree, "lib_symbols")
        if not ls:
            return libs
        for s in ls[1:]:
            if not (isinstance(s, list) and head(s) == "symbol"):
                continue
            name = str(s[1])
            rects, polys, pins = [], [], []
            hide_nums = kid(s, "pin_numbers") is not None and \
                (kid(kid(s, "pin_numbers"), "hide") is not None)
            hide_names = kid(s, "pin_names") is not None and \
                (kid(kid(s, "pin_names"), "hide") is not None)
            for sub in s:
                if not (isinstance(sub, list) and head(sub) == "symbol"):
                    continue
                for leaf in sub:
                    if not isinstance(leaf, list):
                        continue
                    h = head(leaf)
                    if h == "rectangle":
                        st, en = kid(leaf, "start"), kid(leaf, "end")
                        fill = kid(leaf, "fill")
                        filled = fill is not None and str(kid(fill, "type")[1]) != "none"
                        rects.append((float(st[1]), float(st[2]),
                                      float(en[1]), float(en[2]), filled))
                    elif h == "polyline":
                        p = kid(leaf, "pts")
                        if p:
                            xy = [(float(q[1]), float(q[2])) for q in p
                                  if isinstance(q, list) and head(q) == "xy"]
                            if len(xy) >= 2:
                                polys.append(xy)
                    elif h == "pin":
                        at = kid(leaf, "at")
                        ln = kid(leaf, "length")
                        pname = kid(leaf, "name")
                        pnum = kid(leaf, "number")
                        ptype = str(leaf[1])
                        pins.append(dict(
                            x=float(at[1]), y=float(at[2]),
                            ang=float(at[3]) if len(at) > 3 else 0.0,
                            len=float(ln[1]),
                            name=str(pname[1]) if pname else "",
                            num=str(pnum[1]) if pnum else "",
                            type=ptype))
            libs[name] = dict(rects=rects, polys=polys, pins=pins,
                              hide_nums=hide_nums, hide_names=hide_names,
                              power=kid(s, "power") is not None)
        return libs

    def _collect(self):
        paper = kid(self.tree, "paper")
        if paper:
            try:
                w, h = str(paper[1]).split("x")   # напр. "297x210"
                self.paper = (float(w), float(h))
            except Exception:
                pass
        for n in self.tree:
            if not isinstance(n, list):
                continue
            h = head(n)
            if h == "wire":
                p = kid(n, "pts")
                if p:
                    xy = [(float(q[1]), float(q[2])) for q in p
                          if isinstance(q, list) and head(q) == "xy"]
                    if len(xy) == 2:
                        self.wires.append(tuple(xy))
            elif h == "label":
                at = kid(n, "at")
                size = self._size(n, 1.27)
                just = self._justify(n)
                self.labels.append((str(n[1]), float(at[1]), float(at[2]),
                                    float(at[3]) if len(at) > 3 else 0.0,
                                    just, size))
            elif h == "text":
                at = kid(n, "at")
                size = self._size(n, 1.27)
                self.texts.append((str(n[1]), float(at[1]), float(at[2]), size))
            elif h == "no_connect":
                at = kid(n, "at")
                self.ncs.append((float(at[1]), float(at[2])))
            elif h == "symbol" and kid(n, "lib_id"):
                self.syms.append(self._parse_symbol(n))
            elif h == "sheet":
                self.sheets.append(self._parse_sheet(n))

    @staticmethod
    def _size(n, default):
        eff = kid(n, "effects")
        if eff:
            f = kid(eff, "font")
            if f:
                s = kid(f, "size")
                if s and len(s) >= 2:
                    return float(s[1])
        return default

    @staticmethod
    def _justify(n):
        eff = kid(n, "effects")
        if eff:
            j = kid(eff, "justify")
            if j:
                return " ".join(str(x) for x in j[1:])
        return "left bottom"

    @staticmethod
    def _hidden(n):
        # KiCad 8: (hide yes) может быть прямо в узле или внутри (effects ...)
        if kid(n, "hide") is not None:
            return True
        eff = kid(n, "effects")
        return eff is not None and kid(eff, "hide") is not None

    def _parse_symbol(self, n):
        lib = str(kid(n, "lib_id")[1])
        at = kid(n, "at")
        props = {}
        for p in kids(n, "property"):
            props[str(p[1])] = dict(val=str(p[2]),
                                    x=float(kid(p, "at")[1]),
                                    y=float(kid(p, "at")[2]),
                                    hide=self._hidden(p),
                                    size=self._size(p, 1.27))
        return dict(lib=lib, x=float(at[1]), y=float(at[2]),
                    rot=float(at[3]) if len(at) > 3 else 0.0,
                    props=props)

    def _parse_sheet(self, n):
        at, sz = kid(n, "at"), kid(n, "size")
        props = {}
        for p in kids(n, "property"):
            props[str(p[1])] = str(p[2])
        return dict(x=float(at[1]), y=float(at[2]),
                    w=float(sz[1]), h=float(sz[2]), props=props)

    # --- рендер -----------------------------------------------------------
    def render(self, out_png):
        s = SCALE * SS
        pw, ph = self.paper
        W, H = int(pw * s) + 1, int(ph * s) + 1
        img = Image.new("RGB", (W, H), (255, 255, 255))
        d = ImageDraw.Draw(img)

        def T(x, y):
            return (x * s, y * s)

        # рамка листа
        d.rectangle([T(5, 5), T(pw - 5, ph - 5)], outline=C_FRAME, width=max(1, s // 4))

        # провода
        lw = max(2, int(s * 0.18))
        for (x1, y1), (x2, y2) in self.wires:
            d.line([T(x1, y1), T(x2, y2)], fill=C_WIRE, width=lw)

        # NC
        r = 1.0
        for (x, y) in self.ncs:
            d.line([T(x - r, y - r), T(x + r, y + r)], fill=C_NC, width=lw)
            d.line([T(x - r, y + r), T(x + r, y - r)], fill=C_NC, width=lw)

        # текстовые примечания
        for (txt, x, y, size) in self.texts:
            f = FONTS.get(size * s)
            d.text(T(x, y - size), txt, fill=C_TEXT, font=f, anchor="la")

        # локальные метки
        for (txt, x, y, rot, just, size) in self.labels:
            f = FONTS.get(size * s, bold=False)
            ax = "l" if "left" in just else ("r" if "right" in just else "m")
            ay = "a" if "top" in just else ("s" if "bottom" in just else "m")
            # y в KiCad вниз, anchor "s" = низ текста -> привязываем к точке метки
            d.text(T(x, y), txt, fill=C_LABEL, font=f, anchor=ax + ay)

        # символы
        for sym in self.syms:
            self._draw_symbol(d, sym, s, T)

        # иерархические листы (корневой схема)
        for sh in self.sheets:
            d.rectangle([T(sh["x"], sh["y"]),
                         T(sh["x"] + sh["w"], sh["y"] + sh["h"])],
                        outline=C_SHEET, width=max(2, int(s * 0.2)))
            f = FONTS.get(2.0 * s)
            d.text(T(sh["x"] + 1, sh["y"] - 4.2),
                   sh["props"].get("Sheetname", ""),
                   fill=(0, 60, 140), font=f, anchor="la")
            d.text(T(sh["x"] + 1, sh["y"] + sh["h"] + 1),
                   sh["props"].get("Sheetfile", ""),
                   fill=(90, 90, 90), font=FONTS.get(1.5 * s), anchor="la")

        # заголовок в углу
        f = FONTS.get(3.0 * s, bold=True)
        d.text(T(8, 8), self.path.name, fill=(140, 140, 140), font=f, anchor="la")

        if SS != 1:
            img = img.resize((W // SS, H // SS), Image.LANCZOS)
        OUT.mkdir(exist_ok=True)
        img.save(out_png)
        return out_png

    def _draw_symbol(self, d, sym, s, T):
        lib = self.libs.get(sym["lib"].split(":", 1)[-1])
        if lib is None:
            lib = self.libs.get(sym["lib"])
        if lib is None:
            return
        sx, sy, rot = sym["x"], sym["y"], sym["rot"]
        r = math.radians(rot)

        def P(px, py):
            # символные координаты (y вверх) -> листовые (y вниз)
            dx = px * math.cos(r) - py * math.sin(r)
            dy = px * math.sin(r) + py * math.cos(r)
            return T(sx + dx, sy - dy)

        # корпус
        for (x1, y1, x2, y2, filled) in lib["rects"]:
            a, b = P(x1, y1), P(x2, y2)
            box = [min(a[0], b[0]), min(a[1], b[1]),
                   max(a[0], b[0]), max(a[1], b[1])]
            if filled:
                d.rectangle(box, fill=C_BODY_FILL, outline=C_BODY_LINE,
                            width=max(1, int(s * 0.15)))
            else:
                d.rectangle(box, outline=C_BODY_LINE,
                            width=max(1, int(s * 0.15)))
        # полилинии (символы питания и т.п.)
        for poly in lib["polys"]:
            pts = [P(x, y) for (x, y) in poly]
            d.line(pts, fill=C_BODY_LINE, width=max(1, int(s * 0.15)))

        # пины
        lw = max(1, int(s * 0.13))
        for p in lib["pins"]:
            a = P(p["x"], p["y"])
            pr = math.radians(p["ang"])
            # конец пина внутри корпуса
            ex = p["x"] + p["len"] * math.cos(pr)
            ey = p["y"] + p["len"] * math.sin(pr)
            b = P(ex, ey)
            col = C_PIN
            if p["type"] in ("power_in", "power_out", "power_in"):
                col = (140, 0, 0)
            d.line([a, b], fill=col, width=lw)
            # кружок на точке подключения у пассивных/входов
            if p["type"] in ("passive", "input", "power_in", "power_out",
                             "open_collector", "tri_state"):
                rr = s * 0.25
                d.ellipse([a[0] - rr, a[1] - rr, a[0] + rr, a[1] + rr],
                          outline=col, width=lw)
            # номер и имя пина
            if not lib["hide_nums"] and p["num"]:
                # номер снаружи, вдоль пина
                off = p["len"] + 0.8 if p["len"] else 1.5
                nx = p["x"] + off * math.cos(pr)
                ny = p["y"] + off * math.sin(pr)
                anchor = "ls" if abs(math.cos(pr)) > 0.5 else "ms"
                if math.cos(pr) < -0.5:
                    anchor = "rs"
                elif abs(math.cos(pr)) <= 0.5:
                    anchor = "ms" if math.sin(pr) < 0 else "ma"
                d.text(P(nx, ny), p["num"], fill=C_FIELD,
                       font=FONTS.get(1.0 * s), anchor=anchor)
            if not lib["hide_names"] and p["name"] and p["name"] != "~":
                # имя у корпуса, чуть внутрь
                off = max(p["len"] - 0.6, 0.6)
                nx = p["x"] + off * math.cos(pr)
                ny = p["y"] + off * math.sin(pr)
                if abs(math.cos(pr)) > 0.5:
                    anchor = "ls" if math.cos(pr) > 0 else "rs"
                else:
                    anchor = "ma" if math.sin(pr) > 0 else "ms"
                d.text(P(nx, ny), p["name"], fill=C_TEXT,
                       font=FONTS.get(1.0 * s), anchor=anchor)

        # поля Reference / Value (скрытые не рисуем)
        for pname, col in (("Reference", C_FIELD), ("Value", (0, 120, 0))):
            pr = sym["props"].get(pname)
            if not pr or pr["hide"]:
                continue
            f = FONTS.get(pr["size"] * s, bold=(pname == "Reference"))
            # позиция поля уже листовая (генератор пишет абсолютные координаты)
            d.text(T(pr["x"], pr["y"]), pr["val"], fill=col, font=f, anchor="lm")


def main():
    args = sys.argv[1:] or ["seeed-clicker.kicad_sch",
                            "clicker.kicad_sch", "dongle.kicad_sch"]
    for a in args:
        p = ROOT / a if not Path(a).is_absolute() else Path(a)
        sh = Sheet(p)
        out = OUT / (p.stem + ".png")
        sh.render(out)
        print("-> %s  (%s: проводов %d, символов %d, меток %d, текстов %d, NC %d, листов %d)"
              % (out.relative_to(ROOT), p.name, len(sh.wires), len(sh.syms),
                 len(sh.labels), len(sh.texts), len(sh.ncs), len(sh.sheets)))


if __name__ == "__main__":
    main()
