#!/bin/bash

###############################################################################
#
# Script Name:  build-static
#
# Description:  Master build script for a fully static, standalone `magick`
#               Unix executable on macOS. No app bundle, no @rpath, no
#               dylibbundler -- the resulting binary depends only on the
#               system libraries that ship with macOS.
#
###############################################################################

VERSION="1.0"
SCRIPT=$(basename "${0}")

source ./config-static > /dev/null 2>&1
# shellcheck disable=SC2181
[[ $? -ne 0 ]] && { echo "config-static file not found"; exit 1;}
[[ -z $BUILD_DIR ]] && { echo "BUILD_DIR not set"; exit 1; }

# Do once
#brew install cmake pkgconfig

function usage() {
    local exitCode=${1}

    echo;echo "Usage:    ${SCRIPT} [-aclmxhv]

where:
    -a    build all (libs + configure + magick + verify)
    -c    configure ImageMagick
    -l    build libraries (static)
    -m    build magick
    -V    verify the resulting binary with otool -L
    -x    clean up build directory
    -h    help
    -v    version
"
    exit "${exitCode}"
}

SRC_DIR="${BUILD_DIR}/imagemagick_sources"
LIB_DIR="${BUILD_DIR}/imagemagick_library"

# Common cmake flags applied to every delegate so we never accidentally
# build a shared library.
COMMON_CMAKE=(
    -DCMAKE_BUILD_TYPE=Release
    -DCMAKE_FIND_ROOT_PATH="${LIB_DIR}"
    -DCMAKE_INSTALL_PREFIX="${LIB_DIR}"
    -DCMAKE_PREFIX_PATH="${LIB_DIR}"
    -DBUILD_SHARED_LIBS=OFF
    -DCMAKE_POSITION_INDEPENDENT_CODE=ON
    # Honor MACOSX_DEPLOYMENT_TARGET from the env. Some cmake projects
    # (notably libaom and x265) ignore the env var and stamp the SDK
    # version into the binary unless we pass it explicitly.
    ${MACOSX_DEPLOYMENT_TARGET:+-DCMAKE_OSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET}}
)

# --- Helpers for idempotent rebuilds ----------------------------------------
#
# clone_if_missing <dirname> <branch> <url>
#   Clones into <dirname> if it doesn't already exist. Safe to re-run.
function clone_if_missing() {
    local dir="$1" branch="$2" url="$3"
    if [ -d "${dir}" ]; then
        echo "==> ${dir} source already present, skipping clone"
    else
        git clone --branch "${branch}" --depth 1 --single-branch "${url}" "${dir}" || exit
    fi
}

# fetch_tarball <dirname> <url>
#   Downloads + extracts a tarball into <dirname> if it doesn't already exist.
function fetch_tarball() {
    local dir="$1" url="$2"
    if [ -d "${dir}" ]; then
        echo "==> ${dir} source already present, skipping download"
    else
        local tarball="${dir}.tar.gz"
        curl -L "${url}" -o "${tarball}" || exit
        mkdir -p "${dir}"
        tar -zxvf "${tarball}" -C "./${dir}" --strip-components=1 || exit
        rm -f "${tarball}"
    fi
}

# fresh_build_dir <name>
#   Wipes and recreates a build subdirectory, then cds into it. Ensures cmake
#   never reuses a stale CMakeCache.txt that points at old paths or flags.
function fresh_build_dir() {
    local d="${1:-build}"
    rm -rf "${d}" && mkdir -p "${d}" && cd "${d}" || exit
}
# ----------------------------------------------------------------------------

function build_libs() {
    mkdir -p "${SRC_DIR}"
    mkdir -p "${LIB_DIR}"

    # === nasm === #
    # SIMD CPU optimization (build tool, not linked).
    # Autoconf project -- if already installed, skip entirely.
    if [ ! -x "${LIB_DIR}/bin/nasm" ]; then
        cd "${SRC_DIR}" || exit
        fetch_tarball nasm "https://www.nasm.us/pub/nasm/releasebuilds/${NASM_VERSION}/nasm-${NASM_VERSION}.tar.gz"
        cd nasm || exit
        [ -f Makefile ] && make distclean >/dev/null 2>&1
        ./configure --prefix="${LIB_DIR}"
        make -j 8 && make install
    else
        echo "==> nasm already installed, skipping"
    fi

    export PATH="${LIB_DIR}/bin:$PATH"

    # === libpng === #
    cd "${SRC_DIR}" || exit
    clone_if_missing libpng "${LIBPNG_BRANCH}" https://github.com/pnggroup/libpng.git
    cd libpng || exit
    fresh_build_dir build
    cmake "${COMMON_CMAKE[@]}" \
          -DPNG_SHARED=OFF \
          -DPNG_STATIC=ON \
          -DPNG_TESTS=OFF \
          -DPNG_TOOLS=OFF \
          ../
    make -j 8 && make install

    # === libjpeg-turbo === #
    cd "${SRC_DIR}" || exit
    clone_if_missing libjpeg-turbo "${LIBJPEG_BRANCH}" https://github.com/libjpeg-turbo/libjpeg-turbo.git
    cd libjpeg-turbo || exit
    fresh_build_dir build
    cmake "${COMMON_CMAKE[@]}" \
          -DWITH_JPEG8=ON \
          -DENABLE_SHARED=OFF \
          -DENABLE_STATIC=ON \
          ../
    make -j 8 && make install

    # === libwebp === #
    cd "${SRC_DIR}" || exit
    clone_if_missing libwebp "${LIBWEBP_BRANCH}" https://chromium.googlesource.com/webm/libwebp
    cd libwebp || exit
    fresh_build_dir build
    cmake "${COMMON_CMAKE[@]}" \
          -DWEBP_BUILD_ANIM_UTILS=OFF \
          -DWEBP_BUILD_CWEBP=OFF \
          -DWEBP_BUILD_DWEBP=OFF \
          -DWEBP_BUILD_GIF2WEBP=OFF \
          -DWEBP_BUILD_IMG2WEBP=OFF \
          -DWEBP_BUILD_VWEBP=OFF \
          -DWEBP_BUILD_WEBPINFO=OFF \
          -DWEBP_BUILD_WEBPMUX=OFF \
          -DWEBP_BUILD_EXTRAS=OFF \
          ../
    make -j 8 && make install

    # === libtiff === #
    # libtiff ships a build/ subdir in its repo, so we use _build to avoid
    # collisions and to make the wipe-on-rerun unambiguous.
    cd "${SRC_DIR}" || exit
    clone_if_missing libtiff "${LIBTIFF_BRANCH}" https://gitlab.com/libtiff/libtiff.git
    cd libtiff || exit
    fresh_build_dir _build
    cmake "${COMMON_CMAKE[@]}" \
          -Dwebp=OFF \
          -Dtiff-tools=OFF \
          -Dtiff-tests=OFF \
          -Dlibdeflate=OFF \
          -Dlerc=OFF \
          -Dzstd=OFF \
          -Djbig=OFF \
          -Djpeg12=OFF \
          -Dlzma=OFF \
          ../
    make -j 8 && make install

    # libtiff's installed CMake config (TiffTargets.cmake) records auto-
    # detected imported targets like CMath::CMath and liblzma::liblzma in
    # its link interface. Downstream cmake users (libheif's heifio) can't
    # resolve those targets and configure fails. Removing the CMake config
    # forces find_package(TIFF) to fall back to FindTIFF.cmake module mode,
    # which uses pkg-config / direct library search -- which we control.
    rm -rf "${LIB_DIR}/lib/cmake/tiff"

    # === freetype === #
    # Disable brotli + harfbuzz auto-detection: freetype's cmake will happily
    # find them in /opt/homebrew and add them to its Requires line, even
    # though our LDFLAGS prevents them from actually being linked. That
    # leaves a stale dependency in freetype2.pc that breaks IM's pkg-config
    # detection of freetype itself.
    cd "${SRC_DIR}" || exit
    clone_if_missing freetype "${FREETYPE_BRANCH}" https://gitlab.freedesktop.org/freetype/freetype.git
    cd freetype || exit
    fresh_build_dir build
    cmake "${COMMON_CMAKE[@]}" \
          -DFT_DISABLE_BROTLI=TRUE \
          -DFT_DISABLE_HARFBUZZ=TRUE \
          ../
    make -j 8 && make install

    # === lcms2 === #
    # Color management. Without it, IM writes PNGs with no ICC profile chunk,
    # which makes macOS Finder fall back to a generic icon instead of a
    # thumbnail preview.
    # Autoconf project; cmake support is incomplete upstream.
    cd "${SRC_DIR}" || exit
    clone_if_missing lcms2 "${LCMS2_BRANCH}" https://github.com/mm2/Little-CMS.git
    cd lcms2 || exit
    [ -f Makefile ] && make distclean >/dev/null 2>&1
    [ -x configure ] || ./autogen.sh
    CPPFLAGS="-I${LIB_DIR}/include" \
    LDFLAGS="-L${LIB_DIR}/lib" \
    ./configure --prefix="${LIB_DIR}" \
                --enable-static \
                --disable-shared
    make -j 8 && make install

    # === djvulibre === #
    # Autoconf project -- needs --enable-static --disable-shared explicitly.
    cd "${SRC_DIR}" || exit
    fetch_tarball djvulibre "http://downloads.sourceforge.net/djvu/djvulibre-${DJVULIBRE_VERSION}.tar.gz"
    cd djvulibre || exit
    [ -f Makefile ] && make distclean >/dev/null 2>&1
    CPPFLAGS="-I${LIB_DIR}/include" \
    LDFLAGS="-L${LIB_DIR}/lib" \
    ./configure --prefix="${LIB_DIR}" \
                --enable-static \
                --disable-shared \
                --disable-desktopfiles
    make -j 8 && make install

    # === libde265 === #
    # HEVC decoder for HEIF. SDL is only needed for the dec265 demo player; skip it.
    cd "${SRC_DIR}" || exit
    clone_if_missing libde265 "${LIBDE265_BRANCH}" https://github.com/strukturag/libde265.git
    cd libde265 || exit
    fresh_build_dir build
    cmake "${COMMON_CMAKE[@]}" \
          -DENABLE_SDL=OFF \
          ../
    make -j 8 && make install

    # === x265 === #
    # HEVC encoder for HEIF. The repo includes build/xcode; we use a sibling
    # _build dir of our own so wipe-and-rebuild is clean.
    cd "${SRC_DIR}" || exit
    clone_if_missing x265 "${X265_BRANCH}" https://bitbucket.org/multicoreware/x265_git.git
    cd x265 || exit
    fresh_build_dir _build
    cmake "${COMMON_CMAKE[@]}" \
          -DENABLE_SHARED=OFF \
          -DENABLE_CLI=OFF \
          ../source
    make -j 8 && make install

    # === libaom === #
    # AV1 codec support for HEIF. The repo includes a build/ dir; use _build.
    # Adjust AOM_TARGET_CPU according to your architecture (x86_64 for Intel, arm64 for Silicon).
    cd "${SRC_DIR}" || exit
    clone_if_missing aom "${LIBAOM_BRANCH}" https://aomedia.googlesource.com/aom
    cd aom || exit
    fresh_build_dir _build
    cmake "${COMMON_CMAKE[@]}" \
          -DAOM_TARGET_CPU=generic \
          -DENABLE_TESTS=OFF \
          -DENABLE_TOOLS=OFF \
          -DENABLE_EXAMPLES=OFF \
          -DENABLE_DOCS=OFF \
          -DCONFIG_PIC=1 \
          ../
    make -j 8 && make install

    # === libheif === #
    # ENABLE_PLUGIN_LOADING=OFF is critical: it forces the codec backends
    # to be linked statically into libheif instead of dlopen()ed at runtime.
    cd "${SRC_DIR}" || exit
    clone_if_missing libheif "${LIBHEIF_BRANCH}" https://github.com/strukturag/libheif.git
    cd libheif || exit
    fresh_build_dir build
    cmake "${COMMON_CMAKE[@]}" \
          -DENABLE_PLUGIN_LOADING=OFF \
          -DWITH_EXAMPLES=OFF \
          -DBUILD_TESTING=OFF \
          -DWITH_LIBDE265=ON \
          -DWITH_X265=ON \
          -DWITH_AOM_DECODER=ON \
          -DWITH_AOM_ENCODER=ON \
          -DLIBSHARPYUV_INCLUDE_DIR="${LIB_DIR}/include/webp" \
          ../
    make -j 8 && make install

    # === openjpeg === #
    # JPEG 2000 support (jp2, j2k, jpc, jpm, jpt). No external deps beyond
    # zlib (system). BUILD_CODEC=OFF skips the cli tools we don't need.
    cd "${SRC_DIR}" || exit
    clone_if_missing openjpeg "${OPENJPEG_BRANCH}" https://github.com/uclouvain/openjpeg.git
    cd openjpeg || exit
    fresh_build_dir build
    cmake "${COMMON_CMAKE[@]}" \
          -DBUILD_CODEC=OFF \
          -DBUILD_TESTING=OFF \
          -DBUILD_PKGCONFIG_FILES=ON \
          ../
    make -j 8 && make install
    # Same defensive cleanup as libtiff: openjpeg's installed cmake config
    # exports imported targets that downstream consumers may not be able
    # to resolve. Force pkg-config-based detection for any consumer.
    rm -rf "${LIB_DIR}/lib/openjpeg-"* 2>/dev/null || true

    # === libraw === #
    # RAW camera image support (Canon CR2/CR3, Nikon NEF, Sony ARW, Fuji
    # RAF, DNG, etc.). Autoconf project; needs autoreconf since we clone
    # from Git rather than a release tarball that ships with configure.
    # --disable-openmp avoids needing libgomp/libomp at runtime.
    # --disable-jasper / --disable-jpeg2000 because we use openjpeg, not
    # JasPer, for any JPEG2000-compressed RAW data.
    # LIBS="-lc++" works around an autoconf quirk: LibRaw's bin/ sample
    # programs are linked with the C compiler ($CC) instead of $CXX, so
    # libc++ isn't pulled in automatically. Without -lc++ the sample
    # programs fail to link with __cxa_throw / __gxx_personality_v0
    # undefined and the install step never runs.
    cd "${SRC_DIR}" || exit
    clone_if_missing LibRaw "${LIBRAW_BRANCH}" https://github.com/LibRaw/LibRaw.git
    cd LibRaw || exit
    [ -f Makefile ] && make distclean >/dev/null 2>&1
    [ -x configure ] || autoreconf --install --force
    PKG_CONFIG_LIBDIR="${LIB_DIR}/lib/pkgconfig" \
    CPPFLAGS="-I${LIB_DIR}/include" \
    LDFLAGS="-L${LIB_DIR}/lib" \
    LIBS="-lc++" \
    ./configure --prefix="${LIB_DIR}" \
                --enable-static \
                --disable-shared \
                --disable-openmp \
                --disable-jasper \
                --disable-jpeg2000 \
                --disable-examples
    make -j 8 && make install

    # Belt-and-suspenders: remove any stray .dylib files so the IM link
    # step physically cannot pick up a shared version of any delegate.
    echo "==> Removing any stray dylibs from ${LIB_DIR}/lib"
    find "${LIB_DIR}/lib" -maxdepth 1 -name '*.dylib' -delete
    find "${LIB_DIR}/lib" -maxdepth 1 -name '*.dylib.*' -delete

    fixup_pkgconfig
}

# Several of our delegate .pc files declare Requires: on system libraries
# (zlib, bzip2, liblzma) that ship with macOS but have no .pc files.
# Without resolvers, pkg-config refuses to acknowledge libpng/libtiff/
# freetype/libheif at all -- which silently strips those coders out of
# IM's build. We fix this by writing stub .pc files for the system libs.
# Also handles x265, which is built but doesn't always install its .pc
# in a location our PKG_CONFIG_LIBDIR sees.
function fixup_pkgconfig() {
    local pcdir="${LIB_DIR}/lib/pkgconfig"
    mkdir -p "${pcdir}"

    echo "==> Writing system-library pkg-config stubs into ${pcdir}"

    cat > "${pcdir}/zlib.pc" <<'EOF'
prefix=/usr
libdir=${prefix}/lib
includedir=${prefix}/include

Name: zlib
Description: zlib compression library (system)
Version: 1.2.13
Libs: -lz
Cflags:
EOF

    cat > "${pcdir}/bzip2.pc" <<'EOF'
prefix=/usr
libdir=${prefix}/lib
includedir=${prefix}/include

Name: bzip2
Description: bzip2 compression library (system)
Version: 1.0.8
Libs: -lbz2
Cflags:
EOF

    cat > "${pcdir}/liblzma.pc" <<'EOF'
prefix=/usr
libdir=${prefix}/lib
includedir=${prefix}/include

Name: liblzma
Description: liblzma compression library (system)
Version: 5.6.0
Libs: -llzma
Cflags:
EOF

    # x265: sometimes its .pc lands in lib64/pkgconfig instead of lib/pkgconfig.
    # Try to find it; if not present at all, write a stub pointing at our
    # static libx265.a so libheif.pc can resolve.
    if [ ! -f "${pcdir}/x265.pc" ]; then
        local found
        found=$(find "${LIB_DIR}" -name 'x265.pc' 2>/dev/null | head -1)
        if [ -n "${found}" ] && [ "${found}" != "${pcdir}/x265.pc" ]; then
            cp "${found}" "${pcdir}/x265.pc"
            echo "==> Copied x265.pc from ${found}"
        else
            cat > "${pcdir}/x265.pc" <<EOF
prefix=${LIB_DIR}
exec_prefix=\${prefix}
libdir=\${prefix}/lib
includedir=\${prefix}/include

Name: x265
Description: H.265/HEVC video encoder (static)
Version: 4.0
Libs: -L\${libdir} -lx265
Libs.private: -lc++
Cflags: -I\${includedir}
EOF
            echo "==> Wrote stub x265.pc"
        fi
    fi

    # Verification: every package IM relies on should now resolve cleanly.
    echo "==> Verifying pkg-config resolution"
    local pkg fail=0
    for pkg in libpng16 libtiff-4 freetype2 libheif libwebp libjpeg lcms2 libopenjp2 libraw_r; do
        if PKG_CONFIG_LIBDIR="${pcdir}" pkg-config --exists "${pkg}" 2>/dev/null; then
            echo "    ${pkg}: OK"
        else
            echo "    ${pkg}: NOT RESOLVABLE"
            fail=1
        fi
    done
    if [ "${fail}" -ne 0 ]; then
        echo
        echo "WARNING: some delegate .pc files still don't resolve. IM's configure"
        echo "will skip the corresponding coders. Check the per-package errors with:"
        echo "    PKG_CONFIG_LIBDIR=${pcdir} pkg-config --print-errors --exists <pkg>"
    fi
}

function configure_app() {
    cd "${SRC_DIR}" || exit

    mkdir -p "${DOWNLOADS}"
    rm -rf ImageMagick-"${MAGICK_VERSION}"
    curl -fL "https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${MAGICK_VERSION}.tar.gz" \
         -o "${DOWNLOADS}/${MAGICK_VERSION}.tar.gz" || exit
    tar -zxvf "${DOWNLOADS}/${MAGICK_VERSION}.tar.gz" || exit

    cd ImageMagick-"${MAGICK_VERSION}" || exit

    # PKG_CONFIG_LIBDIR (not _PATH) REPLACES the default pkg-config search
    # paths instead of adding to them. Without this, pkg-config still finds
    # /opt/homebrew/lib/pkgconfig/*.pc and IM's configure auto-enables any
    # Homebrew lib it sees -- which then fails to compile because our
    # CPPFLAGS doesn't include /opt/homebrew/include.
    #
    # The explicit --without-* flags below are belt-and-suspenders for
    # libraries we do not ship statically, so even if one slipped past
    # pkg-config isolation, IM would still refuse to use it.
    #
    # --without-modules         : link all coders into the magick binary (no dlopen)
    # --enable-zero-configuration: bake config defaults into the binary so it
    #                              doesn't need delegates.xml / policy.xml /
    #                              type.xml from disk at runtime
    # --disable-shared          : already in your original; keeps libMagick* static too
    # shellcheck disable=SC2034
    PKG_CONFIG_LIBDIR="${LIB_DIR}/lib/pkgconfig" \
    PKG_CONFIG_PATH="${LIB_DIR}/lib/pkgconfig" \
    PKG_CONFIG="pkg-config --static" \
    CPPFLAGS="-I${LIB_DIR}/include" \
    LDFLAGS="-L${LIB_DIR}/lib" \
    ./configure --prefix="${BUILD_DIR}/ImageMagick-Static-V${MAGICK_VERSION}" \
                --disable-shared \
                --enable-static \
                --without-modules \
                --enable-zero-configuration \
                --without-magick-plus-plus \
                --without-openexr \
                --without-x \
                --without-perl \
                --without-zstd \
                --without-lzma \
                --without-raqm \
                --without-pango \
                --without-fontconfig \
                --without-jbig \
                --without-fftw \
                --without-flif
}

function build_app() {
    cd "${SRC_DIR}/ImageMagick-${MAGICK_VERSION}" || exit

    make -j 12 && make install

    echo
    echo "============================================================"
    echo "Built standalone binary:"
    echo "  ${BUILD_DIR}/ImageMagick-Static-V${MAGICK_VERSION}/bin/magick"
    echo "============================================================"
}

function verify_binary() {
    local bin="${BUILD_DIR}/ImageMagick-Static-V${MAGICK_VERSION}/bin/magick"
    if [[ ! -x "${bin}" ]]; then
        echo "magick binary not found at ${bin}"
        exit 1
    fi

    echo
    echo "==> otool -L ${bin}"
    otool -L "${bin}"

    echo
    echo "==> Checking for non-system dynamic dependencies..."
    local bad
    bad=$(otool -L "${bin}" \
        | awk 'NR>1 {print $1}' \
        | grep -Ev '^(/usr/lib/|/System/)' || true)

    if [[ -n "${bad}" ]]; then
        echo
        echo "WARNING: the following non-system dynamic dependencies remain:"
        echo "${bad}"
        echo
        echo "The binary is NOT yet standalone. Find which delegate produced"
        echo "the .dylib above, rebuild it with the appropriate -DBUILD_SHARED_LIBS=OFF"
        echo "(or project-specific static flag), then re-run -c -m."
        exit 2
    fi

    echo
    echo "OK: only system libraries are linked. The binary is standalone."
    echo
    echo "Quick smoke test:"
    "${bin}" -version | head -n 2
    echo
    "${bin}" -list format | head -n 5
}

###############################################################################
#
# Main Entry Point - parse commandline
#

while getopts "aclmVxhv" opt; do
    case ${opt} in
      a)
        BUILD_LIBS=true
        CONFIGURE_APP=true
        BUILD_APP=true
        VERIFY_BIN=true
        ;;

      c)
        CONFIGURE_APP=true
        ;;

      l)
        BUILD_LIBS=true
        ;;

      m)
        BUILD_APP=true
        ;;

      V)
        VERIFY_BIN=true
        ;;

      x)
        if [[ -d ${BUILD_DIR} ]]; then
          rm -rf "${BUILD_DIR}"
        fi
        ;;

      h)
        usage 0
        ;;

      v)
        echo; echo "${SCRIPT} Version ${VERSION}"; exit 0
        ;;

      \?)
        usage 1
        ;;
    esac
done

# Execute args in controlled sequence
[[ -n ${BUILD_LIBS} ]]    && build_libs
[[ -n ${CONFIGURE_APP} ]] && configure_app
[[ -n ${BUILD_APP} ]]     && build_app
[[ -n ${VERIFY_BIN} ]]    && verify_binary
