#!/usr/bin/env python3
"""Menu78 native pixel conversion and full1120-cell graphics owner contract.
The4KB map/8KB pattern chunks are declared VDP routine fixtures. Cold boots
supply real startup/menu screenshots on the unmodified candidate and parent.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,struct,json,hashlib,subprocess,os
from PIL import Image
from native_probe import NativeProbe
ap=argparse.ArgumentParser();ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--baseline',type=Path,required=True);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True);b=a.rom.read_bytes();parent=a.baseline.read_bytes();sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
assert sha(a.baseline)=='6e66c0afe2496eb3f6a688c29888ca62b17aee0b0f51efec96d11dce5e842b31'
asset=(h.parent/'data/menu-background-78.bin').read_bytes();assert b[0x240000:0x240000+len(asset)]==asset
assert b[0x2a0000:0x2aa000]==parent[0x2a0000:0x2aa000],'startup picture/code changed'
assert b[0x2fe280:0x2fe400]==parent[0x2fe280:0x2fe400],'startup routine changed'
subprocess.run(['python3',str(h.parent/'introimg.py'),str(h.parent/'data/menu-background-78.png'),str(o/'rebuilt.bin')],check=True,stdout=subprocess.DEVNULL);assert(o/'rebuilt.bin').read_bytes()==asset,'image conversion is not reproducible'
assert struct.unpack_from('>HH',asset,32)==(40,28);imap=struct.unpack_from('>1120H',asset,36);count=struct.unpack_from('>H',asset,2276)[0];assert len(asset)==2278+count*32 and max(imap)<=count
original=[0]*2048;expected=[0]*2048;expected_vram=bytearray(65536);strip=set(range(40))|{64,65,66,67}
for y in range(28):
 for x in range(40):
  i=y*64+x;tile=0xf3+y*40+x;original[i]=(0x2000 if i in strip else 0x6000)|tile;expected[i]=(0x2000|tile)if i in strip else tile
  if i in strip:continue
  source=asset[2278+(imap[y*40+x]-1)*32:2278+imap[y*40+x]*32];mapped=bytearray()
  for n,v in enumerate(source):
   row=n//4;lut=[0,4,14,7,6,5,5,3,1];hi=(5 if row%2==0 else 6)if v>>4==5 else lut[v>>4];lo=(6 if row%2==0 else 5)if v&15==5 else lut[v&15];mapped.append((hi<<4)|lo)
  expected_vram[tile*32:tile*32+32]=mapped
expected_vram[0xc000:0xd000]=struct.pack('>2048H',*expected)
probe=NativeProbe(a.rom,h)
def command(at,read=False):return((at&0x3fff)<<16)|(at>>14)|(0 if read else 0x40000000)
def trial(chunk):
 body=[' move.w #0x8104,0xc00004',' move.w #0x8f02,0xc00004',' move.l #0x40000000,0xc00004',' move.w #32767,%d0','clear: move.w #0,0xc00000',' dbra %d0,clear',f' move.l #0x{command(0xc000):x},0xc00004',' lea 0x230000,%a0',' move.w #2047,%d0','copy: move.w (%a0)+,0xc00000',' dbra %d0,copy',' move.l #0x7fffffff,0xffffd3d4',' move.w #0x550,%d4',' jsr 0x2ea000',f' move.l #0x{command(0xc000,True):x},0xc00004',' lea 0xffff4000,%a0',' move.w #2047,%d0','map: move.w 0xc00000,(%a0)+',' dbra %d0,map',f' move.l #0x{command(0xf3*32+chunk*8192,True):x},0xc00004',' move.w #4095,%d0','pixels: move.w 0xc00000,(%a0)+',' dbra %d0,pixels']
 raw=probe.run(o/('native-'+str(chunk)),'\n'.join(body),12288,data=struct.pack('>2048H',*original));assert raw[:4096]==expected_vram[0xc000:0xd000],('native background changed foreign strip or map',chunk);start=0xf3*32+chunk*8192;assert raw[4096:]==expected_vram[start:start+8192],('native picture pixels differ, including high signed-word offsets',chunk,next((i for i,(x,y)in enumerate(zip(raw[4096:],expected_vram[start:start+8192]))if x!=y),None));return dict(chunk=chunk,sha256=hashlib.sha256(raw).hexdigest())
with ThreadPoolExecutor(max_workers=3)as pool:rows=list(pool.map(trial,range(5)))
def boot(label,rom):
 p=o/label;p.mkdir(exist_ok=True);(p/'battery.srm').write_bytes(bytes(65536));(p/'commands.txt').write_text('200 snap\n350 snap\n900 snap\n1100 exit\n');env={k:v for k,v in os.environ.items()if not k.startswith('GPGX_')};env.update(GPGX_DEV='513',GPGX_SRM=str(p/'battery.srm'))
 with(p/'log').open('w')as log:subprocess.run(['python3',str(h/'test_core.py'),str(rom.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=45)
 return p
with ThreadPoolExecutor(max_workers=2)as pool:paths=list(pool.map(lambda z:boot(*z),[('current',a.rom),('parent',a.baseline)]))
for name in ['0000.png','0001.png']:assert Image.open(paths[0]/name).tobytes()==Image.open(paths[1]/name).tobytes(),('startup changed visually',name)
assert Image.open(paths[0]/'0002.png').tobytes()!=Image.open(paths[1]/'0002.png').tobytes(),'menu failed to change'
result=dict(rom_sha256=sha(a.rom),source_sha256=sha(h.parent/'data/menu-background-78.png'),asset_sha256=hashlib.sha256(asset).hexdigest(),tiles=count,all1120_map_cells=True,all_background_pixels=True,foreign44cells_preserved=True,startup_asset_and_routine_exact=True,startup_two_native_frames_exact=True,actual_menu_changed=True,native_chunks=rows)
(o/'results.json').write_text(json.dumps(result,indent=2)+'\n');print('PASS menu78: all1120 cell ownership, every native background pixel/high offsets, deterministic source conversion, startup bytes and2 actual native frames unchanged')
