#!/usr/bin/env bash
set -eo pipefail

# Runs commands on Forge projects used in the book, and records
# the commands and outputs into files that are easily
# includeable in the book.

ETH_RPC_URL="${ETH_RPC_URL:=https://ethereum.reth.rs/rpc}"
SNIPPETS_DIR="$(realpath src/snippets)"
OUTPUT_DIR="$(realpath $SNIPPETS_DIR/output)"
ROOT="$(dirname "$(dirname "$(realpath "$0")")")"
# shellcheck disable=SC2034
SCRIPTS="$ROOT/scripts"

# Force color output from Foundry tools
export FORCE_COLOR=1

# Disable nightly warning in output
export FOUNDRY_DISABLE_NIGHTLY_WARNING=1

# Runs a command and outputs text with anchors.
# The anchors are:
# - all: The command and output
# - command: Only the command
# - output: Only the output
run_command() {
  output=$1
  shift

  cmd=("$@")
  display_cmd=()

  # Build display command, excluding --color always from display
  skip_next=false
  for arg in "${cmd[@]}"; do
    if $skip_next; then
      skip_next=false
      continue
    fi
    if [[ "$arg" == "--color" ]]; then
      skip_next=true
      continue
    fi
    # Escape arguments with spaces, parentheses, or pipe for display
    if [[ "$arg" == *' '* ]] || [[ "$arg" == *'('* ]] || [[ "$arg" == *'|'* ]]; then
      display_cmd+=("\"$arg\"")
    else
      display_cmd+=("$arg")
    fi
  done

  echo "Running $" "${display_cmd[*]}" >&2
  # Use script to fake a TTY so commands output ANSI colors
  # Clean up the output:
  # 1. Remove carriage returns
  # 2. Remove erase-line escape sequences
  # 3. Keep only the last spinner line (contains useful info like "Compiling X files...")
  # 4. Remove empty lines and collapse multiple blanks
  # Note: macOS and Linux have different `script` syntax
  # We use `|| true` to ignore exit codes since `script` on macOS doesn't
  # reliably propagate exit codes, and we capture output regardless.
  # We use printf %q to properly quote each argument for shell execution.
  run_with_script() {
    local quoted_cmd
    quoted_cmd=$(printf '%q ' "${cmd[@]}")
    if [[ "$(uname)" == "Darwin" ]]; then
      script -q /dev/null bash -c "$quoted_cmd" || true
    else
      script -q -c "$quoted_cmd" /dev/null || true
    fi
  }

  cmd_output=$(run_with_script \
    | tr -d '\r' \
    | sed $'s/\x1b\\[2K//g' \
    | perl -ne '
      # Spinner pattern: ESC[1m[ESC[32m<braille>ESC[0m]ESC[0m <message>
      # Braille spinner chars are 3 bytes (0xe2 0xa0 0x??)
      # Multiple spinners can appear on same line; extract the last message
      $spinner_re = qr/\x1b\[1m\[\x1b\[32m...\x1b\[0m\]\x1b\[0m /;
      if (/$spinner_re/) {
        # Split on spinner pattern, last element is the final message
        @parts = split(/$spinner_re/, $_);
        $msg = $parts[-1];
        $msg =~ s/\s+$//;
        $last_spinner = $msg if $msg ne "";
      } else {
        if (defined $last_spinner) {
          print "$last_spinner\n";
          undef $last_spinner;
        }
        print;
      }
      END { print "$last_spinner\n" if defined $last_spinner; }
    ' \
    | sed '/^[[:space:]]*$/d' \
    | cat -s)
  print_anchored "${display_cmd[*]}" "$cmd_output" > "$output"
}

print_anchored() {
  cat > /dev/stdout <<EOF
// [!region all]
// [!region command]
\$ $1
// [!endregion command]
// [!region output]
$2
// [!endregion output]
// [!endregion all]
EOF
}

# Moves to a project directory and does some initial preparations.
#
# The project must live in ./projects
in_project() {
  echo "... $1"
  cd "$SNIPPETS_DIR/projects/$1"
  forge clean
}

# Moves to a new temporary directory for a specific project,
# and ensures src/output/<PROJECT> exists.
in_temp() {
  echo "... $1"
  cd "$(mktemp -d)"
  mkdir -p "$OUTPUT_DIR/$1"
}

# A helper to "step" in a project. For example, suppose you have
# a set of contracts, each with clashing signatures/definitions:
#
# - src/test/SomeTest.t.sol.1
# - src/test/SomeTest.t.sol.2
# - src/test/SomeTest.t.sol.3
#
# Calling `step src/test/SomeTest.t.sol 1` would delete src/test/SomeTest.t.sol,
# and then copy the contents of SomeTest.t.sol.1 into its destination.
step() {
  rm -f "$1"
  cp "${1}.${2}" "$1"
}

err() {
  echo "$1" >&2
  exit 1
}

need_cmd() {
  if ! check_cmd "$1"; then
    err "need '$1' (command not found)"
  fi
}

check_cmd() {
  command -v "$1" > /dev/null 2>&1
}
