#!/bin/sh
# This script is used in conjunction with `repl-listener`.
#
# This can be used to give you a C-c C-c to evaluate similar to
# Emacs' CIDER. Right now it only works with Clojure[Script].
#
# There is probably no reason to change these:
# Env overrides:
#   AIT_REPL_REQ   request file repl-listen watches; overrides the auto-located
#                  <root>/.ait-eval (use if your root detection needs a nudge)
#   AIT_LINE_BASE  line numbering base, default 1
#
# HOW TO:
#
# First off, you don't have use ait to use this. So do what you want.
#
# If you don't run this as a command, see `man ait` COMMANDS section,
# then you much supply the current line as argument 1 and the full
# file path and name as argument 4. Highly recommend you use the
# C-c_C-c script included with ait and assign it via:
# ait -C ^c=/path/to/C-c_C-c
# This will automatically run the correct script based on the file's
# extension once you'd set it up to your liking. Read the top comment
# on that script for more information.
#
# Before you can start hacking you'll need to setup three things:
# 1. Make a .ait-repl file and put it in the root directory of the
#    project. See ait-repl-example for more information.
# 2. Start up a repl for your project. i.e. shadow-cljs repl app,
#    lein repl, clojure -M:repl/nrepl, etc.
# 3. In the root directory of the project, run the `repl-listen`
#    program found in ait's scripts/. This will setup a watcher
#    for a file called .ait-eval which will then update the REPL
#    based on the settings placed in that project's .ait-repl.
#
# That's it!

set -eu

LINE="${1:-0}"
# $2 col, $3 point — unused
FILEPATH="${4:-}"

LINE_BASE="${AIT_LINE_BASE:-1}"

find_root() {
  d=$1
  while [ -n "$d" ] && [ "$d" != "/" ]; do
    if [ -f "$d/.ait-repl" ]; then printf '%s\n' "$d"; return 0; fi
    d=$(dirname "$d")
  done
  return 1
}
if [ -n "$FILEPATH" ]; then
  START_DIR="$(cd "$(dirname "$FILEPATH")" 2>/dev/null && pwd || echo "$PWD")"
else
  START_DIR="$PWD"
fi
ROOT="$(find_root "$START_DIR" || echo "$PWD")"
REQ="${AIT_REPL_REQ:-$ROOT/.ait-eval}"

if [ "$LINE_BASE" = "0" ]; then LINE=$((LINE + 1)); fi

TMPD="${TMPDIR:-/tmp}"
BUF="$(mktemp "$TMPD/ait-buf.XXXXXX")"
FORM="$(mktemp "$TMPD/ait-form.XXXXXX")"
cleanup() { rm -f "$BUF" "$FORM"; }
trap cleanup EXIT

cat > "$BUF"

# Paren state machine, LINE-ORIENTED so it stays O(buffer): scan each line with
# substr over that SHORT line only, remember forms by their (line,col) bounds
# (never by index into the whole buffer), and copy just the spanned lines at the
# end. The old version concatenated the whole file and did substr(all,i,1) per
# char — quadratic, ~18s on NetBSD awk for a 24KB buffer.
awk -v L="$LINE" '
function extract(sl, sc, el, ec,   s, i) {
  if (sl == el) return substr(lines[sl], sc, ec - sc + 1)
  s = substr(lines[sl], sc)
  for (i = sl + 1; i < el; i++) s = s "\n" lines[i]
  return s "\n" substr(lines[el], 1, ec)
}
{ lines[NR] = $0 }
END {
  depth = 0; ins = 0; esc = 0; comment = 0; charlit = 0
  startline = 0; startcol = 0
  have_prev = 0; ps = pc = pe = pk = 0

  for (ln = 1; ln <= NR; ln++) {
    row = lines[ln]
    m = length(row)
    comment = 0; charlit = 0          # reset per line
    for (col = 1; col <= m; col++) {
      c = substr(row, col, 1)
      if (comment) continue

      if (ins) {
        if (esc) { esc = 0 }
        else if (c == "\\") { esc = 1 }
        else if (c == "\"") { ins = 0 }
        continue
      }

      if (charlit) { charlit = 0; continue }   # consume the literal char
      if (c == "\\") { charlit = 1; continue }
      if (c == "\"") { ins = 1; continue }
      if (c == ";")  { comment = 1; continue }

      if (c == "(" || c == "[" || c == "{") {
        if (depth == 0) { startline = ln; startcol = col }
        depth++
        continue
      }
      if (c == ")" || c == "]" || c == "}") {
        depth--
        if (depth == 0 && startline > 0) {
          if (startline <= L && L <= ln) {
            printf "%s", extract(startline, startcol, ln, col)
            exit
          }
          if (ln < L) { ps = startline; pc = startcol; pe = ln; pk = col; have_prev = 1 }
          startline = 0
        }
        continue
      }
    }
  }

  # Cursor on a blank line / past the last form: use the nearest form above.
  if (have_prev) printf "%s", extract(ps, pc, pe, pk)
}
' "$BUF" > "$FORM"

if [ ! -s "$FORM" ]; then
  cp "$BUF" "$FORM"
fi

NS="$(grep -m1 -E '^\(ns[[:space:]]' "$BUF" \
        | sed -E 's/^\(ns[[:space:]]+([^[:space:])]+).*/\1/' || true)"

RAND="$(awk -v s="$$" 'BEGIN { srand(); print int((rand()*100000) + s) }')"
NONCE="$(date +%s)-$$-${RAND}"
REQ_DIR="$(dirname "$REQ")"
TMP="$(mktemp "$REQ_DIR/.ait-eval.XXXXXX" 2>/dev/null || echo "$REQ.tmp.$$")"
{
  printf '%s\n' "$NONCE"
  printf '%s\n' "$NS"
  cat "$FORM"
} > "$TMP"
mv -f "$TMP" "$REQ"

cat "$BUF"
