cmake_minimum_required(VERSION 3.20)
project(hoptimal VERSION 0.1.0 LANGUAGES CXX)

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

option(HOPTIMAL_BUILD_TESTS      "Build C++ unit/integration tests" ON)
option(HOPTIMAL_BUILD_BENCHMARKS "Build C++ microbenchmarks"        OFF)
option(HOPTIMAL_BUILD_PYTHON     "Build pybind11 Python bindings"   OFF)
option(HOPTIMAL_USE_SANITIZERS   "Enable ASan + UBSan (non-MSVC)"   OFF)

# ── Dependencies (try find_package first, fall back to FetchContent) ──────────
include(FetchContent)

# Eigen3 — header-only. Prefer a system/vcpkg install; otherwise fetch the
# source from git. We need only the headers, so we populate the source WITHOUT
# running Eigen's own CMake: SOURCE_SUBDIR points at a nonexistent directory so
# FetchContent_MakeAvailable downloads but skips add_subdirectory(). That avoids
# (a) GitLab archive-checksum drift (we clone a tag instead of pinning a tarball
# hash) and (b) a duplicate-target clash on Eigen3::Eigen that Eigen's own CMake
# would otherwise create.
find_package(Eigen3 3.4 QUIET)
if(NOT Eigen3_FOUND)
    message(STATUS "Eigen3 not found — fetching 3.4.0 via FetchContent (needs internet)")
    FetchContent_Declare(eigen3
        GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
        GIT_TAG        3.4.0
        GIT_SHALLOW    TRUE
        SOURCE_SUBDIR  cmake-do-not-configure
    )
    FetchContent_MakeAvailable(eigen3)
    if(NOT TARGET Eigen3::Eigen)
        add_library(Eigen3::Eigen INTERFACE IMPORTED)
        target_include_directories(Eigen3::Eigen INTERFACE ${eigen3_SOURCE_DIR})
    endif()
endif()

# GoogleTest (only when tests are requested)
if(HOPTIMAL_BUILD_TESTS)
    find_package(GTest QUIET)
    if(NOT GTest_FOUND)
        message(STATUS "GTest not found — fetching via FetchContent")
        FetchContent_Declare(googletest
            URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz
        )
        set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
        FetchContent_MakeAvailable(googletest)
    endif()
endif()

# pybind11 (only when Python bindings are requested)
if(HOPTIMAL_BUILD_PYTHON)
    find_package(pybind11 QUIET)
    if(NOT pybind11_FOUND)
        FetchContent_Declare(pybind11
            URL https://github.com/pybind/pybind11/archive/refs/tags/v2.12.0.tar.gz
        )
        FetchContent_MakeAvailable(pybind11)
    endif()
endif()

# ── hoptimal_core static library ──────────────────────────────────────────────────
add_library(hoptimal_core STATIC
    # core
    src/core/search_space.cpp
    src/core/trial.cpp
    src/core/study.cpp
    src/storage/memory_storage.cpp
    src/samplers/random_sampler.cpp
    # Gaussian-process Bayesian optimization
    src/utils/lbfgs.cpp
    src/gp/kernel.cpp
    src/gp/gaussian_process.cpp
    src/gp/acquisition.cpp
    src/samplers/gp_sampler.cpp
    # pruners
    src/pruners/median_pruner.cpp
    src/pruners/hyperband_pruner.cpp
)

target_include_directories(hoptimal_core PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

target_link_libraries(hoptimal_core PUBLIC Eigen3::Eigen)

# Build with position-independent code so the static hoptimal_core library can be
# linked into the _hoptimal shared module (pybind11). On Linux, linking a non-PIC
# static lib into a .so fails ("relocation ... can not be used when making a
# shared object; recompile with -fPIC"). Harmless for the test executables.
set_target_properties(hoptimal_core PROPERTIES POSITION_INDEPENDENT_CODE ON)

target_compile_options(hoptimal_core PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/W4 /wd4100 /wd4127>
    $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wpedantic>
)

if(HOPTIMAL_USE_SANITIZERS AND NOT MSVC)
    target_compile_options(hoptimal_core PRIVATE
        -fsanitize=address,undefined -fno-omit-frame-pointer)
    target_link_options(hoptimal_core PRIVATE
        -fsanitize=address,undefined)
endif()

# ── Tests ─────────────────────────────────────────────────────────────────────
if(HOPTIMAL_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests/unit)
    add_subdirectory(tests/integration)
endif()

# ── Python bindings ───────────────────────────────────────────────────────────
if(HOPTIMAL_BUILD_PYTHON)
    pybind11_add_module(_hoptimal python/bindings/bindings.cpp)
    target_link_libraries(_hoptimal PRIVATE hoptimal_core)
    # scikit-build-core maps wheel.packages = ["python/hoptimal"] → the `hoptimal`
    # package at the wheel root, so the compiled module installs into `hoptimal/`.
    install(TARGETS _hoptimal DESTINATION hoptimal)
endif()
