#!/usr/bin/env python3
# $Id: ppexample_circuit1 72 2026-08-27 08:32:33Z peter $
#
# ppexample_circuit1 --- one of this project's own two
# self-test example circuits: a structurally different circuit, run
# through kicad_layout.py's own generic placement algorithm, to
# confirm it genuinely generalizes rather than being tuned to fit
# only one specific circuit's own shape.
#
# EXTRACTED 2026-08-14 as part of packpay's own creation (a surgical
# extraction from the dudhya project, where this same test originally
# found the bugs described below against dudhya's own real,
# sixteen-component power stage circuit -- see packpay's own
# ResearchJournal.org for the full account, including how this
# circuit was ORIGINALLY built and what it originally found).
#
# Not a real, physically-built circuit -- a synthetic test netlist,
# built directly via the shared Netlist/Component/Net dataclasses
# (netlist.py's own load_netlist(text) round-trips through a hand-
# written S-expression string; constructing the dataclasses directly
# is more robust and avoids any syntax mistakes in that string).
# Reuses real, already-verified PINS geometry for U1 (STUSB4500QTR),
# U2 (XL4015), and the plain 2-pin parts (R, C, L, D_Schottky) --
# kept self-contained in example_pins.py (see its own header),
# genuinely different WIRING and TOPOLOGY from this project's own
# other example circuit, not invented pin offsets for parts nobody
# has actually verified.
#
# Deliberately stresses cases a single real circuit is unlikely to
# exercise on its own:
#   - Only 2 real anchors (1 gap between them) needing to host THREE
#     separate PWR_FLAG nets -- this exercises the "more flags than
#     gaps" fallback in kicad_layout.py's own PWR_FLAG placement.
#   - A three-level passive-to-passive chain (R10->C10->L10) --
#     stresses chain_starters/north_anchor_extra against real depth.
#   - A single-character net name ("A") and a 68-character one, well
#     outside the 1-23 character range label_reach() was originally
#     fitted against.
#   - A 5-member GND-style net that's simultaneously a "rail" (high_
#     degree_nets' own >4-member threshold) AND needs a PWR_FLAG,
#     confirming those two mechanisms compose correctly.

import sys
import os

sys.path.insert(0, os.path.dirname(__file__))
from netlist import Netlist, Component, Net  # noqa: E402
from kicad_gen import write_project_file  # noqa: E402
from auto_layout import classify, high_degree_nets, order_anchors_by_connectivity  # noqa: E402
from kicad_layout import generate_schematic_auto  # noqa: E402
from example_pins import PINS  # noqa: E402 -- real, verified symbol pin geometry,
                                # kept self-contained in this project (see
                                # example_pins.py's own header) rather than
                                # depending on any downstream project's own data.

_SYM_DIR = os.environ.get("KICAD_SYM_DIR", "/usr/share/kicad/symbols")


def build_netlist():
    components = {
        "U1": Component(ref="U1", value="STUSB4500QTR", lib="Interface_USB", part="STUSB4500QTR"),
        "U2": Component(ref="U2", value="XL4015", lib="Regulator_Switching", part="XL4015"),
        "R10": Component(ref="R10", value="4.7k", lib="Device", part="R"),
        "C10": Component(ref="C10", value="10uF", lib="Device", part="C"),
        "L10": Component(ref="L10", value="10uH", lib="Device", part="L"),
        "D10": Component(ref="D10", value="1N5819", lib="Device", part="D_Schottky"),
        "L20": Component(ref="L20", value="4.7uH", lib="Device", part="L"),
        "C20": Component(ref="C20", value="47uF", lib="Device", part="C"),
        "C30": Component(ref="C30", value="1uF", lib="Device", part="C"),
        "C40": Component(ref="C40", value="1uF", lib="Device", part="C"),
    }

    pin_names = {}
    pin_types = {}

    def node(ref, pin, name, typ):
        pin_names[(ref, pin)] = name
        pin_types[(ref, pin)] = typ
        return (ref, pin)

    nets = []

    # Three separate power_in-only nets on just TWO anchors (one gap
    # between them) -- forces the PWR_FLAG "more flags than gaps"
    # fallback.
    nets.append(Net(name="PWRA", code="1", nodes=[
        node("U1", "24", "VDD", "power_in"),
        node("U2", "5", "VIN", "power_in"),
        node("C30", "2", "2", "passive"),
    ]))
    nets.append(Net(name="PWRB", code="2", nodes=[
        node("U1", "22", "VSYS", "power_in"),
        node("C40", "2", "2", "passive"),
    ]))
    # 5 members: also crosses high_degree_nets' own >4 rail threshold,
    # confirmed intentionally -- a rail net that ALSO needs a
    # PWR_FLAG, a combination the real circuit's own GND net has too,
    # but worth a fresh, explicit check on new topology. Two dedicated
    # decoupling caps (C30, C40) round it out to 5 genuinely distinct
    # members without reusing a pin already committed to a different
    # net elsewhere (a real design mistake caught here the first time
    # this circuit was actually run: R10/C10/D10's pins were
    # originally double-booked onto both this net and their own
    # chain nets below, which isn't physically possible -- a single
    # pin is on exactly one net. classify() correctly, if confusingly,
    # treated the double-booked R10/C10 as 3-net "anchors" as a direct
    # result, which is what surfaced the mistake.)
    nets.append(Net(name="GNDTEST", code="3", nodes=[
        node("U1", "10", "GND", "power_in"),
        node("U2", "1", "GND", "power_in"),
        node("C30", "1", "1", "passive"),
        node("C40", "1", "1", "passive"),
        node("D10", "A", "A", "passive"),
    ]))
    # C30/C40 each need a SECOND, non-rail connection too -- a real
    # decoupling cap always has one terminal on a rail and the other
    # on the specific signal it's decoupling (already PWRA/PWRB
    # above, as a third member of each net, not a separate net -- a
    # cap with ONLY a rail connection was tried first and correctly,
    # usefully rejected by generate_schematic() itself ("never found
    # anything already-placed to attach to") rather than silently
    # mishandled -- a genuine, correct finding about the algorithm's
    # own behavior around rail-only passives, not a bug, but it did
    # mean this test circuit's own first draft was unrealistic and
    # needed fixing, not the algorithm.

    # A single-character net name -- well below label_reach()'s own
    # originally-fitted 1-23 character range's short end (it WAS
    # fitted starting at 1 char, "A", so this specifically re-confirms
    # that boundary on different real content, not extending past it).
    nets.append(Net(name="A", code="4", nodes=[
        node("U2", "2", "FB", "input"),
        node("D10", "K", "K", "passive"),
    ]))

    # A 68-character net name -- well past label_reach()'s own
    # originally-fitted range (1-23 chars), a genuine extrapolation
    # test of the linear fit, not just a re-confirmation.
    long_name = "REG_OUT_LONG_NAME_FOR_TESTING_LABEL_REACH_FORMULA_AT_THE_UPPER_END"
    nets.append(Net(name=long_name, code="5", nodes=[
        node("U1", "21", "VREG_1V2", "power_out"),
        node("R10", "1", "1", "passive"),
    ]))

    # The three-level chain: R10 (attached to an anchor via the long
    # net above) -> C10 -> L10.
    nets.append(Net(name="Z", code="6", nodes=[
        node("R10", "2", "2", "passive"),
        node("C10", "1", "1", "passive"),
    ]))
    nets.append(Net(name="W", code="7", nodes=[
        node("C10", "2", "2", "passive"),
        node("L10", "1", "1", "passive"),
    ]))

    nets.append(Net(name="SWTEST", code="8", nodes=[
        node("U2", "3", "SW", "power_out"),
        node("L20", "1", "1", "passive"),
    ]))
    nets.append(Net(name="OUT27", code="9", nodes=[
        node("U1", "23", "VREG_2V7", "power_out"),
        node("C20", "1", "1", "passive"),
    ]))

    return Netlist(components=components, nets=nets, pin_names=pin_names, pin_types=pin_types)


def generate():
    nl = build_netlist()
    anchors, passives = classify(nl)
    rails = high_degree_nets(nl)
    ordered_anchors = order_anchors_by_connectivity(nl, anchors, rails)
    print(f"anchors: {ordered_anchors}, passives: {sorted(passives)}, rails: {sorted(rails)}")
    print(f"PWR_FLAG nets: {nl.nets_needing_pwr_flag()}")

    sch = generate_schematic_auto(
        nl, PINS, _SYM_DIR, ordered_anchors, passives, rails,
        title="dudhya: synthetic test circuit -- Step 3 generalization check",
        date="2026-08-13",
        rev="test",
        company="dudhya project",
        comments=[
            "NOT a real dudhya circuit -- a synthetic netlist built to",
            "stress-test kicad_layout.py's generic placement algorithm",
            "against topology genuinely different from the real Pixel 3a",
            "power stage. See ResearchJournal.org's 2026-08-13 entry.",
        ],
        # GENERALIZED 2026-08-13, same day this circuit first exposed
        # the need for it: paper/anchor_y/north_anchor_extra used to
        # be hand-picked here (paper="A3", anchor_y=150), found by a
        # PERSON reading RuntimeErrors and retrying by hand -- see
        # this file's own git history / ChangeLog for the exact
        # values that were tried and reverted along the way.
        # generate_schematic_auto() now does that same escalation
        # automatically: tries the shared module's own tuned A4
        # defaults first, and only escalates (more north-side
        # headroom, then a bigger sheet) if this circuit's own three-
        # level chain genuinely needs it -- which, confirmed by
        # generate()'s own verbose print below, it does, landing on
        # paper="A3" automatically, the same combination a person
        # found by hand originally.
        verbose=True,
    )

    out_sch = os.path.join(os.path.dirname(__file__), "packpay-example-circuit1.kicad_sch")
    out_pro = os.path.join(os.path.dirname(__file__), "packpay-example-circuit1.kicad_pro")
    root_uuid = sch.write(out_sch)
    write_project_file(out_pro, root_uuid, os.path.basename(out_pro))
    print(f"wrote {out_sch}, root uuid {root_uuid}")
    return out_sch


def _version():
    """This project's own current Dewey-decimal version, read from
    VERSION -- see netlist.py's own identical helper for the full
    reasoning. Duplicated per-script rather than factored into a
    shared module -- see render.py's own identical note on why."""
    here = os.path.dirname(os.path.abspath(__file__))
    try:
        with open(os.path.join(here, "VERSION")) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#"):
                    return line
    except OSError:
        pass
    return "unknown"


if __name__ == "__main__":
    # CLI entry point, ADDED 2026-08-19, this project's own SEVENTH
    # script to get this treatment. A real, if genuinely minimal,
    # case: this script takes no varying input at all -- it always
    # builds the SAME synthetic test circuit and writes it to the
    # SAME fixed path (test_netlist.py's own subprocess call, and
    # this project's own "make check", both depend on that fixed
    # path staying fixed) -- so --version/--help is the whole real
    # option surface, not an oversight. See
    # example_circuit1.1 (this program's own new manual page) and
    # Péter's own direct correction, 2026-08-19: a script taking no
    # options is still real, workflow-relevant tooling and belongs
    # documented, the same as any other -- absence of options was
    # never, by itself, a reason to skip a man page.
    import argparse
    parser = argparse.ArgumentParser(
        prog="ppexample_circuit1",
        description="Build and write this project's own first synthetic "
                     "test circuit (packpay-example-circuit1.kicad_sch), "
                     "used by test_netlist.py and 'make check'. Takes no "
                     "arguments: the circuit and its output path are both "
                     "fixed, by design (see DESCRIPTION in "
                     "example_circuit1.1).")
    parser.add_argument("--version", action="version",
                         version=f"%(prog)s {_version()}")
    parser.parse_args()
    generate()
