#!/usr/bin/env python3
# $Id: pprender 200 2026-10-12 12:00:59Z peter $
#
# pprender --- the CLI entry point for render.py's own library
# functions. ADDED 2026-08-26, per Péter's own direct preference (no
# extension on executable files, a project-specific "pp" prefix,
# specifically to avoid colliding with other tools' own common
# "render" name) -- see ppnetlist's own header comment for the full
# real account of why this is a real, separate file rather than a
# direct rename of render.py itself: that module is genuinely BOTH a
# real CLI tool and a real library (build_instructions.py, and its
# own test, both do `from render import render`), and Python's own
# import statement needs a real ".py" extension to resolve
# `import render`.

import os
import sys
import subprocess
import configparser
from netlist import load_from_schematic
from breadboard_lib import NoFreeColumnError
from auto_layout import (layout, verify_connectivity, verify_within_board_bounds, diagram_fallback,
                          compute_optimal_config, rails_without_power_source,
                          TooManyComponentsError)
from render import render
import breadboard_config as bc


def _version():
    """This project's own current Dewey-decimal version -- CLI-
    specific, so it lives here, not in render.py's own library code.
    See ppnetlist's own identical helper for the fuller reasoning."""
    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"


def main(argv=None):
    """CLI entry point, ADDED 2026-08-18 (replacing the previous
    positional-argv-with-a-demo-specific-default), this project's own
    SECOND script (after netlist.py) brought up to
    DevelopmentStandard.org's own real "Options and Arguments" bar --
    see pprender.1 (this program's own manual page, renamed alongside
    this file itself).

    MOVED 2026-08-26 from render.py's own file into this new,
    separate, extensionless CLI entry point -- unchanged otherwise."""
    import argparse

    parser = argparse.ArgumentParser(
        prog="pprender",
        description="Render a breadboard wiring diagram (SVG) from a real "
                     "KiCad schematic, using auto_layout.py's own "
                     "netlist-to-placement engine and breadboard_lib.py's "
                     "own drawing primitives. Runs this project's own "
                     "connectivity and board-bounds checks first, and "
                     "skips rendering (reporting cleanly, not crashing) if "
                     "either finds a real problem.")
    parser.add_argument("schematic", metavar="SCHEMATIC",
                         help="a real .kicad_sch file (hierarchical "
                              "designs are followed automatically by "
                              "kicad-cli's own netlist export, provided "
                              "every real sheet file is present alongside "
                              "the root -- see ResearchJournal.org's own "
                              "2026-08-18 TestSchemes entry for a real, "
                              "confirmed case of this)")
    parser.add_argument("-o", "--output", metavar="FILE",
                         default="auto_layout_render.svg",
                         help="output SVG path (default: "
                              "%(default)s)")
    parser.add_argument("--kicad-cli", metavar="CMD",
                         default=os.environ.get("KICAD_CLI", "kicad-cli"),
                         help="kicad-cli command (default: $KICAD_CLI if "
                              "set, else 'kicad-cli' on PATH -- same "
                              "convention as this project's own Makefile)")
    parser.add_argument("--max-components", type=int, metavar="N",
                         help="override this project's own real, default "
                              "component-count limit (breadboard_config."
                              "DEFAULT_MAX_COMPONENTS) -- see that "
                              "constant's own docstring for the real, "
                              "direct rationale (2026-09-15)")
    parser.add_argument("--config", metavar="FILE", default=None,
                         help="a real ppbreadboard_config config file "
                              "describing the target board -- highest "
                              "priority, overriding the default lookup "
                              "(see ppbreadboard_config(1) and "
                              "FutureResearch.org's own resolution-"
                              "strategy account). Without this, the "
                              "default lookup is tried: "
                              "$PACKPAY_BREADBOARD_CONFIG, then a "
                              "breadboard.conf next to SCHEMATIC, then "
                              "./breadboard.conf.")
    parser.add_argument("--use-computed", action="store_true",
                         help="use this tool's own computed configuration "
                              "(escalating sector count until the "
                              "circuit genuinely fits) even when a real "
                              "config file was found or given")
    parser.add_argument("--require-config", action="store_true",
                         help="error out rather than silently falling "
                              "back to a computed configuration when "
                              "neither --config nor the default lookup "
                              "resolves to a real file")
    parser.add_argument("--version", action="version",
                         version=f"%(prog)s {_version()}")
    parser.add_argument("--debug", action="store_true",
                         help="also print each placed passive's own real "
                              "placement reason (which net/anchor it "
                              "attached to, and its own real row count)")
    parser.add_argument("--quiet", action="store_true",
                         help="suppress the two checks' own per-run "
                              "explanatory header lines, and the "
                              "\"here's a configuration that might suit "
                              "this circuit better\" proposal (Péter's "
                              "own suggestion, 2026-08-19: silent by "
                              "default is the OPPOSITE of this flag's "
                              "own real behavior -- silence needs this "
                              "flag; still print any real failure "
                              "found, and the final result")
    args = parser.parse_args(argv)

    try:
        nl = load_from_schematic(args.schematic, kicad_cli=args.kicad_cli)
    except (OSError, subprocess.CalledProcessError) as e:
        print(f"pprender: {args.schematic}: {e}", file=sys.stderr)
        raise SystemExit(1)
    except ValueError as e:
        print(f"pprender: {args.schematic}: {e}", file=sys.stderr)
        raise SystemExit(1)

    # Real breadboard-configuration resolution, ADDED 2026-08-21
    # (closing FutureResearch.org's own real CLI-plumbing item):
    # every real piece below (resolve_config_path(),
    # compute_optimal_config(), load_config(), write_config()) already
    # existed and was already independently tested -- this is the
    # real, first place any of them are actually wired into a real
    # tool's own command line, implementing the exact resolution
    # strategy already formalized as pseudocode in FutureResearch.org,
    # not a new design invented here.
    config_path = bc.resolve_config_path(schematic_path=args.schematic,
                                          explicit_path=args.config)
    if config_path is None:
        if args.require_config:
            print(f"pprender: --require-config set, but no real config "
                  f"file was given or found (checked $PACKPAY_BREADBOARD_CONFIG, "
                  f"a breadboard.conf next to {args.schematic!r}, and "
                  f"./breadboard.conf)", file=sys.stderr)
            raise SystemExit(1)
        source = None
    else:
        try:
            source = bc.load_config(config_path)
        except (OSError, configparser.Error) as e:
            print(f"pprender: {config_path}: {e}", file=sys.stderr)
            raise SystemExit(1)

    # Is the resolved (or default, if none resolved) config ALREADY
    # adequate for this real circuit? Checked directly (a real
    # layout() + both real verification functions), not guessed --
    # only escalate to compute_optimal_config() if it genuinely isn't.
    candidate = source or bc.default_config()
    try:
        p, anchors, passives, rails = layout(nl, board_config=candidate,
                                              max_components=args.max_components)
    except TooManyComponentsError as e:
        print(f"pprender: {e}", file=sys.stderr)
        raise SystemExit(1)
    cand_bounds_ok, _ = verify_within_board_bounds(p)
    cand_conn_ok, _ = verify_connectivity(nl, p, rails)
    candidate_adequate = cand_bounds_ok and cand_conn_ok

    if source is None:
        # kind == "none": use the computed config directly -- there
        # was nothing real to compare it against or propose instead
        # of.
        if not candidate_adequate:
            try:
                p, anchors, passives, rails, candidate = compute_optimal_config(
                    nl, base_config=candidate)
            except RuntimeError as e:
                # A real, honest failure -- caught here rather than
                # left to crash with an ugly traceback (a real bug,
                # found and fixed 2026-08-21, testing this exact
                # code path): this circuit's own real needs
                # genuinely exceed what any number of the default
                # board's own shape can hold. The checks below still
                # run against `candidate` (the inadequate default),
                # so the SAME real failure gets reported cleanly,
                # the normal way, rather than duplicating it here.
                print(f"pprender: {e}", file=sys.stderr)
    elif args.use_computed:
        try:
            p, anchors, passives, rails, candidate = compute_optimal_config(
                nl, base_config=source)
        except RuntimeError as e:
            print(f"pprender: {e}", file=sys.stderr)
            raise SystemExit(1)
    elif not candidate_adequate:
        # A real file WAS found/given, but doesn't genuinely fit --
        # propose a real, directly reusable alternative (unless
        # --quiet), but still render with the file the user actually
        # asked for, not silently substitute a different one under
        # them.
        if not args.quiet:
            try:
                _, _, _, _, proposed = compute_optimal_config(nl, base_config=source)
                print(f"pprender: {config_path} may not be adequate for this "
                      f"circuit (bounds/connectivity check failed with it); "
                      f"a configuration that fits:\n")
                print(bc.write_config(proposed))
            except RuntimeError:
                pass  # honest: no config of this same shape, at any real
                      # sector count tried, resolves it either -- nothing
                      # useful to propose; the real failure below still
                      # reports the actual problem.

    if args.debug:
        for ref in sorted(p.passive_col):
            print(f"  {ref}: col {p.passive_col[ref]} row {p.col_row[ref]} "
                  f"({p.reason.get(ref, '?')})")
        if p.unplaced:
            print(f"  unplaced: {', '.join(sorted(p.unplaced))}")
        if p.sizing_fallbacks:
            print(f"  sizing fallbacks (real footprint data not used -- see "
                  f"real_size_rows()'s own diagnostics for the full account):")
            for ref, value, reason in p.sizing_fallbacks:
                print(f"    {ref} ({value}): {reason}")

    conn_ok, conn_failures = verify_connectivity(nl, p, rails)
    if not args.quiet:
        print(f"connectivity check: {len(conn_failures)} failures "
              f"(every real net's own members checked for a genuine connected "
              f"path in the layout itself, before any rendering happens)")
    for f in conn_failures:
        print(f"  {f}")
    bounds_ok, bounds_failures = verify_within_board_bounds(p)
    if not args.quiet:
        print(f"board-bounds check: {len(bounds_failures)} failures "
              f"(every assigned row checked against the real, parametric "
              f"ROWS_PER_HALF limit -- a real breadboard half only has so "
              f"many holes per column, whatever that number currently is)")
    for f in bounds_failures:
        print(f"  {f}")

    if not args.quiet:
        unsourced = rails_without_power_source(nl, rails)
        if unsourced:
            print(f"power-source advisory: {len(unsourced)} rail net(s) with no "
                  f"real power_out pin anywhere and no power_in pin either (the "
                  f"latter would already get a real PWR_FLAG) -- a real signal "
                  f"worth a look, not necessarily a real problem: a genuinely "
                  f"sourced net via a generic connector symbol (common in real, "
                  f"hobbyist circuits) looks the same to this check")
            for net_name in unsourced:
                print(f"  {net_name}")

    if not conn_ok or not bounds_ok:
        print("\nSkipping render: at least one check above failed, and "
              "rendering an invalid layout risks an unhandled crash "
              "rather than a clean report.")
        raise SystemExit(1)
    try:
        ok = render(nl, p, anchors, passives, rails, args.output)
    except NoFreeColumnError as e:
        diagram_fallback("pprender", e, p)          # re-executes in the diagram layout and does not return, or returns False when there is nothing to fall back from
        raise
    # the connectivity pass again, now that the far legs are drawn: before the render it cannot see them, and a far lead put in another net's strip is a real short (0.72.65)
    after_ok, after_failures = verify_connectivity(nl, p, rails)
    shorts = [f for f in after_failures if f.startswith("accidental short")]
    if shorts:
        diagram_fallback("pprender", shorts[0], p)
    if not args.quiet:
        print(f"connectivity check after render (far legs included): {len(after_failures)} failures")
    for f in after_failures:
        print(f"  {f}")
    if not ok:
        raise SystemExit(1)


if __name__ == "__main__":
    main()
