# readsram.py file.nv|file.srm  -- prints the season table stored in battery RAM (and checks the hack's checksum)
import sys, argparse
ap = argparse.ArgumentParser(description='Read legacy or three-slot NHL94 battery saves')
ap.add_argument('save')
ap.add_argument('rom', nargs='?')
ap.add_argument('--slot', choices=['full','light','mini'])
args=ap.parse_args()
d=open(args.save,'rb').read()
if len(d) in (16384,65536):
    even,odd=d[0::2],d[1::2]
    b=odd if odd[0x1000:0x1004]==b'SESN' or odd[0x1EA0:0x1EA4]==b'S3SV' else even
elif len(d) in (8192,32768): b=d
else: raise SystemExit('Expected8/32KB logical, or16/64KB interleaved SRAM')
active=int.from_bytes(b[0x1EA6:0x1EA8],'big') if b[0x1EA0:0x1EA6]==b'S3SV\x00\x01' else 0
if active:
    print('Three-slot save; active:',{1:'Full',2:'Light',3:'Mini'}.get(active,'invalid'))
if args.slot:
    slot={'full':1,'light':2,'mini':3}[args.slot]
    if active and slot!=active:
        mask=int.from_bytes(b[0x1EA8:0x1EAA],'big')
        if not mask&(1<<(slot-1)) or len(b)<(slot+1)*8192:raise SystemExit('That slot has no saved snapshot')
        b=b[slot*8192:(slot+1)*8192]
    elif not active:
        length=int.from_bytes(b[0x100A:0x100C],'big')
        actual=3 if length==8 else 2 if length==31 else 1
        if slot!=actual:raise SystemExit('Legacy file contains only its one existing season')
b=b[:8192]
AB = ['ANA','BOS','BUF','CGY','CAR','CHI','COL','CBJ','DAL','DET','EDM','FLA','LAK','MIN','MTL','NSH',
      'NJD','NYI','NYR','OTT','PHI','PIT','SJS','SEA','STL','TBL','TOR','UTA','VAN','VGK','WSH','WPG']
s = sum(b[:0x1FFE]) & 0xff
print('checksum', 'OK' if b[0x1FFF] == s and b[0x1FFE] == (~s & 0xff) else 'BAD', hex(s), b[0x1FFE:0x2000].hex())
blk = b[0x1000:0x1210]
w = lambda o: int.from_bytes(blk[o:o + 2], 'big')
print('magic', blk[:4], 'games', w(4))
rows = []
for t in range(32):
    r = [w(16 + 16 * t + 2 * k) for k in range(8)]
    if r[0]: rows.append((2 * r[1] + r[3] + r[6], AB[t], r))
print('team  GP  W OTW OTL  L  T  GF  GA  PTS   (W = regulation wins; OTW/OTL include shootouts)')
for pts, ab, r in sorted(rows, reverse=True):
    print(f'{ab:4} {r[0]:3} {r[1] - r[7]:2} {r[7]:3} {r[3]:3} {r[2]:2} {r[6]:2} {r[4]:3} {r[5]:3} {pts:4}')

# player stats (SRAM $1220: 32 teams x 100 bytes: slots 1..25 x (goals, assists, shots word)), names from the ROM
fmt = int.from_bytes(blk[14:16], 'big')
if args.rom and fmt in (2, 3):
    import struct as st
    rom = open(args.rom, 'rb').read()
    def names(t):
        a = st.unpack('>I', rom[0x30E + 4 * t:0x312 + 4 * t])[0]; h = st.unpack('>6H', rom[a:a + 12]); i = a + h[0]; out = []
        while i < a + h[2] - 2:
            n = st.unpack('>H', rom[i:i + 2])[0]; out.append(rom[i + 2:i + n].rstrip(b'\x00').decode('latin1')); i += n + 8
        return out
    rows = []; problems = 0
    for t in range(32):
        nm = names(t); row = b[0x1220 + 100 * t:0x1220 + 100 * t + 100]
        tg = ta = ts = 0
        for k in range(1, 26):
            g, a_, sog = row[4 * (k - 1)], row[4 * (k - 1) + 1], st.unpack('>H', row[4 * (k - 1) + 2:4 * k])[0]
            tg += g; ta += a_; ts += sog
            if sog < g: problems += 1; print('  !! shots < goals', AB[t], nm[k])
            if g or a_ or sog: rows.append((g + a_, g, a_, sog, nm[k] if k < len(nm) else '?', AB[t]))
        gf = int.from_bytes(blk[16 + 16 * t + 8:16 + 16 * t + 10], 'big')
        # A shootout deciding goal counts in the final team score only. Legacy
        # saves need no migration or new field; the difference is bounded by OTW.
        otw = int.from_bytes(blk[16 + 16 * t + 14:16 + 16 * t + 16], 'big')
        deciders = gf - tg
        if not 0 <= deciders <= otw:
            problems += 1; print(f'  !! {AB[t]}: player goals {tg}, team GF {gf}, OTW {otw}')
        elif deciders:
            print(f'  {AB[t]}: {deciders} shootout deciding goal(s), excluded from player statistics')
        if ta > 2 * tg: problems += 1; print(f'  !! {AB[t]}: assists {ta} > 2 x goals {tg}')
    rows.sort(reverse=True)
    print('scoring leaders:  pts  g  a  sog')
    for r in rows[:15]: print(f'  {r[4]:24} {r[5]} {r[0]:3} {r[1]:3} {r[2]:3} {r[3]:4}')
    print('  total goals', sum(r[1] for r in rows), 'assists', sum(r[2] for r in rows), 'shots', sum(r[3] for r in rows), ' problems:', problems)

# goalie stats (format 3, SRAM $E20: 32 teams x 3 goalies x (GP, W, GA:12 | SA:12))
if args.rom and fmt == 3:
    print('goalies:                    GP  W   GA   SA   SV%')
    gp_ = []; gproblems = 0
    for t in range(32):
        nm = names(t); tg = tw = tga = 0
        tr = [int.from_bytes(blk[16 + 16 * t + 2 * k:18 + 16 * t + 2 * k], 'big') for k in range(7)]
        for g in range(3):
            r = b[0xE20 + 15 * t + 5 * g:0xE20 + 15 * t + 5 * g + 5]
            gp, wn = r[0], r[1]; ga = (r[2] << 4) | (r[3] >> 4); sa = ((r[3] & 15) << 8) | r[4]
            tg += gp; tw += wn; tga += ga
            if sa < ga: gproblems += 1; print('  !! SA < GA', AB[t], nm[g])
            if gp: gp_.append((gp, AB[t], nm[g], wn, ga, sa))
        # Keeper GA excludes shootout deciders and native empty-net goals.
        if tr[0] and (tg != tr[0] or tw != tr[1] or tga > tr[5]):
            gproblems += 1; print(f'  !! {AB[t]}: goalies GP {tg} W {tw} GA {tga} vs team GP {tr[0]} W {tr[1]} GA {tr[5]}')
    for gp, ab, n, wn, ga, sa in sorted(gp_, reverse=True)[:12]:
        print(f'  {n:24} {ab} {gp:3} {wn:2} {ga:4} {sa:4}  {(sa - ga) / sa if sa else 0:.3f}')
    print('  goalies with games:', len(gp_), ' problems:', gproblems)

# playoff tree as playoffs.s builds it (top 3 per division + 2 wild cards per conference)
DIV = [3,0,0,3,1,2,2,1,2,0,3,0,3,2,0,2,1,1,1,0,1,1,3,3,2,0,0,2,3,3,1,2]
def seeds():
    key = {}
    for t in range(32):
        r = [w(16 + 16 * t + 2 * k) for k in range(8)]
        gd = max(-128, min(127, r[4] - r[5]))
        key[t] = ((2 * r[1] + r[3] + r[6]) * 256 + gd + 128) * 32 + 31 - t
    taken = set(); dr = {}
    for dv in range(4):
        dr[dv] = []
        for _ in range(3):
            t = max((x for x in range(32) if DIV[x] == dv and x not in taken), key=key.get); taken.add(t); dr[dv].append(t)
    tree = [None] * 16; wc = {}
    for c in (0, 1):
        w1 = max((x for x in range(32) if DIV[x] // 2 == c and x not in taken), key=key.get); taken.add(w1)
        w2 = max((x for x in range(32) if DIV[x] // 2 == c and x not in taken), key=key.get); taken.add(w2)
        a, b = dr[2 * c], dr[2 * c + 1]
        if key[b[0]] > key[a[0]]: a, b = b, a
        base = 8 if c == 0 else 0
        tree[base:base + 8] = [a[0], w2, a[1], a[2], b[0], w1, b[1], b[2]]; wc[c] = (w1, w2)
    return tree
if blk[:4] == b'SESN':
    t = seeds()
    print('playoff tree (West slots 0-7, East 8-15):', ' '.join(AB[x] for x in t))
