#!/bin/bash
#
# runformat - format this project's C++ sources with Uncrustify.
#
# Usage:
#   ./runformat                                # format using the configured Uncrustify
#   ./runformat --install                      # download, build, and use Uncrustify locally
#   ./runformat --install --install-dir /abs/path
#   ./runformat --expected-uncrustify-version  # print ONLY the expected Uncrustify version
#
# You may also set:
#   UNCRUSTIFY=/abs/path/to/uncrustify          # use a specific binary
#   UNCRUSTIFY_INSTALL_DIR=/abs/path            # where `--install` will install
#
# Requirements:
#   - All developers must use the *exact* same Uncrustify version to avoid format churn.
#   - Either:
#       * Have `uncrustify` in PATH, or
#       * Set env var UNCRUSTIFY=/absolute/path/to/uncrustify, or
#       * Run `./runformat --install` to fetch & build the pinned version locally.
#
# Notes:
#   - The local install lives under: ./.runformat-uncrustify/uncrustify-<version>-install
#   - The config file is expected at: ./.uncrustify.cfg
#

set -euo pipefail

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

UNCRUSTIFY_VERSION="0.80.1"
UNCRUSTIFY_HASH="6bf662e05c4140dd4df5e45d6690cad96b4ef23c293b85813f5c725bbf1894d0"

UNCRUSTIFY_WORK_DIR="${SCRIPT_DIR}/.runformat-uncrustify"

# Allow external install dir override (arg or env). If not set, default under work dir.
DEFAULT_INSTALL_DIR="${UNCRUSTIFY_WORK_DIR}/uncrustify-${UNCRUSTIFY_VERSION}-install"
UNCRUSTIFY_INSTALL_DIR="${UNCRUSTIFY_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
UNCRUSTIFY_BIN="${UNCRUSTIFY_INSTALL_DIR}/bin/uncrustify"

# Allow override via env; default to local pinned build path.
UNCRUSTIFY="${UNCRUSTIFY:-$UNCRUSTIFY_BIN}"
UNCRUSTIFY_CONFIG="${SCRIPT_DIR}/.uncrustify.cfg"

err() { echo -e >&2 "ERROR: $@\n"; }
die() { err $@; exit 1; }

install_uncrustify() {
  local root="uncrustify-${UNCRUSTIFY_VERSION}"
  local file="${root}.tar.gz"
  local url="https://github.com/uncrustify/uncrustify/releases/download/${root}/${file}"

  mkdir -p "${UNCRUSTIFY_WORK_DIR}"

  echo "Downloading ${file}..."
  curl -fsSL -o "${UNCRUSTIFY_WORK_DIR}/${file}" "${url}"

  (
    cd "${UNCRUSTIFY_WORK_DIR}"

    echo "${UNCRUSTIFY_HASH}  ${file}" > "${file}.sha256"
    sha256sum -c "${file}.sha256"
    rm -f "${file}.sha256"

    command -v cmake >/dev/null 2>&1 || die "cmake executable not found."

    echo "Extracting archive..."
    rm -rf "${root}" "${root}-build"
    mkdir -p "${root}"
    tar -xzf "${file}" --strip-components=1 -C "${root}"

    echo "Configuring (prefix: ${UNCRUSTIFY_INSTALL_DIR})..."
    cmake \
      -DCMAKE_BUILD_TYPE:STRING=Release \
      -DCMAKE_INSTALL_PREFIX:PATH="${UNCRUSTIFY_INSTALL_DIR}" \
      -S "${root}" -B "${root}-build"

    echo "Building & installing..."
    cmake --build "${root}-build" --config Release --target install --parallel
  )

  echo "Installed Uncrustify to: ${UNCRUSTIFY_INSTALL_DIR}"
}

print_usage_and_exit() {
  sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'
  exit 0
}

# Print ONLY expected Uncrustify version (no extra text).
print_expected_uncrustify_version_and_exit() {
  printf '%s\n' "$UNCRUSTIFY_VERSION"
  exit 0
}

# --------------------------
# Argument parsing
# --------------------------
DO_INSTALL=0
PRINT_EXPECTED_UNCRUSTIFY_VERSION=0
# Accept: --install, --install-dir <dir>, -h/--help
while [[ $# -gt 0 ]]; do
  case "$1" in
    -h|--help)
      print_usage_and_exit
      ;;
    --install)
      DO_INSTALL=1
      shift
      ;;
    --install-dir)
      [[ $# -ge 2 ]] || die "$1 requires a path argument"
      UNCRUSTIFY_INSTALL_DIR="$(readlink -m "$2" 2>/dev/null || realpath -m "$2")"
      UNCRUSTIFY_BIN="${UNCRUSTIFY_INSTALL_DIR}/bin/uncrustify"
      # Only update UNCRUSTIFY default if user hasn't explicitly set it
      if [[ "${UNCRUSTIFY:-}" != "${UNCRUSTIFY_BIN}" ]]; then
        UNCRUSTIFY="${UNCRUSTIFY_BIN}"
      fi
      shift 2
      ;;
    --expected-uncrustify-version)
      PRINT_EXPECTED_UNCRUSTIFY_VERSION=1
      shift
      ;;
    *)
      # ignore unrecognized positional args for now
      shift
      ;;
  esac
done

if [[ "$DO_INSTALL" -eq 1 ]]; then
  install_uncrustify
  # Ensure we use the freshly installed binary for this run
  UNCRUSTIFY="$UNCRUSTIFY_BIN"
fi

# If requested, print ONLY the expected Uncrustify version and exit.
if [[ "$PRINT_EXPECTED_UNCRUSTIFY_VERSION" -eq 1 ]]; then
  print_expected_uncrustify_version_and_exit
fi

# --------------------------
# Validate & run
# --------------------------

# Check Uncrustify availability
if ! command -v "$UNCRUSTIFY" >/dev/null 2>&1; then
  err "Uncrustify executable not found: $UNCRUSTIFY"
  die "Add it to PATH, set UNCRUSTIFY=/path/to/uncrustify, or run: $0 --install [--install-dir DIR]"
fi

# Version check
DETECTED_VERSION="$("$UNCRUSTIFY" --version 2>&1 | grep -oE '[0-9]+(\.[0-9]+)*' | head -n1 || true)"
echo "Detected Uncrustify: ${DETECTED_VERSION:-unknown}"
if [[ "$DETECTED_VERSION" != "${UNCRUSTIFY_VERSION}" ]]; then
  die "Expected Uncrustify ${UNCRUSTIFY_VERSION}. Re-run with --install (and optionally --install-dir) or set UNCRUSTIFY."
fi

# Config check
[[ -f "$UNCRUSTIFY_CONFIG" ]] || die "Uncrustify config not found at: $UNCRUSTIFY_CONFIG"

# Run formatter
echo "Running formatter..."
$UNCRUSTIFY -c "$UNCRUSTIFY_CONFIG" -l CPP --no-backup --replace *.cpp *.h

# Show diff and fail if changes exist
echo "Checking for formatting changes..."
git diff --exit-code || {
  echo
  echo "Formatting changes were applied. Please review and commit."
  exit 1
}

echo "Formatting is clean."
