#!/usr/bin/env python3
"""Rebuild pinned Qwasm2; package the engine and corresponding source, never PAKs.

Source emsdk_env.sh (Emscripten 3.1.57) first. CI uses the committed distribution.
"""
import argparse
import hashlib
import json
import os
import shutil
import subprocess
import tarfile
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
CACHE = Path('/tmp/bgn-quake2')
SOURCE = CACHE / 'source'
WRAPPER = ROOT / 'scripts/hosted-quake2'
OUT = ROOT / 'public/runtime/quake2-v1'
COMMIT = 'c9ab09343d587ee36392aaefe29755e9a149e3af'
INPUTS = {
    'qwasm2.tar.gz': (f'https://codeload.github.com/GMH-Code/Qwasm2/tar.gz/{COMMIT}', '07743c748396733b7148adb7b9c5274bfe3b05a8d56aadf8dd7052536214efb6'),
    'sdl2.28.4.zip': ('https://github.com/libsdl-org/SDL/archive/release-2.28.4.zip', '078183c0603cf59f4a97435284e347fff3ec615635d028cc19deea587ebff31d'),
}


def sha(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def fetch(name):
    path = CACHE / name
    url, expected = INPUTS[name]
    if not path.exists():
        for attempt in range(3):
            try:
                urllib.request.urlretrieve(url, path)
                break
            except OSError:
                if attempt == 2:
                    raise
    assert sha(path) == expected, name
    return path


def build():
    assert '3.1.57' in subprocess.check_output(['emcc', '--version'], text=True)
    if SOURCE.exists():
        raise RuntimeError(f'{SOURCE} exists; move it aside before a clean rebuild')
    SOURCE.mkdir(parents=True)
    with tarfile.open(fetch('qwasm2.tar.gz')) as archive:
        for member in archive.getmembers():
            parts = Path(member.name).parts[1:]
            if not parts:
                continue
            assert '..' not in parts and not member.issym() and not member.islnk()
            member.name = str(Path(*parts))
            archive.extract(member, SOURCE)
    subprocess.run(['patch', '-p1', '-i', str(WRAPPER / 'engine.patch')], cwd=SOURCE, check=True)
    subprocess.run(['emmake', 'make', '-j8'], cwd=SOURCE, check=True)


def package():
    OUT.mkdir(parents=True, exist_ok=True)
    for name in ['index.js', 'index.wasm', 'index.data', 'game_baseq2.wasm', 'ref_gles3.wasm', 'ref_soft.wasm']:
        shutil.copy2(SOURCE / 'release' / name, OUT / name)
    for name in ['index.html', 'boot.js', 'engine.patch']:
        shutil.copy2(WRAPPER / name, OUT / name)
    # Recreate the preferred patched source from the pinned archive, not the build tree.
    # This also prevents accidental distribution of local test data or build products.
    with tarfile.open(fetch('qwasm2.tar.gz')) as source, tarfile.open(OUT / 'qwasm2-source.tar.gz', 'w:gz') as target:
        for member in source.getmembers():
            parts = Path(member.name).parts[1:]
            member.name = str(Path('qwasm2', *parts))
            if member.isfile():
                content = source.extractfile(member)
                # extractfile uses offset_data, so the renamed member is safe here.
                path = SOURCE.joinpath(*parts)
                if str(Path(*parts)) in ['Makefile', 'src/backends/wasm/initfs.c', 'src/backends/unix/system.c', 'wasm/baseq2/wasm.cfg']:
                    member.size = path.stat().st_size
                    with path.open('rb') as patched:
                        target.addfile(member, patched)
                else:
                    target.addfile(member, content)
            else:
                target.addfile(member)
    sdl = Path(os.environ['EMSDK']) / 'upstream/emscripten/cache/ports/sdl2.28.4.zip'
    if sdl.exists() and not (CACHE / 'sdl2.28.4.zip').exists():
        shutil.copy2(sdl, CACHE / sdl.name)
    shutil.copy2(fetch('sdl2.28.4.zip'), OUT / 'sdl2.28.4.zip')
    shutil.copy2(SOURCE / 'LICENSE', OUT / 'LICENSE')
    shutil.copy2(SOURCE / 'README.md', OUT / 'UPSTREAM-README.md')
    sdk = Path(os.environ['EMSDK']) / 'upstream/emscripten'
    shutil.copy2(sdk / 'LICENSE', OUT / 'EMSCRIPTEN-LICENSE.txt')
    shutil.copy2(sdk / 'cache/ports/sdl2/SDL-release-2.28.4/LICENSE.txt', OUT / 'SDL-LICENSE.txt')
    shutil.copy2(ROOT / 'scripts/prepare-quake2.py', OUT / 'prepare-quake2.py')
    messages = {lang: json.loads((ROOT / f'src/i18n/messages/{lang}.json').read_text())['Quake2Player'] for lang in ['en', 'zh', 'es', 'pt', 'ja']}
    (OUT / 'messages.js').write_text('window.QUAKE2_MESSAGES = ' + json.dumps(messages, ensure_ascii=False, indent=2) + ';\n')
    (OUT / 'NOTICES.txt').write_text('Qwasm2 by GMH-Code and contributors; based on Yamagi Quake II and Quake II by id Software. GPL-2.0-or-later where stated in source headers.\nLICENSE includes Quake II GPL, Info-ZIP, zlib, SDL main, stb, miniz, GLAD, Khronos and gyro notices. SDL 2.28.4: zlib license, SDL-LICENSE.txt; complete source ZIP accompanies this build. Emscripten 3.1.57: MIT/University of Illinois/NCSA, EMSCRIPTEN-LICENSE.txt.\nBrowser Games Now changes dated 2026-10-19: SDL2 SDK port, no GL1/GL4ES target, current stack flag, exported FS, isolated browser saves, keyboard attack/jump defaults and 800x600 initial mode. Wrapper GPL-2.0-or-later.\nNo original Quake II PAK, maps, music, EXE or DLL is included. Players supply their own game data locally. Offline single-player only.\n')
    (OUT / 'BUILDING.txt').write_text(f'''Qwasm2 local-file player, Browser Games Now, 2026-10-19
Source: https://github.com/GMH-Code/Qwasm2/tree/{COMMIT}
Emscripten 3.1.57; emmake make -j8. No GL4ES, OpenAL or cURL needed.
Repository: source emsdk_env.sh; python3 scripts/prepare-quake2.py
Verified downloads are reused from /tmp/bgn-quake2. No game data is fetched.

Standalone: tar xzf qwasm2-source.tar.gz; cd qwasm2; emmake make -j8
The archive already includes engine.patch. Do not apply it twice.
SDL 2.28.4 is fetched by the SDK's -sUSE_SDL=2 port. For offline builds,
place the included sdl2.28.4.zip in $EM_CACHE/ports before invoking emmake.
Copy release/index.js, index.wasm, index.data, game_baseq2.wasm,
ref_gles3.wasm and ref_soft.wasm beside this distribution's index.html.
Serve over HTTP(S). Wrapper preferred source is boot.js, index.html and messages.js.
Do not replace index.html with the upstream development shell.

No PAK game data is included. User-selected PAKs enter /baseq2 in memory.
Saves/options use IDBFS /bgn-quake2-v1 and persist in this browser profile.
Reselect original PAK files after reload, then use Game -> Load Game.
Demo data is the acceptance scope; retail, mission packs and online rooms are unverified.
''')
    (OUT / 'source.html').write_text(f'''<!doctype html><html lang="en"><meta charset="utf-8"><title>Quake II Qwasm2 source</title><h1>Quake II · Qwasm2</h1><p>By GMH-Code and contributors, based on Yamagi Quake II and id Software’s Quake II. <a href="https://github.com/GMH-Code/Qwasm2/tree/{COMMIT}">Fixed upstream source</a>. GPL-2.0-or-later where stated in individual source headers.</p><p><a href="qwasm2-source.tar.gz">Complete patched source</a> · <a href="engine.patch">Changes</a> · <a href="sdl2.28.4.zip">SDL source</a> · <a href="BUILDING.txt">Build instructions</a> · <a href="prepare-quake2.py">Packaging script</a> · <a href="LICENSE">Licenses</a> · <a href="NOTICES.txt">Credits</a> · <a href="UPSTREAM-README.md">Upstream README</a></p><p>Wrapper source: <a href="boot.js">boot.js</a>, <a href="messages.js">messages.js</a> and this folder’s HTML. Browser Games Now modifications, 2026-10-19, GPL-2.0-or-later.</p><p>Original Quake II game data is separately licensed and supplied locally by the player. No PAK files are hosted. Acceptance scope: demo, English game, desktop, offline single-player. Retail content, mission packs and online rooms are unverified.</p></html>''')
    files = [{'path': p.name, 'bytes': p.stat().st_size, 'sha256': sha(p)} for p in sorted(OUT.iterdir()) if p.is_file() and p.name != 'manifest.json']
    assert all(f['bytes'] < 25 * 1024 * 1024 for f in files), 'Static Assets file limit'
    (OUT / 'manifest.json').write_text(json.dumps({'engineCommit': COMMIT, 'compiler': 'Emscripten 3.1.57', 'gameDataIncluded': False, 'files': files}, indent=2) + '\n')
    print(json.dumps({'files': len(files), 'bytes': sum(f['bytes'] for f in files), 'largest': max(f['bytes'] for f in files)}))


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--package-only', action='store_true')
    args = parser.parse_args()
    CACHE.mkdir(parents=True, exist_ok=True)
    if not args.package_only:
        build()
    package()
