# Disable in-source builds to prevent source tree corruption
if("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_BINARY_DIR}")
    message(
        FATAL_ERROR
        "
FATAL: In-source builds are not allowed.
       You should create a separate directory for build files.
"
    )
endif()

cmake_minimum_required(VERSION 3.21)
set(CMAKE_MODULE_PATH
    ${CMAKE_MODULE_PATH}
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules"
)

# Make `GIT_SUBMODULES ""` initialize no submodules
cmake_policy(SET CMP0097 NEW)

project(Sleipnir)

set_property(GLOBAL PROPERTY USE_FOLDERS ON)

# Use, i.e. don't skip the full RPATH for the build tree
set(CMAKE_SKIP_BUILD_RPATH FALSE)

# When building, don't use the install RPATH already (but later on when
# installing)
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)

# Add the automatically determined parts of the RPATH which point to directories
# outside the build tree to the install RPATH
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

# The RPATH to be used when installing, but only if it's not a system directory
list(
    FIND
    CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES
    "${CMAKE_INSTALL_PREFIX}/lib"
    isSystemDir
)
if("${isSystemDir}" STREQUAL "-1")
    list(APPEND CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
endif()

# Set default build type to release with debug info (i.e. release mode
# optimizations are performed, but debug info still exists).
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "" FORCE)
endif()

# Generate compile_commands.json by default
if(NOT CMAKE_EXPORT_COMPILE_COMMANDS)
    set(CMAKE_EXPORT_COMPILE_COMMANDS "YES" CACHE STRING "" FORCE)
endif()

include(SleipnirBuildTypes)

# Control where the static and shared libraries are built so that on Windows,
# we don't need to tinker with the path to run the executable
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")

option(BUILD_SHARED_LIBS "Build using shared libraries" ON)

set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS FALSE)

option(BUILD_BENCHMARKING "Build CasADi and Sleipnir benchmarks" OFF)
option(BUILD_EXAMPLES "Build examples" OFF)
option(BUILD_PYTHON "Build Python module" OFF)

include(SleipnirCompilerFlags)

file(GLOB_RECURSE Sleipnir_src src/*.cpp)
add_library(Sleipnir ${Sleipnir_src})
add_library(Sleipnir::Sleipnir ALIAS Sleipnir)
sleipnir_compiler_flags(Sleipnir)
target_include_directories(Sleipnir PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)

set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE)

set_target_properties(Sleipnir PROPERTIES DEBUG_POSTFIX "d")

set_property(TARGET Sleipnir PROPERTY FOLDER "libraries")
target_compile_definitions(Sleipnir PRIVATE SLEIPNIR_EXPORTS)

include(CTest)
include(FetchContent)

# Options for using a package manager (e.g., vcpkg) for certain dependencies
option(USE_SYSTEM_EIGEN "Use system eigen" OFF)
option(USE_SYSTEM_FMT "Use system fmt" OFF)
option(USE_SYSTEM_PYBIND "Use system pybind" OFF)

# Required for std::async()
find_package(Threads REQUIRED)

target_link_libraries(Sleipnir PUBLIC Threads::Threads)

# Eigen dependency
if(NOT USE_SYSTEM_EIGEN)
    set(EIGEN_BUILD_CMAKE_PACKAGE TRUE)
    fetchcontent_declare(
        Eigen3
        GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
        # master on 2024-05-08
        GIT_TAG ff174f79264d3f8dc0115dea7a288f98208b694f
    )
    fetchcontent_makeavailable(Eigen3)
else()
    find_package(Eigen3 CONFIG REQUIRED)
endif()

target_link_libraries(Sleipnir PUBLIC Eigen3::Eigen)

# fmt dependency
if(NOT USE_SYSTEM_FMT)
    fetchcontent_declare(
        fmt
        GIT_REPOSITORY https://github.com/fmtlib/fmt.git
        GIT_TAG 10.2.1
    )
    fetchcontent_makeavailable(fmt)
else()
    find_package(fmt CONFIG REQUIRED)
endif()

target_link_libraries(Sleipnir PUBLIC fmt::fmt)

if(BUILD_TESTING)
    # Catch2 dependency
    fetchcontent_declare(
        Catch2
        GIT_REPOSITORY https://github.com/catchorg/Catch2.git
        GIT_TAG v3.5.3
        CMAKE_ARGS
    )
    fetchcontent_makeavailable(Catch2)
endif()

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

install(
    TARGETS Sleipnir
    COMPONENT Sleipnir
    EXPORT SleipnirTargets
    LIBRARY
    DESTINATION lib
    ARCHIVE
    DESTINATION lib
    RUNTIME
    DESTINATION bin
    INCLUDES DESTINATION include
)
export(TARGETS Sleipnir FILE Sleipnir.cmake NAMESPACE Sleipnir::)
install(DIRECTORY include/ COMPONENT Sleipnir DESTINATION "include")
install(
    EXPORT SleipnirTargets
    FILE Sleipnir.cmake
    NAMESPACE Sleipnir::
    DESTINATION lib/cmake/Sleipnir
)

include(CMakePackageConfigHelpers)

# Generate the config file that includes the exports
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/SleipnirConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/SleipnirConfig.cmake
    INSTALL_DESTINATION "lib/cmake/Sleipnir"
    NO_SET_AND_CHECK_MACRO
    NO_CHECK_REQUIRED_COMPONENTS_MACRO
)

# Install the config file
install(
    FILES ${CMAKE_CURRENT_BINARY_DIR}/SleipnirConfig.cmake
    COMPONENT Sleipnir
    DESTINATION lib/cmake/Sleipnir
)

# Add benchmark executables if CasADi exists
if(BUILD_BENCHMARKING)
    find_package(casadi QUIET)
    if(casadi_FOUND)
        foreach(benchmark "CartPole" "Flywheel")
            file(
                GLOB ${benchmark}ScalabilityBenchmark_src
                benchmarks/scalability/*.cpp
                benchmarks/scalability/${benchmark}/*.cpp
            )
            add_executable(
                ${benchmark}ScalabilityBenchmark
                ${${benchmark}ScalabilityBenchmark_src}
            )
            sleipnir_compiler_flags(${benchmark}ScalabilityBenchmark)
            target_include_directories(
                ${benchmark}ScalabilityBenchmark
                PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks/scalability
            )
            target_link_libraries(
                ${benchmark}ScalabilityBenchmark
                PRIVATE Sleipnir casadi
            )
        endforeach()
    endif()
endif()

if(BUILD_TESTING)
    enable_testing()
    list(APPEND CMAKE_MODULE_PATH ${Catch2_SOURCE_DIR}/extras)
    include(Catch)
endif()

# Build Sleipnir tests
if(BUILD_TESTING)
    file(GLOB_RECURSE Sleipnir_test_src test/src/*.cpp)
    add_executable(SleipnirTest ${Sleipnir_test_src})
    sleipnir_compiler_flags(SleipnirTest)
    target_include_directories(
        SleipnirTest
        PRIVATE
            ${CMAKE_CURRENT_SOURCE_DIR}/src
            ${CMAKE_CURRENT_SOURCE_DIR}/test/include
    )
    target_link_libraries(SleipnirTest PRIVATE Sleipnir Catch2::Catch2)
    if(NOT CMAKE_TOOLCHAIN_FILE)
        catch_discover_tests(SleipnirTest)
    endif()
endif()

# Build examples and example tests
if(BUILD_EXAMPLES)
    include(SleipnirSubdirList)
    sleipnir_subdir_list(EXAMPLES ${CMAKE_CURRENT_SOURCE_DIR}/examples)
    foreach(example ${EXAMPLES})
        # Build example
        file(GLOB_RECURSE sources examples/${example}/src/*.cpp)
        add_executable(${example} ${sources})
        sleipnir_compiler_flags(${example})
        target_include_directories(
            ${example}
            PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/${example}/include
        )
        target_link_libraries(${example} PRIVATE Sleipnir)

        # Build example test if files exist for it
        if(
            BUILD_TESTING
            AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/examples/${example}/test
        )
            file(GLOB_RECURSE test_sources examples/${example}/test/*.cpp)
            add_executable(${example}Test ${sources} ${test_sources})
            target_include_directories(
                ${example}Test
                PRIVATE
                    ${CMAKE_CURRENT_SOURCE_DIR}/examples/${example}/src
                    ${CMAKE_CURRENT_SOURCE_DIR}/examples/${example}/test
            )
            sleipnir_compiler_flags(${example}Test)
            target_compile_definitions(${example}Test PUBLIC RUNNING_TESTS)
            target_include_directories(
                ${example}Test
                PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/${example}/include
            )
            target_link_libraries(
                ${example}Test
                PRIVATE Sleipnir Catch2::Catch2WithMain
            )
            if(NOT CMAKE_TOOLCHAIN_FILE)
                catch_discover_tests(${example}Test)
            endif()
        endif()
    endforeach()
endif()

if(BUILD_PYTHON)
    find_package(Python REQUIRED COMPONENTS Interpreter Development)
    if(DEFINED PY_BUILD_CMAKE_MODULE_NAME)
        set(PY_DEST ${PY_BUILD_CMAKE_MODULE_NAME})
    else()
        set(PY_DEST lib/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR})
    endif()

    # pybind11 dependency
    if(NOT USE_SYSTEM_PYBIND)
        fetchcontent_declare(
            pybind11
            GIT_REPOSITORY https://github.com/pybind/pybind11.git
            GIT_TAG v2.12.0
        )
        fetchcontent_makeavailable(pybind11)
    else()
        find_package(pybind11 CONFIG REQUIRED)
    endif()

    file(GLOB_RECURSE jormungandr_src jormungandr/cpp/*.cpp)

    # Build fmt and Sleipnir dependencies directly into the wheel to avoid having
    # to configure RPATHs
    set(fmt_src ${fmt_SOURCE_DIR}/src/format.cc ${fmt_SOURCE_DIR}/src/os.cc)
    pybind11_add_module(_jormungandr ${jormungandr_src} ${fmt_src} ${Sleipnir_src})
    sleipnir_compiler_flags(_jormungandr)
    target_compile_definitions(_jormungandr PRIVATE JORMUNGANDR=1)
    target_include_directories(
        _jormungandr
        PRIVATE
            ${CMAKE_CURRENT_SOURCE_DIR}/src
            ${CMAKE_CURRENT_SOURCE_DIR}/include
            ${fmt_SOURCE_DIR}/include
            ${CMAKE_CURRENT_SOURCE_DIR}/jormungandr/cpp
    )
    target_link_libraries(
        _jormungandr
        PUBLIC pybind11::module Threads::Threads Eigen3::Eigen
    )

    install(
        TARGETS _jormungandr
        COMPONENT python_modules
        LIBRARY
        DESTINATION ${PY_DEST}
    )

    # pybind11-stubgen and pybind11_mkdoc don't support Windows
    if(NOT WIN32 AND NOT CMAKE_CROSSCOMPILING)
        # pybind11-stubgen dependency
        fetchcontent_declare(
            pybind11-stubgen
            GIT_REPOSITORY https://github.com/sizmailov/pybind11-stubgen.git
            GIT_TAG v.2.5
            GIT_SUBMODULES ""
        )
        fetchcontent_makeavailable(pybind11-stubgen)

        # Generate stubs for the Python module
        include(cmake/modules/Pybind11Stubgen.cmake)
        pybind11_stubgen(_jormungandr)
        pybind11_stubgen_install(_jormungandr ${PY_DEST})

        # pybind11_mkdoc dependency
        fetchcontent_declare(
            pybind11_mkdoc
            GIT_REPOSITORY https://github.com/pybind/pybind11_mkdoc.git
            GIT_TAG master
            GIT_SUBMODULES ""
        )
        fetchcontent_makeavailable(pybind11_mkdoc)

        file(
            GLOB_RECURSE sleipnir_headers
            include/sleipnir/autodiff/*.hpp
            include/sleipnir/control/*.hpp
            include/sleipnir/optimization/*.hpp
        )

        # Generate docs for the Python module
        include(cmake/modules/Pybind11Mkdoc.cmake)
        pybind11_mkdoc(_jormungandr "${sleipnir_headers}")
        add_dependencies(_jormungandr _jormungandr_docstrings)
    endif()
endif()
