#!/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
}

# For Makeconf variables `R CMD config` does not expose (it rejects
# SHLIB_OPENMP_CXXFLAGS with "no information for variable"), read R's own
# Makeconf directly. Empty there means R was built without OpenMP.
makeconf_var() {
  makeconf="${R_HOME}/etc${R_ARCH:-}/Makeconf"
  if [ ! -f "$makeconf" ]; then
    printf '%s' "unavailable"
    return
  fi
  out="$(sed -n "s/^$1[[:space:]]*=[[:space:]]*//p" "$makeconf" | sed -n '1p')"
  if [ -z "$out" ]; then
    printf '%s' "none (R built without OpenMP)"
  else
    printf '%s' "$out"
  fi
}

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: 1 (portable, standard build) everywhere -- GitHub
# source installs, CRAN, r-universe and CI all compile with the same flags, so
# results match across all of them (decision D1 in
# R/package_metadata/new_feature_plans/cross_platform_reproducibility.md,
# 2026-10-14). The tuned, machine-specific build (-march=native, -O3, the
# CXXFLAGS override) is opt-in: set EDI_PORTABLE=0 and compile yourself. An
# explicit EDI_PORTABLE in the environment always wins.
edi_portable="$(env_or_default EDI_PORTABLE 1)"
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 explicitly opts into the tuned build; the default
# (EDI_PORTABLE=1) is the fully portable Makevars that CRAN/CI checks need.
#
# Consequence worth knowing: the portable build compiles Eigen with GCC's
# default -Wignored-attributes enabled, which floods the log with "ignoring
# attributes on template argument '__m128d'" (and '__m256d' whenever R's own
# CXXFLAGS carry -march=native). Do NOT fix that here: R CMD check treats
# every -W* in PKG_CXXFLAGS as non-portable (tools:::.check_make_vars) and
# reports diagnostic-suppressing pragmas (tools:::.check_pragmas). Quiet it
# from the user side instead -- `CXXFLAGS += -Wno-ignored-attributes` (plus
# CXX17FLAGS/CXX20FLAGS, which R's Makeconf defines literally) in
# ~/.R/Makevars; .devcontainer/Dockerfile does exactly this.

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 (now closed by drop_stale_unity_objects, which runs
      # right after generate_unity_wrappers; kept for the history and the
      # -MMD warning below): 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
# build_info.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
  # build_info.cpp also stays its own translation unit: configure deletes
  # build_info.o on every run so the build metadata is never stale (see the
  # end of this script), and inside a unity group that would recompile the
  # whole group on every install.
  member_list="$(cd src && ls -1 *.cpp 2>/dev/null | grep -v '^RcppExports\.cpp$' | grep -v '^build_info\.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# } build_info.o 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.
# Closes the KNOWN LIMITATION described in write_makevars above: a wrapper only
# #includes its members, so editing a member .cpp (or a shared header) leaves
# the wrapper's mtime alone and an unclean incremental install relinks a stale
# unity_NN.o into EDI.so. configure runs on every source-directory install, so
# deleting any unity_NN.o older than one of its sources here makes `make`
# recompile exactly those groups. Fresh builds (no .o yet) are unaffected.
# `find -newer` rather than `test -nt`: the latter is not POSIX.
drop_stale_unity_objects() {
  for wrapper in src/unity_*.cpp; do
    [ -f "$wrapper" ] || continue
    obj="${wrapper%.cpp}.o"
    [ -f "$obj" ] || continue
    stale=""
    for source in $(sed -n 's/^#include "\(.*\)"$/\1/p' "$wrapper") $(cd src && ls -1 *.h *.hpp 2>/dev/null); do
      [ -f "src/$source" ] || continue
      # Regenerated below on every configure run; only build_info.cpp (its own
      # TU, never in a unity group) includes it.
      [ "$source" = "edi_build_flags.h" ] && continue
      if [ -n "$(find "src/$source" -newer "$obj" 2>/dev/null)" ]; then
        stale=1
        break
      fi
    done
    if [ -n "$stale" ]; then
      echo "configure: $obj is older than $source -- removing it so that unity group recompiles"
      rm -f "$obj"
    fi
  done
}

generate_unity_wrappers 10
drop_stale_unity_objects
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 "$(makeconf_var 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

# make tracks .cpp -> .o by mtime only, never the headers a .cpp includes,
# so build_info.o (the only consumer of edi_build_flags.h) would otherwise
# survive every incremental install and keep reporting the build that first
# compiled it -- edi_build_info_cpp() then shows stale flags/timestamp for a
# .so whose other objects were rebuilt since. Deleting it forces exactly one
# small recompile (build_info.cpp includes only Rcpp.h) per install. In
# unity mode build_info.cpp is kept out of the unity groups for the same
# reason (see generate_unity_wrappers), so this never forces a whole group.
rm -f src/build_info.o
