#!/usr/bin/env bash
set -euo pipefail

ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
OPENQ4_VENDOR="${ROOT}/vendor/openq4"
GAMELIBS_VENDOR="${ROOT}/vendor/openq4-game"
SOURCE_ROOT="${ROOT}/build/native-src/openq4"
GAMELIBS_ROOT="${ROOT}/build/native-src/openq4-game"
BUILD_ROOT="${ROOT}/build/native-release"
RUNTIME_ROOT="${ROOT}/runtime/native"
FREE_RUNTIME_SOURCE="${ROOT}/content-src/baseoq4"
FREE_RUNTIME_POLICY="${ROOT}/content-src/public-runtime.json"
FREE_RUNTIME_BUILDER="${ROOT}/tools/build-free-runtime.py"
FREE_RUNTIME_EXPECTED="${ROOT}/build/native-free-runtime"
OPENQ4_PATCH_FILES=(
    "${ROOT}/port/native/patches/0001-fix-bot-client-slot-return.patch"
    "${ROOT}/port/native/patches/0002-load-combined-special-reachabilities.patch"
    "${ROOT}/port/native/patches/0004-full-state-server-demos.patch"
    "${ROOT}/port/native/patches/0005-port-d3wasm-aas-compiler.patch"
    "${ROOT}/port/native/patches/0007-static-gamelibs-and-web-runtime.patch"
    "${ROOT}/port/native/patches/0008-guard-mapload-net-verbose.patch"
    "${ROOT}/port/native/patches/0011-throttle-blocking-load-pacifier.patch"
    "${ROOT}/port/native/patches/0013-require-free-web-runtime-paks.patch"
    "${ROOT}/port/native/patches/0015-guard-partial-openal-shutdown.patch"
    "${ROOT}/port/native/patches/0016-buffer-browser-savegame-writes.patch"
    "${ROOT}/port/native/patches/0018-preserve-bot-userinfo-across-map-change.patch"
    "${ROOT}/port/native/patches/0020-bounded-replay-reliable-backpressure.patch"
    "${ROOT}/port/native/patches/0023-defer-websocket-messages-during-asyncify.patch"
    "${ROOT}/port/native/patches/0024-replace-replay-anchor-with-retail-decl.patch"
    "${ROOT}/port/native/patches/0025-disable-audio-in-dedicated-build.patch"
    "${ROOT}/port/native/patches/0028-flush-dedicated-stdout.patch"
)
GAMELIBS_PATCH_FILES=(
    "${ROOT}/port/native/patches/0003-initialize-and-stabilize-bots.patch"
    "${ROOT}/port/native/patches/0006-enable-replay-repeater.patch"
    "${ROOT}/port/native/patches/0009-port-bots-to-mpgame.patch"
    "${ROOT}/port/native/patches/0017-guard-bot-entity-slots.patch"
    "${ROOT}/port/native/patches/0019-report-registered-bot-status.patch"
    "${ROOT}/port/native/patches/0021-full-state-replay-viewers.patch"
    "${ROOT}/port/native/patches/0022-skip-server-bot-ai-on-web-clients.patch"
)
STAMP_FILE="${ROOT}/build/native-src/source-stamp"

OPENQ4_PIN="975e296b80b66f63b817d16ea78b53e0685c5c55"
GAMELIBS_PIN="eb26e5f3a52358c58ee00bee5602575e5b057164"
JOBS="${JOBS:-4}"
PLATFORM_BACKEND="${OPENQ4_PLATFORM_BACKEND:-native}"
CLEAN=0

usage() {
    cat <<'EOF'
Usage: tools/build-native.sh [--clean] [--jobs N]

Builds the pinned native OpenQ4 client, dedicated server, and SP/MP game
modules, then stages them under runtime/native. Retail q4base data is linked
from assets-src/gog and is never copied into tracked or public paths.
EOF
}

while (($#)); do
    case "$1" in
        --clean)
            CLEAN=1
            shift
            ;;
        --jobs)
            [[ $# -ge 2 && "$2" =~ ^[1-9][0-9]*$ ]] || {
                echo "--jobs requires a positive integer" >&2
                exit 2
            }
            JOBS="$2"
            shift 2
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            echo "Unknown argument: $1" >&2
            usage >&2
            exit 2
            ;;
    esac
done

for command in cmp cp git meson ninja gcc-13 g++-13 patch python3 rsync sha256sum; do
    command -v "${command}" >/dev/null 2>&1 || {
        echo "Required command is missing: ${command}" >&2
        exit 1
    }
done

[[ "${PLATFORM_BACKEND}" == "native" || "${PLATFORM_BACKEND}" == "sdl3" ]] || {
    echo "OPENQ4_PLATFORM_BACKEND must be native or sdl3" >&2
    exit 2
}

for patch_file in "${OPENQ4_PATCH_FILES[@]}" "${GAMELIBS_PATCH_FILES[@]}"; do
    [[ -f "${patch_file}" ]] || {
        echo "Native patch is missing: ${patch_file}" >&2
        exit 1
    }
done
[[ -d "${FREE_RUNTIME_SOURCE}/pak0" && -d "${FREE_RUNTIME_SOURCE}/pak1" \
    && -f "${FREE_RUNTIME_POLICY}" && -f "${FREE_RUNTIME_BUILDER}" ]] || {
    echo "Self-authored free runtime source, policy, or builder is missing" >&2
    exit 1
}
[[ -d "${ROOT}/assets-src/gog/q4base" ]] || {
    echo "Retail q4base data is missing under assets-src/gog/q4base" >&2
    exit 1
}

actual_openq4_pin="$(git -C "${OPENQ4_VENDOR}" rev-parse HEAD)"
actual_gamelibs_pin="$(git -C "${GAMELIBS_VENDOR}" rev-parse HEAD)"
[[ "${actual_openq4_pin}" == "${OPENQ4_PIN}" ]] || {
    echo "OpenQ4 pin mismatch: expected ${OPENQ4_PIN}, got ${actual_openq4_pin}" >&2
    exit 1
}
[[ "${actual_gamelibs_pin}" == "${GAMELIBS_PIN}" ]] || {
    echo "OpenQ4-game pin mismatch: expected ${GAMELIBS_PIN}, got ${actual_gamelibs_pin}" >&2
    exit 1
}

patch_manifest_sha="$(
    for patch_file in "${OPENQ4_PATCH_FILES[@]}" "${GAMELIBS_PATCH_FILES[@]}"; do
        printf '%s %s\n' "$(basename -- "${patch_file}")" "$(sha256sum "${patch_file}" | awk '{print $1}')"
    done | sha256sum | awk '{print $1}'
)"
free_runtime_source_sha="$(
    python3 "${FREE_RUNTIME_BUILDER}" --source "${FREE_RUNTIME_SOURCE}" --fingerprint-only
)"
[[ "${free_runtime_source_sha}" =~ ^[0-9a-f]{64}$ ]] || {
    echo "Free runtime builder returned an invalid source fingerprint" >&2
    exit 1
}
python3 - "${FREE_RUNTIME_POLICY}" "${free_runtime_source_sha}" <<'PY'
import json
import pathlib
import sys

policy = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
if policy.get("schema") != 1 or policy.get("policy") != "free-only-v1":
    raise SystemExit("free runtime policy schema is invalid")
if policy.get("runtimeSourceSha256") != sys.argv[2]:
    raise SystemExit("free runtime source fingerprint differs from its pinned policy")
PY
expected_stamp="${OPENQ4_PIN}|${GAMELIBS_PIN}|${patch_manifest_sha}|${free_runtime_source_sha}"

openq4_materialized_diff_sha() {
    git -C "${SOURCE_ROOT}" add -N .
    git -C "${SOURCE_ROOT}" diff --binary --no-ext-diff -- . \
        ':(exclude)content/baseoq4/pak0' \
        ':(exclude)content/baseoq4/pak0/**' \
        ':(exclude)content/baseoq4/pak1' \
        ':(exclude)content/baseoq4/pak1/**' \
        | sha256sum | awk '{print $1}'
}

gamelibs_materialized_diff_sha() {
    git -C "${GAMELIBS_ROOT}" add -N .
    git -C "${GAMELIBS_ROOT}" diff --binary --no-ext-diff \
        | sha256sum | awk '{print $1}'
}

if ((CLEAN)); then
    rm -rf -- "${ROOT}/build/native-src" "${BUILD_ROOT}" "${RUNTIME_ROOT}"
fi

new_source_stamp=0
if [[ ! -f "${STAMP_FILE}" ]]; then
    if [[ -e "${SOURCE_ROOT}" || -e "${GAMELIBS_ROOT}" ]]; then
        echo "Native source copy exists without a valid stamp; rerun with --clean" >&2
        exit 1
    fi

    mkdir -p -- "$(dirname -- "${SOURCE_ROOT}")"
    git clone --quiet --local --no-checkout "${OPENQ4_VENDOR}" "${SOURCE_ROOT}"
    git -C "${SOURCE_ROOT}" checkout --quiet --detach "${OPENQ4_PIN}"
    git clone --quiet --local --no-checkout "${GAMELIBS_VENDOR}" "${GAMELIBS_ROOT}"
    git -C "${GAMELIBS_ROOT}" checkout --quiet --detach "${GAMELIBS_PIN}"
    for patch_file in "${OPENQ4_PATCH_FILES[@]}"; do
        patch --batch --forward --no-backup-if-mismatch --directory="${SOURCE_ROOT}" --strip=1 <"${patch_file}"
    done
    for patch_file in "${GAMELIBS_PATCH_FILES[@]}"; do
        patch --batch --forward --no-backup-if-mismatch --directory="${GAMELIBS_ROOT}" --strip=1 <"${patch_file}"
    done
    new_source_stamp=1
else
    mapfile -t source_stamp_lines <"${STAMP_FILE}"
    if [[ ${#source_stamp_lines[@]} -ne 3 \
        || "${source_stamp_lines[0]}" != "${expected_stamp}" \
        || ! "${source_stamp_lines[1]}" =~ ^openq4_diff_sha256=([0-9a-f]{64})$ \
        || ! "${source_stamp_lines[2]}" =~ ^gamelibs_diff_sha256=([0-9a-f]{64})$ ]]; then
        echo "Native source stamp is stale or malformed; rerun with --clean" >&2
        exit 1
    fi
fi

[[ "$(git -C "${SOURCE_ROOT}" rev-parse HEAD)" == "${OPENQ4_PIN}" ]]
[[ "$(git -C "${GAMELIBS_ROOT}" rev-parse HEAD)" == "${GAMELIBS_PIN}" ]]
openq4_diff_sha="$(openq4_materialized_diff_sha)"
gamelibs_diff_sha="$(gamelibs_materialized_diff_sha)"
if ((new_source_stamp)); then
    printf '%s\nopenq4_diff_sha256=%s\ngamelibs_diff_sha256=%s\n' \
        "${expected_stamp}" "${openq4_diff_sha}" "${gamelibs_diff_sha}" >"${STAMP_FILE}"
elif [[ "${source_stamp_lines[1]}" != "openq4_diff_sha256=${openq4_diff_sha}" \
    || "${source_stamp_lines[2]}" != "gamelibs_diff_sha256=${gamelibs_diff_sha}" ]]; then
    echo "Native materialized patched source differs from its stamp; rerun with --clean" >&2
    exit 1
fi
rm -rf -- \
    "${SOURCE_ROOT}/content/baseoq4/pak0" \
    "${SOURCE_ROOT}/content/baseoq4/pak1"
mkdir -p -- "${SOURCE_ROOT}/content/baseoq4"
cp -a -- "${FREE_RUNTIME_SOURCE}/pak0" "${SOURCE_ROOT}/content/baseoq4/pak0"
cp -a -- "${FREE_RUNTIME_SOURCE}/pak1" "${SOURCE_ROOT}/content/baseoq4/pak1"
rm -rf -- "${FREE_RUNTIME_EXPECTED}"
mkdir -p -- "${FREE_RUNTIME_EXPECTED}"
python3 "${FREE_RUNTIME_BUILDER}" \
    --source "${FREE_RUNTIME_SOURCE}" \
    --output "${FREE_RUNTIME_EXPECTED}" \
    --manifest "${FREE_RUNTIME_EXPECTED}/manifest.json" >/dev/null
git -C "${SOURCE_ROOT}" add -N .
git -C "${GAMELIBS_ROOT}" add -N .
git -C "${SOURCE_ROOT}" diff --check -- . \
    ':(exclude)content/baseoq4/pak0' \
    ':(exclude)content/baseoq4/pak0/**' \
    ':(exclude)content/baseoq4/pak1' \
    ':(exclude)content/baseoq4/pak1/**'
git -C "${GAMELIBS_ROOT}" diff --check
grep -Fq $'return botClientId;' "${SOURCE_ROOT}/src/framework/async/AsyncServer.cpp"
grep -Fq $'net_serverDemoPath' "${SOURCE_ROOT}/src/framework/async/AsyncNetwork.cpp"
grep -Fq $'fflush( stdout );' "${SOURCE_ROOT}/src/sys/posix/posix_main.cpp"
grep -Fq $'cmdSystem->AddCommand("runAAS"' "${SOURCE_ROOT}/src/framework/Common.cpp"
grep -Fq $'"tools/compilers/aas/*.cpp"' "${SOURCE_ROOT}/tools/build/meson_sources.py"
grep -Fq $'Parse1DMatrix( 3' "${GAMELIBS_ROOT}/src/game/bots/Bot_goal.cpp"
grep -Fq $'networkSystem->RepeaterSendReliableMessage( clientNum, outMsg );' "${GAMELIBS_ROOT}/src/mpgame/Game_network.cpp"
grep -Fq $'#define ASYNC_WRITE_TAGS 1' "${GAMELIBS_ROOT}/src/mpgame/Game_network.cpp"
grep -Fq $'GAME_RELIABLE_MESSAGE_EVENT_ACK' "${GAMELIBS_ROOT}/src/mpgame/Game_network.cpp"
grep -Fq $'MaintainBotMinPlayers();' "${GAMELIBS_ROOT}/src/mpgame/Game_local.cpp"
grep -Fq $'weaponinfo[i].proj = projectileinfo[j];' "${GAMELIBS_ROOT}/src/mpgame/bots/Bot_weapons.cpp"
test -f "${GAMELIBS_ROOT}/src/mpgame/bots/Bot.cpp"

export CC=gcc-13
export CXX=g++-13
export OPENQ4_GAMELIBS_REPO="${GAMELIBS_ROOT}"
mkdir -p -- "${ROOT}/build/tmp"
export TMPDIR="${ROOT}/build/tmp"

setup_args=(
    "${BUILD_ROOT}"
    "${SOURCE_ROOT}"
    --backend ninja
    --buildtype debugoptimized
    --wrap-mode forcefallback
    -Db_ndebug=true
    -Dplatform_backend="${PLATFORM_BACKEND}"
    -Dbuild_engine=true
    -Dbuild_games=true
    -Dbuild_game_sp=true
    -Dbuild_game_mp=true
)

if [[ -f "${BUILD_ROOT}/meson-private/coredata.dat" ]]; then
    meson setup --reconfigure "${setup_args[@]}"
else
    meson setup "${setup_args[@]}"
fi

meson compile -C "${BUILD_ROOT}" --jobs "${JOBS}"
rm -rf -- "${SOURCE_ROOT}/.install"
meson install -C "${BUILD_ROOT}" --no-rebuild --skip-subprojects

rm -rf -- "${RUNTIME_ROOT}"
mkdir -p -- "${RUNTIME_ROOT}"
rsync -a -- "${SOURCE_ROOT}/.install/" "${RUNTIME_ROOT}/"
ln -s -- "../../assets-src/gog/q4base" "${RUNTIME_ROOT}/q4base"
for pack in pak0.pk4 pak1.pk4; do
    [[ -f "${RUNTIME_ROOT}/baseoq4/${pack}" ]] || {
        echo "Native free runtime archive is missing after build: ${pack}" >&2
        exit 1
    }
    cmp -- "${FREE_RUNTIME_EXPECTED}/${pack}" "${RUNTIME_ROOT}/baseoq4/${pack}" || {
        echo "Native free runtime archive differs from deterministic source build: ${pack}" >&2
        exit 1
    }
done

mkdir -p -- "${RUNTIME_ROOT}/build-info"
{
    printf 'openq4=%s\n' "${OPENQ4_PIN}"
    printf 'gamelibs=%s\n' "${GAMELIBS_PIN}"
    printf 'native_patch_manifest_sha256=%s\n' "${patch_manifest_sha}"
    printf 'runtime_source_sha256=%s\n' "${free_runtime_source_sha}"
    printf 'platform_backend=%s\n' "${PLATFORM_BACKEND}"
    printf 'compiler=%s\n' "$(g++-13 --version | head -n 1)"
    printf 'meson=%s\n' "$(meson --version)"
    printf 'built_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} >"${RUNTIME_ROOT}/build-info/build.txt"
sha256sum \
    "${RUNTIME_ROOT}/openQ4-client_x64" \
    "${RUNTIME_ROOT}/openQ4-ded_x64" \
    "${RUNTIME_ROOT}/baseoq4/game-sp_x64.so" \
    "${RUNTIME_ROOT}/baseoq4/game-mp_x64.so" \
    >"${RUNTIME_ROOT}/build-info/SHA256SUMS"

echo "Native OpenQ4 staged at ${RUNTIME_ROOT}"
cat "${RUNTIME_ROOT}/build-info/build.txt"
cat "${RUNTIME_ROOT}/build-info/SHA256SUMS"
