#!/bin/bash
#
# RoboFusion: build Skybrush ArduCopter with the NAV-PVT GPS-week patch
# (MicoAir MA-RTK-AIR-626 / UM626N outputs only UBX NAV-PVT) for any board.
#
# Runs on Ubuntu 22.04 (native or WSL2) with the ArduPilot build prerequisites installed.
# Example:
#   ./build_626_firmware.sh CMCopter-4.6 MicoAir743v2 CubeOrange
#
# Usage:
#   build_626_firmware.sh [-j JOBS] <CMCopter-4.6|CMCopter-4.7> <waf-board> [<waf-board> ...]
#
# How it works
#   * The patched source trees are never touched. Per branch a dedicated git
#     worktree (~/rf-build/<branch>) is created from the same repository, set
#     to the source tree's HEAD commit, and the source tree's uncommitted
#     libraries/AP_GPS diff (the patch state) is applied to it:
#         CMCopter-4.6 -> ~/skybrush-ardupilot      (patch applied)
#         CMCopter-4.7 -> ~/skybrush-ardupilot-47   (hand-ported patch)
#   * Per board: ./waf configure --board <b> && ./waf copter, exactly like
#     Skybrush's buildall.sh (including its "git restore Tools/IO_Firmware/*.bin").
#     No extra options. speedybeef4v3 needs no special handling here: buildall.sh
#     has none, the OSD is already disabled in Skybrush's own hwdef
#     (libraries/AP_HAL_ChibiOS/hwdef/speedybeef4v3, minimize_fpv_osd.inc).
#   * -Werror is never disabled and CFLAGS/CXXFLAGS/LDFLAGS are cleared.
#   * A board that fails to configure/build/fit flash, or whose image lacks the
#     "from NAV-PVT date" string, is reported as FAIL (never skipped silently);
#     the script exits non-zero if any board failed.
#
# Output (OUT_ROOT defaults to builder/out):
#   out/<branch>/RoboFusion-Skybrush<ver>-PVTweek-<board>.apj
#   out/<branch>/RoboFusion-Skybrush<ver>-PVTweek-<board>_with_bl.hex
#   out/<branch>/SHA256SUMS.txt, BUILD_INFO.md, logs/<board>.log
# Running a subset of boards updates only those boards; BUILD_INFO.md and
# SHA256SUMS.txt are regenerated from the per-board results in out/<branch>/.results.
#
# Disk safety: each board's build/<board> directory is deleted after its outputs are
# copied and verified (the whole build/ dir of the worktree is wiped, one board at a time,
# because waf aborts if the dir of an earlier configured variant disappears; KEEP_BUILD=1 keeps it), and the run stops before the next board
# if the filesystem holding OUT_ROOT has less than MIN_FREE_GB free (default 5);
# boards not reached are recorded as NOT-RUN and make the script exit non-zero.
#
# Environment overrides: SRC_TREE, WT_DIR, OUT_ROOT, JOBS, KEEP_BUILD, MIN_FREE_GB.

set -u
set -o pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT_ROOT="${OUT_ROOT:-$SCRIPT_DIR/out}"
JOBS="${JOBS:-}"
KEEP_BUILD="${KEEP_BUILD:-0}"
MIN_FREE_GB="${MIN_FREE_GB:-5}"
MARKER="from NAV-PVT date"
REPO_URL="https://github.com/skybrush-io/ardupilot"

die() { echo "ERROR: $*" >&2; exit 2; }

while getopts "j:h" opt; do
    case "$opt" in
        j) JOBS="$OPTARG" ;;
        h|*) sed -n '2,/^set -u/p' "$0" | sed '$d' | sed 's/^# \{0,1\}//'; exit 1 ;;
    esac
done
shift $((OPTIND - 1))
[ $# -ge 2 ] || die "need a branch and at least one board. Usage: $0 [-j N] <CMCopter-4.6|CMCopter-4.7> <board>..."

BRANCH="$1"; shift
BOARDS=("$@")

case "$BRANCH" in
    CMCopter-4.6) VER=4.6; DEF_SRC="$HOME/skybrush-ardupilot" ;;
    CMCopter-4.7) VER=4.7; DEF_SRC="$HOME/skybrush-ardupilot-47" ;;
    *) die "unsupported branch '$BRANCH' (use CMCopter-4.6 or CMCopter-4.7)" ;;
esac
SRC="${SRC_TREE:-$DEF_SRC}"
WT="${WT_DIR:-$HOME/rf-build/$BRANCH}"
OUT="$OUT_ROOT/$BRANCH"
[ -n "$JOBS" ] || JOBS="$(nproc)"

[ -d "$SRC" ] || die "source tree $SRC not found"
command -v arm-none-eabi-gcc >/dev/null || die "arm-none-eabi-gcc not in PATH (run through 'bash -l')"
for b in "${BOARDS[@]}"; do
    [[ "$b" =~ ^[A-Za-z0-9._+-]+$ ]] || die "suspicious board name '$b'"
done

unset CFLAGS CXXFLAGS LDFLAGS CPPFLAGS
export PYTHONDONTWRITEBYTECODE=1

# ---------------------------------------------------------------- source/patch
COMMIT="$(git -C "$SRC" rev-parse HEAD)" || die "cannot read HEAD of $SRC"
COMMIT_SUBJECT="$(git -C "$SRC" log -1 --format=%s HEAD)"
COMMIT_DATE="$(git -C "$SRC" log -1 --format=%ci HEAD)"
TIP="$(git -C "$SRC" rev-parse -q --verify "refs/heads/$BRANCH" 2>/dev/null || git -C "$SRC" rev-parse -q --verify "refs/remotes/origin/$BRANCH" 2>/dev/null || true)"
if [ -z "$TIP" ]; then TIPNOTE="branch ref not found locally (HEAD detached at the commit above)"
elif [ "$TIP" = "$COMMIT" ]; then TIPNOTE="HEAD is the tip of $BRANCH in the local clone"
else TIPNOTE="WARNING: HEAD differs from the local $BRANCH ref ($TIP)"; fi

CHANGED="$(git -C "$SRC" diff HEAD --name-only)"
[ -n "$CHANGED" ] || die "$SRC has no uncommitted patch (git diff HEAD is empty)"
if echo "$CHANGED" | grep -qv '^libraries/AP_GPS/'; then
    die "$SRC has uncommitted changes outside libraries/AP_GPS/: $(echo $CHANGED); refusing"
fi
mkdir -p "$OUT/logs" "$OUT/.results"
PATCH_FILE="$OUT/applied-patch.diff"
git -C "$SRC" diff HEAD --binary > "$PATCH_FILE"
grep -q "$MARKER" "$PATCH_FILE" || die "patch in $SRC does not contain '$MARKER'"
PATCH_SHA="$(sha256sum "$PATCH_FILE" | cut -d' ' -f1)"

# ----------------------------------------------------------------- worktree
echo "== Preparing worktree $WT at $COMMIT"
if [ -e "$WT/.git" ]; then
    git -C "$WT" checkout -q --detach -f "$COMMIT" || die "cannot check out $COMMIT in $WT"
    git -C "$WT" reset -q --hard "$COMMIT"
else
    [ ! -e "$WT" ] || die "$WT exists but is not a git worktree"
    mkdir -p "$(dirname "$WT")"
    git -C "$SRC" worktree add -q --detach "$WT" "$COMMIT" || die "git worktree add failed"
fi
git -C "$WT" -c protocol.file.allow=always submodule update --init --recursive --reference "$SRC" \
    > "$OUT/logs/_worktree-submodules.log" 2>&1 || die "submodule update failed, see $OUT/logs/_worktree-submodules.log"
git -C "$WT" apply --binary "$PATCH_FILE" || die "patch does not apply to $WT"
[ "$(git -C "$WT" diff HEAD --binary | sha256sum | cut -d' ' -f1)" = "$PATCH_SHA" ] \
    || die "worktree diff differs from the source tree patch"

GCC_VER="$(arm-none-eabi-gcc --version | head -1)"
PY_VER="$(python3 --version 2>&1)"
LIBSKY="$(git -C "$WT" submodule status modules/libskybrush | awk '{print $1}' | tr -d '+-')"
CHIBI="$(git -C "$WT" submodule status modules/ChibiOS | awk '{print $1}' | tr -d '+-')"
FWVER="$(sed -n 's/^#define FIRMWARE_VERSION \(.*\)/\1/p' "$WT/ArduCopter/version.h" | head -1)"

# ------------------------------------------------------------------- builds
fail=0
cd "$WT" || die "cannot cd to $WT"
stopped=""
for b in "${BOARDS[@]}"; do
    LOG="$OUT/logs/$b.log"
    RES="$OUT/.results/$b.env"
    NAME="RoboFusion-Skybrush${VER}-PVTweek-${b}"
    freegb="$(df -BG --output=avail "$OUT" | tail -1 | tr -dc '0-9')"
    if [ -n "$stopped" ] || [ "${freegb:-0}" -lt "$MIN_FREE_GB" ]; then
        [ -n "$stopped" ] || { stopped="only ${freegb:-?} GB free on the output filesystem (< ${MIN_FREE_GB} GB)"; echo "!! STOP: $stopped" >&2; }
        rm -f "$OUT/$NAME.apj" "$OUT/${NAME}_with_bl.hex"
        {
            printf 'R_result=%q\nR_reason=%q\nR_boardid=\nR_used=\nR_total=\nR_free=\nR_textdata=\nR_string=\n' "NOT-RUN" "stopped before building: $stopped"
            printf 'R_secs=0\nR_commit=%q\nR_warn_all=0\nR_warn_ap=0\nR_hexnote=\n' "$COMMIT"
        } > "$RES"
        fail=$((fail + 1)); continue
    fi
    echo "== [$BRANCH] building $b (log: $LOG)"
    rm -f "$OUT/$NAME.apj" "$OUT/${NAME}_with_bl.hex" "$RES"
    t0=$(date +%s)
    result=OK; reason=""; boardid=""; used=""; total=""; free=""; textdata=""; strline=""; hexnote=""
    # waf remembers every configured variant in build/c4che and aborts if the build dir of an
    # earlier one is gone, so wipe the whole build/ dir (one board at a time) instead of build/$b.
    rm -rf build
    git restore Tools/IO_Firmware/*.bin 2>/dev/null   # same as buildall.sh
    if ! ./waf configure --board="$b" > "$LOG" 2>&1; then
        result=FAIL; reason="waf configure failed: $(grep -E 'error|ERROR|not found|unknown' "$LOG" | tail -2 | tr '\n' ' ')"
    elif ! ./waf -j"$JOBS" copter >> "$LOG" 2>&1; then
        result=FAIL
        reason="waf copter failed: $(grep -E 'error:|overflowed|region .* full|Error' "$LOG" | head -3 | tr '\n' ' ')"
        grep -qE 'overflowed|region .* full' "$LOG" && reason="does not fit flash; $reason"
    elif [ ! -f "build/$b/bin/arducopter.apj" ] || [ ! -f "build/$b/bin/arducopter.bin" ]; then
        result=FAIL; reason="build finished but arducopter.apj/.bin missing"
    else
        strline="$(strings -a "build/$b/bin/arducopter.bin" | grep -m1 "$MARKER")"
        if [ -z "$strline" ]; then
            result=FAIL; reason="string '$MARKER' not found in arducopter.bin (patch not in image)"
        fi
        read -r boardid used total free < <(python3 -I - "build/$b/bin/arducopter.apj" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
print(d.get("board_id", "?"), d.get("image_size", "?"), d.get("flash_total", "?"), d.get("flash_free", "?"))
PY
)
        textdata="$(arm-none-eabi-size "build/$b/bin/arducopter" | awk 'NR==2{print $1"+"$2}')"
        if [ "$result" = OK ] && [[ "$free" =~ ^-?[0-9]+$ ]] && [ "$free" -lt 0 ]; then
            result=FAIL; reason="does not fit flash (free flash $free B)"
        fi
        if [ "$result" = OK ]; then
            cp "build/$b/bin/arducopter.apj" "$OUT/$NAME.apj" && cmp -s "build/$b/bin/arducopter.apj" "$OUT/$NAME.apj"                 || { result=FAIL; reason="copying .apj to the output folder failed or is incomplete"; rm -f "$OUT/$NAME.apj"; }
            if [ "$result" != OK ]; then :
            elif [ -f "build/$b/bin/arducopter_with_bl.hex" ]; then
                cp "build/$b/bin/arducopter_with_bl.hex" "$OUT/${NAME}_with_bl.hex" && cmp -s "build/$b/bin/arducopter_with_bl.hex" "$OUT/${NAME}_with_bl.hex"                     || { result=FAIL; reason="copying _with_bl.hex to the output folder failed or is incomplete"; rm -f "$OUT/$NAME.apj" "$OUT/${NAME}_with_bl.hex"; }
            else
                hexnote="no arducopter_with_bl.hex produced (no bootloader for this board in the tree)"
            fi
        fi
    fi
    [ "$KEEP_BUILD" = 1 ] || rm -rf build
    secs=$(( $(date +%s) - t0 ))
    warn_all=$(grep -c 'warning:' "$LOG"); warn_ap=$(grep 'warning:' "$LOG" | grep -vc 'modules/')
    [ "$result" = OK ] || fail=$((fail + 1))
    {
        printf 'R_result=%q\nR_reason=%q\nR_boardid=%q\nR_used=%q\nR_total=%q\nR_free=%q\n' "$result" "$reason" "$boardid" "$used" "$total" "$free"
        printf 'R_textdata=%q\nR_string=%q\nR_secs=%q\nR_commit=%q\nR_warn_all=%q\nR_warn_ap=%q\nR_hexnote=%q\n' "$textdata" "$strline" "$secs" "$COMMIT" "$warn_all" "$warn_ap" "$hexnote"
    } > "$RES"
    echo "   -> $result ${reason} (${secs}s, free flash: ${free:-n/a})"
done

# ----------------------------------------------------- SHA256SUMS + BUILD_INFO
cd "$OUT" || die "cannot cd to $OUT"
ls RoboFusion-Skybrush"${VER}"-PVTweek-*.apj RoboFusion-Skybrush"${VER}"-PVTweek-*_with_bl.hex 2>/dev/null | sort | xargs -r sha256sum > SHA256SUMS.txt || echo "ERROR: could not write SHA256SUMS.txt" >&2
[ -s SHA256SUMS.txt ] && sha256sum -c --quiet SHA256SUMS.txt || { echo "ERROR: SHA256SUMS.txt empty or does not verify" >&2; fail=$((fail + 1)); }

{
    echo "# BUILD_INFO - RoboFusion Skybrush $VER + NAV-PVT GPS-week patch"
    echo
    echo "Generated $(date -u '+%Y-%m-%d %H:%M UTC') by \`builder/build_626_firmware.sh\` (WSL $(. /etc/os-release; echo "$PRETTY_NAME"))."
    echo "Per-board rows come from the latest build of that board; the \`Commit\` column shows which source it was built from."
    echo
    echo "## Source"
    echo
    echo "| Item | Value |"
    echo "|---|---|"
    echo "| Repository | $REPO_URL |"
    echo "| Branch | \`$BRANCH\` |"
    echo "| Commit (latest run) | \`$COMMIT\` |"
    echo "| Commit subject / date | $COMMIT_SUBJECT, $COMMIT_DATE |"
    echo "| Branch check | $TIPNOTE |"
    echo "| Firmware version (version.h) | \`$FWVER\` |"
    echo "| libskybrush / ChibiOS | \`$LIBSKY\` / \`$CHIBI\` |"
    echo "| Patch | libraries/AP_GPS only: $(echo $CHANGED | sed 's/ /, /g'); applied-patch.diff SHA256 \`$PATCH_SHA\` |"
    echo "| Build tree | dedicated worktree \`$WT\` (source tree \`$SRC\` untouched) |"
    echo
    echo "## Toolchain"
    echo
    echo "- Compiler: \`$GCC_VER\`"
    echo "- Python: \`$PY_VER\`"
    echo "- Commands per board: \`./waf configure --board=<board> && ./waf -j$JOBS copter\`. \`-Werror\` untouched; CFLAGS/CXXFLAGS/LDFLAGS cleared."
    echo "- speedybeef4v3: no extra options (Skybrush's buildall.sh has none; OSD is already disabled in its hwdef)."
    echo
    echo "## Results"
    echo
    echo "| Board | Result | Board ID | Flash used (image) | Flash total | Free flash | text+data | NAV-PVT string | Warnings (all/non-modules) | Time | Commit |"
    echo "|---|---|---|---|---|---|---|---|---|---|---|"
    for f in $(ls .results/*.env 2>/dev/null | sort); do
        b="$(basename "$f" .env)"
        # shellcheck disable=SC1090
        . "$f"
        s="present"; [ -n "$R_string" ] || s="MISSING"
        echo "| \`$b\` | **$R_result** | $R_boardid | $R_used | $R_total | $R_free | $R_textdata | $s | $R_warn_all / $R_warn_ap | ${R_secs}s | \`${R_commit:0:10}\` |"
    done
    echo
    echo "Flash numbers are from the \`.apj\` header (bytes). A board with free flash < 0 or a link error is reported as FAIL."
    echo
    nfail=0
    for f in $(ls .results/*.env 2>/dev/null | sort); do
        . "$f"; b="$(basename "$f" .env)"
        if [ "$R_result" != OK ]; then
            [ "$nfail" -eq 0 ] && { echo "## FAILED / NOT-RUN boards"; echo; }
            nfail=$((nfail + 1))
            echo "- \`$b\`: ${R_reason//|/\\|} (see \`logs/$b.log\`)"
        fi
    done
    [ "$nfail" -eq 0 ] && echo "No failed boards."
    echo
    notes=0
    for f in $(ls .results/*.env 2>/dev/null | sort); do
        . "$f"; b="$(basename "$f" .env)"
        if [ -n "$R_hexnote" ]; then
            [ "$notes" -eq 0 ] && { echo "## Notes"; echo; }
            notes=$((notes + 1)); echo "- \`$b\`: $R_hexnote"
        fi
    done
    echo
    echo "## SHA256"
    echo
    echo '```'
    cat SHA256SUMS.txt
    echo '```'
    echo
    echo "Verify with \`sha256sum -c SHA256SUMS.txt\`."
} > BUILD_INFO.md

echo
echo "== Summary [$BRANCH] commit ${COMMIT:0:10}"
for b in "${BOARDS[@]}"; do
    . "$OUT/.results/$b.env"
    printf '   %-20s %-5s free=%s %s\n' "$b" "$R_result" "$R_free" "$R_reason"
done
echo "Output: $OUT"
[ "$fail" -eq 0 ] || { echo "$fail board(s) FAILED" >&2; exit 1; }
exit 0
