
cmake_minimum_required(VERSION 3.22)

if(POLICY CMP0135)
    cmake_policy(SET CMP0135 NEW)
endif()

# Project version is normally supplied by scikit-build-core (via
# setuptools-scm, see pyproject.toml) as SKBUILD_PROJECT_VERSION. Fall back
# to a placeholder for plain, non-Python CMake configuration/testing.
if(NOT DEFINED SKBUILD_PROJECT_VERSION)
  set(SKBUILD_PROJECT_VERSION "0.0.0")
endif()

project(pyoomph LANGUAGES C CXX VERSION ${SKBUILD_PROJECT_VERSION})

# On Apple Silicon, detect whether this CMake configure is itself running
# under Rosetta 2 translation (an x86_64 process on arm64 hardware) - e.g.
# when following the documented "Rosetta 2 terminal" route to get the MKL
# Pardiso solver, which has no arm64 build. If so, default to a matching
# single-arch x86_64 build rather than letting the compiler pick its native
# default (arm64). Without this, CLN/GiNaC's autotools ExternalProject_Add
# subbuilds (see cmake/ThirdPartyGiNaC.cmake, which reads
# CMAKE_OSX_ARCHITECTURES back out to keep their -arch flags in sync) can
# end up targeting a different architecture than the rest of the build,
# which surfaces as clang being invoked for both -arch x86_64 and -arch
# arm64 at once and failing with "redefinition of '_OSSwapInt32'" (the SDK's
# per-architecture endian headers collide). Only takes effect if the caller
# hasn't already set CMAKE_OSX_ARCHITECTURES explicitly.
if(APPLE AND NOT CMAKE_OSX_ARCHITECTURES)
  execute_process(
    COMMAND sysctl -n sysctl.proc_translated
    OUTPUT_VARIABLE PYOOMPH_MACOS_PROC_TRANSLATED
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
  )
  if(PYOOMPH_MACOS_PROC_TRANSLATED STREQUAL "1")
    message(STATUS "pyoomph: detected Rosetta 2 translation - forcing a single-arch x86_64 build (pass -DCMAKE_OSX_ARCHITECTURES=... to override)")
    set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE STRING "Build architecture(s) for macOS" FORCE)
  endif()
endif()

# ----------------------------------------------------------------------------
# Build options
#
# These mirror the old PYOOMPH_* environment variables read by setup.py. They are set either
# straight on the cmake command line (`-DPYOOMPH_...=...`) or, when building through pip/
# scikit-build-core as ./build_for_develop.sh does, via
# `pip install --config-settings=cmake.define.PYOOMPH_...=...`.
# ----------------------------------------------------------------------------
option(PYOOMPH_USE_MPI       "Build with MPI support (defines OOMPH_HAS_MPI)"        OFF)
option(PYOOMPH_PARANOID      "Build with extra runtime sanity checks (defines PARANOID)" OFF)
option(PYOOMPH_DOWNLOAD_CLN  "Download and build CLN from source via its own autotools ./configure" ON)
option(PYOOMPH_DOWNLOAD_GINAC "Download and build GiNaC from source via its own autotools ./configure" ON)
# AUTO (the default) links OpenMP if the toolchain has it and builds without it
# if not; ON makes a missing OpenMP a hard error, OFF never looks for it. The
# tri-state exists because AUTO alone means the capability of a wheel is decided
# silently by whatever the build machine happened to have: manylinux always has
# libgomp, AppleClang usually has no libomp at all, so the same release would
# ship --omp on Linux/Windows and not on macOS without anyone noticing. A CI job
# that means to ship threaded assembly says ON and finds out at configure time.
set(PYOOMPH_USE_OPENMP "AUTO" CACHE STRING "OpenMP for the parallel element assembly (--omp N): AUTO=use if found, ON=require, OFF=never")
set_property(CACHE PYOOMPH_USE_OPENMP PROPERTY STRINGS AUTO ON OFF)
option(PYOOMPH_GENERATE_STUBS "Generate .pyi stubs for the _core extension via nanobind.stubgen"  ON)
# The stub is part of the package, not a nicety: the wheel ships py.typed alongside it, so a wheel
# without the stub tells type checkers the API is described and then hands them an unintrospectable
# binary. Generation used to be best-effort and silently swallowed its failures, which is how the
# Windows wheel shipped without a .pyi indefinitely (see cmake/stubgen_launcher.py for what was
# actually failing). OFF restores the old warn-and-continue behaviour for a build that cannot
# produce one and does not care.
option(PYOOMPH_REQUIRE_STUBS "Fail the build when the .pyi stub cannot be generated, rather than warning" ON)
option(PYOOMPH_COPY_STUBS_TO_SOURCE_TREE "Also mirror the generated .pyi stub into the source-tree pyoomph/ directory, so Pylance/Pyright/mypy can resolve pyoomph._core while editing without a full pip install" ON)
# Only meaningful together with -DPYOOMPH_DOWNLOAD_GINAC=OFF (a system-supplied
# GiNaC, never touched by citools/patches/apply_ginac_patch.sh): asserts that
# said GiNaC was patched by other means to make its term/hash ordering
# deterministic (see PYOOMPH_GINAC_HASH_PATCHED below). Leave OFF unless you
# have actually verified this yourself - the JIT code cache (pyoomph/generic/
# jit_cache.py) disables itself entirely whenever it can't be sure the
# generated code is reproducible across process runs, and a wrong ON here
# would silently defeat that safeguard.
option(PYOOMPH_ASSUME_GINAC_HASH_PATCHED "Assert that a system-supplied GiNaC (PYOOMPH_DOWNLOAD_GINAC=OFF) already has deterministic term/hash ordering" OFF)
# Build-time kill switch for the JIT code cache (pyoomph/generic/jit_cache.py): if OFF, the
# cache is permanently disabled for this build, regardless of runtime flags/env vars
# (--no-cache, PYOOMPH_JIT_CACHE, ...) - those can only ever narrow ON down to OFF, never
# override an OFF set here. Independent of PYOOMPH_GINAC_HASH_PATCHED below: that one governs
# whether caching is *safe*; this one governs whether it exists at all in this build.
option(PYOOMPH_ENABLE_JIT_CACHE "Enable the JIT code cache at build time" ON)
# TQMesh is a header-only 2d triangle/quad mesh generator, downloaded by
# cmake/ThirdPartyTQMesh.cmake and exposed to python by src/nanobind/tqmesh/*.cpp. Switching this
# OFF skips the download and drops those sources from the build; the extension module then reports
# has_tqmesh == False and none of the TQMesh* classes exist.
option(PYOOMPH_HAS_TQMESH "Build with the TQMesh two-dimensional mesh generator" ON)

# Spectra (https://spectralib.org) is a header-only Arnoldi eigensolver on top of Eigen, both
# downloaded by cmake/ThirdPartySpectra.cmake and exposed to python by src/nanobind/spectra/*.cpp as
# the eigensolver named "spectra". It exists for the platforms that have no PETSc/SLEPc at all -
# Windows above all - where it is the only backend that can target an eigenvalue. Switching this OFF
# skips the download and drops those sources; the extension module then reports has_spectra == False,
# pyoomph.solvers.spectra refuses to import, and the eigensolver autodetection falls back to the
# ARPACK-based "pardiso"/"scipy" backends, which cannot handle a target at all.
option(PYOOMPH_HAS_SPECTRA "Build with the Spectra/Eigen serial eigensolver backend" ON)

# Left empty by default: ginac.de only ever hosts the *current* release
# tarball, so a version pinned here goes dead as soon as a new release is
# published. When empty, cmake/ThirdPartyGiNaC.cmake auto-detects the
# current version by scraping ginac.de's own download pages at configure
# time. Set explicitly to pin a specific (still-live) version instead.
set(PYOOMPH_CLN_VERSION "" CACHE STRING "CLN version to download when PYOOMPH_DOWNLOAD_CLN=ON (auto-detected from ginac.de if empty)")
set(PYOOMPH_GINAC_VERSION "" CACHE STRING "GiNaC version to download when PYOOMPH_DOWNLOAD_GINAC=ON (auto-detected from ginac.de if empty)")
set(PYOOMPH_THIRDPARTY_PREFIX "${CMAKE_BINARY_DIR}/thirdparty-install" CACHE PATH
    "Install prefix used for a downloaded/built CLN and/or GiNaC")

# Optional hints for a pre-existing, system-installed CLN/GiNaC (used only
# when PYOOMPH_DOWNLOAD_CLN / PYOOMPH_DOWNLOAD_GINAC are OFF). These mirror
# the old PYOOMPH_GINAC_INCLUDE_DIR / PYOOMPH_GINAC_LIB_DIR / ... env vars.
set(PYOOMPH_GINAC_INCLUDE_DIR "" CACHE PATH "Hint: directory containing ginac/ginac.h")
set(PYOOMPH_GINAC_LIB_DIR     "" CACHE PATH "Hint: directory containing libginac")
set(PYOOMPH_CLN_INCLUDE_DIR   "" CACHE PATH "Hint: directory containing cln/cln.h")
set(PYOOMPH_CLN_LIB_DIR       "" CACHE PATH "Hint: directory containing libcln")

if(PYOOMPH_USE_MPI)
  set(PYOOMPH_NO_MPI_DEFAULT OFF)
else()
  set(PYOOMPH_NO_MPI_DEFAULT ON)
endif()

set(_pyoomph_no_mpi_marker "${CMAKE_SOURCE_DIR}/pyoomph/NO_MPI")
if(PYOOMPH_USE_MPI)
  file(REMOVE "${_pyoomph_no_mpi_marker}")
else()
  file(TOUCH "${_pyoomph_no_mpi_marker}")
endif()


set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE)
endif()

include(cmake/ThirdPartyGiNaC.cmake)
if(PYOOMPH_HAS_TQMESH)
  include(cmake/ThirdPartyTQMesh.cmake)
endif()
if(PYOOMPH_HAS_SPECTRA)
  include(cmake/ThirdPartySpectra.cmake)
endif()

# ----------------------------------------------------------------------------
# nanobind, via CMake.
#
# NOTE: nanobind itself (headers/CMake package) is looked up here, but is
# only actually *used* by the _pyoomph target further down, whose sources
# are restricted to src/nanobind/*.cpp. Everything else in this project
# (libgeneric.a and the pyoomph_core static library) is fully Python-version
# independent and never sees nanobind or the Python headers/flags.
# ----------------------------------------------------------------------------
# Development.SABIModule is optional (only exists from 3.12 onwards, where
# nanobind's STABLE_ABI build below actually activates - see the comment
# there); listing it as an OPTIONAL_COMPONENTS means configuring under an
# older interpreter (3.10-3.11) just silently doesn't find it instead of
# failing CMake configuration.
find_package(Python 3.10 COMPONENTS Interpreter Development.Module REQUIRED
                        OPTIONAL_COMPONENTS Development.SABIModule)

execute_process(
  COMMAND "${Python_EXECUTABLE}" -c "import nanobind; print(nanobind.cmake_dir())"
  OUTPUT_STRIP_TRAILING_WHITESPACE
  OUTPUT_VARIABLE nanobind_ROOT
  COMMAND_ERROR_IS_FATAL ANY)

find_package(nanobind CONFIG REQUIRED)

# ----------------------------------------------------------------------------
# libgeneric.a: all .cc files from oomph-lib's include/ directory. This must
# be built first; pyoomph_core and _pyoomph both depend on it, which
# target_link_libraries() below expresses (CMake/the generator then takes
# care of the ordering automatically, no manual sequencing needed).
#
# Python-independent: built once, reused unchanged across every Python
# version when iterating with `cmake --build` for multiple wheels.
# ----------------------------------------------------------------------------
file(GLOB_RECURSE OOMPH_GENERIC_SOURCES CONFIGURE_DEPENDS
     "${CMAKE_SOURCE_DIR}/src/thirdparty/oomph-lib/include/*.cc")

if(NOT OOMPH_GENERIC_SOURCES)
  message(FATAL_ERROR "No .cc sources found under src/thirdparty/oomph-lib/include - "
                       "is the oomph-lib submodule/sources checked out?")
endif()

# These are template-only translation units (meant to be #included where
# needed, not compiled standalone as part of libgeneric.a) and must be
# excluded from the glob above.
set(OOMPH_GENERIC_EXCLUDED_SOURCES
  "tree.template.cc"
  "multi_domain.template.cc"
  "refineable_mesh.template.cc"
)
foreach(_excluded_name IN LISTS OOMPH_GENERIC_EXCLUDED_SOURCES)
  list(FILTER OOMPH_GENERIC_SOURCES EXCLUDE REGEX "/${_excluded_name}$")
endforeach()

add_library(generic STATIC ${OOMPH_GENERIC_SOURCES})
target_include_directories(generic PUBLIC
  "${CMAKE_SOURCE_DIR}/src/thirdparty/oomph-lib/include"
)
target_compile_definitions(generic PUBLIC
  $<$<BOOL:${PYOOMPH_USE_MPI}>:OOMPH_HAS_MPI>
  $<$<BOOL:${PYOOMPH_PARANOID}>:PARANOID>
)

# ----------------------------------------------------------------------------
# pyoomph_core: all of src/*.cpp EXCEPT src/nanobind/*.cpp. This is the bulk of
# pyoomph's own C++ code, and it does not depend on nanobind or the Python
# headers at all - it only needs to be recompiled when the sources
# themselves change, not when switching Python versions.
# ----------------------------------------------------------------------------
file(GLOB PYOOMPH_CORE_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/src/*.cpp")

add_library(pyoomph_core STATIC ${PYOOMPH_CORE_SOURCES})

target_include_directories(pyoomph_core PUBLIC
  "${CMAKE_SOURCE_DIR}/src"
  "${CMAKE_SOURCE_DIR}/src/thirdparty"
  "${PYOOMPH_GINAC_INCLUDE_DIR_RESOLVED}"
  "${PYOOMPH_CLN_INCLUDE_DIR_RESOLVED}"
)

target_link_libraries(pyoomph_core PUBLIC generic)

target_compile_definitions(pyoomph_core PUBLIC
  $<$<BOOL:${PYOOMPH_USE_MPI}>:OOMPH_HAS_MPI>
  $<$<BOOL:${PYOOMPH_PARANOID}>:PARANOID>
  $<$<BOOL:${PYOOMPH_GINAC_HASH_PATCHED}>:PYOOMPH_GINAC_HASH_PATCHED>
  $<$<BOOL:${PYOOMPH_ENABLE_JIT_CACHE}>:PYOOMPH_ENABLE_JIT_CACHE>
)

if(PYOOMPH_DOWNLOAD_GINAC)
  add_dependencies(pyoomph_core ginac_external)
endif()
if(PYOOMPH_DOWNLOAD_CLN)
  add_dependencies(pyoomph_core cln_external)
endif()

if(PYOOMPH_USE_MPI)
  find_package(MPI REQUIRED COMPONENTS CXX)
  target_link_libraries(generic PUBLIC MPI::MPI_CXX)
endif()

# ----------------------------------------------------------------------------
# OpenMP: only for pyoomph's own element loop (see
# dev_docs/openmp_assembly.md). Not REQUIRED under the default AUTO - the
# parallel assembly is opt-in at runtime (--omp N / problem.set_num_threads),
# so a toolchain without OpenMP (AppleClang without libomp being the usual one)
# must still produce a working build; everything threaded is behind
# PYOOMPH_HAS_OPENMP and set_num_threads(N>1) then says so once. Only
# PYOOMPH_USE_OPENMP=ON turns a missing OpenMP into a configure error.
#
# PUBLIC on pyoomph_core so that the flag reaches the nanobind translation
# units too: they include problem.hpp, which has inline members guarded by
# PYOOMPH_HAS_OPENMP, and a definition mismatch across TUs would be an ODR
# violation that only shows up as a corrupted object at runtime.
#
# The JIT compiler flags (pyoomph/generic/ccompiler.py) are NOT given -fopenmp:
# generated code is called from INSIDE the parallel region and only has to be
# reentrant, which it is since commit 25b24ea retired its file-scope globals.
# ----------------------------------------------------------------------------
# Two backends provide the identical, bit-identical threaded loop; exactly one is compiled in.
#
#   * macOS defaults to GCD/libdispatch. It ships NO OpenMP runtime, so nothing collides with the
#     Intel libiomp5 that MKL's Pardiso pulls in - that collision is "OMP: Error #15" and crashed an
#     Intel-mac test run (see dev_docs/openmp_assembly.md). libdispatch is in libSystem, so nothing
#     extra is linked. OpenMP is still selectable on macOS with -DPYOOMPH_USE_OPENMP=ON, in which
#     case it wins and GCD steps aside (the old static-libomp wheel path).
#   * Linux and Windows always use OpenMP (libgomp comes with GCC/MinGW); GCD is macOS-only.
#
# So the AUTO default is: GCD on macOS, OpenMP everywhere else - and never both, because the code in
# parallel_assembly.cpp would pick OpenMP when both are defined and the second runtime is exactly
# what we are avoiding here.
set(_pyoomph_use_gcd FALSE)
if(APPLE)
  set(PYOOMPH_USE_GCD "AUTO" CACHE STRING "GCD/libdispatch backend for --omp N on macOS: AUTO=the macOS default unless PYOOMPH_USE_OPENMP=ON, ON=require, OFF=never (fall back to OpenMP)")
  set_property(CACHE PYOOMPH_USE_GCD PROPERTY STRINGS AUTO ON OFF)
  if(PYOOMPH_USE_GCD STREQUAL "ON" OR (PYOOMPH_USE_GCD STREQUAL "AUTO" AND NOT PYOOMPH_USE_OPENMP STREQUAL "ON"))
    set(_pyoomph_use_gcd TRUE)
  endif()
endif()

if(_pyoomph_use_gcd)
  target_compile_definitions(pyoomph_core PUBLIC PYOOMPH_HAS_GCD)
  message(STATUS "Threaded element assembly (--omp N): GCD/libdispatch backend (no OpenMP runtime linked)")
else()
  if(PYOOMPH_USE_OPENMP)
    if(PYOOMPH_USE_OPENMP STREQUAL "AUTO")
      find_package(OpenMP COMPONENTS CXX)
    else()
      find_package(OpenMP REQUIRED COMPONENTS CXX)
    endif()
  endif()
  if(OpenMP_CXX_FOUND AND PYOOMPH_USE_OPENMP)
    target_link_libraries(pyoomph_core PUBLIC OpenMP::OpenMP_CXX)
    target_compile_definitions(pyoomph_core PUBLIC PYOOMPH_HAS_OPENMP)
    message(STATUS "Threaded element assembly (--omp N): OpenMP")
  elseif(PYOOMPH_USE_OPENMP)
    message(STATUS "OpenMP NOT found - building without parallel element assembly (pass -DPYOOMPH_USE_OPENMP=ON to make this an error instead)")
  else()
    message(STATUS "OpenMP disabled by PYOOMPH_USE_OPENMP=OFF - building without parallel element assembly")
  endif()
endif()

# ----------------------------------------------------------------------------
# The actual Python extension module: ONLY src/nanobind/*.cpp. This is the
# only target that pulls in nanobind/Python compile flags and include
# directories, and hence the only target that needs rebuilding when
# iterating over multiple Python versions - pyoomph_core and generic above
# are reused as-is.
#
# Built/installed as the "_core" submodule of the "pyoomph" package, i.e.
# importable as `pyoomph._core` (see NB_MODULE(_core, ...) in
# src/nanobind - the module name there must match this target name).
# ----------------------------------------------------------------------------
file(GLOB PYOOMPH_NANOBIND_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/src/nanobind/*.cpp")

# The TQMesh bindings live in their own subdirectory rather than next to the other binding
# sources, since they are the only translation units that may include TQMesh's headers - and the
# only ones that must not be compiled at all when PYOOMPH_HAS_TQMESH is off.
if(PYOOMPH_HAS_TQMESH)
  file(GLOB PYOOMPH_TQMESH_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/src/nanobind/tqmesh/*.cpp")
  if(NOT PYOOMPH_TQMESH_SOURCES)
    message(FATAL_ERROR "PYOOMPH_HAS_TQMESH is ON, but no .cpp sources were found under "
                        "src/nanobind/tqmesh - pass -DPYOOMPH_HAS_TQMESH=OFF to build without TQMesh.")
  endif()
  list(APPEND PYOOMPH_NANOBIND_SOURCES ${PYOOMPH_TQMESH_SOURCES})
endif()

# Same arrangement for the Spectra bindings: their own subdirectory, because they are the only
# translation units that may include Eigen's and Spectra's headers (see the include scoping further
# down for why that matters here in particular), and the only ones that must not be compiled when
# PYOOMPH_HAS_SPECTRA is off.
if(PYOOMPH_HAS_SPECTRA)
  file(GLOB PYOOMPH_SPECTRA_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/src/nanobind/spectra/*.cpp")
  if(NOT PYOOMPH_SPECTRA_SOURCES)
    message(FATAL_ERROR "PYOOMPH_HAS_SPECTRA is ON, but no .cpp sources were found under "
                        "src/nanobind/spectra - pass -DPYOOMPH_HAS_SPECTRA=OFF to build without Spectra.")
  endif()
  list(APPEND PYOOMPH_NANOBIND_SOURCES ${PYOOMPH_SPECTRA_SOURCES})
endif()

if(NOT PYOOMPH_NANOBIND_SOURCES)
  message(FATAL_ERROR "No .cpp sources found under src/nanobind - "
                       "cannot build the Python extension module.")
endif()

# STABLE_ABI: build against Python's Limited API (Py_LIMITED_API=0x030C0000,
# i.e. the 3.12 baseline - see nanobind-config.cmake's nanobind_add_module())
# so the resulting extension is ABI-compatible with every CPython >= 3.12,
# not just the one it was compiled against - one wheel instead of one per
# minor version (see .github/workflows/wheels.yml's PYTHON_STABLE_ABI_BASE /
# pyproject.toml's [[tool.cibuildwheel.overrides]] wheel.py-api=cp312).
# nanobind itself silently disables this (falls back to a classic,
# version-specific build) when configuring under CPython < 3.12 or when
# Python::SABIModule isn't available, so this flag is safe to pass
# unconditionally - no separate toggle is needed for the 3.10/3.11 builds.
#
# NB_DOMAIN pyoomph: nanobind shares its internal type/function registry
# across every nanobind extension loaded into the same domain (the default
# domain, if unspecified) - intentionally, so a binding project split across
# several compiled modules can still interoperate. _pyoomph_core is the only
# nanobind extension in this project, so it doesn't need or want that
# sharing; scoping it to its own domain instead means it can never collide
# ("type 'X' was already registered!") with some unrelated nanobind-based
# package a user happens to have installed alongside pyoomph.
nanobind_add_module(_pyoomph_core
  NB_DOMAIN pyoomph
  STABLE_ABI
  ${PYOOMPH_NANOBIND_SOURCES}
)

target_link_libraries(_pyoomph_core PRIVATE
  pyoomph_core
  ${PYOOMPH_GINAC_LIBRARY}
  ${PYOOMPH_CLN_LIBRARY}
)

if(NOT WIN32)
  target_link_libraries(_pyoomph_core PRIVATE dl)
endif()

if(APPLE)
  # Needed by src/mac_accelerate.cpp (Apple's Accelerate "Sparse Solvers" framework), which is
  # part of pyoomph_core's sources but only compiles to non-empty code on macOS (see the
  # __APPLE__ guard in src/mac_accelerate.hpp/.cpp).
  find_library(PYOOMPH_ACCELERATE_FRAMEWORK Accelerate REQUIRED)
  target_link_libraries(pyoomph_core PUBLIC "${PYOOMPH_ACCELERATE_FRAMEWORK}")
endif()

target_compile_definitions(_pyoomph_core PRIVATE
  $<$<BOOL:${PYOOMPH_USE_MPI}>:OOMPH_HAS_MPI>
  $<$<BOOL:${PYOOMPH_PARANOID}>:PARANOID>
  $<$<BOOL:${PYOOMPH_HAS_TQMESH}>:PYOOMPH_HAS_TQMESH>
  $<$<BOOL:${PYOOMPH_HAS_SPECTRA}>:PYOOMPH_HAS_SPECTRA>
)

if(PYOOMPH_HAS_TQMESH)
  # Kept off pyoomph_core AND off the rest of _pyoomph_core's TUs: nothing outside
  # src/nanobind/tqmesh sees TQMesh. Scoping the include dir to just the TQMesh sources (rather than
  # the whole target) matters because TQMesh's src/algorithm/ carries a Mesh.h that collides
  # case-insensitively with oomph-lib's mesh.h - on macOS a target-wide include dir made every other
  # TU's #include "mesh.h" (via oomph_lib.hpp) resolve to TQMesh's Mesh.h instead
  # (-Wnonportable-include-path, and the wrong header). The sources are only there once
  # tqmesh_external has run, hence the dependency.
  set_source_files_properties(${PYOOMPH_TQMESH_SOURCES}
    PROPERTIES INCLUDE_DIRECTORIES "${PYOOMPH_TQMESH_INCLUDE_DIRS}")
  add_dependencies(_pyoomph_core tqmesh_external)
endif()

if(PYOOMPH_HAS_SPECTRA)
  # Scoped to the Spectra sources for the same reason as TQMesh above, and here it is not a
  # hypothetical: Eigen's include root contains extensionless headers named Core, Dense,
  # Eigenvalues, LU, SVD and Householder. A target-wide include directory would put those in front
  # of every other TU in _pyoomph_core, and on a case-insensitive filesystem they collide with
  # pyoomph's own core.h. PYOOMPH_SPECTRA_DEPENDS is empty when the user supplied both source trees.
  set_source_files_properties(${PYOOMPH_SPECTRA_SOURCES}
    PROPERTIES INCLUDE_DIRECTORIES "${PYOOMPH_SPECTRA_INCLUDE_DIRS}")
  if(PYOOMPH_SPECTRA_DEPENDS)
    add_dependencies(_pyoomph_core ${PYOOMPH_SPECTRA_DEPENDS})
  endif()
endif()

if(PYOOMPH_USE_MPI)
  target_link_libraries(_pyoomph_core PRIVATE MPI::MPI_CXX)
endif()


 
if(PYOOMPH_GENERATE_STUBS)
  set(PYOOMPH_STUB_STAGE_DIR "${CMAKE_BINARY_DIR}/pyi-stage")
 
  set(PYOOMPH_STUB_EXTRA_COPY_ARGS "")
  if(PYOOMPH_COPY_STUBS_TO_SOURCE_TREE)
    list(APPEND PYOOMPH_STUB_EXTRA_COPY_ARGS
         --extra-copy-dir "${CMAKE_SOURCE_DIR}/pyoomph")
  endif()
  if(PYOOMPH_REQUIRE_STUBS)
    list(APPEND PYOOMPH_STUB_EXTRA_COPY_ARGS --required)
  endif()

  add_custom_command(TARGET _pyoomph_core POST_BUILD
    COMMAND "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/cmake/generate_stubs.py"
            --module-dir "$<TARGET_FILE_DIR:_pyoomph_core>"
            --module-name "_pyoomph_core"
            --stage-dir "${PYOOMPH_STUB_STAGE_DIR}"
            --patch-script "${CMAKE_SOURCE_DIR}/src/nanobind/patch_stubs.py"
            --python "${Python_EXECUTABLE}"
            ${PYOOMPH_STUB_EXTRA_COPY_ARGS}
    WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
    COMMENT "Generating .pyi stubs for pyoomph._pyoomph_core (best effort)"
    VERBATIM
  )
 
  # Whatever generate_stubs.py normalizes into PYOOMPH_STUB_STAGE_DIR (either
  # "_core.pyi" or a "_core/" package directory) is copied straight next to
  # the built extension. With PYOOMPH_REQUIRE_STUBS the install is NOT optional:
  # the POST_BUILD step above has already failed the build if it could not write
  # the stage dir, and dropping OPTIONAL means an empty one is caught here too
  # rather than producing a wheel that quietly lacks the stub.
  if(PYOOMPH_REQUIRE_STUBS)
    install(DIRECTORY "${PYOOMPH_STUB_STAGE_DIR}/" DESTINATION pyoomph)
  else()
    install(DIRECTORY "${PYOOMPH_STUB_STAGE_DIR}/" DESTINATION pyoomph OPTIONAL)
  endif()
endif()


# ----------------------------------------------------------------------------
# jitbridge.h defines the ABI the JIT-compiled element code is built against;
# pyoomph/__init__.py points the JIT include dir at the pyoomph/jitbridge/
# package directory, so a copy has to live there. It used to be a checked-in
# symlink to src/jitbridge.h, which degrades to a 21-byte text file holding the
# link path on a Windows clone (git needs core.symlinks plus the privilege to
# create one) - the build still succeeds and only tcc fails, much later, on an
# include it cannot resolve. Copy at configure time instead; configure_file
# re-runs configure whenever the source header changes.
#
# Both a source-tree copy and an install(FILES ...) are needed, for two
# different consumers. The source-tree copy serves editable installs, where
# get_jit_include_dir() derives from pyoomph.__file__ and therefore points at
# the source tree, not at the CMake install prefix. The install rule serves
# wheels: scikit-build-core copies wheel.packages = ["pyoomph"] with the
# repository's .gitignore applied, and this generated header is (rightly)
# ignored there, so the package copy alone would leave it out of the wheel.
# The two do not collide - packages_to_file_mapping() skips any source file
# whose target the CMake install already produced.
# ----------------------------------------------------------------------------
configure_file("${CMAKE_SOURCE_DIR}/src/jitbridge.h"
               "${CMAKE_SOURCE_DIR}/pyoomph/jitbridge/jitbridge.h" COPYONLY)
install(FILES "${CMAKE_SOURCE_DIR}/src/jitbridge.h" DESTINATION pyoomph/jitbridge)


# ----------------------------------------------------------------------------
# Install: _core lands inside the pyoomph/ package directory, so it is
# importable as `pyoomph._core` rather than as a top-level module.
# ----------------------------------------------------------------------------
install(TARGETS _pyoomph_core LIBRARY DESTINATION pyoomph)

# Ensure wheel installs carry the NO_MPI marker whenever MPI support is
# intentionally disabled so runtime imports skip mpi4py.
if(NOT PYOOMPH_USE_MPI)
  install(FILES "${_pyoomph_no_mpi_marker}" DESTINATION pyoomph)
endif()
