#!/bin/sh

set -eu

c_escape() {
  printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

r_cmd_config() {
  out="$("${R_HOME}/bin/R" CMD config "$1" 2>/dev/null || true)"
  case "$out" in
    ""|ERROR:*) printf '%s' "unavailable" ;;
    *) printf '%s' "$out" ;;
  esac
}

env_or_default() {
  var_name="$1"
  default_value="$2"
  eval "var_value=\${$var_name:-}"
  if [ -z "$var_value" ]; then
    printf '%s' "$default_value"
  else
    printf '%s' "$var_value"
  fi
}

# EDI_PORTABLE default: 0 (tuned, machine-specific build) -- except on
# r-universe builders, which export MY_UNIVERSE=https://<owner>.r-universe.dev
# and run R CMD check on every build: the tuned build's -march=native/
# CXXFLAGS-override Makevars trips their checks' "non-portable compilation
# flags" and "GNU extensions in Makefiles" WARNINGs on all 13 platform
# targets (confirmed 2026-08-31, r-universe/kapelner run 33407674433), so
# default to the portable, warning-free build there. An explicit
# EDI_PORTABLE in the environment always wins, on r-universe too.
if [ -n "${EDI_PORTABLE:-}" ]; then
  edi_portable="$EDI_PORTABLE"
elif [ -n "${MY_UNIVERSE:-}" ]; then
  edi_portable=1
else
  edi_portable=1
fi
edi_disable_vectorization="$(env_or_default EDI_DISABLE_VECTORIZATION 0)"
edi_native_speed="$(env_or_default EDI_NATIVE_SPEED 1)"
edi_native_lto="$(env_or_default EDI_NATIVE_LTO 0)"
edi_debug_symbols="$(env_or_default EDI_DEBUG_SYMBOLS 0)"
# EDI_UNITY: compile src/*.cpp (minus RcppExports.cpp, which Rcpp
# regenerates and which must stay its own translation unit) as a small
# number of merged "unity" translation units instead of one TU per file.
# Default on -- see package_metadata/new_feature_plans/
# unity_build_collision_audit.md and release_v1_0_0.md's compile-time
# finding: an RcppEigen-heavy TU pays a large, mostly-redundant header-
# parsing cost, repeated once per source file; merging ~10 files per TU
# amortizes that cost ~10x with no source changes (every collision this
# would otherwise cause was found and fixed in the audit above).
# EDI_UNITY=0 restores the previous one-object-per-file build -- useful for
# a targeted/incremental local edit-compile loop (recompiling one small
# file instead of a ~10-file unity group), or as an escape hatch if a
# future kernel file introduces a new, not-yet-audited collision.
edi_unity="$(env_or_default EDI_UNITY 1)"

# All conditional logic is resolved here, in POSIX sh, so the Makevars files
# written below are plain, unconditional variable assignments -- no GNU Make
# extensions (ifeq/endif, +=, :=), and no non-portable flags (-march=native,
# -mtune=native, -fno-lto, -Wno-ignored-attributes, or a CXXFLAGS override)
# unless EDI_PORTABLE=0 (the default) explicitly opts into the tuned build.
# CRAN/CI-style checks should set EDI_PORTABLE=1 to get a fully portable,
# warning-free Makevars.

pkg_cppflags=""
pkg_cxxflags="\$(SHLIB_OPENMP_CXXFLAGS) -DNDEBUG -DEIGEN_NO_DEBUG"
pkg_cxxstdflags=""
pkg_libs="\$(SHLIB_OPENMP_CXXFLAGS) \$(LAPACK_LIBS) \$(BLAS_LIBS) \$(FLIBS) -fstack-protector"

if [ "$edi_portable" = "0" ]; then
  # SIMD: every instruction set the build machine supports (SSE4.2, AVX2,
  # FMA, AVX-512, ...). Eigen and auto-vectorized loops both benefit
  # automatically. -Wno-ignored-attributes silences the Eigen/SIMD alignment
  # warnings this enables. Opt out with EDI_PORTABLE=1.
  pkg_cxxflags="$pkg_cxxflags -march=native -mtune=native -Wno-ignored-attributes"
fi

if [ "$edi_disable_vectorization" = "1" ]; then
  pkg_cppflags="$pkg_cppflags -DEIGEN_DONT_VECTORIZE -DEIGEN_UNALIGNED_VECTORIZE=0"
  pkg_cxxflags="$pkg_cxxflags -fno-tree-vectorize"
fi

if [ "$edi_native_lto" = "1" ]; then
  pkg_cxxflags="$pkg_cxxflags -flto"
  pkg_libs="$pkg_libs -flto"
elif [ "$edi_portable" = "0" ]; then
  # LTO is opt-in (EDI_NATIVE_LTO=1). GCC/RcppEigen LTO builds have shown
  # severe slowdowns in the small model-fit kernels, so the default
  # benchmark/release (non-portable) path is explicitly non-LTO.
  pkg_cxxflags="$pkg_cxxflags -fno-lto"
fi

if [ "$edi_portable" = "0" ]; then
  # R's package compile rule appends CXXFLAGS after PKG_CXXFLAGS, even for
  # C++20 files, so site defaults (e.g. CXXFLAGS=-g -O2) would otherwise win
  # by ordering. override forces our optimisation/debug-symbol choice.
  override_cxxflags="-O3"
  if [ "$edi_native_speed" != "1" ]; then
    override_cxxflags=""
  fi
  if [ "$edi_debug_symbols" = "1" ]; then
    override_cxxflags="$override_cxxflags -g1 -fno-omit-frame-pointer"
  else
    override_cxxflags="$override_cxxflags -g0"
  fi
  # NOTE: this uses += (a GNU Make extension), not plain '=', because a
  # self-referential 'CXXFLAGS = $(CXXFLAGS) ...' recursive assignment blows
  # up with "Recursive variable 'CXXFLAGS' references itself". That's fine:
  # this line is only emitted in the non-portable (EDI_PORTABLE=0) build,
  # which the portable/CI Makevars (EDI_PORTABLE=1) never contains.
  pkg_cxxstdflags="override CXXFLAGS += $override_cxxflags"
fi

write_makevars() {
  target_file="$1"
  {
    echo "# Generated by EDI/configure -- do not edit by hand."
    echo "# Conditional build logic (EDI_PORTABLE, EDI_NATIVE_SPEED, EDI_NATIVE_LTO,"
    echo "# EDI_DISABLE_VECTORIZATION, EDI_DEBUG_SYMBOLS, EDI_UNITY) is resolved at"
    echo "# configure time; see EDI/configure. Re-run configure (or reinstall) after"
    echo "# changing these."
    echo "PKG_CXXFLAGS = $pkg_cxxflags"
    if [ -n "$pkg_cppflags" ]; then
      echo "PKG_CPPFLAGS = $pkg_cppflags"
    fi
    echo "PKG_LIBS = $pkg_libs"
    if [ -n "$pkg_cxxstdflags" ]; then
      echo "$pkg_cxxstdflags"
    fi
    if [ -n "$pkg_objects" ]; then
      # Overrides R's own default OBJECTS discovery (every *.cpp in src/) so
      # that only the unity_NN.o wrapper objects (each covering ~10 member
      # .cpp files via #include, see generate_unity_wrappers below) plus
      # RcppExports.o actually get compiled -- the individual member .cpp
      # files still physically exist in src/ (never removed; EDI_UNITY=0
      # switches back to compiling them directly) but are not independently
      # built while this is in effect.
      #
      # KNOWN LIMITATION: editing a member .cpp file changes that file's own
      # mtime but not its unity_NN.cpp wrapper's (the wrapper's content, a
      # fixed #include list, is unaffected by what's inside the file it
      # #includes) -- so a plain incremental `make`/`R CMD INSTALL` in this
      # src/ directory has no mtime-based signal telling it unity_NN.o needs
      # recompiling. (NOTE: an earlier version of this script tried to fix
      # this with -MMD -MP + a Makevars "-include $(OBJECTS:.o=.d)" line --
      # do NOT reintroduce that. It broke R CMD SHLIB/INSTALL outright: `make`
      # silently built only the first object in $(OBJECTS) and reported
      # success without ever linking EDI.so, root cause not fully understood
      # but 100% reproducible -- see the fix commit for the reproduction.)
      # Every *fresh* build (CI, CRAN, a first install, or any --preclean /
      # R CMD build tarball round-trip) is unaffected, since generate_unity_
      # wrappers below always regenerates every wrapper from the current
      # source tree before this Makevars is even written. The gap is
      # specifically "edit one kernel .cpp, then re-run an *unclean*
      # incremental install expecting only that file's group to rebuild" --
      # for that workflow, set EDI_UNITY=0 (this package's existing targeted/
      # single-file compile escape hatch) rather than relying on unity mode's
      # own incrementality.
      echo "OBJECTS = $pkg_objects"
    fi
  } > "$target_file"
}

# Groups src/*.cpp (excluding RcppExports.cpp, which Rcpp regenerates and
# which must stay its own translation unit -- see
# unity_build_collision_audit.md) into $1 "unity" wrapper files,
# src/unity_NN.cpp, each starting with #include "_helper_functions.h" (owns
# the NDEBUG-before-Eigen include-order discipline every kernel file relies
# on) followed by one #include "<member>.cpp" per file assigned to that
# group. Round-robin assignment over the sorted file list, not chunked
# ranges, so adding or removing one kernel .cpp shifts group *membership*
# only slightly rather than shifting every file after it into a different
# group on every re-run -- deterministic given a fixed file set either way,
# but round-robin keeps re-generated wrappers closer to the previous run's
# grouping when the file set changes only slightly.
#
# Sets $pkg_objects as a side effect (package-global, read by
# write_makevars): the space-separated "unity_01.o unity_02.o ... .o
# RcppExports.o" list, or "" if $edi_unity != 1 (in which case any leftover
# unity_NN.cpp from a previous run are removed and R's own default
# per-file OBJECTS discovery is left in effect).
#
# Idempotent by content, not just by existence: writes each group's content
# to a scratch file first and only replaces the real src/unity_NN.cpp if it
# differs (or doesn't exist yet), leaving an unchanged group's mtime alone.
# Without this, EVERY configure run (which is EVERY `R CMD INSTALL`, even an
# "incremental" one -- R always re-runs configure for a source-directory
# install) would give every wrapper a fresh mtime regardless of whether the
# underlying source tree actually changed, forcing all $1 groups to
# recompile on every single install.
generate_unity_wrappers() {
  ngroups="$1"
  if [ "$edi_unity" != "1" ]; then
    rm -f src/unity_*.cpp
    pkg_objects=""
    return
  fi
  member_list="$(cd src && ls -1 *.cpp 2>/dev/null | grep -v '^RcppExports\.cpp$' | grep -v '^unity_' | LC_ALL=C sort)"
  if [ -z "$member_list" ]; then
    rm -f src/unity_*.cpp
    pkg_objects=""
    return
  fi
  scratch_dir="$(mktemp -d)"
  printf '%s\n' "$member_list" | awk -v n="$ngroups" -v scratchdir="$scratch_dir" '
    BEGIN {
      for (i = 1; i <= n; i++) {
        fn = sprintf("%s/unity_%02d.cpp", scratchdir, i)
        print "// Generated by EDI/configure (EDI_UNITY=1) -- do not edit by hand." > fn
        print "// Merges several kernel .cpp files into one translation unit to" >> fn
        print "// amortize RcppEigen header-parsing cost across them; see" >> fn
        print "// package_metadata/finished_features/unity_build_collision_audit.md" >> fn
        print "// and release_v1_0_0.md. Re-run configure (or reinstall) after adding," >> fn
        print "// removing, or renaming a src/*.cpp kernel file." >> fn
        print "#include \"_helper_functions.h\"" >> fn
      }
    }
    {
      g = (NR - 1) % n + 1
      fn = sprintf("%s/unity_%02d.cpp", scratchdir, g)
      print "#include \"" $0 "\"" >> fn
    }
  '
  pkg_objects=""
  i=1
  while [ "$i" -le "$ngroups" ]; do
    num=$(printf '%02d' "$i")
    new_file="$scratch_dir/unity_${num}.cpp"
    real_file="src/unity_${num}.cpp"
    if [ -f "$real_file" ] && cmp -s "$new_file" "$real_file"; then
      : # content unchanged -- leave the existing file (and its mtime) alone
    else
      mv "$new_file" "$real_file"
    fi
    pkg_objects="$pkg_objects unity_${num}.o"
    i=$((i + 1))
  done
  pkg_objects="${pkg_objects# } RcppExports.o"
  rm -rf "$scratch_dir"
  # Remove any wrapper left over from a run with a larger $ngroups than this
  # one (only relevant if that constant is ever deliberately changed).
  i=$((ngroups + 1))
  while [ "$i" -le 99 ]; do
    num=$(printf '%02d' "$i")
    stale="src/unity_${num}.cpp"
    [ -f "$stale" ] && rm -f "$stale"
    i=$((i + 1))
  done
}

mkdir -p src
# 10 groups: the group size (~10-12 files each, from 105 kernel .cpp files
# as of this writing) that unity_build_collision_audit.md's trial actually
# validated -- large enough to amortize the header cost, small enough to
# keep per-TU compiler memory and any single collision's blast radius
# bounded. Not read from an env var: changing it is a source-level decision
# (re-validate against that audit's method first), not a per-build tuning
# knob like EDI_PORTABLE/EDI_NATIVE_SPEED above.
generate_unity_wrappers 10
write_makevars src/Makevars
write_makevars src/Makevars.win

{
  echo "#ifndef EDI_BUILD_FLAGS_H"
  echo "#define EDI_BUILD_FLAGS_H"
  echo ""
  echo "#define EDI_BUILD_CAPTURE_METHOD \"configure-generated header compiled into EDI.so\""
  echo "#define EDI_BUILD_TIMESTAMP \"$(c_escape "$(date '+%Y-%m-%d %H:%M:%S %Z')")\""
  echo "#define EDI_BUILD_HOST \"$(c_escape "$(hostname 2>/dev/null || printf unknown)")\""
  echo "#define EDI_BUILD_R_HOME \"$(c_escape "$R_HOME")\""
  echo "#define EDI_BUILD_R_VERSION \"$(c_escape "$("${R_HOME}/bin/R" --version 2>/dev/null | sed -n '1p')")\""
  echo "#define EDI_BUILD_R_CXX20 \"$(c_escape "$(r_cmd_config CXX20)")\""
  echo "#define EDI_BUILD_R_CXX20STD \"$(c_escape "$(r_cmd_config CXX20STD)")\""
  echo "#define EDI_BUILD_R_CXX20FLAGS \"$(c_escape "$(r_cmd_config CXX20FLAGS)")\""
  echo "#define EDI_BUILD_R_SHLIB_OPENMP_CXXFLAGS \"$(c_escape "$(r_cmd_config SHLIB_OPENMP_CXXFLAGS)")\""
  echo "#define EDI_BUILD_ENV_EDI_PORTABLE \"$(c_escape "$edi_portable")\""
  echo "#define EDI_BUILD_ENV_EDI_DISABLE_VECTORIZATION \"$(c_escape "$edi_disable_vectorization")\""
  echo "#define EDI_BUILD_ENV_EDI_NATIVE_SPEED \"$(c_escape "$edi_native_speed")\""
  echo "#define EDI_BUILD_ENV_EDI_NATIVE_LTO \"$(c_escape "$edi_native_lto")\""
  echo "#define EDI_BUILD_ENV_EDI_UNITY \"$(c_escape "$edi_unity")\""
  echo "#define EDI_BUILD_PKG_CPPFLAGS \"$(c_escape "$pkg_cppflags")\""
  echo "#define EDI_BUILD_PKG_CXXFLAGS \"$(c_escape "$pkg_cxxflags${pkg_cxxstdflags:+; }$pkg_cxxstdflags")\""
  echo "#define EDI_BUILD_PKG_LIBS \"$(c_escape "$pkg_libs")\""
  echo ""
  echo "#endif"
} > src/edi_build_flags.h
