#!/bin/bash
# ============================================================================
# build-ffmpeg-lgpl.sh — LGPL-only static ffmpeg for macOS (arm64 + x86_64 + universal)
#
# Reproduces the complete build from zero: download (with SHA-256 check),
# build lame / libogg / libvorbis / opus per arch, build ffmpeg per arch,
# lipo the two binaries into a universal binary.
#
# Requirements (nothing else, no brew/sudo):
#   Apple clang (Command Line Tools), make, python3, curl, tar, lipo, shasum
# NOT required: nasm/yasm (x86_64 built with --disable-x86asm), pkg-config
#   (a tiny Python replacement is embedded below), cmake, autoconf.
#
# Usage:
#   ./build-ffmpeg-lgpl.sh            # full build, both archs, universal
#   ARCHS="arm64" ./build-ffmpeg-lgpl.sh
#   CLEAN=1 ./build-ffmpeg-lgpl.sh    # wipe build/ prefix/ first
#
# Layout (relative to this script):
#   sources/   original tarballs (kept for the source-code offer)
#   build/     extracted sources per arch, build artefacts
#   prefix/    install prefix per arch (lame/ogg/vorbis/opus/ffmpeg)
#   tools/     generated pkg-config replacement
#   out/       final binaries
# ============================================================================
set -euo pipefail

ROOT="${ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"   # override with ROOT=/path if the script is run from a copy
SRC="$ROOT/sources"
BUILD="$ROOT/build"
PREFIX="$ROOT/prefix"
TOOLS="$ROOT/tools"
OUT="$ROOT/out"
LOGDIR="$BUILD/logs"

ARCHS="${ARCHS:-arm64 x86_64}"
JOBS="${JOBS:-$(sysctl -n hw.ncpu)}"
MACOSX_MIN="${MACOSX_MIN:-12.0}"

# ---------------------------------------------------------------- versions ---
FFMPEG_VER=9.0.1
LAME_VER=3.100
OGG_VER=1.3.6
VORBIS_VER=1.3.7
OPUS_VER=1.6.1

FFMPEG_TAR="ffmpeg-$FFMPEG_VER.tar.xz"
LAME_TAR="lame-$LAME_VER.tar.gz"
OGG_TAR="libogg-$OGG_VER.tar.xz"
VORBIS_TAR="libvorbis-$VORBIS_VER.tar.xz"
OPUS_TAR="opus-$OPUS_VER.tar.gz"

FFMPEG_URL="https://ffmpeg.org/releases/$FFMPEG_TAR"
LAME_URL="https://downloads.sourceforge.net/project/lame/lame/$LAME_VER/$LAME_TAR"
OGG_URL="https://downloads.xiph.org/releases/ogg/$OGG_TAR"
VORBIS_URL="https://downloads.xiph.org/releases/vorbis/$VORBIS_TAR"
OPUS_URL="https://downloads.xiph.org/releases/opus/$OPUS_TAR"

FFMPEG_SHA256=cf38e0e28c7e5605942c4a77755349b0145804a397af37eb1fb4c77cb237f635
LAME_SHA256=ddfe36cab873794038ae2c1210557ad34857a4b6bdc515785d1da9e175b1da1e
OGG_SHA256=5c8253428e181840cd20d41f3ca16557a9cc04bad4a3d04cce84808677fa1061
VORBIS_SHA256=b33cc4934322bcbf6efcbacf49e3ca01aadbea4114ec9589d1b1e9d20f72954b
OPUS_SHA256=6ffcb593207be92584df15b32466ed64bbec99109f007c82205f0194572411a1

# ------------------------------------------------------------------ helpers ---
log()  { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
die()  { printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }

fetch() { # fetch <url> <file> <sha256>
    local url="$1" file="$SRC/$2" sha="$3"
    if [ ! -f "$file" ]; then
        log "download $2"
        curl -fL --retry 3 -o "$file.part" "$url"
        mv "$file.part" "$file"
    fi
    local got
    got=$(shasum -a 256 "$file" | awk '{print $1}')
    [ "$got" = "$sha" ] || die "SHA-256 mismatch for $2: got $got, expected $sha"
    echo "sha256 OK  $2"
}

extract() { # extract <tarball> <destdir>  (fresh extraction each time)
    rm -rf "$2"
    mkdir -p "$2"
    tar xf "$SRC/$1" -C "$2" --strip-components=1
}

# ----------------------------------------------- embedded pkg-config clone ---
write_pkg_config() {
mkdir -p "$TOOLS"
cat > "$TOOLS/pkg-config" <<'PYEOF'
#!/usr/bin/env python3
"""Minimal pkg-config replacement. Understands what autotools/ffmpeg configure use:
   --version --exists --print-errors --modversion --cflags --cflags-only-I
   --cflags-only-other --libs --libs-only-l --libs-only-L --libs-only-other
   --static --variable=NAME --define-variable=N=V --atleast-version=V
   --atleast-pkgconfig-version=V --list-all --silence-errors --errors-to-stdout
   Search path: PKG_CONFIG_LIBDIR then PKG_CONFIG_PATH (colon separated)."""
import os, re, sys

VERSION = "0.29.2"
OPS = (">=", "<=", "!=", "=", ">", "<")

def search_dirs():
    dirs = []
    for var in ("PKG_CONFIG_LIBDIR", "PKG_CONFIG_PATH"):
        for d in os.environ.get(var, "").split(":"):
            if d and d not in dirs:
                dirs.append(d)
    return dirs

def find_pc(name):
    for d in search_dirs():
        p = os.path.join(d, name + ".pc")
        if os.path.isfile(p):
            return p
    return None

def vsplit(v):
    return [int(x) if x.isdigit() else x for x in re.findall(r"\d+|[A-Za-z]+", v)]

def vercmp(a, b):
    pa, pb = vsplit(a), vsplit(b)
    for x, y in zip(pa, pb):
        if x == y:
            continue
        if isinstance(x, int) and isinstance(y, int):
            return -1 if x < y else 1
        if isinstance(x, int):
            return 1      # number beats letters (1.0 > 1.0rc)
        if isinstance(y, int):
            return -1
        return -1 if x < y else 1
    return (len(pa) > len(pb)) - (len(pa) < len(pb))

def satisfied(have, op, want):
    c = vercmp(have, want)
    return {">=": c >= 0, "<=": c <= 0, "=": c == 0, "!=": c != 0, ">": c > 0, "<": c < 0}[op]

def parse_specs(text):
    toks = re.findall(r">=|<=|!=|=|>|<|[^\s,<>=!]+", text)
    specs, i = [], 0
    while i < len(toks):
        name = toks[i]; i += 1
        op = ver = None
        if i < len(toks) and toks[i] in OPS:
            op = toks[i]; ver = toks[i + 1] if i + 1 < len(toks) else ""
            i += 2
        specs.append((name, op, ver))
    return specs

class Pkg:
    def __init__(self, name, path, defines):
        self.name, self.path = name, path
        self.vars = {"pcfiledir": os.path.dirname(path)}
        self.vars.update(defines)
        self.fields = {}
        with open(path, encoding="utf-8") as f:
            for raw in f:
                line = raw.split("#", 1)[0].rstrip()
                if not line.strip():
                    continue
                ie, ic = line.find("="), line.find(":")
                if ie != -1 and (ic == -1 or ie < ic):
                    k, v = line[:ie].strip(), line[ie + 1:].strip()
                    if k not in defines:
                        self.vars[k] = v
                elif ic != -1:
                    k, v = line[:ic].strip(), line[ic + 1:].strip()
                    self.fields[k] = v
        self.version = self.expand(self.fields.get("Version", "")).strip()
        self.cflags = self.expand(self.fields.get("Cflags", "")).split()
        self.libs = self.expand(self.fields.get("Libs", "")).split()
        self.libs_private = self.expand(self.fields.get("Libs.private", "")).split()
        self.requires = parse_specs(self.expand(self.fields.get("Requires", "")))
        self.requires_private = parse_specs(self.expand(self.fields.get("Requires.private", "")))

    def expand(self, s, depth=0):
        if depth > 32:
            return s
        def rep(m):
            return self.expand(self.vars.get(m.group(1), ""), depth + 1)
        return re.sub(r"\$\{([^}]+)\}", rep, s)

    def variable(self, n):
        return self.expand(self.vars.get(n, ""))

class Resolver:
    def __init__(self, defines, errors):
        self.defines, self.errors, self.cache = defines, errors, {}

    def load(self, name, op=None, ver=None, needed_by=None):
        if name not in self.cache:
            path = find_pc(name)
            if not path:
                who = " (required by '%s')" % needed_by if needed_by else ""
                self.errors.append("Package %s was not found in the pkg-config search path%s" % (name, who))
                self.cache[name] = None
            else:
                self.cache[name] = Pkg(name, path, self.defines)
        pkg = self.cache[name]
        if pkg and op and not satisfied(pkg.version, op, ver):
            self.errors.append("Requested '%s %s %s' but version of %s is %s" % (name, op, ver, name, pkg.version))
            return None
        return pkg

    def closure(self, roots, static):
        """Return packages in link order (dependents before dependencies), each once."""
        order, seen = [], set()
        def visit(spec, needed_by=None):
            name, op, ver = spec
            pkg = self.load(name, op, ver, needed_by)
            if not pkg or name in seen:
                return
            seen.add(name)
            order.append(pkg)
            for dep in pkg.requires:
                visit(dep, name)
            if static:
                for dep in pkg.requires_private:
                    visit(dep, name)
        for r in roots:
            visit(r)
        return order

def dedupe(tokens, keep_last_libs):
    # group "-framework X" into one unit
    units, i = [], 0
    while i < len(tokens):
        if tokens[i] == "-framework" and i + 1 < len(tokens):
            units.append(tokens[i] + " " + tokens[i + 1]); i += 2
        else:
            units.append(tokens[i]); i += 1
    out = []
    if keep_last_libs:
        for idx, u in enumerate(units):
            if (u.startswith("-l") or u.startswith("-framework")):
                if u in units[idx + 1:]:
                    continue
            elif u in out:
                continue
            out.append(u)
    else:
        for u in units:
            if u not in out:
                out.append(u)
    return " ".join(out)

def main(argv):
    static = False; print_errors = False; errors_to_stdout = False
    actions = []; variable = None; defines = {}; atleast = None; positional = []
    for a in argv:
        if a == "--version":
            print(VERSION); return 0
        if a.startswith("--atleast-pkgconfig-version"):
            return 0
        if a == "--static": static = True
        elif a == "--print-errors": print_errors = True
        elif a == "--silence-errors": print_errors = False
        elif a == "--errors-to-stdout": errors_to_stdout = True
        elif a.startswith("--variable="): variable = a.split("=", 1)[1]; actions.append("variable")
        elif a.startswith("--define-variable="):
            k, v = a.split("=", 1)[1].split("=", 1); defines[k] = v
        elif a.startswith("--atleast-version="): atleast = a.split("=", 1)[1]
        elif a in ("--exists", "--modversion", "--cflags", "--cflags-only-I", "--cflags-only-other",
                   "--libs", "--libs-only-l", "--libs-only-L", "--libs-only-other", "--list-all"):
            actions.append(a[2:])
        elif a.startswith("--"):
            pass  # ignore unknown flags (e.g. --short-errors)
        else:
            positional.append(a)
    if "list-all" in actions:
        for d in search_dirs():
            if os.path.isdir(d):
                for f in sorted(os.listdir(d)):
                    if f.endswith(".pc"):
                        print(f[:-3])
        return 0
    errors = []
    res = Resolver(defines, errors)
    specs = parse_specs(" ".join(positional))
    if not specs:
        sys.stderr.write("Must specify package names on the command line\n"); return 1
    roots = []
    for name, op, ver in specs:
        pkg = res.load(name, op, ver)
        if pkg and atleast and not satisfied(pkg.version, ">=", atleast):
            errors.append("Requested '%s >= %s' but version of %s is %s" % (name, atleast, name, pkg.version))
        roots.append(pkg)
    order = res.closure(specs, static)
    if errors:
        if print_errors:
            (sys.stdout if errors_to_stdout else sys.stderr).write("\n".join(errors) + "\n")
        return 1
    out = []
    for act in actions:
        if act == "exists":
            continue
        if act == "modversion":
            out.append("\n".join(p.version for p in roots))
        elif act == "variable":
            out.append(" ".join(p.variable(variable) for p in roots))
        elif act.startswith("cflags"):
            toks = []
            # cflags of Requires.private are always included (like real pkg-config)
            for p in res.closure(specs, True):
                toks += p.cflags
            if act == "cflags-only-I":
                toks = [t for t in toks if t.startswith("-I")]
            elif act == "cflags-only-other":
                toks = [t for t in toks if not t.startswith("-I")]
            out.append(dedupe(toks, False))
        elif act.startswith("libs"):
            toks = []
            for p in order:
                toks += p.libs
                if static:
                    toks += p.libs_private
            if act == "libs-only-l":
                toks = [t for t in toks if t.startswith("-l")]
            elif act == "libs-only-L":
                toks = [t for t in toks if t.startswith("-L")]
            elif act == "libs-only-other":
                toks = [t for t in toks if not (t.startswith("-l") or t.startswith("-L"))]
            out.append(dedupe(toks, True))
    if out:
        print(" ".join(o for o in out if o != "") if len(out) > 1 else out[0])
    return 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
PYEOF
chmod +x "$TOOLS/pkg-config"
}

# ---------------------------------------------------------------- per arch ---
build_arch() {
    local arch="$1"
    local P="$PREFIX/$arch"
    local B="$BUILD/$arch"
    local host triple
    case "$arch" in
        arm64)  host=aarch64-apple-darwin; triple=aarch64-apple-darwin ;;
        x86_64) host=x86_64-apple-darwin; triple=x86_64-apple-darwin ;;
        *) die "unknown arch $arch" ;;
    esac
    mkdir -p "$P" "$B" "$LOGDIR"

    local CC="clang -arch $arch"
    local CFLAGS="-O2 -mmacosx-version-min=$MACOSX_MIN"
    local LDFLAGS="-mmacosx-version-min=$MACOSX_MIN"
    export PKG_CONFIG="$TOOLS/pkg-config"
    export PKG_CONFIG_LIBDIR="$P/lib/pkgconfig"
    export PKG_CONFIG_PATH=""

    # ---- lame -------------------------------------------------------------
    log "[$arch] lame $LAME_VER"
    extract "$LAME_TAR" "$B/lame"
    ( cd "$B/lame" && \
      ./configure --host="$host" --prefix="$P" --disable-shared --enable-static \
                  --disable-frontend --disable-gtktest --disable-dependency-tracking \
                  CC="$CC" CFLAGS="$CFLAGS" LDFLAGS="$LDFLAGS" \
      && make -j"$JOBS" && make install ) > "$LOGDIR/$arch-lame.log" 2>&1 \
      || die "lame failed, see $LOGDIR/$arch-lame.log"

    # ---- libogg -----------------------------------------------------------
    log "[$arch] libogg $OGG_VER"
    extract "$OGG_TAR" "$B/ogg"
    ( cd "$B/ogg" && \
      ./configure --host="$host" --prefix="$P" --disable-shared --enable-static \
                  --disable-dependency-tracking \
                  CC="$CC" CFLAGS="$CFLAGS" LDFLAGS="$LDFLAGS" \
      && make -j"$JOBS" && make install ) > "$LOGDIR/$arch-ogg.log" 2>&1 \
      || die "libogg failed, see $LOGDIR/$arch-ogg.log"

    # ---- libvorbis --------------------------------------------------------
    log "[$arch] libvorbis $VORBIS_VER"
    extract "$VORBIS_TAR" "$B/vorbis"
    # libvorbis 1.3.7 hard-codes the obsolete linker flag -force_cpusubtype_ALL
    # for Darwin; the current Apple linker (Xcode 15+/CLT 15+) rejects it.
    sed -i '' 's/-force_cpusubtype_ALL//g' "$B/vorbis/configure"
    ( cd "$B/vorbis" && \
      ./configure --host="$host" --prefix="$P" --disable-shared --enable-static \
                  --with-ogg="$P" --disable-oggtest --disable-examples --disable-docs \
                  --disable-dependency-tracking \
                  CC="$CC" CFLAGS="$CFLAGS" LDFLAGS="$LDFLAGS" \
      && make -j"$JOBS" && make install ) > "$LOGDIR/$arch-vorbis.log" 2>&1 \
      || die "libvorbis failed, see $LOGDIR/$arch-vorbis.log"

    # ---- opus -------------------------------------------------------------
    log "[$arch] opus $OPUS_VER"
    extract "$OPUS_TAR" "$B/opus"
    ( cd "$B/opus" && \
      ./configure --host="$host" --prefix="$P" --disable-shared --enable-static \
                  --disable-doc --disable-extra-programs --disable-dependency-tracking \
                  CC="$CC" CFLAGS="$CFLAGS" LDFLAGS="$LDFLAGS" \
      && make -j"$JOBS" && make install ) > "$LOGDIR/$arch-opus.log" 2>&1 \
      || die "opus failed, see $LOGDIR/$arch-opus.log"

    # ---- ffmpeg -----------------------------------------------------------
    log "[$arch] ffmpeg $FFMPEG_VER"
    extract "$FFMPEG_TAR" "$B/ffmpeg"
    local cross=()
    if [ "$arch" = "x86_64" ]; then
        cross=(--enable-cross-compile --arch=x86_64 --target-os=darwin --disable-x86asm)
    else
        cross=(--arch=arm64 --target-os=darwin)
    fi
    ( cd "$B/ffmpeg" && \
      ./configure \
        --prefix="$P" \
        --cc="$CC" \
        --pkg-config="$PKG_CONFIG" --pkg-config-flags=--static \
        --extra-cflags="-I$P/include -mmacosx-version-min=$MACOSX_MIN" \
        --extra-ldflags="-L$P/lib -mmacosx-version-min=$MACOSX_MIN" \
        "${cross[@]}" \
        --disable-everything --disable-network --disable-doc \
        --disable-ffplay --disable-ffprobe --disable-autodetect --disable-debug \
        --disable-gpl --disable-nonfree --disable-version3 \
        --enable-audiotoolbox --enable-libmp3lame --enable-libopus --enable-libvorbis \
        --enable-protocol=file,pipe \
        --enable-filter=loudnorm,volume,asoftclip,aresample,aformat,anull,sine \
        --enable-indev=lavfi \
        --enable-demuxer=mp3,mov,flac,wav,aiff,ogg \
        --enable-decoder=mp3,mp3float,aac,alac,flac,vorbis,opus,libopus,libvorbis,speex,pcm_s16le,pcm_s24le,pcm_s32le,pcm_s16be,pcm_s24be,pcm_s32be,pcm_f32le,pcm_f32be,pcm_s8,pcm_u8 \
        --enable-encoder=libmp3lame,aac,aac_at,alac,flac,libopus,libvorbis,pcm_s16le,pcm_s24le,pcm_s32le,pcm_s16be,pcm_s24be,pcm_s32be \
        --enable-muxer=mp3,ipod,mp4,flac,wav,aiff,ogg,opus,null \
        --enable-parser=mpegaudio,aac,flac,vorbis,opus \
        --enable-bsf=aac_adtstoasc \
      && grep -q '^#define CONFIG_GPL 0' config.h \
      && grep -q '^#define CONFIG_NONFREE 0' config.h \
      && grep -q '^#define CONFIG_VERSION3 0' config.h \
      && make -j"$JOBS" && make install ) > "$LOGDIR/$arch-ffmpeg.log" 2>&1 \
      || die "ffmpeg failed, see $LOGDIR/$arch-ffmpeg.log"

    mkdir -p "$OUT"
    cp "$P/bin/ffmpeg" "$OUT/ffmpeg-$triple"
    chmod +x "$OUT/ffmpeg-$triple"
    log "[$arch] done: $OUT/ffmpeg-$triple ($(du -h "$OUT/ffmpeg-$triple" | cut -f1))"
}

# ---------------------------------------------------------------------- main ---
mkdir -p "$SRC" "$BUILD" "$PREFIX" "$OUT"
if [ "${CLEAN:-0}" = "1" ]; then
    log "clean build/ prefix/"
    rm -rf "$BUILD" "$PREFIX"
    mkdir -p "$BUILD" "$PREFIX"
fi

log "fetch sources"
fetch "$FFMPEG_URL" "$FFMPEG_TAR" "$FFMPEG_SHA256"
fetch "$LAME_URL"   "$LAME_TAR"   "$LAME_SHA256"
fetch "$OGG_URL"    "$OGG_TAR"    "$OGG_SHA256"
fetch "$VORBIS_URL" "$VORBIS_TAR" "$VORBIS_SHA256"
fetch "$OPUS_URL"   "$OPUS_TAR"   "$OPUS_SHA256"

write_pkg_config

for a in $ARCHS; do
    build_arch "$a"
done

if [ -f "$OUT/ffmpeg-aarch64-apple-darwin" ] && [ -f "$OUT/ffmpeg-x86_64-apple-darwin" ]; then
    log "lipo universal"
    lipo -create "$OUT/ffmpeg-aarch64-apple-darwin" "$OUT/ffmpeg-x86_64-apple-darwin" \
         -output "$OUT/ffmpeg-universal-apple-darwin"
    chmod +x "$OUT/ffmpeg-universal-apple-darwin"
    lipo -info "$OUT/ffmpeg-universal-apple-darwin"
fi

log "result"
ls -la "$OUT"
