#!/usr/bin/env python3
"""Test the hypothesis that a unit sub-record's runtime fields are laid out in
*schema declaration order*, one 4-byte slot each.

The IDXD string pool lists a sub-record's field names in declaration order,
including the ones this .tbl leaves at their default (they appear as a bare key
with no preceding value). If the runtime struct is `base + 4*index` over that
list, then the offsets the solver found independently must all satisfy it.

That is a much stronger check than any single field's agreement count: one base
offset has to explain dozens of independently-derived anchors at once.

Usage: order_check.py <raw-token-dump> <tbl-hash> <SubRecord> <solved.txt>
"""
import re
import sys

NUMERIC = re.compile(r"^[-+]?[0-9]*\.?[0-9]+[fF]?$|^0x[0-9a-fA-F]+$")
VALUE_WORDS = {"Yes", "No", "YES", "NO", "On", "Off", "ON", "OFF"}


def section(dump, tbl, sub, subs):
    """Ordered token list of one sub-record of one .tbl."""
    toks, on = [], False
    for line in open(dump):
        if line.startswith("RAW "):
            on = line.split()[1] == tbl
            continue
        if on and line.startswith("T "):
            toks.append(line[2:].rstrip("\n"))
    starts = [i for i, t in enumerate(toks) if t in subs or t.lstrip("(") in subs]
    for n, i in enumerate(starts):
        name = toks[i].lstrip("(")
        if name != sub:
            continue
        end = starts[n + 1] if n + 1 < len(starts) else len(toks)
        return toks[i + 1 : end]
    return []


def keys_in_order(toks):
    """Field names, in declaration order: every token that is not a value.

    In the numeric sub-records every value is a number literal, so a token that
    is not numeric and not a Yes/No word is a key.
    """
    out = []
    for t in toks:
        if NUMERIC.match(t) or t in VALUE_WORDS:
            continue
        if t not in out:
            out.append(t)
    return out


def solved_offsets(path):
    out = {}
    for line in open(path):
        m = re.match(r"\s*(0x[0-9a-f]+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\d+)\s+(\d+)\s*$", line)
        if m:
            off, enc, field, agree, dist, bad = m.groups()
            out[field] = (int(off, 16), enc, int(agree), int(dist), int(bad))
    return out


def main():
    dump, tbl, sub, solvedf = sys.argv[1:5]
    subs = {"Generic", "Maneuver", "Shield", "Explosion", "Mass", "Effect", "SE"}
    keys = keys_in_order(section(dump, tbl, sub, subs))
    solved = solved_offsets(solvedf)
    anchors = [(i, k, solved[k]) for i, k in enumerate(keys) if k in solved]
    if not anchors:
        sys.exit(f"no solved field lands in {sub}")

    # base that the most anchors agree on
    votes = {}
    for i, k, (off, *_) in anchors:
        votes.setdefault(off - 4 * i, []).append(k)
    base, winners = max(votes.items(), key=lambda kv: len(kv[1]))
    print(f"# {sub}: {len(keys)} declared fields, {len(anchors)} solved anchors")
    print(f"# best base = {base:#x}  ->  {len(winners)}/{len(anchors)} anchors fit "
          f"offset = base + 4*index\n")
    for i, k in enumerate(keys):
        off = base + 4 * i
        if k in solved:
            so, enc, agree, dist, bad = solved[k]
            mark = "fits" if so == off else f"CONFLICT solver={so:#x}"
            print(f"  +{off:#05x} [{i:3d}] {k:<34} {enc} agree={agree:<3} "
                  f"dist={dist:<3} {mark}")
        else:
            print(f"  +{off:#05x} [{i:3d}] {k:<34} —   (never valued on disc)")


if __name__ == "__main__":
    main()
