# Python bindings for the core Lagrangian halo (migration + ghosts) and the analytic-SDF geometry. 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()

# mpi_bindings: host-only Lagrangian halo (no Kokkos). Exposed as the `peclet.core.mpi` submodule (the
# nanobind module leaf name is `mpi`; NB_MODULE(mpi, ...) in mpi_bindings.cpp), so the CMake target keeps its
# descriptive name but the built extension is `mpi.*.so` installed under peclet/core/.
nanobind_add_module(mpi_bindings NB_STATIC mpi_bindings.cpp)
if(Kokkos_ENABLE_HIP)
  set_target_properties(mpi_bindings PROPERTIES CXX_VISIBILITY_PRESET default)  # HIP/lld vtable experiment, see flow/CMakeLists.txt
endif()
# -> peclet.core.mpi (NB_MODULE(mpi)). The peclet/core 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/core so `PYTHONPATH=<build> python …` works in dev too.
set_target_properties(mpi_bindings PROPERTIES OUTPUT_NAME mpi
  LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/peclet/core")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/packaging/core_init.py
               ${CMAKE_CURRENT_BINARY_DIR}/peclet/core/__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.
foreach(_stub_mod mpi geom)
  configure_file(${CMAKE_CURRENT_SOURCE_DIR}/packaging/core_${_stub_mod}.pyi
                 ${CMAKE_CURRENT_BINARY_DIR}/peclet/core/${_stub_mod}.pyi COPYONLY)
endforeach()
file(TOUCH ${CMAKE_CURRENT_BINARY_DIR}/peclet/core/py.typed)
target_include_directories(mpi_bindings PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
target_link_libraries(mpi_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(mpi_bindings PRIVATE
  "PECLET_CORE_BUILD_TOOLCHAIN=\"${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} ${CMAKE_BUILD_TYPE} ${CMAKE_SYSTEM_PROCESSOR}\"")
# geom_bindings: host-only analytic-SDF scene authoring + rigid-body mass properties. No MPI, no
# Kokkos -- preprocessing tooling. -> peclet.core.geom (NB_MODULE(geom, ...)).
nanobind_add_module(geom_bindings NB_STATIC geom_bindings.cpp)
if(Kokkos_ENABLE_HIP)
  set_target_properties(geom_bindings PROPERTIES CXX_VISIBILITY_PRESET default)  # HIP/lld vtable experiment, see flow/CMakeLists.txt
endif()
set_target_properties(geom_bindings PROPERTIES OUTPUT_NAME geom
  LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/peclet/core"
  CXX_STANDARD 20)
target_include_directories(geom_bindings PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)

# `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 mpi_bindings LIBRARY DESTINATION peclet/core COMPONENT python)
  install(TARGETS geom_bindings LIBRARY DESTINATION peclet/core COMPONENT python)
  install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/core_init.py
          DESTINATION peclet/core RENAME __init__.py COMPONENT python)
  install(FILES ${CMAKE_CURRENT_BINARY_DIR}/peclet/core/py.typed DESTINATION peclet/core COMPONENT python)
  foreach(_stub_mod mpi geom)
    install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/core_${_stub_mod}.pyi
            DESTINATION peclet/core RENAME ${_stub_mod}.pyi COMPONENT python)
  endforeach()
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.{geom,mpi} entry
# path against python/state_hash_reference.json); 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()
