"""Declared input/probe coordinates for the inserted native Transfers row.

The semantic legacy option stays the same. Only its physical row moves after
Icing; test value/lock/storage/physics/ABI assertions are never changed here.
Both current and historical ROMs derive their own input coordinates.
"""
from functools import lru_cache
from pathlib import Path

@lru_cache(maxsize=64)
def transfer_row_shift(rom):
    data=Path(rom).read_bytes()
    count=data.count(b'\x00\x10Transfers    \x00')
    assert count<=1,'ambiguous native Transfers label in fixture ROM'
    return 2 if count else 0

@lru_cache(maxsize=64)
def refinements_enabled(rom):
    data=Path(rom).read_bytes()
    return data[0x258000:0x258002]==bytes.fromhex('6000')

def row_word(rom,legacy_word):
    value=legacy_word+(transfer_row_shift(rom)if legacy_word>=26 else 0)
    if refinements_enabled(rom):
        if legacy_word in [42,46]:
            raise ValueError('OT SO Rules and Faceoff Dots are permanent On, without menu rows')
        value-=2*int(legacy_word>42)+2*int(legacy_word>46)
    return value

def down_count(rom,legacy_count):
    return legacy_count+(transfer_row_shift(rom)//2 if legacy_count>=13 else 0)-(int(legacy_count>=22)+int(legacy_count>=24) if refinements_enabled(rom) else 0)

def native_down_count(rom,legacy_count):
    """Native New Playoffs skips Goalie row6 before the later option rows.
    Removed OT SO/Faceoff rows therefore have native input indices20/22,
    whereas the season menu indices are21/23. Keep the explicit semantic
    legacy destination, applying the appropriate native input coordinates.
    """
    return legacy_count+(transfer_row_shift(rom)//2 if legacy_count>=12 else 0)-(int(legacy_count>=21)+int(legacy_count>=23) if refinements_enabled(rom) else 0)

def render_probe(source,rom,legacy_words):
    """Move explicit renderer D7 coordinates in one pass, preserving its ABI."""
    import re
    words={legacy_words}if isinstance(legacy_words,int)else set(legacy_words)
    counts={word:0 for word in words}
    def replace(m):
        word=int(m[2])
        if word not in words:return m[0]
        counts[word]+=1
        return m[1]+str(row_word(rom,word))+m[3]
    result=re.sub(r'(cmpi\.w\s+#)([0-9]+)(\s*,\s*%d7)',replace,source)
    assert all(n==1 for n in counts.values()),('explicit menu observer coordinates',counts)
    return result


def requested_menu_expectations(rom,name,checks):
    """Explicit jdc policy, not values inferred from the tested implementation.
    Valid saved companion selections stay intact. Only fresh/reset defaults and
    the two deliberately permanent rules replace historical UI expectations.
    """
    if not refinements_enabled(rom):
        return
    for frame,expected in checks.items():
        for field in ['faceoff','dots','strict']:
            if field in expected:expected[field]=1
        defaults=name.startswith('fresh-') or name.startswith('restart-') and int(frame) in [3900,4600]
        if defaults:
            for field in ['height','rate','alt','plan']:
                if field in expected:expected[field]=1
