#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later

set -euo pipefail

usage() {
  cat <<'EOF'
Usage:
  scripts/build-reviewed-ffmpeg-core.sh st|mt SOURCE_ARCHIVE EMSDK_ROOT [JOBS]

Builds one private, staged FFmpeg WebAssembly core from the exact
0.12.10 Corresponding Source archive. The only upstream source change is
ffmpeg.wasm commit b409e36475bc21f0451b5b1e1d126fa82871439a:
`-sSTACK_SIZE=5MB`, which fixes upstream issues #591 and #775.

Output remains below .core-build/reviewed-stack5m and is never copied into the
application automatically. Re-running resumes at verified component stamps.
EOF
}

if [[ $# -lt 3 || $# -gt 4 ]]; then
  usage >&2
  exit 2
fi

MODE=$1
SOURCE_ARCHIVE=$2
EMSDK_ROOT=$3
JOBS=${4:-2}

if [[ "$MODE" != "st" && "$MODE" != "mt" ]]; then
  echo "Mode must be st or mt." >&2
  exit 2
fi
if [[ ! "$JOBS" =~ ^[1-8]$ ]]; then
  echo "JOBS must be an integer from 1 to 8." >&2
  exit 2
fi

SCRIPT_DIRECTORY=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
REPOSITORY_ROOT=$(cd -- "$SCRIPT_DIRECTORY/.." && pwd -P)
SOURCE_ARCHIVE=$(realpath -- "$SOURCE_ARCHIVE")
EMSDK_ROOT=$(realpath -- "$EMSDK_ROOT")

EXPECTED_ARCHIVE_SHA256=2ab505f72be883d1257bc71cbb93880d1ade5bc029792621cb87b4ecc068399a
EXPECTED_EMCC_SHA256=b0c9551f9155fa9c028efa24591acdfd30f02f32fb247cf9266f5f0f2ede7589
EXPECTED_CMAKE_SHA256=ade9326f83f4996b17e2cb1bbfe4848df30a237ef5769cd3c00239b9f0d3ec58
EXPECTED_SOURCE_ROOT=ffmpeg-core-0.12.10-reviewed-stack5m.1-corresponding-source
SOURCE_DATE_EPOCH=1736208000

if [[ ! -f "$SOURCE_ARCHIVE" || -L "$SOURCE_ARCHIVE" ]]; then
  echo "SOURCE_ARCHIVE must be a regular, non-symlink file." >&2
  exit 2
fi
if [[ ! -d "$EMSDK_ROOT" || -L "$EMSDK_ROOT" ]]; then
  echo "EMSDK_ROOT must be a real directory." >&2
  exit 2
fi

actual_archive_sha256=$(sha256sum -- "$SOURCE_ARCHIVE" | cut -d' ' -f1)
if [[ "$actual_archive_sha256" != "$EXPECTED_ARCHIVE_SHA256" ]]; then
  echo "Corresponding Source archive SHA-256 mismatch." >&2
  exit 1
fi

# shellcheck disable=SC1091
source "$EMSDK_ROOT/emsdk_env.sh" >/dev/null
export PATH="$REPOSITORY_ROOT/scripts/build-tools:$PATH"
if [[ -z "${AV_REVIEWED_CMAKE_BIN:-}" ]]; then
  echo "AV_REVIEWED_CMAKE_BIN must identify the exact CMake 3.27.9 binary directory." >&2
  exit 2
fi
AV_REVIEWED_CMAKE_BIN=$(realpath -- "$AV_REVIEWED_CMAKE_BIN")
if [[ ! -d "$AV_REVIEWED_CMAKE_BIN" || -L "$AV_REVIEWED_CMAKE_BIN" ]]; then
  echo "AV_REVIEWED_CMAKE_BIN must be a real directory." >&2
  exit 2
fi
if [[ $(sha256sum -- "$AV_REVIEWED_CMAKE_BIN/cmake" | cut -d' ' -f1) != "$EXPECTED_CMAKE_SHA256" ]]; then
  echo "CMake binary SHA-256 mismatch; exact PyPI CMake 3.27.9 is required." >&2
  exit 1
fi
export PATH="$AV_REVIEWED_CMAKE_BIN:$PATH"
EMCC=$(realpath -- "$EMSDK_ROOT/upstream/emscripten/emcc")
actual_emcc_sha256=$(sha256sum -- "$EMCC" | cut -d' ' -f1)
if [[ "$actual_emcc_sha256" != "$EXPECTED_EMCC_SHA256" ]]; then
  echo "Emscripten compiler SHA-256 mismatch; activate exact version 3.1.40." >&2
  exit 1
fi
if ! "$EMCC" --version | head -1 | grep -Fq \
  "3.1.40 (5c27e79dd0a9c4e27ef2326841698cdd4f6b5784)"; then
  echo "Emscripten identity mismatch; exact version 3.1.40 is required." >&2
  exit 1
fi
if [[ $(cmake --version | head -1) != "cmake version 3.27.9" ]]; then
  echo "CMake identity mismatch; exact version 3.27.9 is required." >&2
  exit 1
fi

BUILD_PARENT="$REPOSITORY_ROOT/.core-build/reviewed-stack5m"
MODE_ROOT="$BUILD_PARENT/$MODE"
EXTRACT_ROOT="$MODE_ROOT/source"
SOURCE_ROOT="$EXTRACT_ROOT/$EXPECTED_SOURCE_ROOT/sources"
INSTALL_DIR="$MODE_ROOT/install"
OUTPUT_DIR="$MODE_ROOT/output"
STAMP_DIR="$MODE_ROOT/stamps"

case "$MODE_ROOT" in
  "$REPOSITORY_ROOT"/.core-build/reviewed-stack5m/st | \
  "$REPOSITORY_ROOT"/.core-build/reviewed-stack5m/mt) ;;
  *)
    echo "Refusing an unexpected build directory: $MODE_ROOT" >&2
    exit 1
    ;;
esac

mkdir -p -- "$MODE_ROOT" "$STAMP_DIR"
if [[ ! -f "$STAMP_DIR/source-prepared" ]]; then
  if [[ -e "$EXTRACT_ROOT" ]]; then
    echo "Incomplete source staging already exists at $EXTRACT_ROOT." >&2
    echo "Remove only that private mode directory to restart it." >&2
    exit 1
  fi
  mkdir -p -- "$EXTRACT_ROOT"
  tar -xf "$SOURCE_ARCHIVE" -C "$EXTRACT_ROOT"

  lock="$EXTRACT_ROOT/$EXPECTED_SOURCE_ROOT/SOURCE_LOCK.json"
  node -e '
    const fs = require("node:fs");
    const lock = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
    const expected = new Map([
      ["ffmpeg.wasm", "71aa99d37c02a7b4c435275ca9ef50e612f6efa1"],
      ["FFmpeg", "4729204c17f756e186d622060088371d10b34f7e"],
      ["Opus", "e85ed7726db5d677c9c0677298ea0cb9c65bdd23"],
    ]);
    for (const [name, revision] of expected) {
      const entry = lock.sources.find((candidate) => candidate.name === name);
      if (!entry || entry.revision !== revision) {
        throw new Error(`Unexpected ${name} source identity.`);
      }
    }
  ' "$lock"

  wasm_build_script="$SOURCE_ROOT/ffmpeg.wasm/build/ffmpeg-wasm.sh"
  if grep -Fq -- "-sSTACK_SIZE=" "$wasm_build_script"; then
    echo "Base archive unexpectedly already selects a stack size." >&2
    exit 1
  fi
  sed -i \
    '/-sUSE_SDL=2/a\  -sSTACK_SIZE=5MB                         # upstream b409e364: support libopus' \
    "$wasm_build_script"
  if [[ $(grep -Fc -- "-sSTACK_SIZE=5MB" "$wasm_build_script") -ne 1 ]]; then
    echo "Could not apply the reviewed one-line stack patch exactly once." >&2
    exit 1
  fi

  touch "$STAMP_DIR/source-prepared"
fi

# The upstream container uses unbounded `make -j`. Keep local builds bounded
# without changing compiled source, and allow a resumed build to select a new
# bounded job count.
while IFS= read -r -d '' build_script; do
  sed -E -i \
    -e "s/ -j([1-8])?$/ -j$JOBS/" \
    -e "s/ -j([1-8])? / -j$JOBS /g" \
    "$build_script"
done < <(find "$SOURCE_ROOT/ffmpeg.wasm/build" -type f -name '*.sh' -print0)

# The upstream container does not provide native NASM. Some development hosts
# do; allowing CMake to discover it would produce native x86 objects that
# cannot be linked into WebAssembly. Make the container's effective choice
# explicit and deterministic.
X265_BUILD_SCRIPT="$SOURCE_ROOT/ffmpeg.wasm/build/x265.sh"
if ! grep -Fq -- "-DENABLE_ASSEMBLY=OFF" "$X265_BUILD_SCRIPT"; then
  sed -i \
    '/-DENABLE_LIBNUMA=OFF/a\  -DENABLE_ASSEMBLY=OFF' \
    "$X265_BUILD_SCRIPT"
fi
if [[ $(grep -Fc -- "-DENABLE_ASSEMBLY=OFF" "$X265_BUILD_SCRIPT") -ne 1 ]]; then
  echo "Could not disable native x265 assembly exactly once." >&2
  exit 1
fi

# Upstream builds each dependency in an isolated container stage. A cumulative
# local prefix would otherwise let FreeType discover the already-built zlib and
# let HarfBuzz discover FreeType. Their resulting private dependencies are not
# present in FFmpeg's pkg-config probes and, on a developer workstation, optional
# libraries such as libpng may even resolve to host-native /usr objects. Make the
# isolated-stage dependency choices explicit.
FREETYPE_BUILD_SCRIPT="$SOURCE_ROOT/ffmpeg.wasm/build/freetype2.sh"
if ! grep -Fq -- "--with-zlib=no" "$FREETYPE_BUILD_SCRIPT"; then
  sed -i \
    '/--without-harfbuzz/a\  --with-zlib=no\n  --with-bzip2=no\n  --with-png=no\n  --with-brotli=no' \
    "$FREETYPE_BUILD_SCRIPT"
fi
for freetype_flag in \
  "--without-harfbuzz" \
  "--with-zlib=no" \
  "--with-bzip2=no" \
  "--with-png=no" \
  "--with-brotli=no"; do
  if [[ $(grep -Fc -- "$freetype_flag" "$FREETYPE_BUILD_SCRIPT") -ne 1 ]]; then
    echo "Could not select isolated FreeType dependency flags exactly once." >&2
    exit 1
  fi
done

HARFBUZZ_BUILD_SCRIPT="$SOURCE_ROOT/ffmpeg.wasm/build/harfbuzz.sh"
for harfbuzz_flag in \
  "--with-freetype=no" \
  "--with-glib=no" \
  "--with-gobject=no" \
  "--with-cairo=no" \
  "--with-icu=no" \
  "--with-graphite2=no" \
  "--with-uniscribe=no" \
  "--with-directwrite=no" \
  "--with-coretext=no"; do
  if ! grep -Fq -- "$harfbuzz_flag" "$HARFBUZZ_BUILD_SCRIPT"; then
    sed -i \
      "/--enable-static/a\\  $harfbuzz_flag" \
      "$HARFBUZZ_BUILD_SCRIPT"
  fi
  if [[ $(grep -Fc -- "$harfbuzz_flag" "$HARFBUZZ_BUILD_SCRIPT") -ne 1 ]]; then
    echo "Could not select isolated HarfBuzz dependency flags exactly once." >&2
    exit 1
  fi
done

mkdir -p -- "$INSTALL_DIR" "$OUTPUT_DIR"

export INSTALL_DIR
export EM_PKG_CONFIG_PATH="$EMSDK/upstream/emscripten/system/lib/pkgconfig:$INSTALL_DIR/lib/pkgconfig"
export EM_TOOLCHAIN_FILE="$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"
export PKG_CONFIG_PATH="$INSTALL_DIR/lib/pkgconfig:$EM_PKG_CONFIG_PATH"
export LANG=C
export LC_ALL=C
export TZ=UTC
export SOURCE_DATE_EPOCH
export ZERO_AR_DATE=1

MODE_FLAGS="-O3 -msimd128"
if [[ "$MODE" == "st" ]]; then
  export FFMPEG_ST=yes
  unset FFMPEG_MT || true
else
  MODE_FLAGS="$MODE_FLAGS -sUSE_PTHREADS -pthread"
  # HarfBuzz's pinned upstream build script reads this value directly under
  # `set -u`, while the other scripts use its presence to disable threading.
  # An exported empty value keeps both behaviours correct for the MT build.
  export FFMPEG_ST=
  export FFMPEG_MT=yes
fi
export CFLAGS="-I$INSTALL_DIR/include $MODE_FLAGS"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="-L$INSTALL_DIR/lib $CFLAGS"

FFMPEG_WASM="$SOURCE_ROOT/ffmpeg.wasm"

build_component() {
  local stamp=$1
  local directory=$2
  local script=$3
  if [[ -f "$STAMP_DIR/$stamp" ]]; then
    echo "[reviewed-core:$MODE] reuse $stamp"
    return
  fi
  echo "[reviewed-core:$MODE] build $stamp"
  (
    cd -- "$SOURCE_ROOT/$directory"
    if [[ "$stamp" == "libvpx" ]]; then
      # libvpx embeds its configure command in the linked library. A relative
      # prefix keeps that shipped identity independent of the checkout path
      # while still installing into this mode's private prefix.
      INSTALL_DIR=../../../../install \
        CFLAGS="-I../../../../install/include $MODE_FLAGS" \
        CXXFLAGS="-I../../../../install/include $MODE_FLAGS" \
        LDFLAGS="-L../../../../install/lib -I../../../../install/include $MODE_FLAGS" \
        bash "$FFMPEG_WASM/build/$script"
    else
      bash "$FFMPEG_WASM/build/$script"
    fi
  )
  touch "$STAMP_DIR/$stamp"
}

if [[ ! -f "$STAMP_DIR/sdl2" ]]; then
  if [[ "$MODE" == "st" ]]; then
    embuilder build sdl2
  else
    embuilder build sdl2-mt
  fi
  touch "$STAMP_DIR/sdl2"
fi

build_component x264 x264 x264.sh
build_component x265 x265 x265.sh
build_component libvpx libvpx libvpx.sh
build_component lame lame lame.sh
build_component ogg ogg ogg.sh
build_component theora theora theora.sh
build_component opus opus opus.sh
build_component vorbis vorbis vorbis.sh
build_component zlib zlib zlib.sh
build_component libwebp libwebp libwebp.sh
build_component freetype2 freetype2 freetype2.sh
build_component fribidi fribidi fribidi.sh
build_component harfbuzz harfbuzz harfbuzz.sh
build_component libass libass libass.sh
build_component zimg zimg zimg.sh

# Configuration probes from Autotools projects must never smuggle host-native
# include/library paths into the reviewed WebAssembly link.
if grep -R -E -n -- \
  '/usr/(lib|include)|/lib/(x86_64|i[3-6]86)-linux-gnu' \
  "$INSTALL_DIR/lib/pkgconfig" "$INSTALL_DIR/lib"/*.la; then
  echo "A staged dependency recorded a host-native build path." >&2
  exit 1
fi
if emnm "$INSTALL_DIR/lib/libfreetype.a" |
  grep -F -- " U png_" >/dev/null; then
  echo "FreeType unexpectedly retained an unbundled libpng dependency." >&2
  exit 1
fi
if emnm "$INSTALL_DIR/lib/libfreetype.a" |
  grep -E -- ' U (inflate|BZ2_|Brotli)' >/dev/null; then
  echo "FreeType unexpectedly retained a cross-stage compression dependency." >&2
  exit 1
fi

FFMPEG_SOURCE="$SOURCE_ROOT/FFmpeg"
# FFmpeg records --extra-cflags in `ffmpeg -buildconf`. Use a path relative to
# its source root so the shipped runtime identity is independent of the
# maintainer's checkout location. Dependency builds above retain their
# absolute private staging prefix; only the final FFmpeg configuration is
# embedded in the distributed core.
FFMPEG_INSTALL_RELATIVE=../../../../install
export CFLAGS="-I$FFMPEG_INSTALL_RELATIVE/include $MODE_FLAGS"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="-L$INSTALL_DIR/lib $CFLAGS"
if [[ ! -f "$STAMP_DIR/ffmpeg" ]]; then
  echo "[reviewed-core:$MODE] build FFmpeg"
  (
    cd -- "$FFMPEG_SOURCE"
    bash "$FFMPEG_WASM/build/ffmpeg.sh" \
      --enable-gpl \
      --enable-libx264 \
      --enable-libx265 \
      --enable-libvpx \
      --enable-libmp3lame \
      --enable-libtheora \
      --enable-libvorbis \
      --enable-libopus \
      --enable-zlib \
      --enable-libwebp \
      --enable-libfreetype \
      --enable-libfribidi \
      --enable-libass \
      --enable-libzimg
  )
  touch "$STAMP_DIR/ffmpeg"
fi

if [[ ! -f "$STAMP_DIR/core" ]]; then
  echo "[reviewed-core:$MODE] link ffmpeg-core with 5 MiB stack"
  mkdir -p -- "$FFMPEG_SOURCE/src"
  cp -a -- "$FFMPEG_WASM/src/bind" "$FFMPEG_SOURCE/src/"
  cp -a -- "$FFMPEG_WASM/src/fftools" "$FFMPEG_SOURCE/src/"
  cp -- "$FFMPEG_WASM/build/ffmpeg-wasm.sh" "$FFMPEG_SOURCE/build-reviewed-core.sh"
  (
    cd -- "$FFMPEG_SOURCE"
    bash ./build-reviewed-core.sh \
      -lx264 \
      -lx265 \
      -lvpx \
      -lmp3lame \
      -logg \
      -ltheora \
      -lvorbis \
      -lvorbisenc \
      -lvorbisfile \
      -lopus \
      -lz \
      -lwebpmux \
      -lwebp \
      -lsharpyuv \
      -lfreetype \
      -lfribidi \
      -lharfbuzz \
      -lass \
      -lzimg \
      -sEXPORT_ES6 \
      -o "$OUTPUT_DIR/ffmpeg-core.js"
  )
  touch "$STAMP_DIR/core"
fi

expected_files=(ffmpeg-core.js ffmpeg-core.wasm)
if [[ "$MODE" == "mt" ]]; then
  expected_files+=(ffmpeg-core.worker.js)
fi
for expected_file in "${expected_files[@]}"; do
  if [[ ! -s "$OUTPUT_DIR/$expected_file" || -L "$OUTPUT_DIR/$expected_file" ]]; then
    echo "Missing staged output: $OUTPUT_DIR/$expected_file" >&2
    exit 1
  fi
  if grep -aFq -- "$REPOSITORY_ROOT" "$OUTPUT_DIR/$expected_file" ||
    grep -aFq -- "$EMSDK_ROOT" "$OUTPUT_DIR/$expected_file"; then
    echo "Staged output leaks a maintainer-local build path: $expected_file" >&2
    exit 1
  fi
done

(
  cd -- "$OUTPUT_DIR"
  sha256sum -- "${expected_files[@]}" >SHA256SUMS
)

echo "[reviewed-core:$MODE] staged outputs"
cat "$OUTPUT_DIR/SHA256SUMS"
