#!/usr/bin/env python3
"""v130 persistent D108 hardware data-phase model for GDR-8050L unlock-log session.

Keeps the firmware running after each packet returns to the main loop and feeds
captured CDB/DATA IN/DATA OUT steps from the real unlock log in one emulator
process.  This is the bridge from independent packet probes to an eventual
SCSI/iSCSI-facing command server.
"""
from __future__ import annotations

import argparse, json, os, re, subprocess, sys
from pathlib import Path

WORKSPACE = os.environ.get('XDVD_WORKSPACE', r'D:\vscode-workspace\xbox-dvd-tools-v2')
FWBIN = os.environ.get('GDR8050L_FWBIN', r'D:\vscode-workspace\xbox-dvd-tools-v2\firmware\GDR-8050L\8050L_fw_plaintext_256k.bin')
SECTION_RE = re.compile(r"^--- (?P<kind>Sending CDB|Received Data|Sending Data) \[(?P<label>[^\]]+)\] \((?P<n>\d+) bytes\) ---")
HEX_RE = re.compile(r"\b[0-9A-Fa-f]{2}\b")


def parse_log(path: Path) -> dict[tuple[str,str], bytes]:
    out={}
    lines=path.read_text(encoding='utf-8', errors='replace').splitlines()
    i=0
    while i < len(lines):
        m=SECTION_RE.match(lines[i].strip())
        if not m:
            i+=1; continue
        kind,label,n=m.group('kind'),m.group('label'),int(m.group('n'))
        i+=1; toks=[]
        while i < len(lines) and not lines[i].startswith('--- '):
            if lines[i].startswith('----------------'):
                i+=1; break
            toks.extend(HEX_RE.findall(lines[i])); i+=1
        data=bytes(int(x,16) for x in toks[:n])
        if len(data)==n: out[(kind,label)]=data
    return out

def require(sections, kind, label):
    key=(kind,label)
    if key not in sections: raise SystemExit(f'[-] missing {kind} [{label}] in unlock log')
    return sections[key]

def hx(data: bytes) -> str:
    return ' '.join(f'{b:02X}' for b in data)

def run_checked(cmd, cwd: Path, stdout_file: Path|None=None):
    if stdout_file:
        with stdout_file.open('w', encoding='utf-8', errors='replace') as fh:
            res=subprocess.run(cmd, cwd=str(cwd), stdout=fh, stderr=subprocess.STDOUT)
    else:
        res=subprocess.run(cmd, cwd=str(cwd))
    if res.returncode != 0:
        raise SystemExit(res.returncode)


def main() -> int:
    ap=argparse.ArgumentParser(description='Run v130 persistent GDR-8050L D108 hardware data-phase model')
    ap.add_argument('xdvd_json')
    ap.add_argument('iso', nargs='?')
    ap.add_argument('--log', help='Unlock log path; default is .log next to xdvd_json')
    ap.add_argument('--max-cycles', default='220000')
    ap.add_argument('--no-host-gate', action='store_true', help='disable v130 experimental host-command re-entry gate')
    ap.add_argument('--host-gate-08a8', default='0x04', help='forced 0x08A8 state value at 0x900240EC when host gate is enabled')
    ap.add_argument('--no-clear-08a8-after-frame', action='store_true', help='disable v130 experimental 0x08A8 clear edge after packet frame acceptance')
    ap.add_argument('--no-clear-08a8-after-tail', action='store_true', help='do not clear 0x08A8 after the packet tail is accepted')
    ap.add_argument('--no-clear-d950-after-tail', action='store_true', help='do not clear D950 after the packet tail is accepted')
    ap.add_argument('--d950-zero-after-post-frame-reads', default='4', help='number of post-frame D950 reads that still return 0x40 before decaying to zero; -1 means never decay')
    ap.add_argument('--no-irq-after-d108-data-phase', action='store_true', help='disable v130 second service IRQ edge after 0x9001CD4D/D108 data-phase request')
    ap.add_argument('--no-d108-hw-data-phase', action='store_true', help='disable v130 D108 hardware data-phase completion model')
    ap.add_argument('--debug-trace', action='store_true', help='emit verbose hardware/session per-byte events; normally off for speed')
    ap.add_argument('--debug-instructions', action='store_true', help='print every instruction; very slow, only for short debugging')
    args=ap.parse_args()
    script_dir=Path(__file__).resolve().parent
    xdvd=Path(args.xdvd_json)
    log_path=Path(args.log) if args.log else xdvd.with_suffix('.log')
    if not log_path.exists(): raise SystemExit(f'[-] unlock log not found: {log_path}; pass --log PATH')
    print(f'[V130] XDVD sidecar: {args.xdvd_json}', flush=True)
    if args.iso: print(f'[V130] ISO metadata: {args.iso}', flush=True)
    print(f'[V130] Unlock log: {log_path}', flush=True)
    print('[V130] Persistent one-process unlock-log session: firmware is not reset between commands.', flush=True)

    sec=parse_log(log_path)
    steps=[
        ('step01_locked_read_capacity','in','Step 1: Init Read Cap','Step 1: Init Read Cap'),
        ('step02_locked_mode_sense_3e','in','Step 2: Mode Sense','Step 2: Mode Sense'),
        ('step03_read_dvd_structure_ad_c0','in','Step 3: Read DVD Struct 0xAD','Step 3: Read DVD Struct 0xAD'),
        ('step04_mode_select_chal1','out','Step 4: Mode Select Chal 1','Step 4: Mode Select Chal 1'),
        ('step05_mode_sense_verify1','in','Step 5: Mode Sense Verify 1','Step 5: Mode Sense Verify 1'),
        ('step06_mode_select_chal2','out','Step 6: Mode Select Chal 2','Step 6: Mode Select Chal 2'),
        ('step07_mode_sense_verify2','in','Step 7: Mode Sense Verify 2','Step 7: Mode Sense Verify 2'),
        ('step08_mode_select_partition1_unlock','out','Step 8: Mode Select Part1 Unlock','Step 8: Mode Select Part1 Unlock'),
        ('step09_sticky_descramble_mode31','out','Step 9: Sticky Descrambling','Step 9: Sticky Descrambling'),
        ('step10_unlocked_read_capacity','in','Step 10: Final Verify','Step 10: Final Verify'),
    ]
    commands=[]
    for i,(tag,direction,cdb_label,data_label) in enumerate(steps):
        cdb=require(sec,'Sending CDB',cdb_label)
        data=require(sec,'Received Data' if direction=='in' else 'Sending Data',data_label)
        entry={'tag':tag,'title':tag,'direction':direction,'cdb_hex':hx(cdb)}
        if direction=='in': entry['data_in_hex']=hx(data)
        else: entry['data_out_hex']=hx(data)
        commands.append(entry)
        print(f'[V130] {i+1:02d} {tag} {direction} cdb={hx(cdb)} bytes={len(data)}', flush=True)

    stem='gdr8050l_auth_v130_persistent_d108_hw_data_phase'
    trace_csv=f'{stem}_trace.csv'
    config_json=script_dir / f'{stem}_config.json'
    output_txt=script_dir / f'{stem}.output.txt'
    verify_md=f'{stem}_verify.md'
    cfg={
        'test_name':'GDR-8050L v130 persistent D108 hardware data-phase model',
        'workspace':WORKSPACE,
        'target_profile':'GDR-8050L',
        'custom_bin':FWBIN,
        'custom_base_addr':'0x90000000',
        'entry_point':'0x90000020',
        'max_cycles':int(str(args.max_cycles),0),
        'header_skip_bytes':'0x0',
        'config_version':'v100',
        'requires_emulator_version':'V7-direct-config-trace-v100-gcc-version-sync',
        'initial_registers':{'SP':'0x003FF000'},
        'memory_aliases':[{'name':'SRAM alias 0x80000000->0x00000000','from':'0x80000000','to':'0x00000000','size':'0x400000'}],
        'trace':{
            'enabled':True,
            'live_instruction_log': bool(args.debug_instructions),
            'out_csv':trace_csv,
            'watchpoints':[
                {'start':'0x0000DF70','size':'0x40','kind':'write','name':'watch_df70'},
                {'start':'0x0000DF90','size':'0x08','kind':'read','name':'watch_df90'},
                {'start':'0x0000DF2A','size':'0x01','kind':'write','name':'watch_df2a'},
                {'start':'0x000005B1','size':'0x01','kind':'access','name':'watch_05B1'},
                {'start':'0x000005CD','size':'0x02','kind':'access','name':'watch_05CD'},
                {'start':'0x00000143','size':'0x01','kind':'access','name':'watch_0143'},
                {'start':'0x0000014A','size':'0x01','kind':'access','name':'watch_014A'},
                {'start':'0x000000DE','size':'0x01','kind':'access','name':'watch_00DE_host_gate'},
                {'start':'0x0000016D','size':'0x01','kind':'access','name':'watch_016D_host_gate'},
                {'start':'0x000008A8','size':'0x08','kind':'access','name':'watch_08A8_service_state'},
                {'start':'0x000007AC','size':'0x04','kind':'access','name':'watch_07AC_packet_state'},
                {'start':'0x00000778','size':'0x01','kind':'access','name':'watch_0778_dispatch'},
                {'start':'0x000007E8','size':'0x08','kind':'access','name':'watch_07E8_cdb_mirror'},
                {'start':'0x0000D950','size':'0x08','kind':'access','name':'watch_D950_packet_status'},
                {'start':'0x0000DA18','size':'0x01','kind':'access','name':'watch_DA18_packet_branch'},
                {'start':'0x00000278','size':'0x01','kind':'access','name':'watch_0278_media_branch'},
                {'start':'0x000005B6','size':'0x02','kind':'access','name':'watch_05B6_scheduler'},
                {'start':'0x000006F1','size':'0x03','kind':'access','name':'watch_06F1_dispatch'},
                {'start':'0x00000132','size':'0x04','kind':'access','name':'watch_0132_host_flags'},
                {'start':'0x0000002C','size':'0x02','kind':'read','name':'watch_002C_service_irq'},
                {'start':'0x0000D108','size':'0x02','kind':'access','name':'watch_D108_data_phase_irq'},
                {'start':'0x0000D904','size':'0x02','kind':'access','name':'watch_D904_packet_status'},
            ],
            'breakpoints':[
                {'address':'0x90006608','name':'main_loop_90006608'},
                {'address':'0x90008824','name':'packet_timer_done_90008824'},
                {'address':'0x9001BBAC','name':'host_gate_9001BBAC'},
                {'address':'0x900240EC','name':'packet_service_gate_900240EC'},
                {'address':'0x900210C4','name':'packet_service_900210C4'},
                {'address':'0x90024283','name':'packet_fe_frame_copy_90024283'},
                {'address':'0x90021169','name':'packet_tail_copy_90021169'},
                {'address':'0x900210C1','name':'packet_service_ret_900210C1'},
                {'address':'0x900222C9','name':'data_phase_dispatch_900222C9'},
                {'address':'0x900222FA','name':'data_phase_poll_result_900222FA'},
                {'address':'0x900222FF','name':'data_phase_ready_900222FF'},
                {'address':'0x90022301','name':'data_phase_call_9001CD4D_90022301'},
                {'address':'0x9001CD4D','name':'data_inout_helper_9001CD4D'},
                {'address':'0x900210A8','name':'packet_data_ready_poll_900210A8'},
                {'address':'0x90022309','name':'df00_copy_loop_90022309'},
                {'address':'0x9002236C','name':'df00_copy_loop_body_9002236C'},
                {'address':'0x900223C0','name':'df00_copy_d904_wait_900223C0'},
                {'address':'0x900223CF','name':'df00_copy_submit_90006470_900223CF'},
                {'address':'0x90006470','name':'lowlevel_submit_90006470'},
                {'address':'0x900099EA','name':'gdr_data_transfer_helper_900099EA'},
                {'address':'0x900099FF','name':'gdr_data_in_source_read_900099FF'},
                {'address':'0x90009A02','name':'gdr_df70_write_90009A02'},
                {'address':'0x90009A0E','name':'gdr_df2a_submit_90009A0E'},
                {'address':'0x90017775','name':'irq_status_handler_90017775'},
                {'address':'0x9001B285','name':'main_scheduler_9001B285'},
                {'address':'0x9001B71B','name':'packet_scheduler_9001B71B'},
                {'address':'0x9001CD62','name':'data_phase_d108_return_9001CD62'},
                {'address':'0x9001B6D3','name':'packet_branch_9001B6D3'},
                {'address':'0x9001C83E','name':'set_05B1_20_9001C83E'},
                {'address':'0x9001C8B7','name':'set_05B1_04_9001C8B7'},
            ],
            'gdr_hardware_model':{
                'enabled':True,
                'emit_events':bool(args.debug_trace),
                'timer':{'enabled':True,'addr':'0x0000DC54','base':'0xF000','ticks_per_cycle':'0x2000'},
                'packet_status':{'enabled':True,'source':'v123 persistent session shared GDR D904/D905 model'}
            },
            'gdr_packet_session_model':{
                'enabled':True,
                'emit_events':True,
                'commands':commands,
                'packet_flags':'0x20',
                'main_loop_pc':'0x90006608',
                'timer_done_pc':'0x90008824',
                'stop_on_complete':True,
                'host_event_gate':{
                    'enabled': not bool(args.no_host_gate),
                    'force_016d_active': True,
                    'force_016d_value': '0x01',
                    'force_00de_pollable': True,
                    'force_00de_value': '0x01',
                    'force_08a8': str(args.host_gate_08a8),
                    'force_d950_value': '0x10',
                    'post_frame_d950_value': '0x40',
                    'clear_08a8_after_frame': not bool(args.no_clear_08a8_after_frame),
                    'clear_08a8_after_tail': not bool(args.no_clear_08a8_after_tail),
                    'clear_d950_after_tail': not bool(args.no_clear_d950_after_tail),
                    'd950_zero_after_post_frame_reads': str(args.d950_zero_after_post_frame_reads),
                    'irq_after_d108_data_phase': not bool(args.no_irq_after_d108_data_phase),
                    'complete_data_phase_on_d108': not bool(args.no_d108_hw_data_phase),
                    'seed_mem8': {
                        '0x000005B1':'0x20',
                        '0x000000DE':'0x01',
                        '0x0000016D':'0x01',
                        '0x000008A8': str(args.host_gate_08a8),
                        '0x000008A9':'0x00',
                        '0x0000DA18':'0x00',
                    },
                },
            },
        },
        'notes':['v130: persistent session with D108 hardware data-phase completion model; D108 acts as the host-interface/DMA completion edge after packet data-phase setup.']
    }
    if args.iso:
        cfg['media']={'type':'iso9660/raw2048','iso_path':args.iso,'sector_size':2048,'present':True,'tray_state':'closed','source':'--iso'}
    config_json.write_text(json.dumps(cfg, indent=2), encoding='utf-8')
    print(f'[OK] wrote persistent config: {config_json.name}', flush=True)
    run_checked([sys.executable,'mn103s_emu.py',config_json.name], cwd=script_dir, stdout_file=output_txt)
    print(f'[OK] emulator output: {output_txt.name}', flush=True)
    res=subprocess.run([sys.executable,'analyze_gdr_persistent_session.py',trace_csv,'--config',config_json.name,'--out',verify_md], cwd=str(script_dir))
    if res.returncode == 0:
        print('[OK] v130 persistent D108 hardware data-phase model completed an exact session.', flush=True)
    else:
        print(f'[WARN] v130 verifier returned {res.returncode}; this diagnostic may still be incomplete, but output/trace were written.', flush=True)
    print(f'[NEXT] Send {stem}.output.txt, {trace_csv}, {config_json.name}, and {verify_md}.', flush=True)
    return 0

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