#!/usr/bin/env python3
"""Rebuild Cloudflare Chocolate Doom with local WADs and native browser saves.

Source Emscripten 3.1.57 emsdk_env.sh first; needs autoconf and automake.
CI serves the committed distribution. No game data is fetched or packaged.
"""
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-doom')
SOURCE = CACHE / 'source'
WRAPPER = ROOT / 'scripts/hosted-doom'
OUT = ROOT / 'public/runtime/doom-v1'
COMMIT = '65e0d3ae2ffa604155eebd96ed40da6567bd08f4'
INPUTS = {
    'doom-wasm.tar.gz': (f'https://codeload.github.com/cloudflare/doom-wasm/tar.gz/{COMMIT}', '9be6e02bb1f4f4b00f73d21b0c1ba4f2b8087e3c6cb1f477435baa390b3aa482'),
    'sdl2.28.4.zip': ('https://github.com/libsdl-org/SDL/archive/release-2.28.4.zip', '078183c0603cf59f4a97435284e347fff3ec615635d028cc19deea587ebff31d'),
    'sdl2_mixer.8.0.zip': ('https://github.com/libsdl-org/SDL_mixer/archive/release-2.8.0.zip', '3aa9b3abd7cefb3e7952ec2d0acf38c70947f04be14d74ed45f57155c7457386'),
    'sdl2_net.zip': ('https://github.com/emscripten-ports/SDL2_net/archive/version_2.zip', 'fc8caf8ea278f91bff6eb4ac3074fee8657c428a2767bcebc2cf142f2b3e35bd'),
}


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('doom-wasm.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(['emconfigure', 'autoreconf', '-fiv'], cwd=SOURCE, check=True)
    env = dict(os.environ, CFLAGS='-std=gnu11', ac_cv_exeext='.html')
    subprocess.run(['emconfigure', './configure', '--host=wasm32-unknown-emscripten', '--without-libsamplerate', '--without-libpng', '--disable-doc', '--disable-fonts', '--disable-icons', '--disable-bash-completion'], cwd=SOURCE, env=env, check=True)
    subprocess.run(['emmake', 'make', '-j8'], cwd=SOURCE, check=True)


def package():
    OUT.mkdir(parents=True, exist_ok=True)
    for name in ['websockets-doom.js', 'websockets-doom.wasm']:
        shutil.copy2(SOURCE / 'src' / name, OUT / name)
    for name in ['index.html', 'boot.js', 'engine.patch']:
        shutil.copy2(WRAPPER / name, OUT / name)
    patched = ['configure.ac', 'src/doom/d_main.c', 'src/doom/g_game.c']
    with tarfile.open(fetch('doom-wasm.tar.gz')) as source, tarfile.open(OUT / 'doom-source.tar.gz', 'w:gz') as target:
        for member in source.getmembers():
            parts = Path(member.name).parts[1:]
            content = source.extractfile(member) if member.isfile() else None
            member.name = str(Path('doom-wasm', *parts))
            if str(Path(*parts)) in patched:
                path = SOURCE.joinpath(*parts)
                member.size = path.stat().st_size
                with path.open('rb') as data:
                    target.addfile(member, data)
            else:
                target.addfile(member, content)
    sdk = Path(os.environ['EMSDK']) / 'upstream/emscripten'
    for name in ['sdl2.28.4.zip', 'sdl2_mixer.8.0.zip', 'sdl2_net.zip']:
        if not (CACHE / name).exists() and (sdk / 'cache/ports' / name).exists():
            shutil.copy2(sdk / 'cache/ports' / name, CACHE / name)
        shutil.copy2(fetch(name), OUT / name)
    for port, license_name in [('sdl2', 'SDL-LICENSE.txt'), ('sdl2_mixer', 'SDL-MIXER-LICENSE.txt'), ('sdl2_net', 'SDL-NET-LICENSE.txt')]:
        source_license = 'COPYING.txt' if port == 'sdl2_net' else 'LICENSE.txt'
        license_path = next((sdk / 'cache/ports' / port).glob('*/' + source_license))
        shutil.copy2(license_path, OUT / license_name)
    shutil.copy2(SOURCE / 'COPYING.md', OUT / 'LICENSE')
    shutil.copy2(SOURCE / 'AUTHORS.md', OUT / 'AUTHORS.md')
    shutil.copy2(SOURCE / 'README.md', OUT / 'UPSTREAM-README.md')
    shutil.copy2(sdk / 'LICENSE', OUT / 'EMSCRIPTEN-LICENSE.txt')
    shutil.copy2(ROOT / 'scripts/prepare-doom.py', OUT / 'prepare-doom.py')
    messages = {lang: json.loads((ROOT / f'src/i18n/messages/{lang}.json').read_text())['DoomPlayer'] for lang in ['en', 'zh', 'es', 'pt', 'ja']}
    (OUT / 'messages.js').write_text('window.DOOM_MESSAGES = ' + json.dumps(messages, ensure_ascii=False, indent=2) + ';\n')
    (OUT / 'NOTICES.txt').write_text('Chocolate Doom by Simon Howard and contributors, based on DOOM by id Software; Cloudflare WebAssembly port by Celso Martinho and contributors. GPL-2.0-or-later where stated in source headers; see LICENSE and AUTHORS.md.\nBrowser Games Now modifications, 2026-10-20: Emscripten 3.1.57 build flags, immediate native menu and sync native save files to IndexedDB after rename. Wrapper is GPL-2.0-or-later.\nSDL 2.28.4, SDL_mixer 2.8.0, SDL_net version_2: zlib, included licenses and complete source ZIPs. Mixer additional codec formats are disabled. Emscripten: MIT/University of Illinois/NCSA, EMSCRIPTEN-LICENSE.txt. Chocolate Doom source includes OPL/PCSOUND and their original license notices.\nNo original DOOM WAD, maps, sprites, sound, music or executable is distributed. Player supplies doom1.wad locally. Desktop solo only, English game, sound effects only; no multiplayer service.\n')
    (OUT / 'BUILDING.txt').write_text(f'''Cloudflare Chocolate Doom, Browser Games Now local-file player, 2026-10-20
Source: https://github.com/cloudflare/doom-wasm/tree/{COMMIT}
Emscripten 3.1.57, autoconf 2.73 and automake 1.19.
Repository: source emsdk_env.sh; python3 scripts/prepare-doom.py
Verified downloads are reused from /tmp/bgn-doom. No WAD is fetched.

Standalone: tar xzf doom-source.tar.gz; cd doom-wasm
The source archive has engine.patch changes applied. Do not apply it twice.
emconfigure autoreconf -fiv
CFLAGS=-std=gnu11 ac_cv_exeext=.html emconfigure ./configure --host=wasm32-unknown-emscripten --without-libsamplerate --without-libpng --disable-doc --disable-fonts --disable-icons --disable-bash-completion
emmake make -j8
Copy src/websockets-doom.js and .wasm beside the supplied index.html/boot.js/messages.js.
The SDL source ZIPs can be placed in $EM_CACHE/ports for offline SDK builds.
Wrapper preferred source is boot.js, messages.js and index.html. Serve over HTTP(S).

Only local doom1.wad is accepted; its SHA-256 separates native save slots under
/bgn-doom-v1/<hash>. The WAD stays in MEMFS and is not stored in IndexedDB.
IDBFS is restored before main starts. G_DoSaveGame syncs after the native rename.
Wait for the browser save confirmation; reselect the same WAD after refresh,
then use Load Game. Native saves are origin/profile-specific.
Acceptance scope: DOOM shareware E1M1, English, desktop keyboard, solo, SFX.
Retail, DOOM II, PWAD mods, music, touch controls and multiplayer are unverified.
''')
    (OUT / 'source.html').write_text(f'''<!doctype html><html lang="en"><meta charset="utf-8"><title>DOOM engine source and credits</title><h1>DOOM · Chocolate Doom</h1><p>Chocolate Doom by Simon Howard and contributors, based on DOOM by id Software. Cloudflare WebAssembly port. <a href="https://github.com/cloudflare/doom-wasm/tree/{COMMIT}">Fixed upstream source</a>. GPL-2.0-or-later where stated in source headers.</p><p><a href="doom-source.tar.gz">Complete patched source</a> · <a href="engine.patch">Changes</a> · <a href="BUILDING.txt">Build instructions</a> · <a href="prepare-doom.py">Packaging script</a> · <a href="LICENSE">GPL license</a> · <a href="NOTICES.txt">Notices</a> · <a href="AUTHORS.md">Authors</a></p><p>SDL sources: <a href="sdl2.28.4.zip">SDL</a>, <a href="sdl2_mixer.8.0.zip">SDL_mixer</a>, <a href="sdl2_net.zip">SDL_net</a>. Wrapper source: <a href="boot.js">boot.js</a>, <a href="messages.js">messages.js</a> and index.html. Browser Games Now modifications, 2026-10-20, GPL-2.0-or-later.</p><p>No DOOM game data is hosted. Select your own doom1.wad locally. Desktop solo, English game, sound effects only. Music, retail content, DOOM II, mods, touch and multiplayer are outside this delivery.</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 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()
