#!/bin/sh
# prebuild-aether.sh — (re)build the vendored AetherEngine binary + FFmpeg frameworks.
#
# WHY THIS EXISTS
# AetherEngine ships FFmpeg as separate .framework bundles whose headers cross-
# reference each other with lowercase quoted includes (#include "libavutil/pixfmt.h").
# Xcode 26's per-framework Clang MODULE compiler builds each framework in isolation
# and can't resolve those, so `enum AVAlphaMode` reads as incomplete and Libavcodec/
# Libavfilter fail to build — AND AetherEngine references many bare FFmpeg enum
# constants (AV_CODEC_ID_*, AV_FIELD_*) that a header-search-path workaround makes
# Swift import wrong. SwiftPM's OWN build engine compiles it all correctly, so we
# PREBUILD AetherEngine with SwiftPM and link the binary into the app; the app then
# never compiles the FFmpeg headers (it only links the dylibs).
#
# WHAT THE APP STILL NEEDS (see project.pbxproj + patch note)
# Importing the prebuilt AetherEngine.swiftmodule makes Swift build its transitive
# FFmpeg *module* dependencies, which still need the cross-includes to resolve. That
# is handled durably by:
#   * Vendored/ffmpeg-include/  — a flat sibling header tree (libavutil/, libavcodec/…)
#     added via  OTHER_SWIFT_FLAGS = -Xcc -I$(SRCROOT)/Vendored/ffmpeg-include
#     and         OTHER_CFLAGS      = -I$(SRCROOT)/Vendored/ffmpeg-include
#   * SWIFT_ENABLE_EXPLICIT_MODULES = NO  (so the -I reaches the implicit module build)
# The enum-import breakage the -I causes is irrelevant to the app: only AetherEngine's
# own code used those enums, and it is prebuilt.
#
# Run this after bumping the AetherEngine version. Requires a resolved SwiftPM
# checkout of AetherEngine + FFmpegBuild + LibDovi (do a throwaway `swift package
# resolve` of a package that depends on the desired AetherEngine version, or point
# AETHER_SRC / CHECKOUTS at an existing one).
#
#   AETHER_SRC=<path to AetherEngine checkout> CHECKOUTS=<SourcePackages/checkouts> \
#     scripts/prebuild-aether.sh
set -eu

HERE="$(cd "$(dirname "$0")/.." && pwd)"          # tvosApp/
V="$HERE/Vendored"
AETHER_SRC="${AETHER_SRC:?set AETHER_SRC to the AetherEngine package checkout}"
CHECKOUTS="${CHECKOUTS:?set CHECKOUTS to SourcePackages/checkouts (has FFmpegBuild, LibDovi)}"

BIN="$(dirname "$(xcrun -f swiftc)")"
SIMSDK="$(xcrun --sdk appletvsimulator --show-sdk-path)"
DEVSDK="$(xcrun --sdk appletvos --show-sdk-path)"
TMP="$(mktemp -d)"

mk_dest() { # sdk target -> json
    cat > "$TMP/$2.json" <<EOF
{ "version":1, "sdk":"$1", "toolchain-bin-dir":"$BIN", "target":"$2",
  "extra-cc-flags":["-isysroot","$1","-arch","arm64"],
  "extra-swiftc-flags":["-sdk","$1","-target","$2"],
  "extra-cpp-flags":["-isysroot","$1"] }
EOF
}
mk_dest "$SIMSDK" "arm64-apple-tvos16.0-simulator"
mk_dest "$DEVSDK" "arm64-apple-tvos16.0"

# Loud version banner — a stale AETHER_SRC (e.g. an old scratch clone) silently
# vendors the WRONG engine version. Print exactly what we're about to compile so a
# mismatch with the intended pin is impossible to miss. (Cost a wasted verify pass once:
# vendored 5.10.0 from a scratch clone while the project pinned 5.15.0.)
AE_VER="$(git -C "$AETHER_SRC" describe --tags 2>/dev/null || echo '?unknown')"
FF_VER="$(grep -oE 'FFmpegBuild"?, from: "[0-9.]+"' "$AETHER_SRC/Package.swift" 2>/dev/null | grep -oE '[0-9.]+' || echo '?')"
echo "== AetherEngine SOURCE: $AETHER_SRC"
echo "== AetherEngine VERSION: $AE_VER   (wants FFmpegBuild $FF_VER)"

# Reapply our local source patches (upstream fixes not yet merged). Applied on every
# prebuild so a fresh checkout / version bump keeps them until upstream lands. Idempotent:
# a patch already present is skipped; one that no longer applies is a hard error (the
# AetherEngine version changed — regenerate the patch). See tvosApp/aether-patches/.
PATCHES="$HERE/aether-patches"
if [ -d "$PATCHES" ]; then
    for p in "$PATCHES"/*.patch; do
        [ -f "$p" ] || continue
        base="$(basename "$p")"
        if git -C "$AETHER_SRC" apply --reverse --check "$p" 2>/dev/null; then
            echo "== patch already applied: $base"
        elif git -C "$AETHER_SRC" apply --check "$p" 2>/dev/null; then
            git -C "$AETHER_SRC" apply "$p" && echo "== applied patch: $base"
        else
            echo "FATAL: $base does not apply to AetherEngine $AE_VER — regenerate it against this version"
            exit 1
        fi
    done
fi

echo "== building AetherEngine (release) for tvOS sim + device via SwiftPM =="
( cd "$AETHER_SRC" && swift build -c release --destination "$TMP/arm64-apple-tvos16.0-simulator.json" \
    --target AetherEngine --scratch-path "$TMP/sim" )
( cd "$AETHER_SRC" && swift build -c release --destination "$TMP/arm64-apple-tvos16.0.json" \
    --target AetherEngine --scratch-path "$TMP/dev" )

SIMREL="$TMP/sim/arm64-apple-tvos-simulator/release"
DEVREL="$TMP/dev/arm64-apple-tvos/release"

echo "== static libs =="
libtool -static -o "$TMP/libAetherEngine-sim.a" "$SIMREL/AetherEngine.build/"*.o
libtool -static -o "$TMP/libAetherEngine-dev.a" "$DEVREL/AetherEngine.build/"*.o

echo "== assemble AetherEngine.xcframework =="
for s in sim dev; do
    [ "$s" = sim ] && M="$SIMREL/Modules" && T="arm64-apple-tvos-simulator" || { M="$DEVREL/Modules"; T="arm64-apple-tvos"; }
    mkdir -p "$TMP/hdr-$s/AetherEngine.swiftmodule"
    cp "$M/AetherEngine.swiftmodule" "$TMP/hdr-$s/AetherEngine.swiftmodule/$T.swiftmodule"
    cp "$M/AetherEngine.swiftdoc"    "$TMP/hdr-$s/AetherEngine.swiftmodule/$T.swiftdoc"
done
rm -rf "$V/AetherEngine.xcframework"
xcodebuild -create-xcframework \
    -library "$TMP/libAetherEngine-sim.a" -headers "$TMP/hdr-sim" \
    -library "$TMP/libAetherEngine-dev.a" -headers "$TMP/hdr-dev" \
    -output "$V/AetherEngine.xcframework"

# CRITICAL: `xcodebuild -create-xcframework` STRIPS *.swiftmodule files out of the
# -headers tree (it keeps .swiftdoc but drops the actual module interface). Without
# this the app fails with "no such module 'AetherEngine'" on any CLEAN build — the
# device build and any fresh-derived-data/CI build. So copy the modules back in by
# hand and verify they landed. (Diagnosed 2026-07-20: device build broke while a
# dirty sim derived-data dir masked it via leftover SPM module state.)
for s in sim dev; do
    [ "$s" = sim ] && { M="$SIMREL/Modules"; SLICE="tvos-arm64-simulator"; T="arm64-apple-tvos-simulator"; } \
                   || { M="$DEVREL/Modules"; SLICE="tvos-arm64";            T="arm64-apple-tvos"; }
    cp "$M/AetherEngine.swiftmodule" "$V/AetherEngine.xcframework/$SLICE/Headers/AetherEngine.swiftmodule/$T.swiftmodule"
done
for SLICE in tvos-arm64-simulator tvos-arm64; do
    D="$V/AetherEngine.xcframework/$SLICE/Headers/AetherEngine.swiftmodule"
    n=$(find "$D" -name '*.swiftmodule' 2>/dev/null | wc -l | tr -d ' ')
    [ "$n" -ge 1 ] || { echo "FATAL: $SLICE slice is missing its .swiftmodule after repair"; exit 1; }
done
echo "== .swiftmodule restored into both xcframework slices (create-xcframework strips them) =="

echo "== vendor the FFmpeg + Dovi dynamic frameworks =="
for x in "$CHECKOUTS"/FFmpegBuild/Sources/Lib*.xcframework; do rm -rf "$V/$(basename "$x")"; cp -R "$x" "$V/"; done
DOVI="$(find "$CHECKOUTS/LibDovi" -name Dovi.xcframework -maxdepth 3 | head -1)"
[ -n "$DOVI" ] && { rm -rf "$V/Dovi.xcframework"; cp -R "$DOVI" "$V/"; }

echo "== regenerate the ffmpeg-include tree (flat sibling headers for -I) =="
INC="$V/ffmpeg-include"; SLICE="tvos-arm64_x86_64-simulator"
rm -rf "$INC"
for pair in Libavutil:libavutil Libavcodec:libavcodec Libavformat:libavformat Libavfilter:libavfilter Libswscale:libswscale Libswresample:libswresample; do
    fw="${pair%%:*}"; low="${pair##*:}"; H="$V/$fw.xcframework/$SLICE/$fw.framework/Headers"
    [ -d "$H" ] || continue; mkdir -p "$INC/$low"; cp "$H"/*.h "$INC/$low/"
done

rm -rf "$TMP"
echo "== done. Vendored/ updated. Build the app normally (settings are in project.pbxproj). =="
