#!/usr/bin/env python3
"""Re-run the zero-site census behind section 3's deletion candidates.

Section 8 of wiki `quasi-declare-form` names "a dead-list member with a live
construction site" as one of four things that reopen the closure claim, and
warns that a fifth one means the census needs re-running with a different
predicate rather than patching. Four have already been found: `Row::meter`,
the `menu` field on Row and Cells, `Field::multiple` and `Field::keeps_value`.

WHY THE ORIGINAL CENSUS MISSED THEM. It looked for `Type::method(`. Three of the
four are reached another way, and the vocabulary is built by chaining, so that
predicate cannot see most of its own subject:

    Row::meter(..)          an associated call        <- the only shape it saw
    row.meter(..)           a chained builder
    row.meter = Some(..)    a field assignment

`.current` is the documented case: 10 live sites, every one a field assignment.
So this script counts all three reach-paths, per name, over the three shape
directories, with comments, strings, raw strings and char literals blanked
first, tests and `parity.rs` excluded, by reusing `population.py`.

A hit is a CANDIDATE, not a verdict. `.value(` matches any type's `value`
builder, so a name that comes back live still has to be read at its sites to
say whether the receiver is the vocabulary type. The script prints the sites so
that reading is cheap. What it is for is the other direction: a name that comes
back at zero under all three paths is safely dead, and that is a claim the old
predicate could not make.

    python3 scripts/dead-list-census.py           # the census
    python3 scripts/dead-list-census.py --sites   # every hit with its line
"""

from __future__ import annotations

import argparse
import importlib.util
import re
from pathlib import Path

ROOT = Path.home() / "Code"
spec = importlib.util.spec_from_file_location("population", Path(__file__).resolve().parent / "population.py")
pop = importlib.util.module_from_spec(spec)
spec.loader.exec_module(pop)

# Section 3's deletion candidates, by owning type. `Node`'s two variants and the
# two enums are matched as paths rather than as members.
CANDIDATES = {
    "Row": ["open", "chosen", "value", "selected", "toggle", "worth", "role",
            "primary", "identified", "choosing"],
    "Cells": ["values"],
    "Field": ["curve", "as_asked"],
    "Screen": ["saying", "holds", "chooses", "anchors", "feeds", "is_live", "clocks"],
    "Node": ["clock", "and_more", "meter", "holds", "names", "about"],
}
VARIANTS = ["Node::Age", "Node::Prose", "Loading::", "Clock::"]

# The six near-dead members the record KEEPS, each with one consumer. Counted
# as a control: if the script cannot see these, its predicate is broken and its
# zeroes mean nothing.
CONTROL = {
    # The six near-dead members the record keeps, one consumer each.
    "Row::disclosing": 1, "Row::relaxed": 1, "Row::depth": 1,
    "Slot::with_ranked": 1, "Slot::revealed_by": 1, "Choice::plain": 1,
    # The four the old census condemned wrongly. These are the real control:
    # each is reached by a chained builder or a field assignment, which is the
    # exact defect, and a predicate that cannot see them is the old one.
    "Row::meter": 5, "Row::menu": 2, "Cells::menu": 1,
    "Field::many": 1, "Field::keeping_value": 1,
}


def reach_paths(ty: str, name: str) -> dict[str, re.Pattern]:
    """The three ways a member is reached, as named patterns."""
    return {
        "assoc": re.compile(rf"\b{ty}::{name}\s*\("),
        "chained": re.compile(rf"\.{name}\s*\("),
        "assigned": re.compile(rf"\.{name}\s*=(?!=)"),
    }


def scan():
    files = []
    for rel in pop.SHAPE_DIRS:
        d = ROOT / rel
        for f in sorted(d.rglob("*.rs")):
            parts = f.relative_to(ROOT).parts
            if f.name in ("tests.rs", "parity.rs") or "tests" in parts:
                continue
            raw = f.read_text(encoding="utf-8", errors="replace")
            files.append((str(f.relative_to(ROOT)), pop.strip_cfg_test(pop.blank_noncode(raw)), raw))
    return files


def hits_for(files, pattern) -> list[str]:
    out = []
    for rel, blanked, raw in files:
        for m in pattern.finditer(blanked):
            line = blanked.count("\n", 0, m.start()) + 1
            lines = raw.splitlines()
            text = lines[line - 1].strip() if 0 <= line - 1 < len(lines) else ""
            out.append(f"{rel}:{line}  {text[:88]}")
    return out


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--sites", action="store_true", help="print every hit with its line")
    args = ap.parse_args()

    files = scan()

    print("CONTROL -- the six near-dead members the record keeps at one site each.")
    print("If any reads 0 here, this predicate is broken and its zeroes mean nothing.\n")
    broken = False
    for full, expected in CONTROL.items():
        ty, name = full.split("::")
        total = sum(len(hits_for(files, p)) for p in reach_paths(ty, name).values())
        flag = "" if total >= expected else "   <-- PREDICATE BROKEN"
        if total < expected:
            broken = True
        print(f"  {full:24} {total:3}  (record says {expected}){flag}")

    print("\nDELETION CANDIDATES, all three reach-paths.\n")
    live = []
    for ty, names in CANDIDATES.items():
        for name in names:
            paths = reach_paths(ty, name)
            found = {k: hits_for(files, p) for k, p in paths.items()}
            total = sum(len(v) for v in found.values())
            mark = "  LIVE" if total else ""
            detail = " ".join(f"{k}={len(v)}" for k, v in found.items() if v)
            print(f"  {ty + '::' + name:24} {total:3}  {detail}{mark}")
            if total:
                live.append((f"{ty}::{name}", found))

    print("\nVARIANTS AND ENUMS\n")
    for v in VARIANTS:
        p = re.compile(re.escape(v))
        h = hits_for(files, p)
        print(f"  {v:24} {len(h):3}{'  LIVE' if h else ''}")
        if h:
            live.append((v, {"path": h}))

    print(f"\n{len(live)} of {sum(len(v) for v in CANDIDATES.values()) + len(VARIANTS)} "
          f"deletion candidates have at least one candidate site.")
    print("A candidate site is not a live construction: read it before acting. "
          "A name at zero under all three paths is safely dead.")
    if broken:
        print("\nTHE CONTROL FAILED. Do not quote any zero above.")

    if args.sites:
        print("\n--- SITES ---")
        for name, found in live:
            print(f"\n{name}")
            for kind, hs in found.items():
                for h in hs:
                    print(f"  [{kind}] {h}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
