#!/usr/bin/env python3
"""Apply SNESGame' fixes to the EmulatorJS bsnes glue file.

Usage: patch-glue.py <pristine bsnes_libretro.js> <output bsnes_libretro.js>

The input must be the bsnes_libretro.js inside EmulatorJS's nightly
bsnes-wasm.data (built 2026-07-25, see README.md); its SHA-256 is checked.
Every patch must match exactly once, or nothing is written.

bsnes runs its CPU, audio and video chips as coroutines (libco). In this
WebAssembly build libco switches them with Emscripten fibers, which unwind
and rewind the call stack through Asyncify. The build left two libco
functions undefined and loses return values across fiber switches, which
breaks save states. It also has no volume control.
"""
import hashlib
import sys

PRISTINE_SHA256 = '589690cd56f8753a7d59c7993092a0c3bf2ad3b6d86c96786a97958c7d1f1be0'

# The libco Emscripten backend (libco/emscripten_fiber.c) allocates this much
# Asyncify stack per coroutine.
ASYNCIFY_STACK_SIZE = 131072

PATCHES = [
    (
        # bsnes asks whether coroutines can be serialized directly. The answer
        # for fibers is no, which makes bsnes run each chip to a safe point
        # before saving instead. Unpatched, the call aborts the emulator.
        'co_serializable returns 0',
        'function _co_serializable(...args){abort("missing function: co_serializable")}',
        'function _co_serializable(...args){return 0}',
    ),
    (
        # bsnes rebuilds its chip threads in place when a state is loaded. With
        # this backend a thread handle is an emscripten_fiber_t, so re-deriving
        # means resetting that struct the way emscripten_fiber_init does: C
        # stack pointer to its base, entry back to libco's co_thunk with the new
        # entry point as its argument, Asyncify stack pointer to its start.
        'co_derive re-initializes the fiber',
        'function _co_derive(...args){abort("missing function: co_derive")}',
        'function _co_derive(handle,size,entry){'
        'if(!Module.snesCoThunk)abort("co_derive before any fiber started");'
        'HEAPU32[handle+8>>2]=HEAPU32[handle>>2];'
        'HEAPU32[handle+12>>2]=Module.snesCoThunk;'
        'HEAPU32[handle+16>>2]=entry;'
        'HEAPU32[handle+20>>2]=HEAPU32[handle+24>>2]-' + str(ASYNCIFY_STACK_SIZE) + ';'
        'return handle}',
    ),
    (
        # Every libco fiber starts in co_thunk. Remember its function pointer
        # the first time a fiber starts, for co_derive above.
        'remember co_thunk',
        'var entryPoint=HEAPU32[newFiber+12>>2];if(entryPoint!==0){',
        'var entryPoint=HEAPU32[newFiber+12>>2];if(entryPoint!==0){Module.snesCoThunk=entryPoint;',
    ),
    (
        # A fiber switch inside an exported call unwinds it early, and the
        # fiber trampoline finishes the call but discards its return value.
        # Keep the last one.
        'keep the return value of a call finished by the fiber trampoline',
        '_asyncify_start_rewind(asyncifyData);Asyncify.doRewind(asyncifyData)}}};var _emscripten_fiber_swap',
        '_asyncify_start_rewind(asyncifyData);Module.snesFiberResult=Asyncify.doRewind(asyncifyData)}}};var _emscripten_fiber_swap',
    ),
    (
        # Saving a state switches fibers, so save_state_info's result arrives
        # through the trampoline rather than as its return value.
        'EmulatorJSGetState reads the trampoline result',
        'function EmulatorJSGetState(){let info=_save_state_info();',
        'function EmulatorJSGetState(){Module.snesFiberResult=0;let info=_save_state_info()||Module.snesFiberResult;',
    ),
    (
        # save_state_info returns a stack buffer (not freeable) describing a
        # calloc'd state buffer (which is). Upstream freed the former and
        # passed a typed array for the latter. Copy the state, then free it.
        'EmulatorJSGetState frees the right buffer',
        'const data=HEAPU8.subarray(dataStart,dataStart+size);_free(info);_free(data);return new Uint8Array(data)}',
        'const data=new Uint8Array(HEAPU8.subarray(dataStart,dataStart+size));_free(dataStart);return data}',
    ),
    (
        # Route RetroArch's web audio through one gain node the page can reach,
        # for the volume slider and mute button.
        'audio gain node',
        'RWA.context=new ac;',
        'RWA.context=new ac;RWA.gain=RWA.context.createGain();'
        'RWA.gain.gain.value=Module.snesVolume==null?1:Module.snesVolume;'
        'RWA.gain.connect(RWA.context.destination);Module.snesAudio=RWA;',
    ),
    (
        'audio buffers play through the gain node',
        'bufferSource.connect(RWA.context.destination)',
        'bufferSource.connect(RWA.gain||RWA.context.destination)',
    ),
]


def main():
    if len(sys.argv) != 3:
        sys.exit(__doc__)
    src, dst = sys.argv[1], sys.argv[2]
    raw = open(src, 'rb').read()
    digest = hashlib.sha256(raw).hexdigest()
    if digest != PRISTINE_SHA256:
        sys.exit('%s is not the pristine nightly glue (sha256 %s)' % (src, digest))
    text = raw.decode('utf-8')
    for name, old, new in PATCHES:
        count = text.count(old)
        if count != 1:
            sys.exit('patch "%s": expected 1 match, found %d' % (name, count))
        text = text.replace(old, new)
        print('applied:', name)
    open(dst, 'wb').write(text.encode('utf-8'))
    print('wrote', dst, hashlib.sha256(text.encode('utf-8')).hexdigest())


if __name__ == '__main__':
    main()
