cmake_minimum_required(VERSION 3.20)
project(visiontrack_cpp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release)
endif()

# When Eigen is not already on the machine, download a pinned copy rather than
# failing. Pinned, not "latest": Eigen chooses different kernels between
# versions, and this project's correctness claim is bit-for-bit agreement with
# a NumPy reference. A floating version would make the wheel's numerics depend
# on the day it was built. Set -DVT_FETCH_EIGEN=OFF to forbid the download and
# require a system Eigen instead.
option(VT_FETCH_EIGEN "Download a pinned Eigen if none is installed" ON)
# OFF builds against the pinned Eigen even when the machine has one. That is
# the setting a release wheel wants: it makes the binary's numerics a property
# of this repo rather than of whatever the build host happened to have in
# /opt/homebrew. It is also the only way to exercise the download path on a
# machine that already has Eigen.
option(VT_SYSTEM_EIGEN "Prefer an already-installed Eigen over the pinned one" ON)
set(VT_EIGEN_TAG "3.4.0" CACHE STRING "Eigen release to fetch when none is found")

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

# Eigen is header-only. Prefer a real config package, then the usual system
# include paths, and only then the network.
if(VT_SYSTEM_EIGEN)
  find_package(Eigen3 3.3 QUIET NO_MODULE)
endif()
if(Eigen3_FOUND)
  set(VT_EIGEN_TARGET Eigen3::Eigen)
  message(STATUS "Eigen: found via CMake config")
else()
  if(VT_SYSTEM_EIGEN)
    find_path(VT_EIGEN_INCLUDE_DIR NAMES Eigen/Dense
              HINTS /opt/homebrew/opt/eigen/include/eigen3
                    /usr/local/opt/eigen/include/eigen3
                    /usr/include/eigen3)
  endif()
  if(VT_EIGEN_INCLUDE_DIR)
    message(STATUS "Eigen: using include dir ${VT_EIGEN_INCLUDE_DIR}")
  elseif(VT_FETCH_EIGEN)
    message(STATUS "Eigen: not installed, fetching pinned ${VT_EIGEN_TAG}")
    include(FetchContent)
    # CMake 3.30 deprecated single-argument FetchContent_Populate in favour of
    # MakeAvailable. MakeAvailable is wrong here: it would add_subdirectory()
    # Eigen's own project, pulling in its tests, BLAS/LAPACK shims and install
    # rules -- none of which this build wants, and some of which break on
    # toolchains that build the extension fine. Populate-only is the intent, so
    # the old behaviour is requested explicitly rather than tripped into.
    if(POLICY CMP0169)
      cmake_policy(SET CMP0169 OLD)
    endif()
    FetchContent_Declare(
      eigen
      URL https://gitlab.com/libeigen/eigen/-/archive/${VT_EIGEN_TAG}/eigen-${VT_EIGEN_TAG}.tar.gz
    )
    # Populate only. Eigen's own CMake project pulls in tests, BLAS shims and
    # install rules that this build has no use for and that break some
    # toolchains; the headers are the entire dependency.
    FetchContent_Populate(eigen)
    set(VT_EIGEN_INCLUDE_DIR ${eigen_SOURCE_DIR})
  else()
    message(FATAL_ERROR
      "No Eigen available: none installed (or VT_SYSTEM_EIGEN=OFF) and "
      "VT_FETCH_EIGEN=OFF.\n"
      "  macOS:  brew install eigen\n"
      "  Debian: apt install libeigen3-dev")
  endif()
endif()

# PARITY: forbid floating-point contraction. clang defaults to -ffp-contract=fast
# at -O2/-O3, which fuses `a + b*c` into a single FMA that rounds ONCE where
# NumPy rounds twice. That is a 1-ULP difference on every height-scaled term of
# the Kalman process noise -- invisible in isolation, and exactly the kind of
# drift that flips a near-tied association later. Correctness here means
# "identical to the oracle", not "more accurate".
#
# On an unrecognised compiler this stops the build instead of guessing. A wheel
# built without the flag is not slightly off, it is wrong, and silently
# omitting the flag is the one failure this project cannot afford to be quiet
# about. `visiontrack_cpp.build_info()["fp_contract_off"]` re-checks the same
# property against the binary that actually came out, so a toolchain that
# ignores the flag is still caught -- at import, by the tests, not in the field.
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU|IntelLLVM")
  add_compile_options(-ffp-contract=off)
elseif(MSVC)
  # /fp:precise disables contraction -- verified, not assumed: a Windows CI
  # wheel reports fp_contract_is_off() == true and passes the parity suite.
  #
  # /Zc:__cplusplus is needed because MSVC otherwise hardcodes __cplusplus to
  # 199711L for backward compatibility, no matter which standard it is
  # actually compiling. Without it, build_info() reports a C++ standard the
  # build is not using, and the integrity tests correctly call that a lie.
  add_compile_options(/fp:precise /Zc:__cplusplus)
else()
  message(FATAL_ERROR
    "Unknown compiler '${CMAKE_CXX_COMPILER_ID}': no way to disable FP "
    "contraction, which this project's parity guarantee depends on.")
endif()

pybind11_add_module(visiontrack_cpp bind/module.cpp)
target_include_directories(visiontrack_cpp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

if(Eigen3_FOUND)
  target_link_libraries(visiontrack_cpp PRIVATE ${VT_EIGEN_TARGET})
else()
  target_include_directories(visiontrack_cpp PRIVATE ${VT_EIGEN_INCLUDE_DIR})
endif()

install(TARGETS visiontrack_cpp DESTINATION .)
