#!/usr/bin/env python3
"""Rebuild pinned FTE with Emscripten 3.1.57; package source and local-PAK player.

Run after sourcing emsdk_env.sh. Application CI uses the committed distribution.
No Quake game data is downloaded or distributed by this script.
"""
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-quake')
SOURCE = CACHE / 'source'
OUT = ROOT / 'public/runtime/quake-v1'
WRAPPER = ROOT / 'scripts/hosted-quake'
COMMIT = 'f937b9d88f71fc4429db5fe56c6a98d922711b2e'
INPUTS = {
    'fte.tar.gz': (f'https://codeload.github.com/fte-team/fteqw/tar.gz/{COMMIT}', 'd8aede03b56a37eafaf3e11d2ca63fe6a84e45ee602afff72ffd152d18bafef3'),
    'zlib.tar.gz': ('https://zlib.net/fossils/zlib-1.3.1.tar.gz', '9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23'),
}
MAKE = ['make', 'web-rel', 'FTE_CONFIG=fteqw', 'USE_OPUS=0', 'PKGCONFIG=/usr/bin/false', 'AR=emar', 'ARFLAGS=rc', 'LDFLAGS=-sSTACK_SIZE=5242880', '-j8']


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, f'Unexpected upstream bytes: {name}'
    return path


def build():
    assert '3.1.57' in subprocess.check_output(['emcc', '--version'], text=True)
    if SOURCE.exists():
        raise RuntimeError(f'{SOURCE} already exists; move it aside before a clean rebuild')
    SOURCE.mkdir(parents=True)
    with tarfile.open(fetch('fte.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)
    # Upstream mixes CRLF/LF. The reviewable patch uses LF consistently.
    for line in (WRAPPER / 'engine.patch').read_text().splitlines():
        if line.startswith('--- a/'):
            path = SOURCE / line[6:]
            path.write_text(path.read_text())
    subprocess.run(['patch', '-p1', '-i', str(WRAPPER / 'engine.patch')], cwd=SOURCE, check=True)
    # Archive preferred source before creating build outputs; preserve upstream notices.
    with tarfile.open(CACHE / 'fte-source.tar.gz', 'w:gz') as archive:
        archive.add(SOURCE, arcname='fte')
    shutil.copy2(fetch('zlib.tar.gz'), SOURCE / 'engine/zlib-1.3.1.tar.gz')
    subprocess.run(MAKE, cwd=SOURCE / 'engine', check=True)


def package():
    OUT.mkdir(parents=True, exist_ok=True)
    for name in ['ftewebgl.js', 'ftewebgl.wasm']:
        shutil.copy2(SOURCE / 'engine/release' / name, OUT / name)
    for name in ['index.html', 'boot.js', 'default.fmf', 'engine.patch']:
        shutil.copy2(WRAPPER / name, OUT / name)
    for name in ['fte-source.tar.gz', 'zlib.tar.gz']:
        shutil.copy2(CACHE / name, OUT / name)
    shutil.copy2(SOURCE / 'LICENSE', OUT / 'LICENSE')
    shutil.copy2(SOURCE / 'documentation/Credits.md', OUT / 'FTE-CREDITS.md')
    sdk_license = Path(os.environ['EMSDK']) / 'upstream/emscripten/LICENSE'
    shutil.copy2(sdk_license, OUT / 'EMSCRIPTEN-LICENSE.txt')
    shutil.copy2(ROOT / 'scripts/prepare-quake.py', OUT / 'prepare-quake.py')
    messages = {locale: json.loads((ROOT / f'src/i18n/messages/{locale}.json').read_text())['QuakePlayer'] for locale in ['en', 'zh', 'es', 'pt', 'ja']}
    (OUT / 'messages.js').write_text('window.QUAKE_MESSAGES = ' + json.dumps(messages, ensure_ascii=False, indent=2) + ';\n')
    (OUT / 'NOTICES.txt').write_text('FTE Quake by the FTE team and contributors, based on Quake by id Software.\nEngine: GNU GPL version 2; source files allow version 2 or later where stated.\nSee LICENSE, FTE-CREDITS.md and the complete corresponding source archive.\nBrowser Games Now modifications, 2026-10-19: Emscripten version macros; disable voice, package downloads and master browser; fix disabled package-manager ABI; namespace browser storage; prevent duplicate keyboard dispatch.\nLocal-file wrapper: Browser Games Now, GPL-2.0-or-later.\nzlib 1.3.1: Jean-loup Gailly and Mark Adler, zlib license; full source and license in zlib.tar.gz.\nEmscripten 3.1.57: MIT/University of Illinois/NCSA, license notices in SDK. https://github.com/emscripten-core/emscripten/tree/3.1.57\nNo original Quake game files, maps, music, progs.dat or PAK data are distributed here. Players supply their own files locally.\n')
    (OUT / 'BUILDING.txt').write_text(f'''Browser Games Now Quake local-file player, 2026-10-19
Upstream: https://github.com/fte-team/fteqw/tree/{COMMIT}
Emscripten: 3.1.57 (source emsdk_env.sh), make, patch, Python 3.
Repository rebuild: python3 scripts/prepare-quake.py
Existing verified downloads in /tmp/bgn-quake are reused. No game files are fetched.

Standalone engine rebuild from this distribution:
  tar xzf fte-source.tar.gz
  cp zlib.tar.gz fte/engine/zlib-1.3.1.tar.gz
  cd fte/engine
  {' '.join(MAKE)}
The source archive already contains engine.patch; do not apply it twice.
Copy release/ftewebgl.js and release/ftewebgl.wasm beside index.html.
Serve this folder over HTTP(S). boot.js, default.fmf and messages.js are editable source.
Do not use the upstream generated HTML: it enables a different game/download workflow.

Patched files: common/common.c, common/config_fteqw.h, client/m_download.c,
web/prejs.js and web/ftejslib.js. Full patch is included. No trial minimal config.
Native Single Player menu; local PAKs supplied again after reload. Saves/config
use localStorage prefix bgn-quake-v1/; Cache Storage uses bgn-quake-v1.
Checked data: first-episode pak0.pak only. Retail pak1, mods, WebXR and online
QuakeWorld rooms are unverified. No online game server is provided.
''')
    (OUT / 'source.html').write_text(f'''<!doctype html><html lang="en"><meta charset="utf-8"><title>Quake FTE source and credits</title><h1>Quake · FTE</h1><p>FTE by the FTE team and contributors, based on Quake by id Software. Fixed source <a href="https://github.com/fte-team/fteqw/tree/{COMMIT}">{COMMIT}</a>. GPL version 2; version 2 or later where stated in individual files.</p><p><a href="fte-source.tar.gz">Complete patched engine source</a> · <a href="engine.patch">Changes</a> · <a href="zlib.tar.gz">zlib source</a> · <a href="BUILDING.txt">Build instructions</a> · <a href="prepare-quake.py">Packaging script</a> · <a href="LICENSE">GPL</a> · <a href="FTE-CREDITS.md">FTE credits</a> · <a href="NOTICES.txt">Notices</a></p><p>Wrapper source: <a href="boot.js">boot.js</a>, <a href="default.fmf">default.fmf</a>, <a href="messages.js">messages.js</a>, and this folder's HTML. Browser Games Now changes dated 2026-10-19, GPL-2.0-or-later.</p><p>Original Quake game data is separately licensed and is supplied locally by the player. This site distributes no game PAK files. Checked scope: first episode, English game, offline single-player, desktop keyboard. Online QuakeWorld rooms and retail data are not verified.</p></html>''')
    files = [{'path': str(path.relative_to(OUT)), 'bytes': path.stat().st_size, 'sha256': sha(path)} for path in sorted(OUT.iterdir()) if path.is_file() and path.name != 'manifest.json']
    assert all(entry['bytes'] < 25 * 1024 * 1024 for entry in files)
    (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(entry['bytes'] for entry in files), 'largest': max(entry['bytes'] for entry 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()
