#!/bin/bash
# Reproducible local-only, audio-only FFmpeg build. No downloaded binaries.
#
# Link model (LGPL-friendly): FFmpeg is built as shared libraries and packaged as
# four replaceable iOS dynamic frameworks (libavutil, libavcodec, libavformat,
# libswresample), each with install name @rpath/<name>.framework/<name>.
# WidephaseDSD.xcframework is a thin static wrapper containing only the
# project-owned WidephaseDSD.o and DoPOutput.o; it links against the frameworks.
# Numeric compiler flags are unchanged from the previous static build.
set -euo pipefail
ROOT=$(cd -P "$(dirname "$0")/.." && pwd -P)
# Build trees live in the working copy's build.nosync directory (never in src/).
WORK=${WIDEPHASE_CODEC_BUILD_DIR:-$(cd -P "$ROOT/.." && pwd -P)/build.nosync/widephase-dsd-build}

# Canonicalise WORK before anything is created: make it absolute, physically
# resolve the deepest existing ancestor (symlinks, '.', '..'), reject '.'/'..'
# in the not-yet-existing tail, then refuse if any ancestor is the source tree
# (compared by inode with -ef, so case and symlink aliases are caught too).
resolve_work_dir() {
  local P=$1 TAIL="" B BASE D
  case "$P" in /*) ;; *) P="$PWD/$P" ;; esac
  while [ ! -d "$P" ]; do
    if [ -e "$P" ] || [ -L "$P" ]; then
      echo "Build dir path is not a directory: $P" >&2; exit 1
    fi
    B=${P##*/}
    P=${P%/*}
    if [ -z "$P" ]; then P=/; fi
    case "$B" in
      "") continue ;;
      .|..) echo "Refusing '.' or '..' in non-existent build dir path: $1" >&2; exit 1 ;;
    esac
    TAIL="/$B$TAIL"
  done
  BASE=$(cd -P "$P" && pwd -P)
  D=$BASE
  while :; do
    if [ "$D" -ef "$ROOT" ]; then
      echo "Refusing build dir inside source tree: $1 (resolves under $ROOT)" >&2; exit 1
    fi
    if [ "$D" = / ]; then break; fi
    D=${D%/*}
    if [ -z "$D" ]; then D=/; fi
  done
  if [ "$BASE" = / ]; then BASE=""; fi
  WORK="$BASE$TAIL"
  if [ -z "$WORK" ]; then WORK=/; fi
}
resolve_work_dir "$WORK"
ARCHIVE="$ROOT/ThirdParty/FFmpeg/ffmpeg-8.1.3.tar.xz"
FFMPEG_VERSION=8.1.3
FFLIBS="libavutil libswresample libavcodec libavformat"
echo '7138d28c96d9d3e3af4ee3d8cad72741f8ffb40da90c1112235dea3ecd3178a3  '"$ARCHIVE" | shasum -a 256 -c -
mkdir -p "$WORK/source"
if [ ! -f "$WORK/source/configure" ]; then
  tar -xf "$ARCHIVE" -C "$WORK/source" --strip-components=1
fi
python3 "$ROOT/scripts/patch-dst.py" "$WORK/source"

# Minimal framework Info.plist. $1 = output path, $2 = library name, $3 = SDK.
write_framework_plist() {
  local PLATFORM_NAME=iPhoneOS
  if [ "$3" = iphonesimulator ]; then PLATFORM_NAME=iPhoneSimulator; fi
  cat > "$1" <<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>$2</string>
	<key>CFBundleIdentifier</key>
	<string>org.ffmpeg.$2</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>$2</string>
	<key>CFBundlePackageType</key>
	<string>FMWK</string>
	<key>CFBundleShortVersionString</key>
	<string>$FFMPEG_VERSION</string>
	<key>CFBundleVersion</key>
	<string>$FFMPEG_VERSION</string>
	<key>CFBundleSupportedPlatforms</key>
	<array>
		<string>$PLATFORM_NAME</string>
	</array>
	<key>MinimumOSVersion</key>
	<string>18.0</string>
</dict>
</plist>
PLIST
}

# Package installed dylibs as thin frameworks with @rpath install names.
# $1 = per-platform build dir, $2 = SDK.
package_frameworks() {
  local BUILD=$1 SDK=$2 LIB DEP OLD_ID FW BIN
  local STAGE="$BUILD/frameworks"
  rm -rf "$STAGE"
  mkdir -p "$STAGE"
  for LIB in $FFLIBS; do
    FW="$STAGE/$LIB.framework"
    BIN="$STAGE/$LIB.binary"
    cp -L "$BUILD/install/lib/$LIB.dylib" "$BIN"
    chmod u+w "$BIN"
    xcrun install_name_tool -id "@rpath/$LIB.framework/$LIB" "$BIN"
    for DEP in $FFLIBS; do
      OLD_ID=$(xcrun otool -D "$BUILD/install/lib/$DEP.dylib" | sed -n 2p)
      xcrun install_name_tool -change "$OLD_ID" "@rpath/$DEP.framework/$DEP" "$BIN"
    done
    if xcrun otool -L "$BIN" | grep -F "$BUILD/install"; then
      echo "Absolute build path remains in $LIB load commands" >&2; exit 1
    fi
    # install_name_tool invalidates the linker's ad-hoc signature. The raw
    # dylib is deliberately NOT signed here: an ad-hoc signature of a loose
    # file gets a hash-style identifier (<name>-<hex>), which Xcode's Embed
    # Frameworks step preserves (--preserve-metadata=identifier) and device
    # installd rejects. The finished bundle is signed below instead.
    mkdir -p "$FW/Headers"
    mv "$BIN" "$FW/$LIB"
    cp -R "$BUILD/install/include/$LIB/." "$FW/Headers/"
    write_framework_plist "$FW/Info.plist" "$LIB" "$SDK"
    sign_framework "$FW" "$LIB"
  done
}

# Ad-hoc sign a finished framework bundle so its code-signing identifier equals
# its CFBundleIdentifier (org.ffmpeg.<name>), then verify both.
# $1 = framework dir, $2 = library name.
sign_framework() {
  local FW=$1 LIB=$2
  # File Provider / Documents trees attach FinderInfo; codesign --strict rejects it.
  xattr -cr "$FW"
  xcrun codesign --force --sign - --identifier "org.ffmpeg.$LIB" "$FW"
  xcrun codesign --verify --strict "$FW"
  if ! xcrun codesign --display --verbose=2 "$FW" 2>&1 | grep -Fx "Identifier=org.ffmpeg.$LIB" > /dev/null; then
    echo "Code signing identifier of $FW is not org.ffmpeg.$LIB" >&2; exit 1
  fi
  if ! grep -F "<string>org.ffmpeg.$LIB</string>" "$FW/Info.plist" > /dev/null; then
    echo "CFBundleIdentifier of $FW is not org.ffmpeg.$LIB" >&2; exit 1
  fi
}

for PLATFORM in iphoneos-arm64 iphonesimulator-arm64 iphonesimulator-x86_64; do
  SDK=${PLATFORM%-*}
  ARCH=${PLATFORM##*-}
  BUILD="$WORK/$PLATFORM"
  mkdir -p "$BUILD"
  SYSROOT=$(xcrun --sdk "$SDK" --show-sdk-path)
  TRIPLE=$ARCH-apple-ios18.0
  if [ "$SDK" = iphonesimulator ]; then TRIPLE=$ARCH-apple-ios18.0-simulator; fi
  (
    cd "$BUILD"
    "$WORK/source/configure" --arch="$ARCH" --disable-x86asm --target-os=darwin --enable-cross-compile \
      --cc="$(xcrun --sdk "$SDK" -f clang)" --sysroot="$SYSROOT" \
      --extra-cflags="-target $TRIPLE -fPIC" --extra-ldflags="-target $TRIPLE -Wl,-headerpad_max_install_names" \
      --disable-autodetect --disable-everything --disable-programs --disable-doc \
      --disable-network --enable-avformat --disable-avdevice --disable-avfilter \
      --disable-swscale --enable-swresample --disable-debug --enable-shared --disable-static \
      --enable-decoder=dsd_msbf,dst,flac,ape,wavpack,vorbis,opus --enable-demuxer=flac,ape,wv,ogg --enable-parser=flac,opus,vorbis --enable-protocol=file --disable-gpl --disable-nonfree --disable-version3 \
      --prefix="$BUILD/install" > configure.log 2>&1
    make -j8 > build.log 2>&1
    make install > install.log 2>&1
  )
  package_frameworks "$BUILD" "$SDK"
  xcrun --sdk "$SDK" clang -target "$TRIPLE" -isysroot "$SYSROOT" -O3 -fPIC \
    -I"$BUILD/install/include" -I"$ROOT/ThirdParty/WidephaseDSD" \
    -c "$ROOT/ThirdParty/WidephaseDSD/WidephaseDSD.c" -o "$BUILD/WidephaseDSD.o"
  xcrun --sdk "$SDK" clang -target "$TRIPLE" -isysroot "$SYSROOT" -O3 -fPIC -std=c11 \
    -c "$ROOT/ThirdParty/WidephaseDSD/DoPOutput.c" -o "$BUILD/DoPOutput.o"
  # Thin wrapper: project-owned objects only; FFmpeg stays in the dynamic frameworks.
  rm -f "$BUILD/libWidephaseDSD.a"
  xcrun libtool -static -o "$BUILD/libWidephaseDSD.a" "$BUILD/WidephaseDSD.o" "$BUILD/DoPOutput.o"
  if xcrun ar -t "$BUILD/libWidephaseDSD.a" | grep -v -e '^__\.SYMDEF' -e '^WidephaseDSD\.o$' -e '^DoPOutput\.o$'; then
    echo "Unexpected members in thin libWidephaseDSD.a" >&2; exit 1
  fi
done

# Fat simulator products (arm64 + x86_64).
UNIVERSAL="$WORK/iphonesimulator-universal"
rm -rf "$UNIVERSAL"
mkdir -p "$UNIVERSAL/frameworks"
for LIB in $FFLIBS; do
  diff -r "$WORK/iphonesimulator-arm64/frameworks/$LIB.framework/Headers" \
    "$WORK/iphonesimulator-x86_64/frameworks/$LIB.framework/Headers"
  cp -R "$WORK/iphonesimulator-arm64/frameworks/$LIB.framework" "$UNIVERSAL/frameworks/"
  xcrun lipo -create "$WORK/iphonesimulator-arm64/frameworks/$LIB.framework/$LIB" \
    "$WORK/iphonesimulator-x86_64/frameworks/$LIB.framework/$LIB" \
    -output "$UNIVERSAL/frameworks/$LIB.framework/$LIB"
  # lipo replaced the signed binary; re-sign the fat bundle with the same id.
  sign_framework "$UNIVERSAL/frameworks/$LIB.framework" "$LIB"
done
xcrun lipo -create "$WORK/iphonesimulator-arm64/libWidephaseDSD.a" "$WORK/iphonesimulator-x86_64/libWidephaseDSD.a" -output "$UNIVERSAL/libWidephaseDSD.a"

# Replace only these generated xcframeworks; previous copies are moved into the
# work dir. Original audio and application files are untouched.
STAMP=$(date +%s)
move_previous() {
  if [ -d "$ROOT/ThirdParty/$1.xcframework" ]; then
    mv "$ROOT/ThirdParty/$1.xcframework" "$WORK/previous-$STAMP-$1.xcframework"
  fi
}
for LIB in $FFLIBS; do
  move_previous "$LIB"
  xcodebuild -create-xcframework \
    -framework "$WORK/iphoneos-arm64/frameworks/$LIB.framework" \
    -framework "$UNIVERSAL/frameworks/$LIB.framework" \
    -output "$ROOT/ThirdParty/$LIB.xcframework"
done
move_previous WidephaseDSD
xcodebuild -create-xcframework \
  -library "$WORK/iphoneos-arm64/libWidephaseDSD.a" -headers "$ROOT/ThirdParty/WidephaseDSD" \
  -library "$UNIVERSAL/libWidephaseDSD.a" -headers "$ROOT/ThirdParty/WidephaseDSD" \
  -output "$ROOT/ThirdParty/WidephaseDSD.xcframework"
cp "$WORK/source/COPYING.LGPLv2.1" "$ROOT/ThirdParty/FFmpeg/"
cp "$WORK/source/LICENSE.md" "$ROOT/ThirdParty/FFmpeg/"
echo 'Built FFmpeg dynamic frameworks and thin WidephaseDSD wrapper for iPhone and Simulator.'
