# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Susi Lehtola
cmake_minimum_required(VERSION 3.16)
project(wignernj VERSION 0.8.0 LANGUAGES C)

# Default to an optimised build when the consumer hasn't picked a
# CMAKE_BUILD_TYPE (e.g. plain `cmake -B build`).  libwignernj is
# positioned as a high-performance math library, so the unconfigured
# default should be Release rather than the empty (no-optimisation)
# default of single-config generators like Make and Ninja.  Multi-
# config generators (Visual Studio, Xcode) ignore this; they pick the
# configuration at build time via --config.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE Release CACHE STRING
        "Build type (Release|RelWithDebInfo|Debug|MinSizeRel)" FORCE)
endif()

option(BUILD_SHARED_LIBS  "Build shared library"         ON)
option(WIGNERNJ_BUILD_FORTRAN      "Build Fortran interface"      ON)
option(WIGNERNJ_BUILD_PYTHON       "Build Python extension"       OFF)
option(WIGNERNJ_BUILD_MPFR         "Build MPFR arbitrary-precision interface" OFF)
option(WIGNERNJ_BUILD_QUADMATH     "Build libquadmath / __float128 interface"  OFF)
option(WIGNERNJ_BUILD_FLINT        "Use FLINT/GMP/MPFR for the bigint backend"  OFF)
option(WIGNERNJ_BUILD_TESTS        "Build test suite"             ON)
option(WIGNERNJ_BUILD_EXAMPLES     "Build language-binding examples" ON)
option(WIGNERNJ_BUILD_CXX_TESTS    "Build C++ header tests"      ON)
option(WIGNERNJ_BUILD_LTO          "Enable link-time optimisation (LTO/IPO)"  ON)
option(WIGNERNJ_BUILD_COVERAGE     "Build with --coverage instrumentation (gcc/clang)" OFF)

# ── Diagnostic for legacy (pre-0.8.0) unprefixed BUILD_* names ───────────────
# Before 0.8.0 the project-specific options were called BUILD_FORTRAN,
# BUILD_PYTHON, etc.  They were renamed to WIGNERNJ_BUILD_* to avoid
# colliding with other projects when libwignernj is consumed via
# add_subdirectory().  CMake silently ignores unknown cache variables, so
# a downstream caller who still passes -DBUILD_FORTRAN=ON would otherwise
# get a default-configured build with no warning.  We catch each legacy
# name and fatal-error with a pointer to the new spelling.  BUILD_SHARED_LIBS
# is the standard CMake builtin and intentionally kept unprefixed.
set(_wignernj_legacy_options
    BUILD_FORTRAN BUILD_PYTHON BUILD_MPFR BUILD_QUADMATH BUILD_FLINT
    BUILD_TESTS BUILD_EXAMPLES BUILD_CXX_TESTS BUILD_LTO BUILD_COVERAGE)
foreach(_legacy_opt IN LISTS _wignernj_legacy_options)
    if(DEFINED ${_legacy_opt})
        message(FATAL_ERROR
            "${_legacy_opt} was renamed to WIGNERNJ_${_legacy_opt} in "
            "libwignernj 0.8.0 to avoid colliding with downstream "
            "options.  Update your build invocation to use "
            "-DWIGNERNJ_${_legacy_opt}=${${_legacy_opt}}.")
    endif()
endforeach()
unset(_wignernj_legacy_options)

# Probe the toolchain for LTO/IPO support and silently fall back if it
# isn't available -- LTO defaults to ON because libwignernj is
# positioned as a high-performance math library and the cross-TU
# inlining of pfrac.c helpers into the Racah-sum hot path is worth a
# few percent across every consumer who just runs `cmake`.  Users who
# need a fast build (Debug, sanitizers, static analysis) can pass
# -DWIGNERNJ_BUILD_LTO=OFF to opt out.
if(WIGNERNJ_BUILD_LTO)
    include(CheckIPOSupported)
    check_ipo_supported(RESULT _wignernj_ipo_supported OUTPUT _wignernj_ipo_error)
    if(NOT _wignernj_ipo_supported)
        message(STATUS
            "Link-time optimisation not supported by this toolchain "
            "(${_wignernj_ipo_error}); WIGNERNJ_BUILD_LTO disabled.")
        set(WIGNERNJ_BUILD_LTO OFF CACHE BOOL "" FORCE)
    endif()
endif()

# MSVC's /GL (LTO) emits LLVM-bitcode-style "anonymous IL" object files
# instead of COFF, which then breaks the WINDOWS_EXPORT_ALL_SYMBOLS
# auto-export step:  cmake -E __create_def reads the .obj files to
# discover exported symbols, and reports
#   "unrecognized file format in '...wignernj.dir/Release/xalloc.obj'"
# when LTO is on.  We rely on auto-export to keep wignernj.lib in sync
# with the public API, so the cleanest fix is to disable LTO on MSVC.
# Consumers who want LTO with MSVC can hand-write a .def file or sprinkle
# __declspec(dllexport) on every public function and pass
# -DWINDOWS_EXPORT_ALL_SYMBOLS=OFF -DWIGNERNJ_BUILD_LTO=ON, but that's a hostile
# default for a library positioned for embeddability across toolchains.
if(WIGNERNJ_BUILD_LTO AND MSVC)
    message(STATUS
        "WIGNERNJ_BUILD_LTO disabled on MSVC: /GL emits IL .obj files that "
        "the WINDOWS_EXPORT_ALL_SYMBOLS auto-export step cannot parse.")
    set(WIGNERNJ_BUILD_LTO OFF CACHE BOOL "" FORCE)
endif()

# ── Coverage instrumentation ──────────────────────────────────────────────────
# When WIGNERNJ_BUILD_COVERAGE=ON, every target in this build gets compiled and
# linked with --coverage, and -O0 is forced so that the line-by-line
# coverage map is not mangled by inlining or dead-code elimination.
# The instrumentation works on gcc and clang; on MSVC the option is a
# no-op (the toolchain does not understand --coverage).  Pair this with
# `lcov` (or grcov for clang's source-based coverage) to merge .gcda
# files into a report.  See the "Ubuntu / GCC / coverage" cell in
# .github/workflows/ci.yml for the exact command sequence.
if(WIGNERNJ_BUILD_COVERAGE)
    if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
        add_compile_options(--coverage -O0)
        add_link_options(--coverage)
        add_compile_definitions(WIGNERNJ_COVERAGE)
    else()
        message(WARNING
            "WIGNERNJ_BUILD_COVERAGE=ON requested but compiler '${CMAKE_C_COMPILER_ID}' "
            "does not recognise --coverage; flag ignored.")
    endif()
endif()

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# On Windows, the dynamic linker only searches the executable's own
# directory and PATH for required DLLs.  Without intervention, CMake
# would put wignernj.dll in <build>/Release and the test executables
# in <build>/tests/Release, and ctest would fail with status 0xc0000135
# (STATUS_DLL_NOT_FOUND).  Co-locate every runtime output in a single
# directory so the in-tree tests resolve the library naturally.  The
# Linux/macOS layout is left untouched: rpath/runpath already takes
# care of in-tree resolution there, and downstream tooling such as
# benchmarks/Makefile expects libwignernj.so directly under build/.
if(WIN32)
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
endif()

include(GNUInstallDirs)

# Core C library.  The bigint backend is selected at configure time:
# the schoolbook src/bigint.c is the default; the FLINT backend
# (src/bigint_flint.c) replaces it when WIGNERNJ_BUILD_FLINT=ON.
if(WIGNERNJ_BUILD_FLINT)
    set(BIGINT_SOURCE src/bigint_flint.c)
else()
    set(BIGINT_SOURCE src/bigint.c)
endif()
set(LIB_SOURCES
    src/xalloc.c
    src/primes.c
    ${BIGINT_SOURCE}
    src/pfrac.c
    src/scratch.c
    src/wigner_exact.c
    src/wigner3j.c
    src/wigner6j.c
    src/wigner9j.c
    src/clebsch.c
    src/racah.c
    src/fano_x.c
    src/gaunt.c
    src/real_ylm_in_complex_ylm.c
)

add_library(wignernj ${LIB_SOURCES})
set_target_properties(wignernj PROPERTIES
    C_STANDARD 99
    C_STANDARD_REQUIRED ON
    VERSION   ${PROJECT_VERSION}
    SOVERSION ${PROJECT_VERSION_MAJOR}
    INTERPROCEDURAL_OPTIMIZATION ${WIGNERNJ_BUILD_LTO}
    # Auto-generate the MSVC import library by exporting every function
    # symbol.  On ELF/Mach-O the public-by-default visibility makes this
    # a no-op; on Windows it is what saves us from sprinkling
    # __declspec(dllexport) over every public function (or supplying a
    # hand-written .def file) to make wignernj.lib appear next to
    # wignernj.dll.  Note that data symbols (the prime tables) are *not*
    # auto-exported and are decorated explicitly via the WIGNERNJ_DATA
    # macro in src/primes.h, gated on WIGNERNJ_BUILDING_DLL below.
    WINDOWS_EXPORT_ALL_SYMBOLS ON
)
if(BUILD_SHARED_LIBS)
    # Drive the WIGNERNJ_DATA macro alternation in src/primes.h:
    # WIGNERNJ_BUILDING_DLL only on the library's own translation units
    # (PRIVATE), WIGNERNJ_DLL also on every consumer (PUBLIC) so that
    # the data symbols carry __declspec(dllimport) where consumed.  In
    # a static build neither macro is defined and the macro stays
    # inert, which is what we want for static linking.
    target_compile_definitions(wignernj
        PRIVATE WIGNERNJ_BUILDING_DLL
        PUBLIC  WIGNERNJ_DLL)
endif()
target_include_directories(wignernj
    PUBLIC  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
            $<INSTALL_INTERFACE:include>
    PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
)
if(NOT MSVC)
    # Optimisation level is taken from CMAKE_BUILD_TYPE (Release →
    # -O3 -DNDEBUG, RelWithDebInfo → -O2 -g -DNDEBUG, Debug → -O0 -g).
    # The previous explicit -O2 here actively defeated Release builds
    # because target_compile_options appends after build-type flags
    # and the trailing -O wins.  We keep just the warning flags now.
    target_compile_options(wignernj PRIVATE -Wall -Wextra)
else()
    # Same idea on MSVC: /Ox-class flags come from CMAKE_BUILD_TYPE
    # (Release adds /O2 /Ob2 /DNDEBUG); we add only the warning level.
    target_compile_options(wignernj PRIVATE /W3)
endif()
if(NOT WIN32)
    # libm is needed for sqrt/ldexp/etc. on every Unix-like target;
    # MSVC bundles the math functions into the default C runtime.
    target_link_libraries(wignernj PRIVATE m)
endif()

# libquadmath / __float128 interface (optional)
#
# __float128 is a GCC/Clang/ICC extension on Linux/macOS; not provided
# by MSVC.  The runtime functions (sqrtq, ldexpq, ...) and the header
# <quadmath.h> live in the libquadmath package shipped with GCC.
# Clang supports __float128 as a builtin and links libquadmath at link
# time, but its driver does *not* add GCC's internal include directory
# to the search path, so <quadmath.h> is unreachable by default on
# Clang/Linux.  We probe in two stages:
#
#   1. Try a direct compile-and-link with <quadmath.h> + libquadmath.
#   2. If that fails, ask `gcc -print-file-name=include` for the
#      directory containing quadmath.h and retry with that on the
#      include path.  If the second probe succeeds, propagate the
#      include directory to consumers via target_include_directories.
#
# Apple Clang and MSVC have no __float128 at all and fall through to
# the FATAL_ERROR.
if(WIGNERNJ_BUILD_QUADMATH)
    include(CheckCSourceCompiles)
    set(_qm_test
        "#include <quadmath.h>\nint main(void){__float128 x=sqrtq(2.0Q);return (int)x;}\n")
    set(_qm_saved_libs "${CMAKE_REQUIRED_LIBRARIES}")
    set(CMAKE_REQUIRED_LIBRARIES quadmath m)
    check_c_source_compiles("${_qm_test}" WIGNERNJ_QUADMATH_DIRECT)
    set(CMAKE_REQUIRED_LIBRARIES "${_qm_saved_libs}")

    set(WIGNERNJ_QUADMATH_INCLUDE "")
    if(NOT WIGNERNJ_QUADMATH_DIRECT)
        # Fall back to gcc's include directory (where libquadmath-devel /
        # libgcc-N-dev installs quadmath.h).
        find_program(WIGNERNJ_GCC_FOR_QUADMATH
            NAMES gcc gcc-15 gcc-14 gcc-13 gcc-12 gcc-11)
        if(WIGNERNJ_GCC_FOR_QUADMATH)
            execute_process(
                COMMAND ${WIGNERNJ_GCC_FOR_QUADMATH} -print-file-name=include
                OUTPUT_VARIABLE _qm_gcc_inc
                OUTPUT_STRIP_TRAILING_WHITESPACE)
            if(_qm_gcc_inc AND EXISTS "${_qm_gcc_inc}/quadmath.h")
                set(_qm_saved_inc "${CMAKE_REQUIRED_INCLUDES}")
                set(_qm_saved_libs "${CMAKE_REQUIRED_LIBRARIES}")
                set(CMAKE_REQUIRED_INCLUDES  ${_qm_gcc_inc})
                set(CMAKE_REQUIRED_LIBRARIES quadmath m)
                check_c_source_compiles("${_qm_test}" WIGNERNJ_QUADMATH_GCC_INC)
                set(CMAKE_REQUIRED_INCLUDES  "${_qm_saved_inc}")
                set(CMAKE_REQUIRED_LIBRARIES "${_qm_saved_libs}")
                if(WIGNERNJ_QUADMATH_GCC_INC)
                    set(WIGNERNJ_QUADMATH_INCLUDE "${_qm_gcc_inc}")
                endif()
            endif()
        endif()
    endif()

    if(NOT WIGNERNJ_QUADMATH_DIRECT AND NOT WIGNERNJ_QUADMATH_GCC_INC)
        message(FATAL_ERROR
            "WIGNERNJ_BUILD_QUADMATH=ON, but neither <quadmath.h> nor the gcc-shipped "
            "fallback path was usable.  __float128 / libquadmath are "
            "available only on GCC, Clang (with libquadmath-devel / "
            "libgcc-N-dev installed), and Intel compilers on Linux/macOS.  "
            "Disable WIGNERNJ_BUILD_QUADMATH on unsupported toolchains.")
    endif()

    target_compile_definitions(wignernj PUBLIC WIGNERNJ_HAVE_QUADMATH)
    target_link_libraries     (wignernj PUBLIC quadmath)
    if(WIGNERNJ_QUADMATH_INCLUDE)
        target_include_directories(wignernj PUBLIC ${WIGNERNJ_QUADMATH_INCLUDE})
    endif()
    install(FILES include/wignernj_quadmath.h
                  include/wignernj_quadmath.hpp
            DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
endif()

# MPFR arbitrary-precision interface (optional)
#
# mpfr.h does `#include <gmp.h>`, but on multi-prefix packagings (notably
# Homebrew on macOS) GMP lives in its own prefix and mpfr.pc does not
# propagate the GMP include path.  We therefore pull GMP in alongside MPFR.
if(WIGNERNJ_BUILD_MPFR)
    find_package(PkgConfig QUIET)
    if(PKG_CONFIG_FOUND)
        # Do NOT request IMPORTED_TARGET here: linking PkgConfig::MPFR
        # PUBLIC would leak that IMPORTED target into the exported
        # wignernjTargets.cmake, where it is not redefined on the
        # consumer side and triggers
        #   "The link interface ... contains: PkgConfig::MPFR but the
        #    target was not found"
        # at find_package(wignernj) time.  Resolving via the bare
        # absolute paths in *_LINK_LIBRARIES keeps the exported interface
        # self-contained: downstream consumers do not need to re-find
        # pkg-config.
        pkg_check_modules(MPFR REQUIRED mpfr)
        pkg_check_modules(GMP            gmp)
        target_include_directories(wignernj PUBLIC ${MPFR_INCLUDE_DIRS})
        target_link_libraries     (wignernj PUBLIC ${MPFR_LINK_LIBRARIES})
        if(GMP_FOUND)
            target_include_directories(wignernj PUBLIC ${GMP_INCLUDE_DIRS})
            target_link_libraries     (wignernj PUBLIC ${GMP_LINK_LIBRARIES})
        endif()
    else()
        find_library(MPFR_LIB NAMES mpfr REQUIRED)
        find_path   (MPFR_INC mpfr.h     REQUIRED)
        find_path   (GMP_INC  gmp.h)
        target_include_directories(wignernj PUBLIC ${MPFR_INC})
        if(GMP_INC)
            target_include_directories(wignernj PUBLIC ${GMP_INC})
        endif()
        target_link_libraries     (wignernj PUBLIC ${MPFR_LIB})
    endif()
    target_compile_definitions(wignernj PUBLIC WIGNERNJ_HAVE_MPFR)
    install(FILES include/wignernj_mpfr.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
endif()

# FLINT bigint backend (optional)
#
# FLINT pulls in GMP and MPFR transitively, so when WIGNERNJ_BUILD_FLINT=ON we
# unconditionally link all three.  Floating-point conversions in the
# FLINT path go through MPFR (correct round-to-nearest-even at every
# IEEE 754 binary precision), and the binary128 conversion extracts
# the 113-bit mantissa via mpfr_get_z_2exp (so no MPFR --enable-float128
# build option is required).  Detection uses find_library/find_path
# directly:  FLINT does not ship a pkg-config file on every distribution
# (notably Ubuntu's libflint-dev), so the explicit search is more robust
# than pkg_check_modules.
if(WIGNERNJ_BUILD_FLINT)
    find_library(FLINT_LIB NAMES flint REQUIRED)
    find_path   (FLINT_INC flint/fmpz.h REQUIRED)
    find_library(MPFR_LIB  NAMES mpfr REQUIRED)
    find_path   (MPFR_INC  mpfr.h REQUIRED)
    find_library(GMP_LIB   NAMES gmp REQUIRED)
    find_path   (GMP_INC   gmp.h REQUIRED)
    target_include_directories(wignernj PRIVATE
        ${FLINT_INC} ${MPFR_INC} ${GMP_INC})
    target_link_libraries(wignernj PRIVATE
        ${FLINT_LIB} ${MPFR_LIB} ${GMP_LIB})
    target_compile_definitions(wignernj PRIVATE WIGNERNJ_USE_FLINT)
endif()

# Fortran interface (optional, only if compiler found)
set(WIGNERNJ_HAS_FORTRAN OFF)
if(WIGNERNJ_BUILD_FORTRAN)
    include(CheckLanguage)
    check_language(Fortran)
    if(CMAKE_Fortran_COMPILER)
        enable_language(Fortran)
        add_library(wignernj_f03 src/fortran/wignernj_f90.F90)
        target_link_libraries(wignernj_f03 PUBLIC wignernj)
        # Forward the quadmath gate to the Fortran preprocessor so the
        # corresponding interface block and convenience wrappers compile
        # in.  iso_fortran_env::real128 is also gated on this define
        # because, on toolchains where __float128 is unavailable, real128
        # may be -1 (no kind), which would render the module unparseable.
        if(WIGNERNJ_BUILD_QUADMATH)
            target_compile_definitions(wignernj_f03 PRIVATE WIGNERNJ_HAVE_QUADMATH)
        endif()
        set_target_properties(wignernj_f03 PROPERTIES
            Fortran_MODULE_DIRECTORY
                ${CMAKE_CURRENT_BINARY_DIR}/fortran_modules
            VERSION   ${PROJECT_VERSION}
            SOVERSION ${PROJECT_VERSION_MAJOR}
            INTERPROCEDURAL_OPTIMIZATION ${WIGNERNJ_BUILD_LTO}
            WINDOWS_EXPORT_ALL_SYMBOLS ON
            # Bake $ORIGIN into the installed library so that, when a
            # downstream binary's DT_RUNPATH points at some non-system
            # prefix where libwignernj_f03.so lives, the dynamic linker
            # still finds its co-located libwignernj.so.0 transitive
            # dependency.  Linkers configured with
            # --no-copy-dt-needed-entries (the Ubuntu default) do not
            # propagate libwignernj.so.0 into the consuming binary's
            # NEEDED list, and DT_RUNPATH (unlike DT_RPATH) is *not*
            # searched for transitive deps -- so without this the
            # downstream Fortran consumer fails on those distros.
            INSTALL_RPATH "$ORIGIN"
        )
        # Propagate the .mod-file directory to consumers, both at build
        # time and after install, so downstream Fortran compilations
        # find `use wignernj` automatically.
        target_include_directories(wignernj_f03 INTERFACE
            $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/fortran_modules>
            $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/wignernj/fortran>
        )
        install(TARGETS wignernj_f03
            EXPORT  wignernjTargets
            LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
            ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
            RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
        )
        install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/fortran_modules/
                DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/wignernj/fortran)

        # pkg-config file for the Fortran wrapper -- exposes the
        # .mod-file directory via -I and depends transitively on the C
        # library's .pc file via Requires: libwignernj.
        configure_file(libwignernj_f03.pc.in libwignernj_f03.pc @ONLY)
        install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libwignernj_f03.pc
            DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

        set(WIGNERNJ_HAS_FORTRAN ON)
    else()
        message(STATUS "No Fortran compiler found -- skipping Fortran interface")
    endif()
endif()

# Python extension (optional)
#
# Two distribution paths exist for the Python extension:
#
#   1. The pip-installable wheel built from setup.py.  This recompiles
#      the C sources directly into the extension module to produce a
#      self-contained `_wignernj.so` with no runtime dependency on
#      libwignernj.so.  This is the canonical path for `pip install
#      wignernj` end users.
#
#   2. The CMake-driven build below (WIGNERNJ_BUILD_PYTHON=ON).  This links
#      `_wignernj.so` dynamically against libwignernj.so so that distro
#      packages (Fedora, etc.) ship one copy of the C library on disk
#      rather than embedding it inside every consumer.  Picking up
#      WIGNERNJ_BUILD_FLINT, WIGNERNJ_BUILD_QUADMATH, WIGNERNJ_BUILD_MPFR is automatic via the
#      transitive link to the wignernj target -- no separate switches
#      needed here.
#
# The Python API is identical between the two paths; the
# `precision='float'|'double'|'longdouble'` keyword still selects the
# C precision regardless of how `_wignernj.so` was built.  The CMake
# path additionally installs `_wignernj.so` plus `wignernj/__init__.py`
# under `Python3_SITEARCH`, so the resulting RPM (or other distro
# package) can register a `python3-wignernj` namespace package out of
# the same build that produced `libwignernj.so`.
if(WIGNERNJ_BUILD_PYTHON)
    find_package(Python3 COMPONENTS Interpreter Development)
    if(Python3_FOUND)
        # Linking a static libwignernj into the shared _wignernj.so
        # (the Python module) requires every object in wignernj to be
        # compiled position-independent.  When BUILD_SHARED_LIBS=ON
        # this is automatic (CMake gives shared targets PIC by
        # default); when BUILD_SHARED_LIBS=OFF, we have to set the
        # property explicitly or the link fails with "relocation
        # R_X86_64_PC32 ... can not be used when making a shared
        # object; recompile with -fPIC".  Setting the property
        # unconditionally is cheap (no-op when already shared).
        set_target_properties(wignernj PROPERTIES
            POSITION_INDEPENDENT_CODE ON)

        Python3_add_library(_wignernj MODULE src/python/wignernjmodule.c)
        target_include_directories(_wignernj PRIVATE include)
        target_link_libraries(_wignernj PRIVATE wignernj)
        set_target_properties(_wignernj PROPERTIES
            C_STANDARD 99
            C_STANDARD_REQUIRED ON
            INTERPROCEDURAL_OPTIMIZATION ${WIGNERNJ_BUILD_LTO}
            OUTPUT_NAME "_wignernj"
            LIBRARY_OUTPUT_DIRECTORY
                ${CMAKE_CURRENT_BINARY_DIR}/wignernj
        )
        configure_file(wignernj/__init__.py
            ${CMAKE_CURRENT_BINARY_DIR}/wignernj/__init__.py COPYONLY)

        # Install the namespace package under Python3_SITEARCH so a
        # distro `python3-wignernj` RPM can be packaged out of this
        # build alongside the libwignernj-devel and -fortran subpackages.
        install(TARGETS _wignernj
            LIBRARY DESTINATION ${Python3_SITEARCH}/wignernj)
        install(FILES wignernj/__init__.py
            DESTINATION ${Python3_SITEARCH}/wignernj)

        # Write the PEP 376 / PEP 427 .dist-info directory so the
        # CMake-installed package is recognised by `pip list`,
        # `pip uninstall`, and `importlib.metadata.version()` just like
        # a `pip install wignernj` would produce.  Done at install time
        # via `install(CODE ...)` because RECORD depends on the exact
        # installed file paths (including the Python-ABI-specific .so
        # suffix) and must honour `cmake --install --prefix=` /
        # DESTDIR=.  Metadata fields are read from pyproject.toml so
        # description, license, authors, classifiers, URLs etc. stay in
        # sync with the wheel-based PyPI install path.
        install(CODE "
            execute_process(
                COMMAND \"${Python3_EXECUTABLE}\"
                        \"${CMAKE_CURRENT_SOURCE_DIR}/tools/install_python_metadata.py\"
                        \"\$ENV{DESTDIR}${Python3_SITEARCH}\"
                        \"${PROJECT_VERSION}\"
                        \"${CMAKE_CURRENT_SOURCE_DIR}/pyproject.toml\"
                        \"cmake\"
                RESULT_VARIABLE _wignernj_metadata_result
            )
            if(NOT _wignernj_metadata_result EQUAL 0)
                message(FATAL_ERROR \"Failed to write Python .dist-info metadata for the CMake-installed wignernj package (exit code \${_wignernj_metadata_result})\")
            endif()
        ")
    else()
        message(STATUS "Python3 not found -- skipping Python extension")
    endif()
endif()

if(WIGNERNJ_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

if(WIGNERNJ_BUILD_EXAMPLES)
    enable_testing()
    add_subdirectory(examples)
endif()

# Installation
install(TARGETS wignernj
    EXPORT  wignernjTargets
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(FILES include/wignernj.h   DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES include/wignernj.hpp DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

# pkg-config file: libm only on Unix-like targets (MSVC bundles math
# into the default C runtime, so an explicit -lm there would resolve
# to a non-existent m.lib).
if(WIN32)
    set(WIGNERNJ_PC_LIBM)
else()
    set(WIGNERNJ_PC_LIBM "-lm")
endif()
configure_file(libwignernj.pc.in libwignernj.pc @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libwignernj.pc
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

install(EXPORT wignernjTargets
    FILE      wignernjTargets.cmake
    NAMESPACE wignernj::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wignernj)

include(CMakePackageConfigHelpers)
configure_package_config_file(
    wignernjConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/wignernjConfig.cmake
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wignernj
)
write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/wignernjConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion
)
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/wignernjConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/wignernjConfigVersion.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wignernj
)
