# Copyright (C) 2025 Andreas Rettig
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

"""
lochraster_core.py
Erzeugt das Lochraster-Layout auf dem KiCAD-Board via pcbnew-API.
Alle Elemente werden in einer PCB_GROUP zusammengefasst, damit das
erzeugte Layout als Einheit ausgewählt und verschoben werden kann.
"""

import pcbnew


PITCH_MM = 2.54          # Standard-Lochraster-Raster
MARGIN_MM = 3.81         # Rand (1.5 × Pitch) – Platz für Beschriftung & Montagelöcher
OUTLINE_WIDTH_MM = 0.05  # Strichbreite Edge.Cuts
LABEL_SIZE_MM = 1.0      # Schriftgröße Silkscreen
LABEL_THICK_MM = 0.12    # Strichbreite Schrift


class LochrasterGenerator:
    """Erzeugt Vias, Kupferstreifen und Board-Umriss auf dem übergebenen Board."""

    def __init__(self, board: pcbnew.BOARD, settings: dict):
        self.board = board
        self.s = settings
        self.pitch = pcbnew.FromMM(PITCH_MM)
        self._group = None
        self._tracks = []  # Tracks separat merken (können nicht in die Gruppe)
        self._bx = 0  # obere linke Ecke des Boards (X)
        self._by = 0  # obere linke Ecke des Boards (Y)

    # ------------------------------------------------------------------
    # Öffentliche API
    # ------------------------------------------------------------------

    def generate(self):
        s = self.s
        board = self.board

        drill    = pcbnew.FromMM(s["drill_mm"])
        pad_size = pcbnew.FromMM(s["pad_mm"])
        track_w  = pcbnew.FromMM(s["track_mm"])
        cols = s["cols"]
        rows = s["rows"]

        # Gruppe anlegen
        self._group = pcbnew.PCB_GROUP(board)
        self._group.SetName("Lochraster")
        board.Add(self._group)


        # Board-Abmessungen
        margin = pcbnew.FromMM(MARGIN_MM)
        bw = pcbnew.FromMM((cols - 1) * PITCH_MM + 2 * MARGIN_MM)
        bh = pcbnew.FromMM((rows - 1) * PITCH_MM + 2 * MARGIN_MM)

        # Obere linke Ecke so setzen, dass die Platine im Viewport zentriert ist
        self._bx, self._by = self._viewport_center_offset(bw, bh)

        ox = self._bx + margin
        oy = self._by + margin

        self._create_outline(self._bx, self._by, bw, bh)
        self._add_silkscreen_labels(ox, oy, cols, rows, bw, bh)

        # Inhalt
        sides = s["sides"]           # "single" | "double_ortho" | "double_same"
        double_sided = sides != "single"

        if s["type"] == "vias":
            self._place_vias(ox, oy, cols, rows, drill, pad_size)
        else:
            self._place_strips(ox, oy, cols, rows, drill, pad_size, track_w,
                               s["direction"], sides)

        self._clear_solder_mask(self._bx, self._by, bw, bh, double_sided)
        self._disable_via_tenting()

        if s.get("wire_bridges"):
            self._setup_wire_bridge_layers()

        # Ansicht auf die erzeugte Platine zentrieren
        try:
            pcbnew.FocusOnItem(self._group)
        except Exception:
            pass

        # Gruppe und alle Tracks auswählen → gemeinsam mit M verschiebbar
        self._group.SetSelected()
        for track in self._tracks:
            track.SetSelected()
        pcbnew.Refresh()

    # ------------------------------------------------------------------
    # Viewport-Mittelpunkt berechnen
    # ------------------------------------------------------------------

    @staticmethod
    def _viewport_center_offset(bw: int, bh: int):
        """Platine bei (0,0) platzieren; FocusOnItem zentriert die Ansicht danach."""
        return 0, 0

    # ------------------------------------------------------------------
    # Hilfsmethode: Element zum Board und zur Gruppe hinzufügen
    # ------------------------------------------------------------------

    def _add(self, item):
        self.board.Add(item)
        # PCB_TRACK-Objekte dürfen nicht zur Gruppe: AddItem() verändert die
        # Ownership in KiCAD 10 so, dass der Track aus GetTracks() verschwindet.
        if self._group is not None and not self._is_plain_track(item):
            try:
                self._group.AddItem(item)
            except Exception:
                pass

    @staticmethod
    def _is_plain_track(item) -> bool:
        return item.GetClass() == "PCB_TRACK"

    # ------------------------------------------------------------------
    # Board-Umriss
    # ------------------------------------------------------------------

    def _create_outline(self, x: int, y: int, w: int, h: int):
        corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
        for i in range(4):
            seg = pcbnew.PCB_SHAPE(self.board)
            seg.SetShape(pcbnew.SHAPE_T_SEGMENT)
            seg.SetLayer(pcbnew.Edge_Cuts)
            seg.SetStart(pcbnew.VECTOR2I(corners[i][0], corners[i][1]))
            seg.SetEnd(pcbnew.VECTOR2I(corners[(i + 1) % 4][0], corners[(i + 1) % 4][1]))
            seg.SetWidth(pcbnew.FromMM(OUTLINE_WIDTH_MM))
            self._add(seg)

    # ------------------------------------------------------------------
    # Silkscreen-Beschriftung
    # ------------------------------------------------------------------

    def _add_silkscreen_labels(self, ox: int, oy: int, cols: int, rows: int,
                                bw: int, bh: int):
        half = pcbnew.FromMM(MARGIN_MM / 2)
        bx = self._bx
        by = self._by

        # Spaltenbeschriftung (Buchstaben) oben und unten – Vorder- und Rückseite
        for col in range(cols):
            x = ox + col * self.pitch
            label = self._col_label(col)
            self._add_text(x, by + half,        label, pcbnew.F_SilkS)
            self._add_text(x, by + bh - half,   label, pcbnew.F_SilkS)
            self._add_text(x, by + half,        label, pcbnew.B_SilkS, mirrored=True)
            self._add_text(x, by + bh - half,   label, pcbnew.B_SilkS, mirrored=True)

        # Zeilenbeschriftung (Nummern) links und rechts – Vorder- und Rückseite
        for row in range(rows):
            y = oy + row * self.pitch
            label = self._row_label(rows - 1 - row)  # 01 unten, höchste Zahl oben
            self._add_text(bx + half,        y, label, pcbnew.F_SilkS)
            self._add_text(bx + bw - half,   y, label, pcbnew.F_SilkS)
            self._add_text(bx + half,        y, label, pcbnew.B_SilkS, mirrored=True)
            self._add_text(bx + bw - half,   y, label, pcbnew.B_SilkS, mirrored=True)

    def _add_text(self, x: int, y: int, text_str: str, layer: int, mirrored: bool = False):
        size      = pcbnew.FromMM(LABEL_SIZE_MM)
        thickness = pcbnew.FromMM(LABEL_THICK_MM)

        text = pcbnew.PCB_TEXT(self.board)
        text.SetText(text_str)
        text.SetLayer(layer)
        text.SetPosition(pcbnew.VECTOR2I(x, y))
        text.SetTextSize(pcbnew.VECTOR2I(size, size))
        text.SetTextThickness(thickness)
        text.SetMirrored(mirrored)
        try:
            text.SetHorizJustify(pcbnew.GR_TEXT_H_ALIGN_CENTER)
            text.SetVertJustify(pcbnew.GR_TEXT_V_ALIGN_CENTER)
        except AttributeError:
            pass  # Fallback: KiCAD-Default-Ausrichtung
        self._add(text)

    @staticmethod
    def _col_label(col: int) -> str:
        """0→A, 1→B, …, 25→Z, 26→AA, 27→AB, …"""
        result = ""
        n = col + 1
        while n > 0:
            n -= 1
            result = chr(ord("A") + n % 26) + result
            n //= 26
        return result

    @staticmethod
    def _row_label(row: int) -> str:
        return f"{row + 1:02d}"

    # ------------------------------------------------------------------
    # Vias
    # ------------------------------------------------------------------

    def _place_vias(self, ox: int, oy: int, cols: int, rows: int,
                    drill: int, pad_size: int):
        for row in range(rows):
            for col in range(cols):
                self._add_via(ox + col * self.pitch, oy + row * self.pitch,
                              drill, pad_size)

    def _add_via(self, x: int, y: int, drill: int, pad_size: int):
        via = pcbnew.PCB_VIA(self.board)
        via.SetPosition(pcbnew.VECTOR2I(x, y))
        via.SetDrill(drill)
        via.SetWidth(pad_size)
        via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
        self._add(via)

    # ------------------------------------------------------------------
    # Kupferstreifen
    # ------------------------------------------------------------------

    def _place_strips(self, ox: int, oy: int, cols: int, rows: int,
                      drill: int, pad_size: int, track_w: int,
                      direction: str, sides: str):
        for row in range(rows):
            for col in range(cols):
                self._add_via(ox + col * self.pitch, oy + row * self.pitch,
                              drill, pad_size)

        add_h = self._add_horizontal_strips
        add_v = self._add_vertical_strips

        if sides == "single":
            if direction == "horizontal":
                add_h(ox, oy, cols, rows, track_w, pcbnew.B_Cu)
            else:
                add_v(ox, oy, cols, rows, track_w, pcbnew.B_Cu)

        elif sides == "double_ortho":
            if direction == "horizontal":
                add_h(ox, oy, cols, rows, track_w, pcbnew.B_Cu)
                add_v(ox, oy, cols, rows, track_w, pcbnew.F_Cu)
            else:
                add_v(ox, oy, cols, rows, track_w, pcbnew.B_Cu)
                add_h(ox, oy, cols, rows, track_w, pcbnew.F_Cu)

        else:  # double_same
            if direction == "horizontal":
                add_h(ox, oy, cols, rows, track_w, pcbnew.B_Cu)
                add_h(ox, oy, cols, rows, track_w, pcbnew.F_Cu)
            else:
                add_v(ox, oy, cols, rows, track_w, pcbnew.B_Cu)
                add_v(ox, oy, cols, rows, track_w, pcbnew.F_Cu)

    def _add_horizontal_strips(self, ox: int, oy: int, cols: int, rows: int,
                                track_w: int, layer: int):
        for row in range(rows):
            y = oy + row * self.pitch
            for col in range(cols - 1):
                x1 = ox + col * self.pitch
                x2 = ox + (col + 1) * self.pitch
                self._add_track(x1, y, x2, y, layer, track_w)

    def _add_vertical_strips(self, ox: int, oy: int, cols: int, rows: int,
                              track_w: int, layer: int):
        for col in range(cols):
            x = ox + col * self.pitch
            for row in range(rows - 1):
                y1 = oy + row * self.pitch
                y2 = oy + (row + 1) * self.pitch
                self._add_track(x, y1, x, y2, layer, track_w)

    def _add_track(self, x1: int, y1: int, x2: int, y2: int,
                   layer: int, width: int):
        track = pcbnew.PCB_TRACK(self.board)
        track.SetStart(pcbnew.VECTOR2I(x1, y1))
        track.SetEnd(pcbnew.VECTOR2I(x2, y2))
        track.SetLayer(layer)
        track.SetWidth(width)
        self._add(track)
        self._tracks.append(track)

    # ------------------------------------------------------------------
    # Drahtbrücken-Lagen
    # ------------------------------------------------------------------

    def _setup_wire_bridge_layers(self):
        """Aktiviert In1_Cu / In2_Cu als Drahtbrücken-Lagen und schreibt DRC-Regel."""
        board = self.board

        # 4 Kupferlagen setzen
        board.SetCopperLayerCount(4)

        # Lagen im Layer-Set explizit aktivieren und sichtbar schalten
        try:
            enabled = board.GetEnabledLayers()
            enabled = enabled | pcbnew.LSET(pcbnew.In1_Cu) | pcbnew.LSET(pcbnew.In2_Cu)
            board.SetEnabledLayers(enabled)
        except Exception:
            pass

        try:
            visible = board.GetVisibleLayers()
            visible = visible | pcbnew.LSET(pcbnew.In1_Cu) | pcbnew.LSET(pcbnew.In2_Cu)
            board.SetVisibleLayers(visible)
        except Exception:
            pass

        # Lagenbezeichnungen (sprachabhängig aus Settings)
        try:
            board.SetLayerName(pcbnew.In1_Cu, self.s.get("wire_name_top", "Wire_Bridges_top"))
            board.SetLayerName(pcbnew.In2_Cu, self.s.get("wire_name_bot", "Wire_Bridges_bottom"))
        except Exception:
            pass

        # DRC-Regel: Kreuzungen auf Drahtbrücken-Lagen erlauben (Abstand = 0)
        drc_rule = (
            "(version 1)\n\n"
            "; Drahtbruecken-Lagen: Ueberlappungen mit Vias und Pads erlaubt\n"
            "(rule \"Drahtbruecken_Kreuzungen\"\n"
            "  (condition \"A.Layer == 'In1_Cu' || A.Layer == 'In2_Cu'"
            " || B.Layer == 'In1_Cu' || B.Layer == 'In2_Cu'\")\n"
            "  (constraint clearance (min 0mm))\n"
            ")\n"
        )

        # Versuch 1: direkt in die Design-Settings schreiben
        try:
            ds = board.GetDesignSettings()
            if hasattr(ds, "SetCustomRules"):
                ds.SetCustomRules(drc_rule)
        except Exception:
            pass

        # Versuch 2: .kicad_dru-Datei neben der Board-Datei schreiben
        try:
            import os
            board_file = board.GetFileName()
            if board_file:
                dru_path = os.path.splitext(board_file)[0] + ".kicad_dru"
                with open(dru_path, "w", encoding="utf-8") as f:
                    f.write(drc_rule)
        except Exception:
            pass

        # UI-Lagenübersicht aktualisieren
        try:
            pcbnew.UpdateUserInterface()
        except Exception:
            pass

    # ------------------------------------------------------------------
    # Via-Tenting deaktivieren
    # ------------------------------------------------------------------

    def _disable_via_tenting(self):
        """Deaktiviert Via-Tenting auf Board-Ebene, damit Vias ohne Lötlack sind."""
        ds = self.board.GetDesignSettings()
        for attr in ("m_TentViasFront", "m_TentViasBack"):
            if hasattr(ds, attr):
                setattr(ds, attr, False)

    # ------------------------------------------------------------------
    # Lötmasken-Freistellung
    # ------------------------------------------------------------------

    def _clear_solder_mask(self, x: int, y: int, w: int, h: int, double_sided: bool):
        layers = [pcbnew.B_Mask]
        if double_sided:
            layers.append(pcbnew.F_Mask)

        for layer in layers:
            rect = pcbnew.PCB_SHAPE(self.board)
            rect.SetShape(pcbnew.SHAPE_T_RECT)
            rect.SetLayer(layer)
            rect.SetFilled(True)
            rect.SetWidth(0)
            rect.SetStart(pcbnew.VECTOR2I(x, y))
            rect.SetEnd(pcbnew.VECTOR2I(x + w, y + h))
            self._add(rect)
