cmake_minimum_required(VERSION 3.18)

# ---------------------------------------------------------------------------
# scikit-build-core calls CMake before the project() command to detect the
# Python interpreter; the actual languages are declared below.
# ---------------------------------------------------------------------------
project(selinv LANGUAGES C CXX Fortran)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# ---------------------------------------------------------------------------
# Dependencies
# ---------------------------------------------------------------------------

# Python – must be found BEFORE nanobind (nanobind's cmake config requires it).
# scikit-build-core sets this up automatically; for a plain cmake invocation
# the interpreter and development headers must be on PATH / in the prefix.
find_package(Python 3.9
    REQUIRED COMPONENTS Interpreter Development.Module
    OPTIONAL_COMPONENTS Development.SABIModule
)

# nanobind – provided as a build requirement via scikit-build-core.
# In a plain CMake build outside scikit-build-core, run:
#   pip install nanobind
# and CMake will find it through the Python site-packages.
find_package(nanobind CONFIG REQUIRED)

# BLAS / LAPACK – required by the Fortran solver.
find_package(BLAS REQUIRED)
find_package(LAPACK REQUIRED)

# ---------------------------------------------------------------------------
# Optional: METIS reordering (Node Nested Dissection)
# ---------------------------------------------------------------------------
option(SELINV_USE_METIS "Enable METIS reordering support" OFF)

if(SELINV_USE_METIS)
    find_library(METIS_LIB NAMES metis REQUIRED
        DOC "Path to the METIS library (libmetis)")
    find_path(METIS_INCLUDE_DIR NAMES metis.h REQUIRED
        DOC "Path to the METIS include directory")
    if(NOT METIS_LIB OR NOT METIS_INCLUDE_DIR)
        message(FATAL_ERROR
            "METIS requested but not found.  "
            "Set METIS_LIB and METIS_INCLUDE_DIR manually or disable "
            "SELINV_USE_METIS.")
    endif()
    message(STATUS "selinv: METIS support enabled (${METIS_LIB})")
endif()

# ---------------------------------------------------------------------------
# Fortran sources – all .f files in extern/selinv/
# ---------------------------------------------------------------------------
file(GLOB FORTRAN_SOURCES
    "${CMAKE_CURRENT_SOURCE_DIR}/extern/selinv/*.f"
)

if(NOT FORTRAN_SOURCES)
    message(FATAL_ERROR "No Fortran sources found in extern/selinv/")
endif()

# ---------------------------------------------------------------------------
# Static library: Fortran core (supernodal LDL' + SelInv)
# ---------------------------------------------------------------------------
add_library(selinv_fortran STATIC
    ${FORTRAN_SOURCES}
)

# Position-independent code is required for shared libraries / Python extensions
set_target_properties(selinv_fortran PROPERTIES
    POSITION_INDEPENDENT_CODE ON
)

target_link_libraries(selinv_fortran PUBLIC
    ${LAPACK_LIBRARIES}
    ${BLAS_LIBRARIES}
)

# On macOS use Accelerate instead of the separately-found BLAS/LAPACK when the
# detected libraries resolve to the Accelerate framework (cmake ≥ 3.22 sets the
# BLA_VENDOR to Apple for this case).
if(APPLE)
    target_link_options(selinv_fortran PUBLIC -framework Accelerate)
endif()

# ---------------------------------------------------------------------------
# Static library: thread-safe C interface (replaces C2Finterface.c)
# ---------------------------------------------------------------------------
add_library(selinv_c_interface STATIC
    "${CMAKE_CURRENT_SOURCE_DIR}/csrc/selinv_interface.c"
)

target_include_directories(selinv_c_interface PUBLIC
    "${CMAKE_CURRENT_SOURCE_DIR}/csrc"
)

target_link_libraries(selinv_c_interface PUBLIC selinv_fortran)

set_target_properties(selinv_c_interface PROPERTIES
    POSITION_INDEPENDENT_CODE ON
)

if(SELINV_USE_METIS)
    target_compile_definitions(selinv_c_interface PRIVATE METIS)
    target_include_directories(selinv_c_interface PRIVATE "${METIS_INCLUDE_DIR}")
    target_link_libraries(selinv_c_interface PUBLIC "${METIS_LIB}")
endif()

# ---------------------------------------------------------------------------
# nanobind Python extension: _selinv_core
# ---------------------------------------------------------------------------
nanobind_add_module(_selinv_core
    NB_STATIC          # link nanobind statically → fewer runtime dependencies
    "${CMAKE_CURRENT_SOURCE_DIR}/csrc/selinv_bind.cpp"
)

# On Linux, bind all function references inside _selinv_core.so to the definitions
# inside the same .so (i.e. our local 3-argument dscal_ from dscal.f), so
# that MKL's 4-argument dscal_ — which is loaded transitively via numpy/scipy
# at runtime — cannot override them and cause a segfault.
# The equivalent on macOS (two-level namespace) is not needed because dylibs
# resolve symbols at link time rather than load time.
target_link_options(_selinv_core PRIVATE
    $<$<PLATFORM_ID:Linux>:-Wl,-Bsymbolic-functions>
)

target_include_directories(_selinv_core PRIVATE
    "${CMAKE_CURRENT_SOURCE_DIR}/csrc"
)

target_link_libraries(_selinv_core PRIVATE selinv_c_interface)

if(SELINV_USE_METIS)
    target_compile_definitions(_selinv_core PRIVATE SELINV_HAS_METIS)
endif()

# Install the extension alongside the Python package sources
install(TARGETS _selinv_core
    LIBRARY DESTINATION selinv
    RUNTIME DESTINATION selinv
)

# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
message(STATUS "")
message(STATUS "selinv configuration summary")
message(STATUS "  Fortran compiler : ${CMAKE_Fortran_COMPILER_ID} ${CMAKE_Fortran_COMPILER_VERSION}")
message(STATUS "  C   compiler     : ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION}")
message(STATUS "  C++ compiler     : ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}")
message(STATUS "  BLAS             : ${BLAS_LIBRARIES}")
message(STATUS "  LAPACK           : ${LAPACK_LIBRARIES}")
message(STATUS "  METIS support    : ${SELINV_USE_METIS}")
message(STATUS "")
