cmake_minimum_required(VERSION 3.25)

project(easybind LANGUAGES CXX)

include(GNUInstallDirs)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

option(EASYBIND_SKIP_INSTALL "Skip installing easybind artifacts" OFF)
if(NOT PROJECT_IS_TOP_LEVEL)
    set(EASYBIND_SKIP_INSTALL ON)
endif()

# Ensure Python development headers/targets are available for nanobind + modules.
find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)

# Prefer Python::Module (extension modules), fall back to Python::Python.
set(EASYBIND_PYTHON_TARGET "")
if(TARGET Python::Module)
    set(EASYBIND_PYTHON_TARGET Python::Module)
elseif(TARGET Python::Python)
    set(EASYBIND_PYTHON_TARGET Python::Python)
else()
    message(FATAL_ERROR "Python imported target not found (expected Python::Module or Python::Python)")
endif()

# Third-party sources: pins live in cmake/easybind_dependencies.cmake (also installed for consumers).
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/easybind_dependencies.cmake")
easybind_fetch_third_party_deps()

# Some nanobind configurations don't automatically attach Python include dirs.
foreach(_nb_tgt IN ITEMS nanobind nanobind_shared nanobind-static)
    if(TARGET ${_nb_tgt})
        target_link_libraries(${_nb_tgt} PRIVATE ${EASYBIND_PYTHON_TARGET})
        set_target_properties(${_nb_tgt} PROPERTIES
            CXX_VISIBILITY_PRESET default
            VISIBILITY_INLINES_HIDDEN OFF
        )
        target_compile_options(${_nb_tgt} PRIVATE
            $<$<CXX_COMPILER_ID:GNU,Clang>:-fvisibility=default>
        )
    endif()
endforeach()

function(easybind_add_extension target_name)
    nanobind_add_module(${target_name} NB_SHARED ${ARGN})
    target_link_libraries(${target_name} PRIVATE ${EASYBIND_PYTHON_TARGET})
    if(APPLE)
        set_target_properties(${target_name} PROPERTIES
            BUILD_RPATH "@loader_path"
            INSTALL_RPATH "@loader_path"
        )
    else()
        set_target_properties(${target_name} PROPERTIES
            BUILD_RPATH "$ORIGIN"
            INSTALL_RPATH "$ORIGIN"
        )
    endif()
endfunction()

add_library(easybind__module__ SHARED
    src/easybind/module/node.cpp
)
set_target_properties(easybind__module__ PROPERTIES
    OUTPUT_NAME "easybind"
    LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/easybind"
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/easybind"
)
target_include_directories(easybind__module__ PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
)
target_compile_definitions(easybind__module__ PRIVATE EASYBIND_BUILD)
if(DEFINED nanobind_SOURCE_DIR)
    target_include_directories(easybind__module__ PRIVATE
        ${nanobind_SOURCE_DIR}/include
    )
endif()
target_link_libraries(easybind__module__ PRIVATE ${EASYBIND_PYTHON_TARGET})
if(TARGET nanobind)
    target_link_libraries(easybind__module__ PRIVATE nanobind)
    add_dependencies(easybind__module__ nanobind)
elseif(TARGET nanobind_shared)
    target_link_libraries(easybind__module__ PRIVATE nanobind_shared)
    add_dependencies(easybind__module__ nanobind_shared)
elseif(TARGET nanobind-static)
    target_link_libraries(easybind__module__ PRIVATE nanobind-static)
    target_compile_definitions(easybind__module__ PRIVATE NB_STATIC)
    add_dependencies(easybind__module__ nanobind-static)
endif()
if(APPLE)
    set_target_properties(easybind__module__ PROPERTIES
        BUILD_RPATH "@loader_path"
        INSTALL_RPATH "@loader_path"
    )
else()
    set_target_properties(easybind__module__ PROPERTIES
        BUILD_RPATH "$ORIGIN"
        INSTALL_RPATH "$ORIGIN"
    )
endif()
set_target_properties(easybind__module__ PROPERTIES
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN ON
)
target_compile_options(easybind__module__ PRIVATE
    $<$<CXX_COMPILER_ID:GNU,Clang>:-fvisibility=hidden>
)

add_library(easybind_sample__module__ SHARED
    src/easybind/sample/sample.cpp
)
set_target_properties(easybind_sample__module__ PROPERTIES
    OUTPUT_NAME "easybind-sample"
    LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/easybind/sample"
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/easybind/sample"
)
target_include_directories(easybind_sample__module__ PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
)
if(TARGET magic_enum::magic_enum)
    target_link_libraries(easybind_sample__module__ PRIVATE magic_enum::magic_enum)
    target_include_directories(easybind_sample__module__ PRIVATE
        ${magic_enum_SOURCE_DIR}/include
    )
else()
    target_include_directories(easybind_sample__module__ PRIVATE
        ${magic_enum_SOURCE_DIR}/include
    )
endif()
target_compile_definitions(easybind_sample__module__ PRIVATE EASYBIND_SAMPLE_BUILD)
if(APPLE)
    set_target_properties(easybind_sample__module__ PROPERTIES
        BUILD_RPATH "@loader_path"
        INSTALL_RPATH "@loader_path"
    )
else()
    set_target_properties(easybind_sample__module__ PROPERTIES
        BUILD_RPATH "$ORIGIN"
        INSTALL_RPATH "$ORIGIN"
    )
endif()
set_target_properties(easybind_sample__module__ PROPERTIES
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN ON
)
target_compile_options(easybind_sample__module__ PRIVATE
    $<$<CXX_COMPILER_ID:GNU,Clang>:-fvisibility=hidden>
)

easybind_add_extension(easybind_sample__init__
    src/easybind/sample/__init__.cpp
)
target_link_libraries(easybind_sample__init__ PRIVATE
    easybind__module__
    easybind_sample__module__
)
set_target_properties(easybind_sample__init__ PROPERTIES
    OUTPUT_NAME "__init__"
    LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/easybind/sample"
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/easybind/sample"
)
target_include_directories(easybind_sample__init__ PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
)
if(TARGET magic_enum::magic_enum)
    target_link_libraries(easybind_sample__init__ PRIVATE magic_enum::magic_enum)
    target_include_directories(easybind_sample__init__ PRIVATE
        ${magic_enum_SOURCE_DIR}/include
    )
else()
    target_include_directories(easybind_sample__init__ PRIVATE
        ${magic_enum_SOURCE_DIR}/include
    )
endif()
if(DEFINED reflect_cpp_SOURCE_DIR)
    target_include_directories(easybind_sample__init__ PRIVATE
        ${reflect_cpp_SOURCE_DIR}/include
    )
endif()
# sample/__init__.so lives in easybind/sample/; it links libeasybind-sample.so (same
# dir), libeasybind.so + libnanobind.so (parent). RPATH needs BOTH $ORIGIN and
# $ORIGIN/.. or auditwheel cannot resolve libeasybind-sample.so (path null).
if(APPLE)
    set_target_properties(easybind_sample__init__ PROPERTIES
        BUILD_RPATH "@loader_path;@loader_path/.."
        INSTALL_RPATH "@loader_path;@loader_path/.."
    )
elseif(UNIX)
    set_target_properties(easybind_sample__init__ PROPERTIES
        BUILD_RPATH "$ORIGIN;$ORIGIN/.."
        INSTALL_RPATH "$ORIGIN;$ORIGIN/.."
    )
else()
    set_target_properties(easybind_sample__init__ PROPERTIES
        BUILD_RPATH "$ORIGIN"
        INSTALL_RPATH "$ORIGIN"
    )
endif()

if(NOT EASYBIND_SKIP_INSTALL)
    install(TARGETS easybind_sample__module__
        LIBRARY DESTINATION easybind/sample
        RUNTIME DESTINATION easybind/sample
        ARCHIVE DESTINATION easybind/sample
    )
    install(TARGETS easybind_sample__init__
        LIBRARY DESTINATION easybind/sample
        RUNTIME DESTINATION easybind/sample
        ARCHIVE DESTINATION easybind/sample
    )

    # If nanobind is built as a shared library, ship it alongside extensions so
    # `libnanobind.so` is resolvable at runtime.
    if(TARGET nanobind)
        install(TARGETS nanobind
            LIBRARY DESTINATION easybind
            RUNTIME DESTINATION easybind
            ARCHIVE DESTINATION easybind
        )
    endif()
endif()

easybind_add_extension(easybind__init__
    src/easybind/__init__.cpp
    src/easybind/module/__init__.cpp
    src/easybind/module/node__init__.cpp
)
target_link_libraries(easybind__init__ PRIVATE easybind__module__)
set_target_properties(easybind__init__ PROPERTIES
    OUTPUT_NAME "__init__"
    LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/easybind"
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/easybind"
)
target_include_directories(easybind__init__ PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
)
if(TARGET magic_enum::magic_enum)
    target_link_libraries(easybind__init__ PRIVATE magic_enum::magic_enum)
    target_include_directories(easybind__init__ PRIVATE
        ${magic_enum_SOURCE_DIR}/include
    )
else()
    target_include_directories(easybind__init__ PRIVATE
        ${magic_enum_SOURCE_DIR}/include
    )
endif()
if(DEFINED reflect_cpp_SOURCE_DIR)
    target_include_directories(easybind__init__ PRIVATE
        ${reflect_cpp_SOURCE_DIR}/include
    )
endif()
if(APPLE)
    set_target_properties(easybind__init__ PROPERTIES
        BUILD_RPATH "@loader_path"
        INSTALL_RPATH "@loader_path"
    )
else()
    set_target_properties(easybind__init__ PROPERTIES
        BUILD_RPATH "$ORIGIN"
        INSTALL_RPATH "$ORIGIN"
    )
endif()

# Build-time stub generation (package layout).
function(easybind_add_stub_package stub_target_prefix module_target)
    foreach(module_name IN LISTS ARGN)
        string(REPLACE "." "/" module_path "${module_name}")
        set(output_path "${CMAKE_CURRENT_SOURCE_DIR}/src/${module_path}/__init__.pyi")
        get_filename_component(output_dir "${output_path}" DIRECTORY)
        file(MAKE_DIRECTORY "${output_dir}")

        string(REPLACE "." "_" module_target_suffix "${module_name}")
        set(stub_target "${stub_target_prefix}_${module_target_suffix}_stub")

        nanobind_add_stub(${stub_target}
            MODULE ${module_name}
            OUTPUT ${output_path}
            PYTHON_PATH $<TARGET_FILE_DIR:${module_target}>
            DEPENDS ${module_target}
        )
    endforeach()
endfunction()

easybind_add_stub_package(easybind
    easybind__init__
    easybind
    easybind.module)
easybind_add_stub_package(easybind_sample
    easybind_sample__init__
    easybind.sample)

if(NOT EASYBIND_SKIP_INSTALL)
    install(TARGETS easybind__module__
        LIBRARY DESTINATION easybind
        RUNTIME DESTINATION easybind
        ARCHIVE DESTINATION easybind
    )
    install(TARGETS easybind__init__
        LIBRARY DESTINATION easybind
        RUNTIME DESTINATION easybind
        ARCHIVE DESTINATION easybind
    )

    install(DIRECTORY src/easybind
        DESTINATION .
    )
    # Third-party licenses (nanobind, magic_enum, reflect-cpp): see NOTICE.
    install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/NOTICE"
        DESTINATION easybind
    )
    install(FILES
        "${CMAKE_CURRENT_SOURCE_DIR}/cmake/easybind_dependencies.cmake"
        "${CMAKE_CURRENT_SOURCE_DIR}/cmake/easybind_pip.cmake"
        DESTINATION easybind/cmake
    )
endif()
