#!/bin/bash
# build-ffmpeg.sh — pinned, reproducible FFmpegBridge.xcframework build.
#
# Slices: ios-arm64  ios-sim-arm64  tvos-arm64  tvos-sim-arm64
# Layers: dav1d → freetype → harfbuzz → fribidi → libass → ffmpeg → merge → xcframework
#
# Usage:
#   build-ffmpeg.sh fetch                      download + sha256-verify tarballs
#   build-ffmpeg.sh deps   [slice]             build the 5 dependency libs
#   build-ffmpeg.sh ffmpeg [slice]             configure + build ffmpeg libs (+fftools objs)
#   build-ffmpeg.sh merge  [slice]             merge static libs into libFFmpegBridge.a
#   build-ffmpeg.sh xcframework                assemble out/FFmpegBridge.xcframework
#   build-ffmpeg.sh all    [slice]             deps+ffmpeg+merge (then xcframework if no slice given)
#
# RECORD_CHECKSUMS=1 build-ffmpeg.sh fetch     append missing sha256 pins (first run only)
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# The repo lives under "KSK Git" — a path WITH A SPACE. FFmpeg's configure and
# pkg-config -I flags both break on spaces, so all compilation happens in a
# space-free work dir; only scripts (and the final xcframework copy) live in-repo.
WORK="${MEDIABRIDGE_WORK:-$HOME/.kskpix/mediabridge}"
case "$WORK" in *" "*) echo "✗ MEDIABRIDGE_WORK must not contain spaces: $WORK"; exit 1;; esac
SRC="$WORK/src";  BUILD="$WORK/build";  OUT="$WORK/out"
CHECKSUMS="$ROOT/scripts/checksums.txt"
mkdir -p "$SRC" "$BUILD" "$OUT"

# ── Version pins ─────────────────────────────────────────────────────────────
FFMPEG_V=7.1.5
DAV1D_V=1.5.4
FREETYPE_V=2.14.3
HARFBUZZ_V=14.2.1
FRIBIDI_V=1.0.16
LIBASS_V=0.17.5
# libdovi (dovi_tool's dolby_vision crate, MIT) — the P7→8.1 RPU converter
# behind our custom dovi_convert bsf. Built with Rust NIGHTLY via cargo-c
# (`rustup toolchain install nightly && cargo install cargo-c`); tvOS is a
# tier-3 target so its std is built from source (-Zbuild-std).
DOVI_TOOL_COMMIT=4b36af992a54182f4f8e11198c884d53d5fb5054   # dolby_vision 3.4.0

# iOS floor matches kskpix-flutter Podfile; tvOS floor matches tvosApp target.
IOS_MIN=15.0
TVOS_MIN=17.0

ALL_SLICES=(ios-arm64 ios-sim-arm64 tvos-arm64 tvos-sim-arm64)

url_for() {
  case "$1" in
    ffmpeg)   echo "https://ffmpeg.org/releases/ffmpeg-$FFMPEG_V.tar.xz" ;;
    dav1d)    echo "https://downloads.videolan.org/pub/videolan/dav1d/$DAV1D_V/dav1d-$DAV1D_V.tar.xz" ;;
    freetype) echo "https://download.savannah.gnu.org/releases/freetype/freetype-$FREETYPE_V.tar.xz" ;;
    harfbuzz) echo "https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_V/harfbuzz-$HARFBUZZ_V.tar.xz" ;;
    fribidi)  echo "https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_V/fribidi-$FRIBIDI_V.tar.xz" ;;
    libass)   echo "https://github.com/libass/libass/releases/download/$LIBASS_V/libass-$LIBASS_V.tar.xz" ;;
    *) echo "unknown lib $1" >&2; exit 1 ;;
  esac
}

# ── fetch ────────────────────────────────────────────────────────────────────
fetch_one() {
  local lib="$1" url tar
  url="$(url_for "$lib")"; tar="$SRC/$(basename "$url")"
  [ -f "$tar" ] || { echo "↓ $url"; curl -fL --retry 3 -o "$tar.tmp" "$url"; mv "$tar.tmp" "$tar"; }
  local sum have
  sum="$(shasum -a 256 "$tar" | awk '{print $1}')"
  have="$(grep -E "^${sum}  $(basename "$tar")\$" "$CHECKSUMS" 2>/dev/null || true)"
  if [ -z "$have" ]; then
    if grep -q "  $(basename "$tar")\$" "$CHECKSUMS" 2>/dev/null; then
      echo "✗ CHECKSUM MISMATCH for $(basename "$tar")"; exit 1
    elif [ "${RECORD_CHECKSUMS:-0}" = "1" ]; then
      echo "$sum  $(basename "$tar")" >> "$CHECKSUMS"; echo "recorded $(basename "$tar")"
    else
      echo "✗ no pin for $(basename "$tar") (run with RECORD_CHECKSUMS=1 once)"; exit 1
    fi
  fi
  echo "✓ $(basename "$tar")"
}
do_fetch() { for l in ffmpeg dav1d freetype harfbuzz fribidi libass; do fetch_one "$l"; done; }

# ── per-slice environment ────────────────────────────────────────────────────
slice_env() {
  SLICE="$1"
  case "$SLICE" in
    ios-arm64)      SDK=iphoneos;         TRIPLE="arm64-apple-ios$IOS_MIN" ;;
    ios-sim-arm64)  SDK=iphonesimulator;  TRIPLE="arm64-apple-ios$IOS_MIN-simulator" ;;
    tvos-arm64)     SDK=appletvos;        TRIPLE="arm64-apple-tvos$TVOS_MIN" ;;
    tvos-sim-arm64) SDK=appletvsimulator; TRIPLE="arm64-apple-tvos$TVOS_MIN-simulator" ;;
    *) echo "unknown slice $SLICE" >&2; exit 1 ;;
  esac
  SYSROOT="$(xcrun --sdk "$SDK" --show-sdk-path)"
  CLANG="$(xcrun --sdk "$SDK" -f clang)"
  CLANGXX="$(xcrun --sdk "$SDK" -f clang++)"
  AR="$(xcrun --sdk "$SDK" -f ar)"; RANLIB="$(xcrun --sdk "$SDK" -f ranlib)"
  STRIP="$(xcrun --sdk "$SDK" -f strip)"; LIBTOOL="$(xcrun --sdk "$SDK" -f libtool)"
  PREFIX="$BUILD/prefix/$SLICE"; mkdir -p "$PREFIX"
  BCFLAGS="-target $TRIPLE -isysroot $SYSROOT -O2 -fPIC"
  BLDFLAGS="-target $TRIPLE -isysroot $SYSROOT"
  export PKG_CONFIG_LIBDIR="$PREFIX/lib/pkgconfig"   # isolate from host libs
}

# Meson cross file, regenerated per slice.
write_cross_file() {
  CROSS="$BUILD/cross-$SLICE.txt"
  cat > "$CROSS" <<EOF
[binaries]
c = ['$CLANG', '-target', '$TRIPLE', '-isysroot', '$SYSROOT']
cpp = ['$CLANGXX', '-target', '$TRIPLE', '-isysroot', '$SYSROOT']
objc = ['$CLANG', '-target', '$TRIPLE', '-isysroot', '$SYSROOT']
ar = '$AR'
strip = '$STRIP'
pkg-config = '$(command -v pkg-config)'

[built-in options]
default_library = 'static'
b_staticpic = true

[host_machine]
system = 'darwin'
cpu_family = 'aarch64'
cpu = 'aarch64'
endian = 'little'
EOF
}

extract() { # extract <lib> <version-dir-name> → prints build dir
  local lib="$1" name="$2" dest="$BUILD/$SLICE/$2"
  rm -rf "$dest"; mkdir -p "$BUILD/$SLICE"
  tar -xf "$SRC/$(basename "$(url_for "$lib")")" -C "$BUILD/$SLICE"
  echo "$dest"
}

meson_build() { # meson_build <srcdir> [extra meson args...]
  local sdir="$1"; shift
  (cd "$sdir" && meson setup _build --cross-file "$CROSS" --prefix "$PREFIX" \
      --buildtype release --default-library static "$@" \
   && ninja -C _build -j "$(sysctl -n hw.ncpu)" && ninja -C _build install) \
   > "$sdir/_build.log" 2>&1 || { tail -40 "$sdir/_build.log"; exit 1; }
}

# ── dependency libs ──────────────────────────────────────────────────────────
build_deps() {
  write_cross_file
  echo "── dav1d ($SLICE)"
  meson_build "$(extract dav1d "dav1d-$DAV1D_V")" \
    -Denable_tools=false -Denable_tests=false -Denable_examples=false
  echo "── freetype ($SLICE)"
  meson_build "$(extract freetype "freetype-$FREETYPE_V")" \
    -Dharfbuzz=disabled -Dbrotli=disabled -Dpng=disabled -Dbzip2=disabled -Dzlib=system
  echo "── harfbuzz ($SLICE)"
  meson_build "$(extract harfbuzz "harfbuzz-$HARFBUZZ_V")" \
    -Dfreetype=disabled -Dglib=disabled -Dgobject=disabled -Dcairo=disabled \
    -Dicu=disabled -Dcoretext=disabled -Dtests=disabled -Ddocs=disabled \
    -Dbenchmark=disabled -Dutilities=disabled
  echo "── fribidi ($SLICE)"
  meson_build "$(extract fribidi "fribidi-$FRIBIDI_V")" -Ddocs=false -Dbin=false -Dtests=false
  echo "── libass ($SLICE)"
  local d; d="$(extract libass "libass-$LIBASS_V")"
  # -target/-isysroot live inside CC so EVERY autoconf probe (incl. framework
  # checks like CoreText) compiles against the right SDK, not the host.
  (cd "$d" && \
    CC="$CLANG -target $TRIPLE -isysroot $SYSROOT" CFLAGS="-O2 -fPIC" LDFLAGS="" \
    ./configure --host=aarch64-apple-darwin --prefix="$PREFIX" \
      --disable-shared --enable-static --disable-fontconfig \
      --disable-require-system-font-provider \
    && make -j"$(sysctl -n hw.ncpu)" && make install) \
    > "$d/_build.log" 2>&1 || { tail -40 "$d/_build.log"; exit 1; }
}

# ── FFmpeg component set (the plan's ProbePlanner matrix, nothing more) ──────
PROTOCOLS=(file http https tls tcp)
DEMUXERS=(matroska mov mpegts avi flv asf mpegps hls mp3 aac ac3 eac3 flac ogg wav srt ass webvtt)
VDECODERS=(h264 hevc mpeg2video mpeg4 msmpeg4v2 msmpeg4v3 vc1 wmv3 vp8 vp9 libdav1d)
ADECODERS=(aac aac_latm ac3 eac3 dca truehd mlp opus vorbis flac alac mp3 mp2
           pcm_s16le pcm_s16be pcm_s24le pcm_s24be pcm_s32le pcm_s32be
           pcm_f32le pcm_u8 pcm_alaw pcm_mulaw)
SDECODERS=(subrip srt ass ssa mov_text webvtt pgssub dvdsub dvbsub)
ENCODERS=(aac eac3 alac webvtt mov_text h264_videotoolbox hevc_videotoolbox)
PARSERS=(h264 hevc aac aac_latm ac3 mpegaudio mpeg4video mpegvideo opus vorbis
         flac vp8 vp9 av1 vc1 dca mlp dvdsub dvbsub)
BSFS=(h264_mp4toannexb hevc_mp4toannexb extract_extradata aac_adtstoasc dovi_rpu
      dovi_convert)   # ours — see src/dovi_convert_bsf.c (copied in below)
MUXERS=(mp4 mov webvtt segment)
FILTERS=(scale format yadif bwdif overlay aresample anull null copy pan settb fps
         subtitles ass
         # filtergraph plumbing fftools auto-inserts around every encoder —
         # without these, audio transcode dies with "Error reinitializing
         # filters" + ENOMEM (avfilter_get_by_name → NULL):
         buffer buffersink abuffer abuffersink aformat
         # -ss/-t on a TRANSCODED stream makes fftools insert trim/atrim into
         # the auto graph. Without them every seekHint kill+restart of a
         # transcode plan dies instantly with AVERROR_FILTER_NOT_FOUND
         # (-1279870712) right after the banner — copy plans never showed it.
         trim atrim)

enable_flags() { local kind="$1"; shift; for x in "$@"; do printf -- "--enable-%s=%s " "$kind" "$x"; done; }

# fftools sources that make up the `ffmpeg` tool in 7.1.x (no ffprobe/ffplay).
FFTOOLS_SRCS=(ffmpeg.c ffmpeg_dec.c ffmpeg_demux.c ffmpeg_enc.c ffmpeg_filter.c
              ffmpeg_hw.c ffmpeg_mux.c ffmpeg_mux_init.c ffmpeg_opt.c
              ffmpeg_sched.c cmdutils.c opt_common.c sync_queue.c thread_queue.c
              objpool.c)

# ── libdovi (Rust staticlib via cargo-c; cached by cargo — cheap when fresh) ─
build_libdovi() {
  echo "── libdovi ($SLICE)"
  local dovi="$WORK/dovi_tool"
  if [ ! -d "$dovi" ]; then
    git clone --no-checkout https://github.com/quietvoid/dovi_tool "$dovi"
  fi
  (cd "$dovi" && git rev-parse HEAD | grep -q "$DOVI_TOOL_COMMIT" \
    || { git fetch --all -q; git checkout -q "$DOVI_TOOL_COMMIT"; })
  [ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
  local target extra=()
  case "$SLICE" in
    ios-arm64)      target=aarch64-apple-ios ;;
    ios-sim-arm64)  target=aarch64-apple-ios-sim ;;
    tvos-arm64)     target=aarch64-apple-tvos;     extra=(-Zbuild-std=std,panic_abort) ;;
    tvos-sim-arm64) target=aarch64-apple-tvos-sim; extra=(-Zbuild-std=std,panic_abort) ;;
  esac
  # ${extra[@]+…} guard: bash 3.2's set -u treats an EMPTY array as unbound.
  (cd "$dovi/dolby_vision" && cargo +nightly cbuild ${extra[@]+"${extra[@]}"} --release \
      --target "$target" --features capi --library-type staticlib) \
    > "$WORK/build/$SLICE-libdovi.log" 2>&1 \
    || { tail -20 "$WORK/build/$SLICE-libdovi.log"; exit 1; }
  cp "$dovi/dolby_vision/target/$target/release/libdovi.a" "$PREFIX/lib/libdovi.a"
  mkdir -p "$PREFIX/include/libdovi"
  cp "$dovi/dolby_vision/target/$target/release/include/libdovi/rpu_parser.h" \
     "$PREFIX/include/libdovi/rpu_parser.h"
}

build_ffmpeg() {
  local d; d="$(extract ffmpeg "ffmpeg-$FFMPEG_V")"
  echo "── ffmpeg ($SLICE)"
  # In-process wrapper patch: renames main → mediabridge_ffmpeg_main, exports
  # mediabridge_ffmpeg_cancel(), skips signal-handler installs, guards exit().
  (cd "$d" && patch -p1 -s < "$ROOT/patches/ffmpeg-$FFMPEG_V-mediabridge.patch")
  # dovi_convert bsf (spec 7): source lives in-repo; drop it into the tree and
  # register it. configure auto-discovers the bsf from the extern declaration.
  build_libdovi
  cp "$ROOT/bsf/dovi_convert_bsf.c" "$d/libavcodec/bsf/dovi_convert.c"
  sed -i '' 's|^extern const FFBitStreamFilter ff_dovi_rpu_bsf;|extern const FFBitStreamFilter ff_dovi_convert_bsf;\'$'\n''&|' \
    "$d/libavcodec/bitstream_filters.c"
  sed -i '' 's|^OBJS-$(CONFIG_DOVI_RPU_BSF)               += bsf/dovi_rpu.o|OBJS-$(CONFIG_DOVI_CONVERT_BSF)           += bsf/dovi_convert.o\'$'\n''&|' \
    "$d/libavcodec/bsf/Makefile"
  grep -q ff_dovi_convert_bsf "$d/libavcodec/bitstream_filters.c" || { echo "✗ bsf extern sed failed"; exit 1; }
  grep -q dovi_convert.o "$d/libavcodec/bsf/Makefile" || { echo "✗ bsf Makefile sed failed"; exit 1; }
  (cd "$d" && ./configure \
    --prefix="$PREFIX" \
    --enable-cross-compile --target-os=darwin --arch=arm64 \
    --cc="$CLANG" --as="$CLANG" --sysroot="$SYSROOT" \
    --extra-cflags="$BCFLAGS -I$PREFIX/include" --extra-ldflags="$BLDFLAGS" \
    --pkg-config="$(command -v pkg-config)" --pkg-config-flags=--static \
    --enable-static --disable-shared --enable-pic \
    --disable-debug --disable-doc --disable-programs \
    --disable-avdevice --disable-postproc \
    --disable-everything \
    --enable-network --enable-zlib --enable-iconv \
    --enable-securetransport \
    --enable-videotoolbox \
    --enable-libdav1d --enable-libass \
    $(enable_flags protocol  "${PROTOCOLS[@]}") \
    $(enable_flags demuxer   "${DEMUXERS[@]}") \
    $(enable_flags decoder   "${VDECODERS[@]}" "${ADECODERS[@]}" "${SDECODERS[@]}") \
    $(enable_flags encoder   "${ENCODERS[@]}") \
    $(enable_flags parser    "${PARSERS[@]}") \
    $(enable_flags bsf       "${BSFS[@]}") \
    $(enable_flags muxer     "${MUXERS[@]}") \
    $(enable_flags filter    "${FILTERS[@]}") \
    && make -j"$(sysctl -n hw.ncpu)" && make install) \
    > "$d/_build.log" 2>&1 || { tail -60 "$d/_build.log"; exit 1; }
  build_fftools "$d"
}

# Compile the ffmpeg tool itself into libfftools.a (the in-process runner).
build_fftools() {
  local d="$1"
  echo "── fftools ($SLICE)"
  # NOTE: no -DHAVE_AV_CONFIG_H — fftools include config.h explicitly and rely
  # on avcodec.h's external mode (which is what pulls in codec_desc.h etc.).
  # `|| exit 1` because set -e is suppressed inside the `(...) || {}` guard.
  (cd "$d" && mkdir -p _fftools && \
    for f in "${FFTOOLS_SRCS[@]}"; do
      "$CLANG" -target "$TRIPLE" -isysroot "$SYSROOT" -O2 -fPIC \
        -DMEDIABRIDGE_IN_PROCESS \
        -I. -Ifftools -Icompat/stdbit -c "fftools/$f" -o "_fftools/${f%.c}.o" || exit 1
    done && \
    "$AR" rcs "$PREFIX/lib/libfftools.a" _fftools/*.o && "$RANLIB" "$PREFIX/lib/libfftools.a") \
    > "$d/_fftools.log" 2>&1 || { tail -40 "$d/_fftools.log"; exit 1; }
  # Public entry points for MediaBridgeKit / harnesses.
  cat > "$PREFIX/include/mediabridge_ffmpeg.h" <<'EOF'
/* MediaBridge in-process fftools entry points (see patches/ffmpeg-*-mediabridge.patch).
 * One job per process at a time — fftools keeps global state. The session
 * manager must serialize calls. argv[0] is conventionally "ffmpeg". */
#ifndef MEDIABRIDGE_FFMPEG_H
#define MEDIABRIDGE_FFMPEG_H
#include <libavutil/log.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
int  mediabridge_ffmpeg_main(int argc, char **argv);
void mediabridge_ffmpeg_cancel(void);
static inline void mediabridge_ffmpeg_log_reset(void)
{
    av_log_set_callback(av_log_default_callback);
}
/* Ring capture of av_log WARNING+ (impl: MediaBridgeLogShim.m in each app —
 * static state in a header never materializes for Swift-imported inline C). */
void mediabridge_log_capture_begin(int max_level);
void mediabridge_log_capture_take(char *out, size_t outsz);
#ifdef __cplusplus
}
#endif
#endif
EOF
}

# ── merge every static lib into one, per slice ───────────────────────────────
do_merge() {
  echo "── merge ($SLICE)"
  # harfbuzz 14 installs split variants (subset/gpu/raster) that SHARE object
  # files with the core lib — fine under lazy archive loading, but the app
  # links with -force_load (to beat TVVLCKit's exported FFmpeg-4.4 symbols),
  # where duplicate members are fatal. libass only needs core libharfbuzz.a.
  local libs=()
  while IFS= read -r l; do libs+=("$l"); done < <(ls "$PREFIX"/lib/lib*.a | grep -v 'harfbuzz-')
  "$LIBTOOL" -static -o "$PREFIX/libFFmpegBridge.a" "${libs[@]}" 2> >(grep -v 'same member name' >&2 || true)
  echo "   $(du -h "$PREFIX/libFFmpegBridge.a" | cut -f1)  libFFmpegBridge.a"
}

# ── xcframework ──────────────────────────────────────────────────────────────
do_xcframework() {
  local args=()
  for s in "${ALL_SLICES[@]}"; do
    local p="$BUILD/prefix/$s"
    [ -f "$p/libFFmpegBridge.a" ] || { echo "✗ missing $s (build it first)"; exit 1; }
    args+=(-library "$p/libFFmpegBridge.a" -headers "$p/include")
  done
  rm -rf "$OUT/FFmpegBridge.xcframework"
  xcodebuild -create-xcframework "${args[@]}" -output "$OUT/FFmpegBridge.xcframework"
  # Copy into the repo (git-ignored) so Xcode projects reference a stable path.
  rm -rf "$ROOT/out/FFmpegBridge.xcframework"; mkdir -p "$ROOT/out"
  cp -R "$OUT/FFmpegBridge.xcframework" "$ROOT/out/"
  echo "✓ $ROOT/out/FFmpegBridge.xcframework"
}

# ── main ─────────────────────────────────────────────────────────────────────
CMD="${1:-all}"; ONLY="${2:-}"
slices=("${ALL_SLICES[@]}"); [ -n "$ONLY" ] && slices=("$ONLY")

case "$CMD" in
  fetch) do_fetch ;;
  deps)        do_fetch; for s in "${slices[@]}"; do slice_env "$s"; build_deps;   done ;;
  ffmpeg)      do_fetch; for s in "${slices[@]}"; do slice_env "$s"; build_ffmpeg; done ;;
  merge)                 for s in "${slices[@]}"; do slice_env "$s"; do_merge;     done ;;
  xcframework) do_xcframework ;;
  all)
    do_fetch
    for s in "${slices[@]}"; do slice_env "$s"; build_deps; build_ffmpeg; do_merge; done
    [ -z "$ONLY" ] && do_xcframework || true
    ;;
  *) echo "usage: $0 {fetch|deps|ffmpeg|merge|xcframework|all} [slice]"; exit 1 ;;
esac
echo "done."
