#!/usr/bin/env python3
"""
Build Seven Kingdoms: Ancient Adversaries (7kaa 2.15.6) to WebAssembly via
Emscripten. Faithful port of the real GPL C++ source (the3dfxdude/7kaa).

Single-player web build: curl + enet disabled (no networking in browser),
SDL2 video, OpenAL audio, gettext UI translations (English default).

    python build.py            # incremental compile + link
    python build.py clean
    python build.py link
"""
import os, sys, subprocess, glob, hashlib, shutil
from concurrent.futures import ThreadPoolExecutor, as_completed

ROOT      = os.path.dirname(os.path.abspath(__file__))
GAME      = os.path.join(os.path.dirname(ROOT), "7kaa-2156")
EMSDK     = os.path.join(os.path.dirname(ROOT), "emsdk")
EM_PYTHON = os.path.join(EMSDK, "python", "3.13.3_64bit", "python.exe")
EM_CONFIG = os.path.join(EMSDK, ".emscripten")
EMPP      = [EM_PYTHON, os.path.join(EMSDK, "upstream", "emscripten", "em++.py")]
EMCC      = [EM_PYTHON, os.path.join(EMSDK, "upstream", "emscripten", "emcc.py")]
OBJ       = os.path.join(ROOT, "build", "obj")
DIST      = os.path.join(ROOT, "dist")
DATASRC   = os.path.join(GAME, "data")              # uppercase data tree (matches code paths)

EM_ENV = dict(os.environ)
EM_ENV["EM_CONFIG"]    = EM_CONFIG
EM_ENV["EMSDK_PYTHON"] = EM_PYTHON

INCLUDES = [ os.path.join(GAME, "include"), ROOT ]

DEFINES = [
    "USE_POSIX",                            # 2.15.6 gates unistd/sys-stat/errno on this
    "USE_SDL", "USE_OPENAL", "NO_ASM",
    "HAVE_STRINGS_STRCASECMP",              # -> strcmpi/strnicmp via <strings.h>
    "DISABLE_MULTI_PLAYER",                 # no curl/enet in the browser build
    "ENABLE_NLS", "HAVE_LIBINTL_H", "HAVE_LC_MESSAGES", "HAVE_SETENV",
    "HAVE_VARIABLE_SIZE_ARRAYS",
    "REGISTER=",                            # configure normally sets this (register storage class)
    'PACKAGE="7kaa"',                       # gettext textdomain (config.h isn't auto-included)
    'PACKAGE_DATA_PATH="/data"',
    'PACKAGE_DATA_DIR="/data"',             # chdir_to_game_dir() fallback used by 2.15.6
    'LOCALE_PATH="/data/locale"',
    "OPENAL_AL_H=<AL/al.h>",
    "OPENAL_ALC_H=<AL/alc.h>",
]

COMMON_FLAGS = ["-O2", "-w", "-fno-strict-aliasing", "-sUSE_SDL=2",
                "-include", "stdlib.h", "-include", "string.h", "-include", "locale.h"]  # a few 2.15.6 files rely on transitive includes
CXX_FLAGS    = ["-std=gnu++11"] + COMMON_FLAGS   # 2.15.6 is C++11
C_FLAGS      = ["-std=gnu11"]   + COMMON_FLAGS

# Flat src/ layout in 2.15.6. Use the generic (no-asm) imgfun, OpenAL audio.
# Exclude enet/ and curl/ (multiplayer disabled) and imgfun/x86 (assembly).
CPP_GLOBS = [
    "src/*.cpp",
    "src/enet/*.cpp",          # multiplayer.cpp -> stub MultiPlayer (no USE_ENET) so core links
    "src/imgfun/generic/*.cpp",
    "src/openal/*.cpp",
]
C_GLOBS = []


def collect():
    cpp = []
    for g in CPP_GLOBS:
        cpp += glob.glob(os.path.join(GAME, g))
    c = []
    for g in C_GLOBS:
        c += glob.glob(os.path.join(GAME, g))
    return sorted(set(cpp)), sorted(set(c))


def obj_path(src):
    h = hashlib.md5(src.encode()).hexdigest()[:8]
    base = os.path.splitext(os.path.basename(src))[0]
    return os.path.join(OBJ, f"{base}_{h}.o")


def _flags_sig():
    return hashlib.md5(("|".join(DEFINES + CXX_FLAGS + C_FLAGS + INCLUDES)).encode()).hexdigest()

def _check_sig():
    """Force a full recompile when compile flags/defines change (mtime alone misses this)."""
    sig_file = os.path.join(OBJ, ".flags.sig")
    cur = _flags_sig()
    old = open(sig_file).read().strip() if os.path.exists(sig_file) else ""
    if old != cur:
        shutil.rmtree(OBJ, ignore_errors=True)
        os.makedirs(OBJ, exist_ok=True)
        open(sig_file, "w").write(cur)
        if old:
            print("compile flags changed -> full rebuild")

def needs_rebuild(src, obj):
    return (not os.path.exists(obj)) or os.path.getmtime(src) > os.path.getmtime(obj)


def compile_one(src, is_cpp):
    obj = obj_path(src)
    if not needs_rebuild(src, obj):
        return (src, 0, "")
    comp  = EMPP if is_cpp else EMCC
    flags = CXX_FLAGS if is_cpp else C_FLAGS
    cmd = list(comp) + ["-c", src, "-o", obj]
    for inc in INCLUDES: cmd += ["-I", inc]
    for d in DEFINES:    cmd += ["-D" + d]
    cmd += flags
    p = subprocess.run(cmd, capture_output=True, text=True, env=EM_ENV)
    return (src, p.returncode, (p.stderr or p.stdout))


def do_clean():
    shutil.rmtree(os.path.join(ROOT, "build"), ignore_errors=True)
    print("cleaned object cache")


def do_compile():
    os.makedirs(OBJ, exist_ok=True)
    _check_sig()
    cpp, c = collect()
    print(f"sources: {len(cpp)} C++ + {len(c)} C")
    jobs = [(s, True) for s in cpp] + [(s, False) for s in c]
    fails, done = [], 0
    with ThreadPoolExecutor(max_workers=os.cpu_count()) as ex:
        futs = {ex.submit(compile_one, s, cpp_): s for (s, cpp_) in jobs}
        for f in as_completed(futs):
            src, rc, err = f.result(); done += 1
            if rc != 0:
                fails.append((src, err))
                print(f"  [FAIL {done}/{len(jobs)}] {os.path.basename(src)}")
            elif done % 25 == 0:
                print(f"  ...{done}/{len(jobs)}")
    if fails:
        print(f"\n{len(fails)} file(s) failed:\n")
        for src, err in fails[:6]:
            print(f"===== {os.path.basename(src)} =====")
            print("\n".join(err.strip().splitlines()[-22:])); print()
        if len(fails) > 6: print(f"... and {len(fails)-6} more")
        return False
    print(f"compiled {len(jobs)} objects OK")
    return True


def do_link():
    os.makedirs(DIST, exist_ok=True)
    objs = glob.glob(os.path.join(OBJ, "*.o"))
    # Output JS (not a self-running .html). Our own shell.html loads 7kaa.js
    # ON DEMAND (after the language is chosen) so the ~100MB data download only
    # starts when the player clicks "Start".
    out  = os.path.join(DIST, "7kaa.js")
    cmd  = list(EMPP) + objs + ["-o", out] + [
        "-O2", "-sUSE_SDL=2", "-sASYNCIFY", "-sASYNCIFY_STACK_SIZE=131072",
        "-sALLOW_MEMORY_GROWTH=1", "-sINITIAL_MEMORY=268435456",
        "-sSTACK_SIZE=5242880", "-sEXIT_RUNTIME=0",
        "-sFORCE_FILESYSTEM=1", "-sMODULARIZE=0",
        "-sEXPORTED_RUNTIME_METHODS=FS,IDBFS,ENV,callMain,ccall,addRunDependency,removeRunDependency",
        "-lopenal", "-lidbfs.js",
        "--preload-file", f"{DATASRC}@/data",
        "--preload-file", f"{os.path.join(ROOT, 'locale_build')}@/data/locale",
    ]
    print("linking ->", out)
    p = subprocess.run(cmd, capture_output=True, text=True, env=EM_ENV)
    if p.returncode != 0:
        print("LINK FAILED:\n"); print("\n".join((p.stderr or p.stdout).strip().splitlines()[-60:]))
        return False
    # our standalone shell loads 7kaa.js on demand
    shell = open(os.path.join(ROOT, "shell.html"), encoding="utf-8").read()
    open(os.path.join(DIST, "7kaa.html"), "w", encoding="utf-8").write(shell)
    open(os.path.join(DIST, "index.html"), "w", encoding="utf-8").write(shell)
    for extra in ("manifest.json", "sw.js", "icon-192.png", "icon-512.png", ".htaccess",
                  "LICENSE.txt", "SOURCE.txt"):
        src = os.path.join(ROOT, extra)
        if os.path.exists(src):
            shutil.copy(src, os.path.join(DIST, extra))
    print("LINK OK ->", out, "(+ shell/manifest/sw/icons copied)"); return True


if __name__ == "__main__":
    arg = sys.argv[1] if len(sys.argv) > 1 else ""
    if arg == "clean":  do_clean()
    elif arg == "link": do_link()
    else:
        if do_compile(): do_link()
