cmake_minimum_required(VERSION 3.24)

# One version source: pyproject.toml (`version = "x.y.z"`). packaging/pyproject-cuda.toml,
# CITATION.cff and docs/Doxyfile carry the same number and are checked against it by the suite's
# release pre-flight (../tools/release/check_release_state.sh).
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/pyproject.toml" _peclet_pnm_version_line REGEX "^version")
string(REGEX REPLACE "^version *= *\"([^\"]+)\".*$" "\\1" PECLET_PNM_VERSION "${_peclet_pnm_version_line}")
if(NOT PECLET_PNM_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+")
  message(FATAL_ERROR "could not read the version from pyproject.toml (got '${_peclet_pnm_version_line}')")
endif()
project(peclet_pnm VERSION ${PECLET_PNM_VERSION} LANGUAGES CXX)

# peclet.pnm — pore-network extraction from SDF geometry (Kokkos GPU compute + the pure-C++ SDFReader
# VTI reader), as an importable Python module. Split out of peclet-flow (2026-07): the CFD solve lives
# in peclet.flow; this package is the "pnm_from_sdf" extraction feature on its own.
#
# The Kokkos backend (CUDA / HIP / OpenMP) is selected by the install prefix
# (extern/install/<backend>, built by ../tools/bootstrap_deps.sh), exactly like flow/dem.
#
# Build (nanobind is found via the active interpreter):
#   cmake -S . -B build -DCMAKE_PREFIX_PATH="$PWD/../extern/install/nvidia-cuda"
#   cmake --build build -j        ->  build/peclet/pnm/_pnm.*.so   (import as peclet.pnm)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()

# Redistributable single-GPU CUDA wheel: libcudart is provided by the `nvidia-cuda-runtime` PyPI
# dependency (installed to site-packages/nvidia/cu13/lib), not the system. Mirrors peclet-flow.
option(PECLET_CUDA_RUNTIME_WHEEL "RPATH the module to the nvidia-cuda-runtime wheel's libcudart" OFF)

# Multi-rank: -DPECLET_PNM_MPI=ON links MPI + exposes mpi_rank/mpi_size/mpi_block and
# extract_pore_network_mpi (the distributed extraction on the core ORB decomposition, bit-exact to
# single-rank). OFF (default) leaves the single-rank module byte-identical. Mirrors PECLET_FLOW_MPI.
option(PECLET_PNM_MPI "Build the pnm module with the distributed (MPI) extraction exposed" OFF)

# Tests: -DPECLET_PNM_BUILD_TESTS=ON registers the single-rank C++ contract (tests/kokkos), the
# Python binding smoke + the 7199-pore packing_ring gate (tests/python; the gate SKIPS when the
# suite data file is absent) and, with PECLET_PNM_MPI, the distributed np=1,2,4 suite
# (tests/kokkos_mpi). OFF by default so wheel builds are unaffected; ON in CI and the dev recipe.
option(PECLET_PNM_BUILD_TESTS "Build and register the pnm test suites (ctest)" OFF)

# Dependencies via the vendored PecletDeps helper: an installed Kokkos prefix + sibling checkout for
# the dev/suite build, or FetchContent-built Kokkos + fetched core headers for a self-contained
# sdist/wheel (cibuildwheel). See cmake/PecletDeps.cmake.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(PecletDeps)
peclet_require_kokkos()
peclet_require_nanobind()
peclet_sibling_include(peclet-core "${PECLET_CORE_TAG}" "../core" PECLET_CORE_INCLUDE)

# pnm — pore-network extraction (Kokkos compute + pure-C++ SDFReader VTI reader).
# NB_STATIC: bundle nanobind's runtime into the module. NOMINSIZE: nanobind's -Os is rejected by nvcc
# since the Kokkos device sources compile as CXX through the launch compiler. The extension is
# assembled into the PEP-420 peclet namespace as `peclet.pnm` (private `_pnm` re-exported by
# peclet/pnm/__init__.py, kept as a plain file under packaging/ and staged into <build>/peclet/... so
# `PYTHONPATH=<build> python …` works in the dev loop too).
nanobind_add_module(pnm NB_STATIC NOMINSIZE src/pnm_bindings.cpp src/sdf_reader.cpp)
set_target_properties(pnm PROPERTIES OUTPUT_NAME _pnm        # -> peclet.pnm._pnm (NB_MODULE(_pnm))
  LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/peclet/pnm")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/packaging/pnm_init.py
               ${CMAKE_CURRENT_BINARY_DIR}/peclet/pnm/__init__.py COPYONLY)
target_include_directories(pnm PRIVATE src "${PECLET_CORE_INCLUDE}")
target_link_libraries(pnm PRIVATE Kokkos::kokkos)
# HIP/lld strictness: nanobind's --gc-sections can drop Kokkos SharedAllocationRecord<HIPSpace>
# vtables it only later finds referenced. Keep sections on HIP (nvcc/ld is lenient).
if(Kokkos_ENABLE_HIP)
  target_link_options(pnm PRIVATE -Wl,--no-gc-sections)
  # HIP/lld experiment 1 (docs/RELEASE.md §8): nanobind sets CXX_VISIBILITY_PRESET hidden, and under hipcc
  # the host objects then reference the Kokkos SharedAllocationRecord / shared_ptr control-block vtables
  # as hidden symbols that nothing defines ("undefined hidden symbol: vtable for ..."). Default visibility
  # on the HIP path only; CUDA/OpenMP builds are untouched.
  set_target_properties(pnm PROPERTIES CXX_VISIBILITY_PRESET default)
endif()

# Distributed (MPI) extraction: compile the binding TU with the gating flag + link MPI (the core
# grid halo lives in the header-only PECLET_CORE_INCLUDE). Mirrors flow's PECLET_FLOW_MPI wiring.
if(PECLET_PNM_MPI)
  find_package(MPI REQUIRED COMPONENTS CXX)
  target_compile_definitions(pnm PRIVATE PECLET_PNM_MPI=1)
  target_link_libraries(pnm PRIVATE MPI::MPI_CXX)
  message(STATUS "pnm: distributed extraction ENABLED (MPI)")
endif()

# Redistributable CUDA wheel: point the module at the nvidia-cuda-runtime wheel's libcudart via a
# relative $ORIGIN RPATH (peclet/pnm -> site-packages -> nvidia/cu13/lib).
if(PECLET_CUDA_RUNTIME_WHEEL)
  set_target_properties(pnm PROPERTIES
    INSTALL_RPATH "$ORIGIN/../../nvidia/cu13/lib" INSTALL_RPATH_USE_LINK_PATH OFF)
endif()

# --- tests (one tree per backend: the same Kokkos/MPI targets + core headers as the module) -----
if(PECLET_PNM_BUILD_TESTS)
  enable_testing()
  add_subdirectory(tests/kokkos)
  if(PECLET_PNM_MPI)
    add_subdirectory(tests/kokkos_mpi)
  endif()
  # Python binding tests run against the module staged in this build tree (PYTHONPATH=<build>).
  add_test(NAME pnm_python_smoke
           COMMAND "${Python_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/python/test_smoke.py")
  # The 7199-pore gate needs the suite's ring packing (../flow/data, not shipped with this repo):
  # the script exits 77 when the file is absent and ctest reports the test as SKIPPED, not passed.
  set(PECLET_PNM_PACKING_RING_VTI "${CMAKE_CURRENT_SOURCE_DIR}/../flow/data/packing_ring.vti"
      CACHE FILEPATH "The 256^3 ring-packing SDF the packing_ring gate extracts (7199 pores)")
  add_test(NAME pnm_python_packing_ring
           COMMAND "${Python_EXECUTABLE}"
                   "${CMAKE_CURRENT_SOURCE_DIR}/tests/python/test_packing_ring.py"
                   "${PECLET_PNM_PACKING_RING_VTI}")
  set_tests_properties(pnm_python_smoke pnm_python_packing_ring PROPERTIES
    ENVIRONMENT "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}")
  set_tests_properties(pnm_python_packing_ring PROPERTIES SKIP_RETURN_CODE 77)
endif()

# --- pip / scikit-build-core install rule -------------------------------------------------------
if(DEFINED SKBUILD)
  install(TARGETS pnm LIBRARY DESTINATION peclet/pnm COMPONENT python)
  install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/pnm_init.py
          DESTINATION peclet/pnm RENAME __init__.py COMPONENT python)
endif()
