cmake_minimum_required(VERSION 3.20)

# ---------------------------------------------------------------------
# Version
#
# VERSION.txt is the single source of truth. semantic-release writes it
# (see .releaserc.json) and everything else derives from it: this project()
# call, the installed package version file, docs/conf.py, and the Python
# module's __version__. Nothing else may hardcode a version. Three files
# used to, and all three had drifted to 0.1.0 while the repository was
# tagged 1.2.0-pre.6.
#
# The file may carry a pre-release suffix such as 1.2.0-pre.6. project()
# accepts only a numeric triple, so the full string is kept separately.
# ---------------------------------------------------------------------
set(SITTA_VERSION_FULL "0.0.0")
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION.txt")
    file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION.txt" SITTA_VERSION_FULL LIMIT_COUNT 1)
    string(STRIP "${SITTA_VERSION_FULL}" SITTA_VERSION_FULL)
else()
    message(WARNING "sitta: VERSION.txt not found; using ${SITTA_VERSION_FULL}")
endif()

if(SITTA_VERSION_FULL MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)")
    set(SITTA_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
else()
    message(WARNING "sitta: VERSION.txt holds '${SITTA_VERSION_FULL}', "
                    "which is not MAJOR.MINOR.PATCH; using 0.0.0")
    set(SITTA_VERSION "0.0.0")
endif()

project(sitta VERSION ${SITTA_VERSION} LANGUAGES CXX)
message(STATUS
    "sitta: version ${SITTA_VERSION_FULL} (package version ${PROJECT_VERSION})")

# ---------------------------------------------------------------------
# Core library: header-only, zero third-party dependencies.
# ---------------------------------------------------------------------
add_library(sitta INTERFACE)
add_library(sitta::sitta ALIAS sitta)

target_include_directories(sitta INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)
target_compile_features(sitta INTERFACE cxx_std_20)

# ---------------------------------------------------------------------
# Optional third-party interoperability
#
# These are detected but NEVER required by the core target above. They
# only affect examples/ and tests/, which build extra targets guarded by
# SITTA_HAS_XTENSOR / SITTA_HAS_KOKKOS.
# ---------------------------------------------------------------------

# ---------------------------------------------------------------------
# nanobind: FetchContent for Python bindings
# ---------------------------------------------------------------------
if(SITTA_ENABLE_PYTHON)
    include(FetchContent)
    
    # Allow users to specify nanobind location or use FetchContent
    option(SITTA_NANOBIND_FETCH "Fetch nanobind from GitHub" ON)
    
    if(SITTA_NANOBIND_FETCH)
        # Fetch nanobind from GitHub
        FetchContent_Declare(
            nanobind
            GIT_REPOSITORY https://github.com/wjakob/nanobind.git
            # Python 3.14 support landed in nanobind 2.11.0; anything older
            # cannot compile against a 3.14 interpreter.
            GIT_TAG        v2.15.0
            GIT_SHALLOW    TRUE
        )
        FetchContent_MakeAvailable(nanobind)
        
        # nanobind is header-only, so just add its include directory
        set(NANOBIND_INCLUDE_DIR ${nanobind_SOURCE_DIR}/include)
        message(STATUS "nanobind: fetched from GitHub (${nanobind_SOURCE_DIR})")
    else()
        # Try to find nanobind in standard locations
        option(SITTA_NANOBIND_ROOT "Path to nanobind include directory" "")
        
        if(NOT SITTA_NANOBIND_ROOT STREQUAL "")
            set(NANOBIND_INCLUDE_DIR ${SITTA_NANOBIND_ROOT})
        else()
            find_path(NANOBIND_INCLUDE_DIR
                NAMES nanobind/nanobind.h
                PATHS
                    ${CMAKE_SOURCE_DIR}/third_party/nanobind/include
                    ${CMAKE_INSTALL_PREFIX}/include
                    /usr/local/include
                    /usr/include
            )
        endif()
        
        if(NOT EXISTS ${NANOBIND_INCLUDE_DIR}/nanobind/nanobind.h)
            message(WARNING "nanobind not found. Set SITTA_NANOBIND_ROOT or enable SITTA_NANOBIND_FETCH")
            set(SITTA_ENABLE_PYTHON OFF)
        else()
            message(STATUS "nanobind: found at ${NANOBIND_INCLUDE_DIR}")
        endif()
    endif()
endif()

option(SITTA_ENABLE_XTENSOR          "Enable xtensor interop (auto-detected if available)"          ON)
option(SITTA_ENABLE_KOKKOS           "Enable Kokkos interop (auto-detected if available)"           ON)
option(SITTA_ENABLE_BOOST_MULTIARRAY "Enable Boost.MultiArray interop (auto-detected if available)" ON)
option(SITTA_ENABLE_HDF5             "Enable HDF5 interop (auto-detected if available)"             ON)
option(SITTA_ENABLE_TBB              "Enable TBB parallel iteration (auto-detected if available)"   ON)
option(SITTA_ENABLE_PYTHON  "Enable Python bindings (requires nanobind and Python dev)" OFF)
option(SITTA_PYTHON_INSTALL "Install Python module" ON)
option(SITTA_BUILD_EXAMPLES "Build examples" ON)
option(SITTA_BUILD_TESTS    "Build unit tests" ON)
# Device benchmarks only have anything to measure when Kokkos carries a GPU
# backend; the subdirectory is a no-op otherwise.
option(SITTA_BUILD_BENCHMARKS "Build device benchmarks (requires a Kokkos GPU backend)" ON)

option(SITTA_ENABLE_DOCS "Build Sphinx documentation (requires sphinx-build and clang-doc)" OFF)
if(SITTA_ENABLE_DOCS)
    # Declared here, above every add_subdirectory() below, and not next to the
    # docs target at the end of this file. CMake reads this variable when a
    # target is created, so a target defined earlier never opts in, and setting
    # it after the subdirectories produced no compile_commands.json at all.
    # docs/conf.py passes the standard and the include path to clang-doc
    # explicitly, so the database is a convenience rather than a requirement.
    set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE BOOL
        "Export compile commands (required for clang-doc)" FORCE)
endif()

set(SITTA_HAS_XTENSOR          OFF)
set(SITTA_HAS_KOKKOS           OFF)
set(SITTA_HAS_KOKKOS_CUDA      OFF)
set(SITTA_HAS_KOKKOS_HIP       OFF)
set(SITTA_HAS_KOKKOS_SYCL      OFF)
set(SITTA_HAS_KOKKOS_DEVICE    OFF)
set(SITTA_HAS_BOOST_MULTIARRAY OFF)
set(SITTA_HAS_HDF5             OFF)
set(SITTA_HAS_TBB              OFF)

if(SITTA_ENABLE_XTENSOR)
    find_package(xtensor QUIET)
    if(xtensor_FOUND)
        set(SITTA_HAS_XTENSOR ON)
        message(STATUS "sitta: xtensor found -- enabling xtensor interop")
    else()
        message(STATUS "sitta: xtensor not found -- xtensor interop disabled")
    endif()
endif()

if(SITTA_ENABLE_KOKKOS)
    # NOTE on the Kokkos compiler requirement:
    #
    # If Kokkos was built with CUDA enabled (Kokkos_ENABLE_CUDA=ON) and you
    # are not using a CUDA-aware compiler (clang with CUDA support, or
    # nvc++), you must configure this project with Kokkos's nvcc_wrapper as
    # the C++ compiler, e.g.:
    #
    #   cmake -DCMAKE_CXX_COMPILER=<kokkos_install>/bin/nvcc_wrapper ...
    #
    # find_package(Kokkos) must run before any target that links against it
    # is configured, since Kokkos may itself adjust compile flags via
    # Kokkos::kokkos's INTERFACE properties (this happens automatically once
    # the package is found -- no extra action needed beyond picking the
    # right compiler above).
    find_package(Kokkos QUIET)
    if(Kokkos_FOUND)
        set(SITTA_HAS_KOKKOS ON)
        message(STATUS "sitta: Kokkos found -- enabling Kokkos interop")

        # Record which device backends this Kokkos build carries. The
        # interop tests that need a real accelerator (device- and
        # managed-space round trips) are only built when at least one is
        # present; on a host-only Kokkos those code paths compile out.
        if(Kokkos_ENABLE_CUDA)
            set(SITTA_HAS_KOKKOS_CUDA ON)
            set(SITTA_HAS_KOKKOS_DEVICE ON)
            message(STATUS "sitta: Kokkos CUDA backend detected. "
                            "Ensure CMAKE_CXX_COMPILER is CUDA-capable "
                            "(e.g. Kokkos's nvcc_wrapper, clang, or nvc++).")
        endif()
        if(Kokkos_ENABLE_HIP)
            set(SITTA_HAS_KOKKOS_HIP ON)
            set(SITTA_HAS_KOKKOS_DEVICE ON)
            message(STATUS "sitta: Kokkos HIP backend detected "
                            "(architectures: ${Kokkos_HIP_ARCHITECTURES}). "
                            "Ensure CMAKE_CXX_COMPILER is HIP-capable "
                            "(e.g. ROCm's hipcc).")
        endif()
        if(Kokkos_ENABLE_SYCL)
            set(SITTA_HAS_KOKKOS_SYCL ON)
            set(SITTA_HAS_KOKKOS_DEVICE ON)
            message(STATUS "sitta: Kokkos SYCL backend detected. "
                            "Ensure CMAKE_CXX_COMPILER is SYCL-capable "
                            "(e.g. icpx).")
        endif()
    else()
        message(STATUS "sitta: Kokkos not found -- Kokkos interop disabled")
    endif()
endif()

if(SITTA_ENABLE_BOOST_MULTIARRAY)
    find_package(Boost QUIET)
    if(Boost_FOUND)
        set(SITTA_HAS_BOOST_MULTIARRAY ON)
        message(STATUS "sitta: Boost found -- enabling Boost.MultiArray interop")
    else()
        message(STATUS "sitta: Boost not found -- Boost.MultiArray interop disabled")
    endif()
endif()

if(SITTA_ENABLE_HDF5)
    find_package(HDF5 QUIET COMPONENTS C CXX)
    if(HDF5_FOUND)
        set(SITTA_HAS_HDF5 ON)
        message(STATUS "sitta: HDF5 found -- enabling HDF5 interop")
    else()
        message(STATUS "sitta: HDF5 not found -- HDF5 interop disabled")
    endif()
endif()

if(SITTA_ENABLE_TBB)
    find_package(TBB QUIET)
    if(TBB_FOUND)
        set(SITTA_HAS_TBB ON)
        message(STATUS "sitta: TBB found -- enabling TBB parallel iteration")
    else()
        message(STATUS "sitta: TBB not found -- TBB parallel iteration disabled")
    endif()
endif()

if(SITTA_BUILD_TESTS)
    enable_testing()
endif()

if(SITTA_ENABLE_PYTHON)
    add_subdirectory(python)
endif()

if(SITTA_BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

if(SITTA_BUILD_TESTS)
    add_subdirectory(tests)
endif()

if(SITTA_BUILD_BENCHMARKS)
    add_subdirectory(benchmarks)
endif()

# ---------------------------------------------------------------------
# Documentation (optional)
# ---------------------------------------------------------------------
if(SITTA_ENABLE_DOCS)
    find_program(SPHINX_BUILD sphinx-build)
    if(NOT SPHINX_BUILD)
        message(WARNING "sphinx-build not found; SITTA_ENABLE_DOCS target will not work")
    else()
        add_custom_target(docs
            COMMAND ${SPHINX_BUILD} -b html
                    ${CMAKE_CURRENT_SOURCE_DIR}/docs
                    ${CMAKE_CURRENT_BINARY_DIR}/docs_html
            WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
            COMMENT "Building Sphinx + clang-doc API documentation"
        )
        message(STATUS "sitta: docs target enabled (cmake --build build --target docs)")
    endif()
endif()

# ---------------------------------------------------------------------
# Install rules
# ---------------------------------------------------------------------
include(GNUInstallDirs)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(TARGETS sitta EXPORT sitta-targets)
install(EXPORT sitta-targets
        FILE sitta-targets.cmake
        NAMESPACE sitta::
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/sitta)

# find_package(sitta) needs a package *config* file. The exported
# targets file above is not one, and CMake never looks at it on its own: with
# only that file installed, find_package fails with "Could not find a package
# configuration file" no matter where it is pointed.
include(CMakePackageConfigHelpers)

configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/sitta-config.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/sitta-config.cmake
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/sitta)

# ARCH_INDEPENDENT: the library is header-only, so an install made on one
# architecture is usable on another. Without it the generated file bakes in a
# CMAKE_SIZEOF_VOID_P equality check that would reject a 32-bit consumer for no
# reason. Added in CMake 3.14, below the 3.20 this project already requires.
write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/sitta-config-version.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion
    ARCH_INDEPENDENT)

# This DESTINATION must stay identical to the install(EXPORT ...) one above.
# The config file reaches the targets file through ${CMAKE_CURRENT_LIST_DIR},
# so splitting the two directories breaks the include at consumer-configure
# time, inside a generated file, with a confusing error.
install(FILES
        ${CMAKE_CURRENT_BINARY_DIR}/sitta-config.cmake
        ${CMAKE_CURRENT_BINARY_DIR}/sitta-config-version.cmake
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/sitta)
