cmake_minimum_required(VERSION 3.24)

project(hawkes_engine LANGUAGES CXX VERSION 0.2.0)

# C++20 is required by ADR-001. No compiler-specific optimisation flags are added
# here; ADR-006 governs those and they must stay portable.
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# Compiler warnings on the extension. hawkes_core sets its own; the bindings are
# a separate target and were compiling with none of them.
if(MSVC)
    set(HAWKES_WARNINGS /W4 /permissive-)
else()
    set(HAWKES_WARNINGS -Wall -Wextra -Wpedantic)
endif()

option(HAWKES_BUILD_TESTS "Build the Catch2 test suite" OFF)
# Warnings are always on; promoting them to errors is opt-in so that installing
# from source on a compiler whose diagnostic set is wider than the one the code
# was developed against does not fail outright. The dev preset turns it on, so
# CI still enforces a warning-free build on at least one configuration.
option(HAWKES_WERROR "Treat warnings as errors" OFF)

add_library(hawkes_core STATIC
    cpp/src/loglik.cpp
    cpp/src/diagnostics.cpp
    cpp/src/loglik_shared.cpp
    cpp/src/rng.cpp
    cpp/src/simulate_cluster.cpp
    cpp/src/simulate_thin.cpp
    cpp/src/validation.cpp
    cpp/src/workspace.cpp
)
target_include_directories(hawkes_core PUBLIC cpp/include)

# Required, not optional. hawkes_core is STATIC and the extension is a MODULE
# (a shared object). CMake enables position-independent code for shared targets
# but not for static ones, so on ELF platforms the link fails with
# "relocation R_X86_64_PC32 ... can not be used when making a shared object".
# MSVC does not need this, which is why the failure was Linux-only.
set_target_properties(hawkes_core PROPERTIES POSITION_INDEPENDENT_CODE ON)

# Sanitizers for local and CI verification (ADR-006). Never part of a wheel.
set(HAWKES_SANITIZE "" CACHE STRING "address, undefined, or empty")
if(HAWKES_SANITIZE)
    if(MSVC)
        message(FATAL_ERROR "HAWKES_SANITIZE is not supported on MSVC")
    endif()
    # PUBLIC, not PRIVATE: hawkes_core is a static library, so flags attached
    # to it privately never reach the final link of the test executable. The
    # link then has instrumented objects and no runtime, which fails with
    # undefined __asan_* symbols. Both the compile flags and the link flag have
    # to propagate to anything that links the core.
    target_compile_options(hawkes_core PUBLIC -fsanitize=${HAWKES_SANITIZE}
                                               -fno-omit-frame-pointer
                                               -fno-sanitize-recover=all)
    target_link_options(hawkes_core INTERFACE -fsanitize=${HAWKES_SANITIZE})
endif()

# Deterministic floating point. ADR-006: -ffast-math is forbidden because the
# likelihood uses -inf as a value, and /fp:fast on MSVC has the same defect.
if(MSVC)
    target_compile_options(hawkes_core PRIVATE /W4 /permissive- /fp:precise)
    if(HAWKES_WERROR)
        target_compile_options(hawkes_core PRIVATE /WX)
    endif()
else()
    # Exactly the documented warning set. -Wconversion and -Wshadow were
    # removed: they are not part of the contract, and GCC's diagnostic set is
    # wider than MSVC's or Apple clang's, so they turned a portable build into
    # a GCC-only failure.
    target_compile_options(hawkes_core PRIVATE -Wall -Wextra -Wpedantic -fno-fast-math)
    if(HAWKES_WERROR)
        target_compile_options(hawkes_core PRIVATE -Werror)
    endif()
endif()

find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)
find_package(pybind11 CONFIG REQUIRED)

pybind11_add_module(_core MODULE cpp/src/_core.cpp cpp/src/bindings.cpp cpp/src/bindings_sim.cpp)
target_link_libraries(_core PRIVATE hawkes_core)
# The version the core was compiled as, readable from Python as
# _core.build_info()["version"]. tests/test_version.py holds it equal to
# hawkes_engine.__version__ and to the metadata, so a bump cannot miss one.
target_compile_definitions(_core PRIVATE
    HAWKES_ENGINE_VERSION="${PROJECT_VERSION}")
target_include_directories(_core PRIVATE cpp/include)
# The shared input contract for every binding lives in cpp/src, beside its users.
target_include_directories(_core PRIVATE cpp/src)
target_compile_options(_core PRIVATE ${HAWKES_WARNINGS})

install(TARGETS _core DESTINATION hawkes_engine)

# The measured trust map ships inside the package so an installed wheel can answer
# trust_lookup() without the repository. trust_map.py prefers this copy and falls
# back to the source-checkout path.
install(FILES data/trust/v1_excitation_only.csv DESTINATION hawkes_engine/data)

# User opt-in native build, per ADR-006. Off by default so published wheels stay
# portable across every x86-64 machine.
option(NATIVE_OPTIMIZATIONS "Build for the host CPU (-march=native)" OFF)
if(NATIVE_OPTIMIZATIONS)
    if(MSVC)
        message(WARNING "NATIVE_OPTIMIZATIONS has no effect on MSVC builds")
    else()
        target_compile_options(hawkes_core PRIVATE -march=native)
    endif()
endif()

if(HAWKES_BUILD_TESTS)
    enable_testing()
    include(FetchContent)
    FetchContent_Declare(
        Catch2
        GIT_REPOSITORY https://github.com/catchorg/Catch2.git
        GIT_TAG v3.5.4
        GIT_SHALLOW TRUE
    )
    FetchContent_MakeAvailable(Catch2)
    add_executable(hawkes_tests
        cpp/tests/test_loglik.cpp
        cpp/tests/test_allocation.cpp
        cpp/tests/test_rng.cpp
        cpp/tests/test_simulate.cpp
    )
    target_link_libraries(hawkes_tests PRIVATE hawkes_core Catch2::Catch2WithMain)
    target_include_directories(hawkes_tests PRIVATE cpp/include)
    target_compile_definitions(hawkes_tests PRIVATE HAWKES_GOLDEN_DIR="${CMAKE_SOURCE_DIR}/tests/golden")
    add_test(NAME hawkes_tests COMMAND hawkes_tests)
endif()
