#!/usr/bin/env bash
#
# Builds a minimal, LGPL-only FFmpeg as an XCFramework for Loft.
#
# This script is part of the app's licence compliance, not just its build: LGPL-2.1 requires
# that a user be able to rebuild and relink the library, so it is published alongside the
# app and must stay runnable by someone who is not us. Keep it dependency-free and pinned.
# See docs/decisions/0021-ffmpeg-for-demuxing-only.md.
#
# What it deliberately does NOT build:
#   - Any encoder, decoder, filter or scaler. Loft never decodes with FFmpeg; VideoToolbox
#     does that after the remux. Passthrough only.
#   - Any protocol or networking. Input and output are custom AVIOContexts driven by Swift,
#     so FFmpeg performs no I/O of its own and never sees a credential or a URL.
#   - Anything under --enable-gpl or --enable-nonfree. The result is LGPL-2.1+ only.
#
# Usage:  Tools/build-ffmpeg.sh [output-directory]
set -euo pipefail

FFMPEG_VERSION="n7.1"
IOS_MIN="18.0"
TVOS_MIN="18.0"
MACOS_MIN="14.0"

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Inside the package: SwiftPM will not reference a binary target outside its root.
OUTPUT="${1:-$ROOT/Packages/MediaKit/Vendor/FFmpeg}"
# Built outside the project, at a neutral path: FFmpeg records its full configure command
# line — including every directory passed to it — inside the library, and a shipped app
# should not carry the builder's folder layout. Override with LOFT_FFMPEG_WORK.
WORK="${LOFT_FFMPEG_WORK:-/tmp/loft-ffmpeg-build}"
SOURCE="$WORK/ffmpeg-$FFMPEG_VERSION"

command -v xcrun >/dev/null || { echo "error: Xcode command line tools required"; exit 1; }

# --- source ------------------------------------------------------------------------------
mkdir -p "$WORK"
if [ ! -d "$SOURCE" ]; then
    echo "==> Fetching FFmpeg $FFMPEG_VERSION"
    git clone --depth 1 --branch "$FFMPEG_VERSION" \
        https://git.ffmpeg.org/ffmpeg.git "$SOURCE"
fi

# --- what we build -----------------------------------------------------------------------
# Start from nothing and add only what a remux needs.
COMMON_CONFIG=(
    --disable-everything
    --disable-gpl --disable-nonfree --disable-version3
    --disable-programs --disable-doc
    # Debug info is compiled in, then moved into a dSYM and stripped from the shipped
    # library (see make_framework). Without it App Store Connect cannot symbolicate a crash
    # inside FFmpeg, and warns on every upload that the archive has no dSYM for it.
    --disable-stripping
    --disable-avdevice --disable-avfilter --disable-swscale --disable-swresample
    --disable-postproc --disable-network --disable-protocols --disable-devices
    --disable-encoders --disable-decoders --disable-filters --disable-iconv
    --disable-sdl2 --disable-audiotoolbox --disable-videotoolbox
    --enable-pic --enable-small --enable-static --disable-shared

    # The containers the catalogue actually contains, plus MP4 so we can read as well as
    # write it. See the container mix in ADR-0021.
    --enable-demuxer=matroska,mpegts,mpegtsraw,avi,mov
    # mp4 is what we play. matroska is not used in the app: it exists so the test suite can
    # generate real .mkv fixtures, which no Apple framework can write. A muxer carries no
    # codec, so this adds about 20 KB and no licence surface.
    --enable-muxer=mp4,matroska

    # Parsers supply the codec configuration the MP4 muxer needs. MPEG-TS carries no
    # extradata, so without these a TS remux produces a file with no decodable tracks.
    --enable-parser=h264,hevc,aac,aac_latm,ac3,mpegaudio

    # Bitstream filters convert framing, not content: Annex-B to length-prefixed for
    # H.264/HEVC, and ADTS to raw AAC. Still passthrough — no samples are re-encoded.
    --enable-bsf=h264_mp4toannexb,hevc_mp4toannexb,aac_adtstoasc,extract_extradata
)

# --- one slice ---------------------------------------------------------------------------
# Paths are remapped so the binary does not carry the builder's directory layout — assertion
# messages embed __FILE__, and a local path in a shipped app is both a leak and noise.
build_slice() {
    local name="$1" sdk="$2" arch="$3" min_flag="$4"
    local prefix="$WORK/$name"
    # x86 assembly needs nasm, and this script must stay runnable by anyone without extra
    # tools — it is part of the licence obligation, not only our build. Nothing is lost:
    # the assembly accelerates codecs, and this build contains none.
    local asm_config=()
    [ "$arch" = "x86_64" ] && asm_config=(--disable-x86asm)
    [ -f "$prefix/lib/libavformat.a" ] && { echo "==> $name already built"; return; }

    echo "==> Building $name ($sdk, $arch)"
    local sysroot; sysroot="$(xcrun --sdk "$sdk" --show-sdk-path)"
    local build_dir="$WORK/build-$name"
    rm -rf "$build_dir"; mkdir -p "$build_dir"

    ( cd "$build_dir" && "$SOURCE/configure" \
        "${COMMON_CONFIG[@]}" \
        "${asm_config[@]+"${asm_config[@]}"}" \
        --prefix="$prefix" \
        --enable-cross-compile \
        --target-os=darwin \
        --arch="$arch" \
        --cc="$(xcrun --sdk "$sdk" -f clang)" \
        --extra-cflags="-arch $arch -isysroot $sysroot $min_flag -fno-common -ffile-prefix-map=$SOURCE=ffmpeg -ffile-prefix-map=$build_dir=ffmpeg-build" \
        --extra-ldflags="-arch $arch -isysroot $sysroot $min_flag" \
        >"$build_dir/configure.log" 2>&1 \
        || { echo "configure failed; see $build_dir/configure.log"; tail -30 "$build_dir/configure.log"; exit 1; } )

    make -C "$build_dir" -j"$(sysctl -n hw.ncpu)" >"$build_dir/make.log" 2>&1 \
        || { echo "make failed; see $build_dir/make.log"; tail -30 "$build_dir/make.log"; exit 1; }
    make -C "$build_dir" install >/dev/null
}

# Fat slices: an XCFramework needs one library per platform, with both architectures inside
# for the simulator and for Macs that still run Intel.
merge_slice() {
    local output="$1"; shift
    mkdir -p "$output/lib" "$output/include"
    local first="$1"
    cp -R "$WORK/$first/include/." "$output/include/"
    # A module map at the headers root is what lets Swift `import CFFmpeg`. SwiftPM picks
    # it up from the XCFramework rather than needing a separate system-library target.
    cat > "$output/include/module.modulemap" <<'MODULEMAP'
module CFFmpeg [system] {
    header "libavformat/avformat.h"
    header "libavformat/avio.h"
    header "libavcodec/avcodec.h"
    header "libavutil/avutil.h"
    header "libavutil/opt.h"
    header "libavutil/dict.h"
    export *
}
MODULEMAP
    for library in libavformat libavcodec libavutil; do
        local inputs=()
        for slice in "$@"; do inputs+=("$WORK/$slice/lib/$library.a"); done
        lipo -create "${inputs[@]}" -output "$output/lib/$library.a"
    done
    # One archive per platform keeps the XCFramework simple to consume.
    libtool -static -o "$output/lib/libffmpeg.a" \
        "$output/lib/libavformat.a" "$output/lib/libavcodec.a" "$output/lib/libavutil.a" 2>/dev/null
}

build_slice "ios-arm64"      iphoneos          arm64  "-mios-version-min=$IOS_MIN"
build_slice "iossim-arm64"   iphonesimulator   arm64  "-mios-simulator-version-min=$IOS_MIN"
build_slice "iossim-x86_64"  iphonesimulator   x86_64 "-mios-simulator-version-min=$IOS_MIN"
build_slice "macos-arm64"    macosx            arm64  "-mmacosx-version-min=$MACOS_MIN"
build_slice "macos-x86_64"   macosx            x86_64 "-mmacosx-version-min=$MACOS_MIN"
build_slice "tvos-arm64"     appletvos         arm64  "-mtvos-version-min=$TVOS_MIN"
build_slice "tvossim-arm64"  appletvsimulator  arm64  "-mtvos-simulator-version-min=$TVOS_MIN"
build_slice "tvossim-x86_64" appletvsimulator  x86_64 "-mtvos-simulator-version-min=$TVOS_MIN"

echo "==> Merging slices"
merge_slice "$WORK/fat-ios"    "ios-arm64"
merge_slice "$WORK/fat-iossim" "iossim-arm64" "iossim-x86_64"
merge_slice "$WORK/fat-macos"  "macos-arm64" "macos-x86_64"
merge_slice "$WORK/fat-tvos"   "tvos-arm64"
merge_slice "$WORK/fat-tvossim" "tvossim-arm64" "tvossim-x86_64"

# --- dynamic framework --------------------------------------------------------------------
# LGPL-2.1 §6 is met most plainly by a shared library the user can replace: the app loads
# CFFmpeg.framework from its Frameworks folder by @rpath, so a modified FFmpeg built with
# this script can be dropped in its place. The static archives above are linked whole into
# one dynamic library per platform. See docs/decisions/0022-ffmpeg-dynamic-framework.md.
FRAMEWORK=CFFmpeg

framework_plist() {
    local platforms="$1" minimum_key="$2" minimum="$3"
    cat <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key><string>en</string>
    <key>CFBundleExecutable</key><string>$FRAMEWORK</string>
    <key>CFBundleIdentifier</key><string>dev.abishevs.loft.ffmpeg</string>
    <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
    <key>CFBundleName</key><string>$FRAMEWORK</string>
    <key>CFBundlePackageType</key><string>FMWK</string>
    <key>CFBundleShortVersionString</key><string>${FFMPEG_VERSION#n}</string>
    <key>CFBundleVersion</key><string>1</string>
    <key>CFBundleSupportedPlatforms</key><array>$platforms</array>
    <key>$minimum_key</key><string>$minimum</string>
</dict>
</plist>
PLIST
}

module_map() {
    cat <<'MODULEMAP'
framework module CFFmpeg {
    header "libavformat/avformat.h"
    header "libavformat/avio.h"
    header "libavcodec/avcodec.h"
    header "libavutil/avutil.h"
    header "libavutil/opt.h"
    header "libavutil/dict.h"
    export *
}
MODULEMAP
}

# make_framework <fat-dir> <sdk> <min-flag> <layout: flat|versioned> <platforms-xml> <min-key> <min>
make_framework() {
    local fat="$1" sdk="$2" min_flag="$3" layout="$4" platforms="$5" minimum_key="$6" minimum="$7"
    local root="$fat/$FRAMEWORK.framework"
    local sysroot; sysroot="$(xcrun --sdk "$sdk" --show-sdk-path)"
    local archs; archs="$(lipo -archs "$fat/lib/libffmpeg.a")"
    local arch_flags=(); for arch in $archs; do arch_flags+=(-arch "$arch"); done
    rm -rf "$root"

    local contents install_name
    if [ "$layout" = versioned ]; then
        contents="$root/Versions/A"
        install_name="@rpath/$FRAMEWORK.framework/Versions/A/$FRAMEWORK"
        mkdir -p "$contents/Resources"
        framework_plist "$platforms" "$minimum_key" "$minimum" > "$contents/Resources/Info.plist"
    else
        contents="$root"
        install_name="@rpath/$FRAMEWORK.framework/$FRAMEWORK"
        mkdir -p "$contents"
        framework_plist "$platforms" "$minimum_key" "$minimum" > "$contents/Info.plist"
    fi
    mkdir -p "$contents/Headers" "$contents/Modules"
    cp -R "$fat/include/." "$contents/Headers/"
    rm -f "$contents/Headers/module.modulemap"
    # FFmpeg's headers include each other as "libavutil/…", which resolves only when the
    # include directory is on the search path — never true inside a framework. The shipped
    # copies are rewritten to framework form. Only these interface copies change: the
    # library itself is built from unmodified source.
    find "$contents/Headers" -name '*.h' -exec \
        sed -i '' -E 's#include "(libav[a-z]+/[^"]+)"#include <CFFmpeg/\1>#' {} +
    module_map > "$contents/Modules/module.modulemap"

    xcrun --sdk "$sdk" clang -dynamiclib "${arch_flags[@]}" -isysroot "$sysroot" $min_flag \
        -Wl,-all_load "$fat/lib/libffmpeg.a" -lz -lbz2 \
        $STRIPPED_SYMBOLS -Wl,-dead_strip \
        -install_name "$install_name" \
        -compatibility_version 1 -current_version 1 \
        -o "$contents/$FRAMEWORK"

    # Debug symbols out into a dSYM that matches this binary's UUID, then stripped from the
    # binary itself, so what ships is no larger for carrying them.
    rm -rf "$fat/$FRAMEWORK.framework.dSYM"
    xcrun dsymutil "$contents/$FRAMEWORK" -o "$fat/$FRAMEWORK.framework.dSYM"
    xcrun strip -S -x "$contents/$FRAMEWORK"

    if [ "$layout" = versioned ]; then
        ln -sfh A "$root/Versions/Current"
        for entry in "$FRAMEWORK" Headers Modules Resources; do
            ln -sfh "Versions/Current/$entry" "$root/$entry"
        done
    fi
}

# av_file_map/av_file_unmap are the only code that calls fstat, one of Apple's
# "required reason" file-timestamp APIs. Nothing in FFmpeg or Loft calls them. A static link
# stripped them as unused; a dynamic library exports everything, so they are unexported and
# dead-stripped here rather than declared in the privacy manifest for code that never runs.
STRIPPED_SYMBOLS="-Wl,-unexported_symbol,_av_file_map -Wl,-unexported_symbol,_av_file_unmap"

echo "==> Linking dynamic frameworks"
make_framework "$WORK/fat-ios"    iphoneos        "-mios-version-min=$IOS_MIN"           flat \
    "<string>iPhoneOS</string>"        MinimumOSVersion "$IOS_MIN"
make_framework "$WORK/fat-iossim" iphonesimulator "-mios-simulator-version-min=$IOS_MIN" flat \
    "<string>iPhoneSimulator</string>" MinimumOSVersion "$IOS_MIN"
make_framework "$WORK/fat-macos"  macosx          "-mmacosx-version-min=$MACOS_MIN"      versioned \
    "<string>MacOSX</string>"          LSMinimumSystemVersion "$MACOS_MIN"
make_framework "$WORK/fat-tvos"   appletvos        "-mtvos-version-min=$TVOS_MIN"           flat \
    "<string>AppleTVOS</string>"       MinimumOSVersion "$TVOS_MIN"
make_framework "$WORK/fat-tvossim" appletvsimulator "-mtvos-simulator-version-min=$TVOS_MIN" flat \
    "<string>AppleTVSimulator</string>" MinimumOSVersion "$TVOS_MIN"

echo "==> Assembling XCFramework"
rm -rf "$OUTPUT/FFmpeg.xcframework" "$OUTPUT/$FRAMEWORK.xcframework"
# Each framework carries its dSYM, which Xcode copies into an archive's dSYMs folder.
xcodebuild -create-xcframework \
    -framework "$WORK/fat-ios/$FRAMEWORK.framework" -debug-symbols "$WORK/fat-ios/$FRAMEWORK.framework.dSYM" \
    -framework "$WORK/fat-iossim/$FRAMEWORK.framework" -debug-symbols "$WORK/fat-iossim/$FRAMEWORK.framework.dSYM" \
    -framework "$WORK/fat-macos/$FRAMEWORK.framework" -debug-symbols "$WORK/fat-macos/$FRAMEWORK.framework.dSYM" \
    -framework "$WORK/fat-tvos/$FRAMEWORK.framework" -debug-symbols "$WORK/fat-tvos/$FRAMEWORK.framework.dSYM" \
    -framework "$WORK/fat-tvossim/$FRAMEWORK.framework" -debug-symbols "$WORK/fat-tvossim/$FRAMEWORK.framework.dSYM" \
    -output "$OUTPUT/$FRAMEWORK.xcframework" >/dev/null

# --- compliance --------------------------------------------------------------------------
cp "$SOURCE/COPYING.LGPLv2.1" "$OUTPUT/COPYING.LGPLv2.1"
cat > "$OUTPUT/LICENCE-NOTICE.md" <<NOTICE
# FFmpeg in Loft

Loft uses [FFmpeg](https://ffmpeg.org) ($FFMPEG_VERSION), licensed under the
**GNU Lesser General Public License version 2.1 or later**. A copy is in
\`COPYING.LGPLv2.1\`.

FFmpeg is used **only to rewrite media containers** — reading Matroska, MPEG-TS and AVI and
writing fragmented MP4, copying the compressed audio and video through unchanged. It
performs no decoding, encoding or filtering, and no network access; playback is Apple's
AVFoundation throughout.

The build is configured with \`--disable-gpl --disable-nonfree\`, so it contains no
GPL-licensed components. The exact configuration is in \`Tools/build-ffmpeg.sh\`, which
reproduces this binary from unmodified upstream source.

**FFmpeg source is unmodified.** The only change in what ships is packaging: the public
headers inside the framework have their \`#include "libav…"\` lines rewritten to framework form
(\`<CFFmpeg/libav…>\`) so they resolve inside a framework. To rebuild and relink: run \`Tools/build-ffmpeg.sh\`, which
fetches tag \`$FFMPEG_VERSION\` from the upstream repository and produces
\`CFFmpeg.xcframework\`. The app loads FFmpeg as a replaceable dynamic framework,
\`CFFmpeg.framework\`, from its Frameworks folder.
NOTICE

echo
echo "==> Done: $OUTPUT/$FRAMEWORK.xcframework"
du -sh "$OUTPUT/$FRAMEWORK.xcframework"
