# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (c) 2026 ss0832
#
# Build rules for the C++ kernel, the gfn0-cli binary, and the pybind11
# extension. The library uses TORCH_LIBRARIES coming from FindTorch.

# ----------------------------------------------------------------------
# Source list
# ----------------------------------------------------------------------
set(GFN0_CORE_SOURCES
    src/core/basis.cpp
    src/core/eeq.cpp
    src/core/overlap.cpp
    src/core/slater_overlap.cpp
    src/core/external_fields.cpp
    src/core/external_point_charges.cpp
    src/core/analytical_gradient.cpp
    src/core/autograd_impl.cpp
    src/core/pair_stream.cpp
    src/core/srb.cpp
    src/core/calculator.cpp
    src/dispersion/d4_reference.cpp
    src/dispersion/dispersion_d4.cpp
    src/parameters/params.cpp
    src/parameters/param_targets.cpp
    src/lab/hessian.cpp
    src/lab/mo_response.cpp
    src/lab/sensitivity.cpp)

set(GFN0_PUBLIC_HEADERS
    include/gfn0/analytical_gradient.hpp
    include/gfn0/autograd_impl.hpp
    include/gfn0/basis.hpp
    include/gfn0/calculator.hpp
    include/gfn0/d4_reference.hpp
    include/gfn0/dispersion_d4.hpp
    include/gfn0/eeq.hpp
    include/gfn0/external_fields.hpp
    include/gfn0/external_point_charges.hpp
    include/gfn0/gfn0.hpp
    include/gfn0/overlap.hpp
    include/gfn0/pair_stream.hpp
    include/gfn0/param_targets.hpp
    include/gfn0/params.hpp
    include/gfn0/slater_overlap.hpp
    include/gfn0/srb.hpp
    include/gfn0/tensor_utils.hpp
    include/gfn0/lab/hessian.hpp
    include/gfn0/lab/mo_response.hpp
    include/gfn0/lab/sensitivity.hpp)

# ----------------------------------------------------------------------
# Static library: gfn0_torch
# ----------------------------------------------------------------------
add_library(gfn0_torch STATIC ${GFN0_CORE_SOURCES} ${GFN0_PUBLIC_HEADERS})
add_library(gfn0::gfn0_torch ALIAS gfn0_torch)

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

target_compile_features(gfn0_torch PUBLIC cxx_std_17)

target_link_libraries(gfn0_torch PUBLIC ${TORCH_LIBRARIES})
set_target_properties(gfn0_torch PROPERTIES POSITION_INDEPENDENT_CODE ON)

if(MSVC)
    target_compile_options(gfn0_torch PRIVATE /W4 /permissive- /bigobj)
else()
    target_compile_options(gfn0_torch PRIVATE
        -Wall -Wextra -Wpedantic
        -Wno-unused-parameter -Wno-sign-compare)

    # GCC 13 can emit a false-positive -Warray-bounds warning from
    # LibTorch's torch::autograd::Function<T>::apply implementation.
    # The warning is triggered in the STL vector<bool> bookkeeping inside
    # custom_function.h after the Mermin custom autograd nodes are inlined
    # through calculator.cpp, not by user memory access in the node code.
    # Keep the suppression source-local so the rest of the project remains
    # covered by -Warray-bounds.
    if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        foreach(_gfn0_custom_autograd_src
                src/core/autograd_impl.cpp
                src/core/calculator.cpp
                src/core/eeq.cpp)
            set_source_files_properties(${_gfn0_custom_autograd_src} PROPERTIES
                COMPILE_OPTIONS "-Wno-array-bounds;-Wno-error=array-bounds;-Wno-stringop-overflow;-Wno-error=stringop-overflow")
        endforeach()
    endif()
endif()

# ----------------------------------------------------------------------
# pybind11 module
# ----------------------------------------------------------------------
if(GFN0_BUILD_PYBIND)
    # pybind11 >= 3 expects CMake's modern FindPython target/functions to be
    # initialized before pybind11_add_module is evaluated. Do this
    # unconditionally, even when pybind11 itself is already discoverable.
    find_package(Python3 COMPONENTS Interpreter Development.Module REQUIRED)
    set(PYBIND11_FINDPYTHON ON CACHE BOOL "Use modern CMake FindPython for pybind11" FORCE)

    find_package(pybind11 CONFIG QUIET)
    if(NOT pybind11_FOUND)
        execute_process(
            COMMAND "${Python3_EXECUTABLE}" -c
                "import pybind11; print(pybind11.get_cmake_dir())"
            OUTPUT_VARIABLE _pybind11_cmake_dir
            OUTPUT_STRIP_TRAILING_WHITESPACE)
        if(_pybind11_cmake_dir)
            list(PREPEND CMAKE_PREFIX_PATH "${_pybind11_cmake_dir}")
        endif()
        find_package(pybind11 CONFIG REQUIRED)
    endif()

    # The bindings expose torch::Tensor-native methods (evaluate_tensor,
    # parameter_gradients_tensor, mo_response_tensor). pybind11's Tensor
    # type_caster for at::Tensor is implemented in libtorch_python, not in
    # libtorch/libtorch_cpu alone. Without this explicit link, importing the
    # extension can fail with an undefined symbol such as:
    #   pybind11::detail::type_caster<at::Tensor>::load(...)
    # even though libtorch_cpu.so/libtorch.so/libc10.so are found by ldd.
    find_library(GFN0_TORCH_PYTHON_LIBRARY
        NAMES torch_python
        PATHS "${_gfn0_torch_lib_dir}"
        NO_DEFAULT_PATH)
    if(NOT GFN0_TORCH_PYTHON_LIBRARY)
        find_library(GFN0_TORCH_PYTHON_LIBRARY NAMES torch_python)
    endif()
    if(NOT GFN0_TORCH_PYTHON_LIBRARY)
        message(FATAL_ERROR
            "GFN0_BUILD_PYBIND=ON requires libtorch_python because the "
            "Python extension exposes torch::Tensor arguments/returns. "
            "Could not find libtorch_python in '${_gfn0_torch_lib_dir}'. "
            "Use the active Python torch provider, or remove/disable the "
            "tensor-native pybind methods before building against a pure "
            "C++ LibTorch tree.")
    endif()
    message(STATUS "  torch_python lib : ${GFN0_TORCH_PYTHON_LIBRARY}")

    set(GFN0_PYBIND_SOURCES
        src/bindings/_module.cpp
        src/bindings/kernel_bindings.cpp
        src/bindings/binding_validation.cpp)

    pybind11_add_module(_gfn0_torch MODULE ${GFN0_PYBIND_SOURCES})
    target_link_libraries(_gfn0_torch PRIVATE gfn0_torch ${GFN0_TORCH_PYTHON_LIBRARY})
    target_include_directories(_gfn0_torch PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)

    if(UNIX)
        set_target_properties(_gfn0_torch PROPERTIES
            BUILD_RPATH "${_gfn0_torch_lib_dir};${_gfn0_origin_rpath}"
            INSTALL_RPATH "${_gfn0_origin_rpath};${_gfn0_torch_lib_dir}"
            INSTALL_RPATH_USE_LINK_PATH TRUE)
    endif()

    # When driven by scikit-build-core, install the extension into the
    # pygfn0_torch package directory.
    if(SKBUILD)
        install(TARGETS _gfn0_torch LIBRARY DESTINATION .)
    else()
        install(TARGETS _gfn0_torch LIBRARY DESTINATION pygfn0_torch)
    endif()
endif()

# ----------------------------------------------------------------------
# CLI executable
# ----------------------------------------------------------------------
if(GFN0_BUILD_CLI)
    add_executable(gfn0-cli src/cli/gfn0_cli.cpp)
    target_link_libraries(gfn0-cli PRIVATE gfn0_torch)
    set_target_properties(gfn0-cli PROPERTIES OUTPUT_NAME gfn0-cli)

    if(SKBUILD)
        # The Python entry-point in pyproject.toml is the canonical way to
        # invoke the CLI from a wheel. We still ship the native binary in
        # bin/ for non-Python use cases.
        install(TARGETS gfn0-cli RUNTIME DESTINATION bin OPTIONAL)
    else()
        install(TARGETS gfn0-cli RUNTIME DESTINATION bin)
    endif()
endif()

# ----------------------------------------------------------------------
# Public install rules (only used outside scikit-build-core)
# ----------------------------------------------------------------------
if(NOT SKBUILD)
    install(DIRECTORY include/ DESTINATION include FILES_MATCHING PATTERN "*.hpp")
    install(TARGETS gfn0_torch ARCHIVE DESTINATION lib)
endif()
