# Python bindings for the core Lagrangian halo — migration + ghosts (`peclet.core.mpi`, and
# `peclet.halo` from 1.2.0). Standalone (host C++ + MPI + nanobind + header-only core); nanobind is found via the
# active interpreter (cmake/SuiteNanobind.cmake at the suite root).
#   cmake -S python -B python/build -DCMAKE_PREFIX_PATH=../extern/install/host-openmp
#   cmake --build python/build -j
#   ctest --test-dir python/build --output-on-failure      # test_mpi.py np=1,2,4(,8), state_hash, ndarray interop
#   PYTHONPATH=python/build mpirun -np 4 python3 python/test_mpi.py
# Run with a Python that has mpi4py built against the same MPI (+ numpy, pytest).
cmake_minimum_required(VERSION 3.24)
project(peclet_core_python LANGUAGES CXX)

# Test registration helpers (SKIP_RETURN_CODE 77 + the mpi / np8 labels) shared with the C++ battery.
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/PecletCoreTest.cmake)
option(PECLET_CORE_PYTHON_TESTS "Register the Python tests (python/test_*.py, tests/python) with ctest" ON)

# C++20 throughout (suite/docs/STYLE.md; the core headers are C++20).
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(MPI REQUIRED COMPONENTS CXX)
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/PecletCorePinMpiexec.cmake)  # launcher = the MPI we link

# SuiteNanobind: the umbrella's copy (../../cmake) in a suite checkout, the vendored copy in ../cmake
# otherwise -- an sdist built from this repo alone has no ../../cmake (pip install peclet-core /
# peclet[mpi] failed on exactly that include from 0.1.0 through 0.6.0). Keep core/cmake/SuiteNanobind.cmake
# byte-identical to the umbrella's; the suite path is searched first.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake" "${CMAKE_CURRENT_SOURCE_DIR}/../cmake")
include(SuiteNanobind)
suite_require_nanobind()

# halo_bindings: host-only Lagrangian halo (no Kokkos). Exposed as the `peclet.halo` package
# (NB_MODULE(_halo, ...) in halo_bindings.cpp -> peclet/halo/_halo.*.so beside an __init__.py that
# re-exports it). `peclet.core.mpi` remains available through the peclet-core compatibility shell
# (packaging/pyproject-core.toml) until 2.0.0 — suite/docs/CORE_BOUNDARY.md.
nanobind_add_module(halo_bindings NB_STATIC halo_bindings.cpp)
if(Kokkos_ENABLE_HIP)
  set_target_properties(halo_bindings PROPERTIES CXX_VISIBILITY_PRESET default)  # HIP/lld vtable experiment, see flow/CMakeLists.txt
endif()
# The peclet/halo package __init__.py is kept as a plain file under packaging/ (OUTSIDE any importable
# peclet/ dir, so an incomplete source package can never shadow the installed one) and staged into
# <build>/peclet/halo so `PYTHONPATH=<build> python …` works in dev too.
set_target_properties(halo_bindings PROPERTIES OUTPUT_NAME _halo
  LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/peclet/halo")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/packaging/halo_init.py
               ${CMAKE_CURRENT_BINARY_DIR}/peclet/halo/__init__.py COPYONLY)
# Type stubs (packaging/core_<mod>.pyi, generated by `python -m nanobind.stubgen` against the built
# modules -- regenerate after changing a binding) are staged beside the extensions as <mod>.pyi, with
# the PEP 561 marker, so editors and type checkers see the surface in dev and in the wheel alike.
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/packaging/_halo.pyi
               ${CMAKE_CURRENT_BINARY_DIR}/peclet/halo/_halo.pyi COPYONLY)
file(TOUCH ${CMAKE_CURRENT_BINARY_DIR}/peclet/halo/py.typed)
target_include_directories(halo_bindings PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
target_link_libraries(halo_bindings PRIVATE MPI::MPI_CXX)
# `peclet.core.mpi.build_toolchain`: the byte gate (state_hash.py) compares hashes only between builds
# of one toolchain (compiler, version, build type) and SKIPS (77) against a reference recorded elsewhere.
target_compile_definitions(halo_bindings PRIVATE
  "PECLET_CORE_BUILD_TOOLCHAIN=\"${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} ${CMAKE_BUILD_TYPE} ${CMAKE_SYSTEM_PROCESSOR}\"")
# geom_bindings moved OUT on 2026-09-21 to the peclet-geom package (`peclet.geom`) —
# suite/docs/CORE_BOUNDARY.md. It was host-only with no MPI in it, and living here made it
# unobtainable without an MPI toolchain because of the find_package(MPI REQUIRED) above. The C++
# headers (include/peclet/core/geom/) STAY here; only the bindings and their packaging moved.

# `pip install .` (scikit-build-core, via ../pyproject.toml with cmake.source-dir=python) installs the
# built modules into the `peclet.core` package. Inert for a plain `cmake --build`, so the developer
# workflow is unchanged.
if(DEFINED SKBUILD)
  install(TARGETS halo_bindings LIBRARY DESTINATION peclet/halo COMPONENT python)
  install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/halo_init.py
          DESTINATION peclet/halo RENAME __init__.py COMPONENT python)
  install(FILES ${CMAKE_CURRENT_BINARY_DIR}/peclet/halo/py.typed DESTINATION peclet/halo COMPONENT python)
  install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/_halo.pyi DESTINATION peclet/halo COMPONENT python)
endif()

# The AMR octree bindings (peclet.core.amr) moved to the peclet-amr package (../amr, `peclet.amr`)
# on 2026-09-10 — suite/docs/QUALITY_PLAN.md D6 / G.2. Kokkos is located here only for the
# ndarray-interop test module below.
find_package(Kokkos CONFIG QUIET)

# ---- The Python tests as ctests (suite/docs/QUALITY_PLAN.md §3.D: nothing runs only by hand). ----
# PECLET_CORE_TEST_NP tells the script how many ranks ctest launched; the script exits non-zero if
# comm.size differs, so a launcher/mpi4py MPI mismatch (N singletons) is a FAILURE, not a pass.
# test_mpi.py drives peclet.core.mpi through mpi4py at np=1,2,4,8 (np8 labelled like the C++ tests);
# state_hash.py is the structural byte gate (fixed-seed runs of every peclet.core.mpi entry
# path against python/state_hash_reference.json; geom's half lives in peclet-geom now); the ndarray interop pytest (tests/python) builds
# its own probe module against Kokkos.
if(PECLET_CORE_PYTHON_TESTS)
  enable_testing()
  set(_py_env "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}")
  foreach(np IN ITEMS 1 2 4 8)
    add_test(NAME python_mpi_np${np}
             COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} ${np} ${MPIEXEC_PREFLAGS}
                     "${Python_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/test_mpi.py" ${MPIEXEC_POSTFLAGS})
    set(_labels "mpi;python")
    if(np GREATER 4)
      list(APPEND _labels np8)
    endif()
    set_tests_properties(python_mpi_np${np} PROPERTIES LABELS "${_labels}"
      ENVIRONMENT "${_py_env};PECLET_CORE_TEST_NP=${np}")
  endforeach()
  add_test(NAME python_state_hash
           COMMAND "${Python_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/state_hash.py"
                   --check "${CMAKE_CURRENT_SOURCE_DIR}/state_hash_reference.json")
  # The reference is toolchain-specific (its `toolchain` key): another compiler / version / build
  # type exits 77 and ctest reports the gate SKIPPED (CI), never passed.
  set_tests_properties(python_state_hash PROPERTIES LABELS "python" SKIP_RETURN_CODE 77
    ENVIRONMENT "${_py_env};OMP_NUM_THREADS=1")
  if(Kokkos_FOUND)
    add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../tests/python tests_python)
  else()
    message(STATUS "python tests: Kokkos not found — ndarray_interop not registered")
  endif()
endif()
