#!/usr/bin/env bash
# ============================================================================
# retroDE_ps2 — Ch152 Quartus build driver for the DE25-Nano
# top_psmct32_raster_demo board top.
#
# Asks the toolchain the honest question: "does this board top
# synthesize, fit, and pass static timing analysis?" Captures resource
# usage, warning summaries, and unconstrained-path reports so a future
# audit can compare across chapters.
#
# Usage (from repo root):
#   ./synth/de25_nano/top_psmct32_raster_demo/build_quartus.sh           # default flow
#   ./synth/de25_nano/top_psmct32_raster_demo/build_quartus.sh clean     # wipe outputs first
#   ./synth/de25_nano/top_psmct32_raster_demo/build_quartus.sh syn-only  # synthesis only (fast)
#
# Convenience: `make -C sim quartus_compile` wraps this.
#
# What this script DOES:
#   - Resolves Quartus binaries via QUARTUS_ROOTDIR or PATH.
#   - Re-bakes the .mem fixtures via the existing make target so
#     $readmemh resolves at synth time.
#   - Runs quartus_syn (synthesis) → quartus_fit (placement+routing) →
#     quartus_sta (timing analysis) → quartus_asm (bitstream). The
#     final step is gated on a clean STA so a .sof is produced only
#     when timing actually closes (Ch160).
#   - Captures stdout/stderr per step into output_files/ for audit.
#
# What this script does NOT do:
#   - Define USE_PLL_IP — keeps the Ch151 self-contained stub path
#     under test, per Codex's Ch152 framing.
# ============================================================================

set -euo pipefail

PROJECT=de25_nano_psmct32_raster_demo_top

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
QSF_DIR="$SCRIPT_DIR"

# Quartus binaries.
if [ -n "${QUARTUS_ROOTDIR:-}" ]; then
    QUARTUS="$QUARTUS_ROOTDIR/bin"
elif command -v quartus_sh >/dev/null 2>&1; then
    QUARTUS="$(dirname "$(command -v quartus_sh)")"
else
    echo "error: Quartus not found. Set QUARTUS_ROOTDIR or add quartus to PATH." >&2
    exit 1
fi

MODE="${1:-default}"

# All Quartus output goes here (under the project dir).
OUT_DIR="$QSF_DIR/output_files"
LOG_DIR="$OUT_DIR/build_logs"

if [ "$MODE" = "clean" ]; then
    echo "=== Cleaning output_files / db / incremental_db ==="
    rm -rf "$OUT_DIR" "$QSF_DIR/db" "$QSF_DIR/incremental_db" \
           "$QSF_DIR/qdb" "$QSF_DIR/synthesis"
    shift
    MODE="${1:-default}"
fi

mkdir -p "$LOG_DIR"

# Re-bake .mem fixtures so $readmemh resolves at elaboration.
echo ""
echo "=== Re-baking .mem fixtures ==="
make -C "$REPO_ROOT/sim" --no-print-directory top_psmct32_raster_demo_mem

# The QSF deliberately mixes project-relative sources (`rtl/`, `sim/`,
# `qsys/`, `ip/`) with a sibling-repository source path
# (`../../../../retroDE_splash`).  Run Quartus from the QSF directory so
# both path families retain their authored meaning.  A former nested
# `work/` directory shifted the sibling path by one level and also omitted
# qsys/, making otherwise valid source trees appear missing during
# elaboration.
WORK_DIR="$QSF_DIR"

for required_path in rtl sim qsys ip; do
    if [ ! -e "$WORK_DIR/$required_path" ]; then
        echo "error: required Quartus source path missing: $WORK_DIR/$required_path" >&2
        exit 1
    fi
done

if [ ! -d "$REPO_ROOT/../retroDE_splash" ]; then
    echo "error: sibling retroDE_splash checkout is required by the QSF" >&2
    exit 1
fi

cd "$WORK_DIR"

run_step() {
    local step="$1"
    local cmd="$2"
    local log="$LOG_DIR/${step}.log"
    echo ""
    echo "=== [$step] ==="
    # Run; tee output to log; capture exit status. We do NOT want
    # `set -e` to abort here — the chapter contract is to capture a
    # complete report even when a step errored, and the script's
    # exit code (computed at the bottom from SYN/FIT/STA together)
    # is what the caller (make / CI) sees.
    set +e
    eval "$cmd" 2>&1 | tee "$log"
    local rc=${PIPESTATUS[0]}
    set -e
    if [ $rc -ne 0 ]; then
        echo ""
        echo "FAIL: $step exited $rc — see $log"
        echo ""
    fi
    return $rc
}

# Each step's rc is captured via `if run_step ...; then RC=0; else
# RC=$?; fi`. The earlier idiom (`run_step ... || true; RC=$?`)
# silently masked tool failures because `$?` then captured `true`'s
# exit (always 0). The `if ...; then` form is the correct
# `set -e`-friendly capture: the exit status of `run_step` is what
# `else $?` sees, and the `if` block keeps `set -e` from aborting
# the script when a step fails (Ch160 audit High fix).
if run_step "quartus_syn" \
       "$QUARTUS/quartus_syn --read_settings_files=on --write_settings_files=off $PROJECT -c $PROJECT"
then
    SYN_RC=0
else
    SYN_RC=$?
fi

# 99 sentinel = "step skipped (upstream failed or syn-only mode)".
if [ "$MODE" = "syn-only" ]; then
    FIT_RC=99
    STA_RC=99
    ASM_RC=99
elif [ $SYN_RC -ne 0 ]; then
    # syn failed — don't bother with downstream steps.
    FIT_RC=99
    STA_RC=99
    ASM_RC=99
else
    if run_step "quartus_fit" \
           "$QUARTUS/quartus_fit --read_settings_files=on --write_settings_files=off $PROJECT -c $PROJECT"
    then
        FIT_RC=0
    else
        FIT_RC=$?
    fi

    if [ $FIT_RC -eq 0 ]; then
        if run_step "quartus_sta" \
               "$QUARTUS/quartus_sta $PROJECT -c $PROJECT"
        then
            STA_RC=0
        else
            STA_RC=$?
        fi

        # Ch161 audit High #2 fix — quartus_sta exits 0 even when
        # individual timing classes (Setup / Hold / Recovery /
        # Removal / Min Pulse Width) report negative slack. Without
        # parsing the summary file, a "tool rc=0" verdict could let
        # quartus_asm run on a design with a real timing violation
        # and produce a .sof under a failing timing class. Inspect
        # the .sta.summary, fail the script if any "Slack :" line
        # is negative.
        if [ $STA_RC -eq 0 ]; then
            STA_SUMMARY="$WORK_DIR/output_files/$PROJECT.sta.summary"
            if [ -f "$STA_SUMMARY" ]; then
                NEG_SLACK_LINES=$(awk '/^Slack[[:space:]]*:/ {
                    # strip "Slack : " prefix and any trailing fields.
                    sub(/^Slack[[:space:]]*:[[:space:]]*/, "", $0);
                    # first whitespace-separated token is the slack value.
                    s=$1; if (s+0 < 0) print s
                }' "$STA_SUMMARY")
                if [ -n "$NEG_SLACK_LINES" ]; then
                    echo ""
                    echo "FAIL: quartus_sta tool exit was 0 but the .sta.summary"
                    echo "      reports negative slack(s): $NEG_SLACK_LINES"
                    echo "      (build_quartus.sh promotes this to a build failure)"
                    echo ""
                    STA_RC=2
                fi
            else
                # Ch161 audit Medium fix — a missing .sta.summary
                # disables the slack-parse gate, which is functionally
                # the same hole as "tool rc=0 but timing failed". A
                # report-path or tool-output change must not be
                # allowed to bypass the gate by deleting the artifact
                # we parse. Promote to a build failure with the same
                # STA_RC=2 used for the negative-slack case.
                echo ""
                echo "FAIL: quartus_sta tool exit was 0 but $STA_SUMMARY is missing —"
                echo "      the slack-parse gate cannot run. Promoting to STA_RC=2."
                echo ""
                STA_RC=2
            fi
        fi
    else
        STA_RC=99
    fi

    # Ch160 — bitstream generation, gated on a clean STA so a .sof
    # is produced only when timing actually closes. Ch161 audit High
    # #2 strengthens "clean STA" to also require all summary slacks
    # to be non-negative (see the .sta.summary parse above).
    if [ $FIT_RC -eq 0 ] && [ $STA_RC -eq 0 ]; then
        if run_step "quartus_asm" \
               "$QUARTUS/quartus_asm $PROJECT -c $PROJECT"
        then
            # Direct quartus_asm invocation does not execute the QSF
            # POST_FLOW_SCRIPT_FILE hook (that hook is normally dispatched by
            # quartus_sh --flow compile).  Run it explicitly so a successful
            # command-line build always produces the loader-ready split RBF,
            # not merely the .sof.
            if run_step "post_flow" \
                   "$QUARTUS/quartus_sh -t post_flow.tcl quartus_asm $PROJECT $PROJECT" && \
               [ -s "$OUT_DIR/retroDE_ps2.core.rbf" ]
            then
                ASM_RC=0
            else
                ASM_RC=2
                echo "FAIL: assembler completed but loader-ready retroDE_ps2.core.rbf was not produced"
            fi
        else
            ASM_RC=$?
        fi
    else
        ASM_RC=99
    fi
fi

# Move output_files from a distinct work dir into the canonical location.
if [ "$WORK_DIR" != "$QSF_DIR" ] && [ -d "$WORK_DIR/output_files" ]; then
    rsync -a --remove-source-files "$WORK_DIR/output_files/" "$OUT_DIR/" 2>/dev/null || \
        cp -a "$WORK_DIR/output_files/." "$OUT_DIR/"
fi

echo ""
echo "================================================================"
echo " Compile summary"
echo "================================================================"
echo "  quartus_syn : rc=$SYN_RC"
echo "  quartus_fit : rc=$FIT_RC  (99 = skipped)"
echo "  quartus_sta : rc=$STA_RC  (99 = skipped)"
echo "  quartus_asm : rc=${ASM_RC:-99}  (99 = skipped)"
echo "  logs in     : $LOG_DIR"
echo "  outputs in  : $OUT_DIR"
echo "================================================================"

# Final exit code:
#   syn-only mode  → exit SYN_RC (only the syn step is meaningful).
#   default mode   → exit the FIRST nonzero of (SYN, FIT, STA, ASM);
#                    99 sentinels are skipped-step markers and only
#                    propagate when the upstream step actually
#                    failed (so default mode never exits 99). Ch160
#                    audit Medium fix: ASM is now part of the
#                    deliverable so its rc participates here.
if [ "$MODE" = "syn-only" ]; then
    exit $SYN_RC
fi
if [ $SYN_RC -ne 0 ]; then
    exit $SYN_RC
fi
if [ $FIT_RC -ne 0 ] && [ $FIT_RC -ne 99 ]; then
    exit $FIT_RC
fi
if [ $STA_RC -ne 0 ] && [ $STA_RC -ne 99 ]; then
    exit $STA_RC
fi
if [ $ASM_RC -ne 0 ] && [ $ASM_RC -ne 99 ]; then
    exit $ASM_RC
fi
exit 0
