#!/usr/bin/env bash
# Creates two self-contained macOS .app bundles:
#   EWSDR.app          — normal launch
#   EWSDR_opengl.app   — launches with --opengl flag via wrapper script
#
# Usage: ./bundle_app.sh [path/to/Source.app] [output/dir]
#   If no arguments given, auto-detects the .app in bin/aarch64-darwin/
#
# Skips system libs: /System/, /usr/lib/, @executable_path, @loader_path, @rpath

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Project root holds bin/, icon/, dist/. This script lives in other/, so go up one.
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

# ── Optional runtime libraries ────────────────────────────────────────────────
# These are loaded dynamically (dlopen) rather than linked, so they never show
# up in the executable's otool -L. They are only needed for optional hardware
# (PlutoSDR / AD936x). Bundle them — together with their dependency trees — only
# if they are present on the build machine; otherwise the bundle builds fine
# without them.
OPTIONAL_LIB_NAMES=(libiio libad9361)
OPTIONAL_LIB_DIRS=(/usr/local/lib /opt/homebrew/lib /opt/local/lib)

# ── Locate source .app ────────────────────────────────────────────────────────
if [[ $# -ge 1 ]]; then
    SRC_APP="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
else
    SRC_APP="$(find "$PROJECT_ROOT/bin" -maxdepth 3 -name "*.app" | head -1)"
    [[ -n "$SRC_APP" ]] || { echo "ERROR: no .app found under bin/"; exit 1; }
fi

# ── Output directory ──────────────────────────────────────────────────────────
if [[ $# -ge 2 ]]; then
    OUT_DIR="$2"
else
    OUT_DIR="$PROJECT_ROOT/dist"
fi

mkdir -p "$OUT_DIR"

# ── Generate .icns from icon/ folder ─────────────────────────────────────────
ICNS_PATH=""
ICON_SRC="$PROJECT_ROOT/icon"
if [[ -d "$ICON_SRC" ]]; then
    echo "Generating EWSDR.icns..."
    ICONSET_TMP="$(mktemp -d)/EWSDR.iconset"
    mkdir -p "$ICONSET_TMP"

    # iconset naming: icon_WxH.png and icon_WxH@2x.png (where @2x = double pixels)
    [[ -f "$ICON_SRC/icon16.png"  ]] && cp "$ICON_SRC/icon16.png"  "$ICONSET_TMP/icon_16x16.png"
    [[ -f "$ICON_SRC/icon32.png"  ]] && cp "$ICON_SRC/icon32.png"  "$ICONSET_TMP/icon_16x16@2x.png"
    [[ -f "$ICON_SRC/icon32.png"  ]] && cp "$ICON_SRC/icon32.png"  "$ICONSET_TMP/icon_32x32.png"
    [[ -f "$ICON_SRC/icon64.png"  ]] && cp "$ICON_SRC/icon64.png"  "$ICONSET_TMP/icon_32x32@2x.png"
    [[ -f "$ICON_SRC/icon128.png" ]] && cp "$ICON_SRC/icon128.png" "$ICONSET_TMP/icon_128x128.png"
    [[ -f "$ICON_SRC/icon256.png" ]] && cp "$ICON_SRC/icon256.png" "$ICONSET_TMP/icon_128x128@2x.png"
    [[ -f "$ICON_SRC/icon256.png" ]] && cp "$ICON_SRC/icon256.png" "$ICONSET_TMP/icon_256x256.png"
    if [[ -f "$ICON_SRC/icon.png" ]]; then
        sips -z 512 512 "$ICON_SRC/icon.png" --out "$ICONSET_TMP/icon_256x256@2x.png" >/dev/null
        sips -z 512 512 "$ICON_SRC/icon.png" --out "$ICONSET_TMP/icon_512x512.png"    >/dev/null
        cp "$ICON_SRC/icon.png" "$ICONSET_TMP/icon_512x512@2x.png"
    fi

    ICNS_PATH="$OUT_DIR/EWSDR.icns"
    iconutil -c icns "$ICONSET_TMP" -o "$ICNS_PATH"
    rm -rf "$(dirname "$ICONSET_TMP")"
    echo "  -> $ICNS_PATH"
    echo ""
fi

# ── Resolve source executable name ───────────────────────────────────────────
SRC_EXEC_NAME="$(defaults read "$SRC_APP/Contents/Info" CFBundleExecutable 2>/dev/null || true)"
[[ -z "$SRC_EXEC_NAME" ]] && SRC_EXEC_NAME="$(basename "$SRC_APP" .app)"

# Resolve the real binary path (Lazarus may place it alongside the .app)
SRC_BIN=""
CANDIDATE="$(dirname "$SRC_APP")/$SRC_EXEC_NAME"
if [[ -f "$CANDIDATE" ]]; then
    SRC_BIN="$CANDIDATE"
fi
if [[ -z "$SRC_BIN" ]]; then
    SRC_EXEC="$SRC_APP/Contents/MacOS/$SRC_EXEC_NAME"
    if [[ -L "$SRC_EXEC" ]]; then
        RAW="$(readlink "$SRC_EXEC")"
        if [[ "$RAW" == /* ]]; then
            SRC_BIN="$RAW"
        else
            SRC_BIN="$(cd "$(dirname "$SRC_EXEC")" && cd "$(dirname "$RAW")" && pwd)/$(basename "$RAW")"
        fi
    elif [[ -f "$SRC_EXEC" ]]; then
        SRC_BIN="$SRC_EXEC"
    fi
fi
[[ -f "$SRC_BIN" ]] || { echo "ERROR: executable not found for $SRC_EXEC_NAME"; exit 1; }

# ── Helpers ───────────────────────────────────────────────────────────────────

is_system_lib() {
    local path="$1"
    [[ "$path" == /System/* ]]            && return 0
    [[ "$path" == /usr/lib/* ]]           && return 0
    [[ "$path" == @executable_path/* ]]   && return 0
    [[ "$path" == @loader_path/* ]]       && return 0
    [[ "$path" == @rpath/* ]]             && return 0
    return 1
}

# ── Build a self-contained .app bundle ───────────────────────────────────────
# Args: <out_app_path> <CFBundleName> <CFBundleIdentifier>
build_bundle() {
    local OUT_APP="$1"
    local BUNDLE_NAME="$2"
    local BUNDLE_ID="$3"
    local FWK_DIR="$OUT_APP/Contents/Frameworks"

    echo "==> Building $OUT_APP"

    rm -rf "$OUT_APP"
    cp -R "$SRC_APP" "$OUT_APP"
    mkdir -p "$FWK_DIR"
    rm -f "$OUT_APP/Contents/MacOS/wdspWisdom"*

    # Install the real binary (replace symlink left by Lazarus)
    local EXEC_PATH="$OUT_APP/Contents/MacOS/$SRC_EXEC_NAME"
    rm -f "$EXEC_PATH"
    cp "$SRC_BIN" "$EXEC_PATH"
    chmod +x "$EXEC_PATH"

    # Update bundle metadata
    /usr/libexec/PlistBuddy -c "Set :CFBundleName $BUNDLE_NAME" \
        "$OUT_APP/Contents/Info.plist"
    /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" \
        "$OUT_APP/Contents/Info.plist"

    # Install icon
    if [[ -n "${ICNS_PATH:-}" && -f "$ICNS_PATH" ]]; then
        mkdir -p "$OUT_APP/Contents/Resources"
        cp "$ICNS_PATH" "$OUT_APP/Contents/Resources/EWSDR.icns"
        /usr/libexec/PlistBuddy -c "Set :CFBundleIconFile EWSDR" \
            "$OUT_APP/Contents/Info.plist" 2>/dev/null || \
        /usr/libexec/PlistBuddy -c "Add :CFBundleIconFile string EWSDR" \
            "$OUT_APP/Contents/Info.plist"
    fi

    # Recursive dylib collector
    declare -A PROCESSED=()

    collect_and_copy() {
        local binary="$1"
        local deps
        deps="$(otool -L "$binary" | tail -n +2 | awk '{print $1}')"

        while IFS= read -r dep; do
            [[ -z "$dep" ]] && continue
            is_system_lib "$dep" && continue
            local libname
            libname="$(basename "$dep")"
            local dst="$FWK_DIR/$libname"

            if [[ -f "$dst" || -f "$dep" ]]; then
                install_name_tool -change "$dep" \
                    "@executable_path/../Frameworks/$libname" \
                    "$binary" 2>/dev/null || true
            fi

            [[ -n "${PROCESSED[$libname]:-}" ]] && continue

            if [[ ! -f "$dep" ]]; then
                echo "  WARNING: $dep not found, skipping"
                continue
            fi

            echo "  Bundling $libname"
            cp "$dep" "$dst"
            chmod 755 "$dst"
            PROCESSED[$libname]=1
            install_name_tool -id "@executable_path/../Frameworks/$libname" "$dst"
            collect_and_copy "$dst"
        done <<< "$deps"
    }

    # Seed an optional, dlopen'd dylib into the bundle (with its dependency tree).
    # Arg: path to a dylib (possibly a symlink, e.g. /usr/local/lib/libiio.dylib).
    seed_optional_lib() {
        local link="$1"

        # Follow the symlink chain to the real file.
        local real
        real="$(cd "$(dirname "$link")" && pwd)/$(basename "$link")"
        while [[ -L "$real" ]]; do
            local target
            target="$(readlink "$real")"
            [[ "$target" == /* ]] || target="$(dirname "$real")/$target"
            real="$target"
        done
        [[ -f "$real" ]] || { echo "  (optional $(basename "$link") not found)"; return; }

        # Bundle the real file under the basename advertised by its install id,
        # so dependents referencing that name (and our alias symlinks) resolve.
        local idname
        idname="$(basename "$(otool -D "$real" | tail -n +2 | head -1)")"
        [[ -n "$idname" ]] || idname="$(basename "$real")"
        local dst="$FWK_DIR/$idname"

        if [[ -z "${PROCESSED[$idname]:-}" ]]; then
            echo "  Bundling optional $idname"
            cp "$real" "$dst"
            chmod 755 "$dst"
            PROCESSED[$idname]=1
            install_name_tool -id "@executable_path/../Frameworks/$idname" "$dst"
            collect_and_copy "$dst"
        fi

        # Recreate the symlink aliases (libfoo.dylib, libfoo.N.dylib) alongside
        # it, so a dlopen() by any of those names finds the bundled copy.
        local srcdir base an
        srcdir="$(dirname "$link")"
        base="${idname%%.*}"
        for alias in "$srcdir/$base".dylib "$srcdir/$base".*.dylib; do
            [[ -e "$alias" ]] || continue
            an="$(basename "$alias")"
            [[ "$an" == "$idname" ]] && continue
            [[ -e "$FWK_DIR/$an" ]] && continue
            ln -s "$idname" "$FWK_DIR/$an"
        done
    }

    echo "  Scanning dependencies..."
    collect_and_copy "$EXEC_PATH"

    echo "  Seeding optional dynamically-loaded libs..."
    for optname in "${OPTIONAL_LIB_NAMES[@]}"; do
        for optdir in "${OPTIONAL_LIB_DIRS[@]}"; do
            if [[ -e "$optdir/$optname.dylib" ]]; then
                seed_optional_lib "$optdir/$optname.dylib"
                break
            fi
        done
    done

    while IFS= read -r existing; do
        local libname
        libname="$(basename "$existing")"
        [[ -n "${PROCESSED[$libname]:-}" ]] && continue
        echo "  Re-scanning bundled $libname"
        PROCESSED[$libname]=1
        collect_and_copy "$existing"
    done < <(find "$FWK_DIR" -name "*.dylib" -type f)

    # Verify
    local BAD=0
    while IFS= read -r binary; do
        while IFS= read -r dep; do
            [[ -z "$dep" ]] && continue
            is_system_lib "$dep" && continue
            echo "  UNRESOLVED in $(basename "$binary"): $dep"
            BAD=1
        done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}')
    done < <(find "$OUT_APP" -type f \( -name "*.dylib" -o -perm +0111 \))
    [[ $BAD -ne 0 ]] && echo "  WARNING: some dependencies are still unresolved."

    # Sign
    echo "  Signing (ad-hoc)..."
    find "$FWK_DIR" -name "*.dylib" -type f | while IFS= read -r lib; do
        codesign --sign - --force "$lib" 2>/dev/null || true
    done
    codesign --sign - --force --deep "$OUT_APP"

    echo "  Done: $OUT_APP"
    echo ""
}

# ── EWSDR.app ─────────────────────────────────────────────────────────────────
echo "Source:  $SRC_APP  ($SRC_BIN)"
echo ""

build_bundle "$OUT_DIR/EWSDR.app" "EWSDR" "com.company.ewsdr"

# ── EWSDR_opengl.app ──────────────────────────────────────────────────────────
OPENGL_APP="$OUT_DIR/EWSDR_opengl.app"
echo "==> Building $OPENGL_APP"

rm -rf "$OPENGL_APP"
cp -R "$OUT_DIR/EWSDR.app" "$OPENGL_APP"

# Update metadata
/usr/libexec/PlistBuddy -c "Set :CFBundleName EWSDR OpenGL" \
    "$OPENGL_APP/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.company.ewsdr-opengl" \
    "$OPENGL_APP/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable EWSDR_opengl" \
    "$OPENGL_APP/Contents/Info.plist"

# Wrapper script that passes --opengl to the real binary
cat > "$OPENGL_APP/Contents/MacOS/EWSDR_opengl" << EOF
#!/usr/bin/env bash
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
exec "\$DIR/$SRC_EXEC_NAME" --opengl "\$@"
EOF
chmod +x "$OPENGL_APP/Contents/MacOS/EWSDR_opengl"

# Re-sign with updated contents
echo "  Signing (ad-hoc)..."
find "$OPENGL_APP/Contents/Frameworks" -name "*.dylib" -type f | while IFS= read -r lib; do
    codesign --sign - --force "$lib" 2>/dev/null || true
done
codesign --sign - --force --deep "$OPENGL_APP"

echo "  Done: $OPENGL_APP"
echo ""
echo "All bundles ready in $OUT_DIR/"

# ── Clean up dist artifacts that are not part of the bundles ─────────────────
rm -f "$ICNS_PATH"
find "$OUT_DIR" -maxdepth 1 -name ".DS_Store" -delete
