# The floor is the CMake Ubuntu's latest LTS ships (26.04: 4.2); the range
# asks the policies of every CMake up to 4.4, the newest in use (#1121).
cmake_minimum_required(VERSION 4.2...4.4)

# The version is the latest release tag, v0.<YYYYMMDD>.<n>+<hash> (see
# .github/workflows/release.yml): its numeric part is the project version,
# and the whole of `git describe` names the build.  Outside a checkout with
# release tags, as in a shallow CI clone, it is 0.0.0.
set(DEDEKIND_VERSION "0.0.0")
set(DEDEKIND_VERSION_FULL "0.0.0")
execute_process(
  COMMAND git describe --tags --match "v0.*" --abbrev=7
  WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
  OUTPUT_VARIABLE _dedekind_describe
  RESULT_VARIABLE _dedekind_describe_result
  OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
if(_dedekind_describe_result EQUAL 0 AND _dedekind_describe MATCHES "^v([0-9]+\\.[0-9]+\\.[0-9]+)")
  set(DEDEKIND_VERSION "${CMAKE_MATCH_1}")
  set(DEDEKIND_VERSION_FULL "${_dedekind_describe}")
endif()

project(Dedekind VERSION ${DEDEKIND_VERSION} LANGUAGES CXX)

# --- The compiler ---
# LLVM's own clang: CI builds and tests with 23, and 22 has built the wheel.
# Said here, as pip shows this to whoever builds the sdist.
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_VERSION VERSION_LESS 22)
    message(FATAL_ERROR
        "Dedekind builds with LLVM clang 22 or later, not "
        "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}.  "
        "Set CC and CXX to LLVM's clang and clang++.")
endif()

# --- Toolchain/Platform Adjustments ---
if(APPLE)
    # Move the Makefile's "CXXFLAGS" logic here so it's not overwritten
    add_compile_options("-stdlib=libc++")
    # libc++ from the LLVM that compiles, wherever Homebrew put it
    # (/usr/local on Intel, /opt/homebrew on Apple Silicon).
    cmake_path(GET CMAKE_CXX_COMPILER PARENT_PATH _dedekind_llvm_bin)
    cmake_path(GET _dedekind_llvm_bin PARENT_PATH _dedekind_llvm_root)
    add_link_options("-stdlib=libc++" "-L${_dedekind_llvm_root}/lib")
    
    # Auto-find SDK path on Mac
    execute_process(COMMAND xcrun --sdk macosx --show-sdk-path
                    OUTPUT_VARIABLE MACOS_SDK_PATH
                    OUTPUT_STRIP_TRAILING_WHITESPACE)
    add_compile_options("-isysroot" "${MACOS_SDK_PATH}")
endif()

# --- Instrumentation and Warnings ---
# Clang's warnings, in CI and on the macOS dev machine alike
set(DEDEKIND_WARNING_FLAGS
    -Wall -Wextra -Wpedantic -Wshadow -Wunused 
    -Wunreachable-code -Wconversion -Werror
    -Wno-c2y-extensions
)

# --- Instrumentation ---
# Flags for a benchmark on the machine that runs it (`make bench-native`), after
# the build's own, so they win, and on every target, so modules share them.
set(DEDEKIND_BENCH_FLAGS "" CACHE STRING "Extra flags for a benchmark build")
add_compile_options(${DEDEKIND_BENCH_FLAGS})

# On in a developer's build; a wheel turns it off (pyproject.toml), and carries
# none.
option(DEDEKIND_COVERAGE "Instrument every target for LLVM coverage" ON)
if(DEDEKIND_COVERAGE)
    add_compile_options(-fprofile-instr-generate -fcoverage-mapping)
    add_link_options(-fprofile-instr-generate)
endif()
set(CMAKE_CXX_STANDARD 23)
# One optimisation level for every target, -O2, the level the Jlt paper reads
# its IR at: a module's consumers then share its flags, and CMake reuses its
# BMI instead of synthesising another copy of every module library for the
# one with other flags (the IR fixture at -O2 against -O3 cost 8 minutes of
# CI, #1121).  The tests run at compile time, so -O3 bought them nothing.
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG")
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# A consumer whose flags differ from a module's makes CMake 4 compile a second copy of
# that module, 7 minutes of CI.  nanobind's static library hands its consumers these
# two publicly off macOS, so every target takes them.
if(NOT APPLE)
    add_compile_options(-ffunction-sections -fdata-sections)
endif()
# For clangd:
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Disable global scanning to protect FetchContent/dependencies
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)

# Optional: runtime/noinline bridge wrappers for coverage attribution in
# dedekind.category:numeric. Leave OFF for final builds (constexpr bridges).
# Usage:
#   Final/default build (constexpr bridges):
#     cmake -S . -B build -DDEDEKIND_RUNTIME_COVERAGE_BRIDGES=OFF
#   Coverage-attribution build (runtime/noinline bridges):
#     cmake -S . -B build -DDEDEKIND_RUNTIME_COVERAGE_BRIDGES=ON
option(DEDEKIND_RUNTIME_COVERAGE_BRIDGES
    "Use runtime/noinline numeric bridge wrappers for coverage attribution"
    OFF)

# Explicit policy switch: allow raw double as a machine-real proxy in geometry.
# OFF by default to keep machine-real semantics opt-in.
option(DEDEKIND_ENABLE_DOUBLE_REAL_PROXY
    "Allow double as explicit proxy for real scalars in geometry"
    OFF)

# The options' definitions hold for every target, libraries, tests and the
# Python bindings alike: a module's consumers then share its flags, and CMake
# reuses its BMI rather than synthesising one per differing consumer (#1121).
if(DEDEKIND_RUNTIME_COVERAGE_BRIDGES)
    add_compile_definitions(DEDEKIND_RUNTIME_COVERAGE_BRIDGES)
endif()
if(DEDEKIND_ENABLE_DOUBLE_REAL_PROXY)
    add_compile_definitions(DEDEKIND_ENABLE_DOUBLE_REAL_PROXY=1)
endif()

option(DEDEKIND_BUILD_PYTHON_BINDINGS
    "Build the nanobind-based Python MVP facade"
    ON)

# Enable testing with Catch2, its latest release pinned (no tracking of main),
# built from source because distribution packages lag behind.
include(FetchContent)
FetchContent_Declare(
  Catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG        v3.16.1
    GIT_SHALLOW    TRUE
)

# This populates the 'catch2_SOURCE_DIR' variable
FetchContent_MakeAvailable(Catch2)

# CRITICAL: Use the variable name generated by FetchContent (lowercase)
list(APPEND CMAKE_MODULE_PATH "${catch2_SOURCE_DIR}/extras")

# This provides the catch_discover_tests command
include(Catch)

enable_testing()


# Run build on a per module basis. I.e.
# Module A -> Test A -> Module B -> Test B -> ...
# This function describes the build of one module.
# Note, there is a bit of CMake magic to cope with code coverage measurement during testing.
# The key is to link the test executable with the library target in two ways:
# 1. Link the library target normally to provide metadata (PCMs) without providing objects again. We use the INTERFACE properties of the library to avoid double-linking .o files.
# --- Global tracking for coverage ---
set_property(GLOBAL PROPERTY DEDEKIND_ALL_TEST_BINARIES "")
set_property(GLOBAL PROPERTY DEDEKIND_ALL_LIB_ARCHIVES "")

# A test, for ctest and as a target of the build, run-test-<NAME>: the build
# runs it as soon as what it tests is built, beside the compilation of the
# layers above (`check`, below).  The command and its environment are said
# once, for both; a program that is a target of the build is run once built.
function(add_dedekind_test NAME TEST_NAME ENVIRONMENT PROGRAM)
    set(RUN ${PROGRAM})
    if(TARGET ${PROGRAM})
        set(RUN $<TARGET_FILE:${PROGRAM}>)
    endif()
    add_custom_target(run-test-${NAME}
        COMMAND ${CMAKE_COMMAND} -E env ${ENVIRONMENT} ${RUN} ${ARGN}
        VERBATIM)
    if(TARGET ${PROGRAM})
        add_dependencies(run-test-${NAME} ${PROGRAM})
    endif()
    add_test(NAME ${TEST_NAME} COMMAND ${PROGRAM} ${ARGN})
    set_tests_properties(${TEST_NAME} PROPERTIES ENVIRONMENT "${ENVIRONMENT}")
    set_property(GLOBAL APPEND PROPERTY DEDEKIND_RUN_TESTS run-test-${NAME})
endfunction()

function(add_dedekind_layer LAYER_NAME DEPENDS_ON)
    set(LIB_TARGET dedekind_${LAYER_NAME})
    set(TEST_TARGET test_${LAYER_NAME})

    file(GLOB MODULE_SOURCES CONFIGURE_DEPENDS "src/main/modules/dedekind/${LAYER_NAME}/*.cppm")
    file(GLOB TEST_SOURCES   CONFIGURE_DEPENDS "src/test/cpp/modules/dedekind/${LAYER_NAME}/*_test.cpp")

    add_library(${LIB_TARGET} STATIC)
    set_target_properties(${LIB_TARGET} PROPERTIES
        CXX_SCAN_FOR_MODULES ON
        # The dedekind_* static libs are linked into the Python bindings .so
        # (the _jlt / _pst targets).  Without -fPIC, COMDAT-emitted
        # symbols from inline-variable dynamic-init (esp. closures large
        # enough to defeat std::function SBO, where the Path destructor is
        # not inlined) produce non-PIC relocations that fail the .so link.
        # POSITION_INDEPENDENT_CODE ON makes the static libs PIC-compatible
        # for both shared-library linkage and the test executables.
        POSITION_INDEPENDENT_CODE ON)
    target_sources(${LIB_TARGET} PUBLIC FILE_SET CXX_MODULES BASE_DIRS src/main/modules FILES ${MODULE_SOURCES})
    
    if(DEPENDS_ON)
        target_link_libraries(${LIB_TARGET} PUBLIC ${DEPENDS_ON})
    endif()

    if(TEST_SOURCES)
        add_executable(${TEST_TARGET} ${TEST_SOURCES})
        set_target_properties(${TEST_TARGET} PROPERTIES CXX_SCAN_FOR_MODULES ON)
        target_link_libraries(${TEST_TARGET} PRIVATE ${LIB_TARGET} Catch2::Catch2WithMain)
    endif()

    # Instrumentation
    # Only apply the warnings here (Coverage is now global via add_compile_options)
    target_compile_options(${LIB_TARGET} PRIVATE ${DEDEKIND_WARNING_FLAGS})
    if(TEST_SOURCES)
        target_compile_options(${TEST_TARGET} PRIVATE ${DEDEKIND_WARNING_FLAGS})
    endif()

    # Force whole-archive to ensure all module code is instrumented
    if(TEST_SOURCES)
        if(APPLE)
            target_link_options(${TEST_TARGET} PRIVATE "-Wl,-force_load,$<TARGET_FILE:${LIB_TARGET}>")
        else()
            target_link_options(${TEST_TARGET} PRIVATE "-Wl,--whole-archive" "$<TARGET_FILE:${LIB_TARGET}>" "-Wl,--no-whole-archive")
        endif()
    endif()

    # Use an absolute path for the profile file, utilizing GITHUB_WORKSPACE if available
    if(DEFINED ENV{GITHUB_WORKSPACE})
        set(PROFILE_DIR "$ENV{GITHUB_WORKSPACE}/build")
    else()
        set(PROFILE_DIR "${CMAKE_BINARY_DIR}")
    endif()

    if(TEST_SOURCES)
        add_dedekind_test(${LAYER_NAME} ${TEST_TARGET}
            "LLVM_PROFILE_FILE=${PROFILE_DIR}/dedekind-${LAYER_NAME}-%m.profraw"
            ${TEST_TARGET})
    endif()

    # Track binaries for the final coverage report
    if(TEST_SOURCES)
        set_property(GLOBAL APPEND PROPERTY DEDEKIND_ALL_TEST_BINARIES $<TARGET_FILE:${TEST_TARGET}>)
    endif()
    set_property(GLOBAL APPEND PROPERTY DEDEKIND_ALL_LIB_ARCHIVES $<TARGET_FILE:${LIB_TARGET}>)
    # The layers, in order, for the linter (tools/jlt-lint).
    set_property(GLOBAL APPEND PROPERTY DEDEKIND_LAYERS ${LAYER_NAME})
endfunction()

# --- THE PROPOSITIONS (Serialized by Libraries) ---
add_dedekind_layer(category "")
add_dedekind_layer(ieee dedekind_category)
add_dedekind_layer(sets dedekind_ieee)
# dedekind.relational (#792) — the relation-algebra's own module, popped OUT of
# sets (above sets, below order).  Partitions: :tables (Codd's relational model
# σ ⋈ ∪ −), :dyadic (Tarski's calculus of relations R° R;S Δ +), :graph
# (graphs of arrows, binary relations — a graph IS a relation, so it tags along).  order/halfspace's
# converse/relative-product surface moves DOWN into :dyadic, hence order depends
# on relational, not the reverse.
add_dedekind_layer(relational dedekind_sets)
add_dedekind_layer(order dedekind_relational)
add_dedekind_layer(topology dedekind_order)
add_dedekind_layer(sequences dedekind_topology)
add_dedekind_layer(algebra dedekind_sequences)
add_dedekind_layer(morphologies dedekind_algebra)
add_dedekind_layer(geometry dedekind_morphologies)
add_dedekind_layer(numbers dedekind_geometry)
add_dedekind_layer(analysis dedekind_numbers)
add_dedekind_layer(linear_algebra dedekind_analysis)
add_dedekind_layer(optimization dedekind_linear_algebra)

# --- IR FIXTURE FOR COMPILER OPTIMIZATION WITNESS ---
# Compile-time pruning witness for dedekind.sets: built as a static library
# for compile-command capture and LLVM IR generation by the helper script.
# Placed in the python test tree so it can resolve all module dependencies.
add_library(set-pruning-ir-fixture STATIC EXCLUDE_FROM_ALL
    src/test/cpp/modules/dedekind/python/pruning_noop_vs_runtime_fixture.cpp
    src/test/cpp/modules/dedekind/python/showcase_01_diagonal_contradiction.cpp
    src/test/cpp/modules/dedekind/python/showcase_02_lattice_singleton.cpp
    src/test/cpp/modules/dedekind/python/showcase_03_halfspace_contradiction.cpp
    src/test/cpp/modules/dedekind/python/showcase_05_halfspace_real_ambient.cpp
    # The paper's claim is the realistic one: @em sometimes the membership
    # query evaluates all the way to a constant; when an argument is not yet
    # known at compile time, the collapse is partial (cf. Turchin's
    # supercompilation literature on specialisation depth).  Whether a *given*
    # toolchain + ambient BMI takes the fold all the way through is downstream
    # of inliner cost models that don't bear on the structural claim --- which
    # is witnessed by the in-source static_assert chain.  The .cpp file remains
    # as compile-check fodder.
    src/test/cpp/modules/dedekind/python/showcase_09_lp_vertex_typed_constant.cpp
    # showcase_09b is the runtime call site of the same kernel as 09:
    # the coefficients arrive as runtime arguments, the IR retains the
    # algorithm's residual structure.  Read side-by-side with 09's
    # `ret i64 2` IR, this exhibits the bridge in IR form — one
    # constexpr kernel, two evaluation modes selected by the call site.
    src/test/cpp/modules/dedekind/python/showcase_09b_lp_runtime_residual.cpp
    # showcase_12 is the compile-time NTTP form of the bit-ops fast path
    # (signed-unimodular + axis-aligned pack — the unit square).  The
    # NTTP entry maximize<>() dispatches to detail::maximize_impl_axis_aligned
    # under that gate and the fold collapses to a typed constant
    # `Vec2<Rat, 1, 1>`; witness IR is `ret i64 1`.  Middle panel of the
    # §5 triptych (compile-time fast path); showcase_09 holds the
    # generic Cramer compile-time panel.
    src/test/cpp/modules/dedekind/python/showcase_12_lp_unimodular_fast_path.cpp
    # showcase_12b is the runtime call site of the fast-path kernel
    # (carrier `int` for IR readability).  Read side-by-side with
    # showcase_09b (generic Cramer runtime: MUL, DIV, full active-set
    # enumeration) and showcase_12 (compile-time fold), this is the
    # third panel of the §5 triptych: bit-ops IR on an axis-aligned
    # signed-unimodular pack, with MUL and DIV absent on the carrier.
    src/test/cpp/modules/dedekind/python/showcase_12b_lp_unimodular_runtime.cpp
    # showcase_13 is the Jlt paper's Listing 5: restriction sites counted at
    # four binding times, the last a constant.
    src/test/cpp/modules/dedekind/python/showcase_13_site_occurrences.cpp
    # showcase_14 is its right panel: EcoRI's table, a constant, and the pass
    # over a sequence that remains.
    src/test/cpp/modules/dedekind/python/showcase_14_ecori_table.cpp
    # showcase_15 is Listing 3 at run time: Fibonacci as a fold over a chain,
    # the loop Appendix G reads, kept as the fold wrote it.
    src/test/cpp/modules/dedekind/python/showcase_15_listing3_fibonacci.cpp
    # showcase_10_image_intensional_naturals.cpp is added for compile-time
    # validation of the layer-1 image overload on intensional Sets (#602 / #631);
    # it has no checked-in `.ll` fixture because its IR demo
    # (`ret i1 false` for `Kleene::Unknown == True`) is shape-identical
    # to showcase_03's empty-meet reduction (`ret i1 false` for
    # `L::False == L::True`).  The structural reduction itself
    # (image-of-intensional-Set carries the always-Unknown predicate) is
    # exhibited at compile time via the static_assert chain in the source.
    # Same precedent as showcase_alpha_prime below.
    src/test/cpp/modules/dedekind/python/showcase_10_image_intensional_naturals.cpp
    # showcase_alpha_prime.cpp is added for compile-time validation of the
    # paper §3 α′ listing; it has no checked-in `.ll` fixture because its
    # IR demo is reused from showcase_03 (the empty-meet structural pattern).
    # See showcase_alpha_prime.cpp's header for the cross-reference.
    src/test/cpp/modules/dedekind/python/showcase_alpha_prime.cpp)

set_target_properties(set-pruning-ir-fixture PROPERTIES
    CXX_STANDARD 23
    CXX_STANDARD_REQUIRED ON
    CXX_EXTENSIONS OFF
    CXX_SCAN_FOR_MODULES ON)

# -O2 is the build's own level in Release (CMAKE_CXX_FLAGS_RELEASE above), so
# nothing is added there and the fixture shares the modules' BMIs; any other
# configuration asks it here, since the IR checks read optimised IR.
target_compile_options(set-pruning-ir-fixture PRIVATE
    ${DEDEKIND_WARNING_FLAGS}
    $<$<NOT:$<CONFIG:Release>>:-O2>)

target_link_libraries(set-pruning-ir-fixture PRIVATE dedekind_optimization)
# Appendix G reads showcase_15's loop as the fold wrote it: neither unrolled
# nor vectorised, so its header's phi-nodes are the fold's state.
set_source_files_properties(
    src/test/cpp/modules/dedekind/python/showcase_15_listing3_fibonacci.cpp
    PROPERTIES COMPILE_OPTIONS "-fno-unroll-loops;-fno-vectorize;-fno-slp-vectorize")

if(DEDEKIND_BUILD_PYTHON_BINDINGS)
    # Development.SABIModule too: nanobind builds STABLE_ABI only where
    # Python::SABIModule exists, and silently falls back to the full ABI otherwise.
    find_package(Python 3.15 QUIET COMPONENTS Interpreter Development.Module
                 Development.SABIModule)
    if(NOT Python_FOUND)
        find_package(Python COMPONENTS Interpreter QUIET)
        if(Python_Interpreter_FOUND)
            message(FATAL_ERROR
                "Dedekind Python bindings/integration tests require Python 3.15 or newer. "
                "Found ${Python_VERSION} at ${Python_EXECUTABLE}. "
                "Activate a supported Python environment (for example: python3.15 -m venv .venv && source .venv/bin/activate) "
                "or configure CMake with a 3.15 or newer interpreter.")
        else()
            message(FATAL_ERROR
                "Dedekind Python bindings/integration tests require Python 3.15 or newer, but no Python interpreter was found.")
        endif()
    endif()
    add_subdirectory(ext/nanobind EXCLUDE_FROM_ALL)

    set(DEDEKIND_PYTHON_PACKAGE_ROOT "${CMAKE_BINARY_DIR}/python")
    set(DEDEKIND_PYTHON_PACKAGE_DIR "${DEDEKIND_PYTHON_PACKAGE_ROOT}/pynzli")
    # The package's pure-Python modules, staged next to the extensions so the
    # tests and notebooks import the build as it is, and installed alike.
    set(PYNZLI_MODULES __init__.py _version.py jlt.py pst.py trsk.py)
    list(TRANSFORM PYNZLI_MODULES PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/python/pynzli/"
         OUTPUT_VARIABLE PYNZLI_MODULE_PATHS)

    file(MAKE_DIRECTORY "${DEDEKIND_PYTHON_PACKAGE_DIR}")
    foreach(module IN LISTS PYNZLI_MODULES)
        configure_file(${CMAKE_CURRENT_SOURCE_DIR}/python/pynzli/${module}
                       ${DEDEKIND_PYTHON_PACKAGE_DIR}/${module} COPYONLY)
    endforeach()

    # Both modules are built against CPython's stable ABI (STABLE_ABI, Python
    # 3.15 and later, the floor in pyproject.toml), so one wheel per platform,
    # cp315-abi3, serves every Python from 3.15 on.
    # The Jlt exhibit (#961) is its own private native module, pynzli._jlt
    # (NumPy-style: one private compiled module per cohesive component,
    # re-exported by the pure-Python pynzli.jlt facade).
    # NOMINSIZE: nanobind's default is to optimise these for size, which would
    # hold the scans they instantiate to other flags than the rest of the build.
    nanobind_add_module(_jlt NOMINSIZE PROTECT_STACK STABLE_ABI
        src/main/modules/dedekind/python/nanobind_jlt.cpp)
    set_target_properties(_jlt PROPERTIES
        CXX_SCAN_FOR_MODULES ON
        LIBRARY_OUTPUT_DIRECTORY "${DEDEKIND_PYTHON_PACKAGE_DIR}"
        RUNTIME_OUTPUT_DIRECTORY "${DEDEKIND_PYTHON_PACKAGE_DIR}")
    target_compile_options(_jlt PRIVATE ${DEDEKIND_WARNING_FLAGS})
    target_compile_options(_jlt PRIVATE -Wno-sign-conversion)
    target_link_libraries(_jlt PRIVATE dedekind_optimization)

    # The Pst exhibit (#1001) is its own private native module, pynzli._pst
    # (the bounded chains the sets are valued in, the canonical ring u64 and
    # field 𝔽ₚ, and the term surface over them; re-exported by pynzli.pst).
    nanobind_add_module(_pst NOMINSIZE PROTECT_STACK STABLE_ABI
        src/main/modules/dedekind/python/nanobind_pst.cpp)
    set_target_properties(_pst PROPERTIES
        CXX_SCAN_FOR_MODULES ON
        LIBRARY_OUTPUT_DIRECTORY "${DEDEKIND_PYTHON_PACKAGE_DIR}"
        RUNTIME_OUTPUT_DIRECTORY "${DEDEKIND_PYTHON_PACKAGE_DIR}")
    target_compile_options(_pst PRIVATE ${DEDEKIND_WARNING_FLAGS})
    target_compile_options(_pst PRIVATE -Wno-sign-conversion)
    target_link_libraries(_pst PRIVATE dedekind_optimization)

    install(TARGETS _jlt _pst LIBRARY DESTINATION pynzli)
    install(FILES ${PYNZLI_MODULE_PATHS} DESTINATION pynzli)

    add_dedekind_test(python test_python_bindings
        "PYTHONPATH=${DEDEKIND_PYTHON_PACKAGE_ROOT}"
        ${Python_EXECUTABLE} -m unittest discover
                -s ${CMAKE_SOURCE_DIR}/src/test/python
                -p test_*.py)
    add_dependencies(run-test-python _jlt _pst)
endif()


# The book of math according to the dedekind project.
# Each chapter corresponds the book to an object file which in turn
# corresponds to a module interface unit.
# The "dedekind" library is the final product
add_library(dedekind INTERFACE)
add_dependencies(dedekind dedekind_category dedekind_ieee dedekind_sets dedekind_order dedekind_relational dedekind_topology dedekind_sequences dedekind_algebra dedekind_morphologies dedekind_geometry dedekind_numbers dedekind_analysis dedekind_linear_algebra dedekind_optimization)
# No source files needed here; it just bundles the objects
target_link_libraries(dedekind INTERFACE dedekind_category dedekind_ieee dedekind_sets dedekind_order dedekind_relational dedekind_topology dedekind_sequences dedekind_algebra dedekind_morphologies dedekind_geometry dedekind_numbers dedekind_analysis dedekind_linear_algebra dedekind_optimization)
target_compile_features(dedekind INTERFACE cxx_std_23)

target_compile_options(dedekind INTERFACE ${DEDEKIND_WARNING_FLAGS})

install(TARGETS dedekind dedekind_category dedekind_ieee dedekind_sets dedekind_order dedekind_relational dedekind_topology dedekind_sequences dedekind_algebra dedekind_morphologies dedekind_geometry dedekind_numbers dedekind_analysis dedekind_linear_algebra dedekind_optimization
    EXPORT dedekind-targets
    # Ensure CXX_MODULES are installed for all targets
    FILE_SET CXX_MODULES DESTINATION lib/modules/dedekind
)

install(EXPORT dedekind-targets
    FILE dedekindConfig.cmake
    NAMESPACE dedekind::
    DESTINATION lib/cmake/dedekind
)

# --- Doxygen Documentation (Maven-style) ---
find_package(Doxygen)
if (DOXYGEN_FOUND)
    # Define where Doxygen looks for code and where it outputs HTML
    set(DOXYGEN_INPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/main/modules)
    # DOXYGEN_OUTPUT_DIRECTORY is the variable consumed by doxygen_add_docs();
    # HTML will be written to ${DOXYGEN_OUTPUT_DIRECTORY}/html = build/docs/html.
    set(DOXYGEN_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/docs)

    # Customise for C++20 Modules (requires manual Doxyfile settings if needed)
    set(DOXYGEN_EXTENSION_MAPPING "cppm=C++") # Treat .cppm files as C++
    set(DOXYGEN_FILE_PATTERNS "*.cppm" "*.cpp")

    # Hide symbols not marked with 'export' or documented specifically
    set(DOXYGEN_EXTRACT_ALL NO)
    set(DOXYGEN_EXTRACT_PRIVATE NO)
    set(DOXYGEN_EXTRACT_STATIC NO)

    # Improves how Doxygen resolves imports/includes for modules
    set(DOXYGEN_BUILTIN_STL_SUPPORT YES)
    set(DOXYGEN_CPP_CLI_SUPPORT NO)
    set(DOXYGEN_SIP_SUPPORT NO)

    # Crucial for ETCS/Topos documentation: 
    # Allows Doxygen to parse LaTeX-style math if you use it in the future
    set(DOXYGEN_USE_MATHJAX YES) 

    # Ensure the "Level" sections show up in the hierarchy
    set(DOXYGEN_TOC_INCLUDE_HEADINGS 3)

    # The full `git describe` of the latest release tag, see the top of this file.
    set(DOXYGEN_PROJECT_NUMBER ${DEDEKIND_VERSION_FULL})
    
    # Optional: Add a project brief (like a Maven description)
    set(DOXYGEN_PROJECT_BRIEF "A structuralist C++23 Module Library")
    # @cite keys in the Doxygen resolve against the papers' bibliography.
    set(DOXYGEN_CITE_BIB_FILES ${CMAKE_CURRENT_SOURCE_DIR}/docs/paper/references.bib)
    
    # Use the FindDoxygen built-in macro to configure a Doxyfile automatically
    # The front page: what the site is, and the way to the README, the paper
    # and the Python reference.
    set(DOXYGEN_MAINPAGE ${CMAKE_CURRENT_SOURCE_DIR}/docs/doxygen/mainpage.md)
    set(DOXYGEN_USE_MDFILE_AS_MAINPAGE ${DOXYGEN_MAINPAGE})
    doxygen_add_docs(docs
        ${DOXYGEN_INPUT_DIR}
        ${DOXYGEN_MAINPAGE}
        COMMENT "Generating API documentation with Doxygen"
    )
    
endif()

# The Listing 5 benchmark (#1154), which `make bench` configures on its own.
option(DEDEKIND_BENCH "Build the benchmark" OFF)
if(DEDEKIND_BENCH)
    add_executable(bench_listing5 src/bench/cpp/listing5_bench.cpp)
    set_target_properties(bench_listing5 PROPERTIES CXX_SCAN_FOR_MODULES ON)
    target_link_libraries(bench_listing5 PRIVATE dedekind_category)
endif()

# The linter, a lint-<layer> target per layer (tools/jlt-lint/CMakeLists.txt).
add_subdirectory(tools/jlt-lint)

# The build's own check: every test and the linter, each layer's as soon as
# the layer is built, so ninja overlaps them with the layers above; and the IR
# fixture's showcases, type-checked.  `make check` builds it with `all`.
get_property(_dedekind_run_tests GLOBAL PROPERTY DEDEKIND_RUN_TESTS)
add_custom_target(run-test)
add_dependencies(run-test ${_dedekind_run_tests})
add_custom_target(check)
add_dependencies(check run-test set-pruning-ir-fixture)
if(TARGET lint)  # absent only when turned off (DEDEKIND_JLT_LINT)
    add_dependencies(check lint)
endif()

get_property(ALL_TESTS GLOBAL PROPERTY DEDEKIND_ALL_TEST_BINARIES)
get_property(ALL_LIBS GLOBAL PROPERTY DEDEKIND_ALL_LIB_ARCHIVES)
if(DEDEKIND_COVERAGE)
# --- Find LLVM Tools (Cross-Platform) ---
# Next to the compiler first: Homebrew's LLVM is keg-only, not on PATH.
cmake_path(GET CMAKE_CXX_COMPILER PARENT_PATH _dedekind_compiler_dir)
find_program(LLVM_PROFDATA_TOOL NAMES llvm-profdata-23 llvm-profdata
             HINTS ${_dedekind_compiler_dir} REQUIRED)
find_program(LLVM_COV_TOOL      NAMES llvm-cov-23 llvm-cov
             HINTS ${_dedekind_compiler_dir} REQUIRED)

# Prepare the -object list for the command line
set(OBJ_ARGS "")
foreach(lib IN LISTS ALL_LIBS)
    list(APPEND OBJ_ARGS -object ${lib})
endforeach()

add_custom_target(generate_coverage
    # 1. Merge the raw profiles into a single .profdata file
    COMMAND sh -c "${LLVM_PROFDATA_TOOL} merge -sparse $(find ${CMAKE_BINARY_DIR} -name '*.profraw') -o ${CMAKE_BINARY_DIR}/dedekind.profdata"
    
    # 2. Export to LCOV
    COMMAND ${LLVM_COV_TOOL} export 
        -instr-profile=${CMAKE_BINARY_DIR}/dedekind.profdata 
        -format=lcov 
        -ignore-filename-regex=".*_deps/.*|.*catch2.*|.*src/test/.*|.*test/.*"
        ${ALL_TESTS} 
        ${OBJ_ARGS} > ${CMAKE_BINARY_DIR}/coverage.info
    
     # 3. Strip absolute paths using Perl (Identical syntax on Mac & Linux)
    COMMAND perl -i -pe "s|${CMAKE_SOURCE_DIR}/||g" ${CMAKE_BINARY_DIR}/coverage.info
    COMMAND perl -i -pe "s|SF:\./|SF:|g" ${CMAKE_BINARY_DIR}/coverage.info

    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    VERBATIM
    COMMENT "Generating unified LCOV coverage report..."
)
endif()
