#
# eiskaltdcpp-py — Python SWIG bindings for libeiskaltdcpp
#
# Copyright (C) 2026 Verlihub Team
# Licensed under GPL-3.0-or-later
#

cmake_minimum_required(VERSION 3.14)
project(eiskaltdcpp-py VERSION 2.6.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Generate _version.py so the Python package always knows the version
file(WRITE "${CMAKE_BINARY_DIR}/python/eiskaltdcpp/_version.py"
    "# Auto-generated by CMake — do not edit\n"
    "__version__ = \"${PROJECT_VERSION}\"\n"
)

include(GNUInstallDirs)
include(FetchContent)

# ===========================================================================
# Options
# ===========================================================================

option(BUILD_TESTS "Build tests" ON)
option(USE_SYSTEM_EISKALTDCPP "Prefer system libeiskaltdcpp if available" ON)
set(EISKALTDCPP_SDK_DIR "" CACHE PATH
    "Path to a prebuilt eiskaltdcpp core SDK (contains include/ and lib/). Takes priority over any system library.")

# ===========================================================================
# Find Python
# ===========================================================================

# Development.Module (headers + the Python3::Module target) is all a Python
# extension needs. Requesting the full Development component pulls in
# Development.Embed, which requires a linkable libpython that is absent in
# manylinux images, breaking the cibuildwheel Linux build.
find_package(Python3 COMPONENTS Interpreter Development.Module REQUIRED)

if(Python3_VERSION VERSION_LESS "3.10")
    message(FATAL_ERROR "Python 3.10 or higher required, found ${Python3_VERSION}")
endif()

message(STATUS "Python ${Python3_VERSION} found: ${Python3_EXECUTABLE}")
message(STATUS "  Include: ${Python3_INCLUDE_DIRS}")

# ===========================================================================
# Find SWIG
# ===========================================================================

find_package(SWIG 4.0 REQUIRED)
include(UseSWIG)

message(STATUS "SWIG ${SWIG_VERSION} found: ${SWIG_EXECUTABLE}")

# ===========================================================================
# Find libeiskaltdcpp — system package or FetchContent fallback
# ===========================================================================

set(EISKALTDCPP_FOUND FALSE)

# 1) Prefer an explicit prebuilt SDK (see eiskaltdcpp's EISKALTDCPP_SDK build).
#    Searched with NO_DEFAULT_PATH and before the system package so a stale
#    system copy cannot shadow the pinned version.
if(EISKALTDCPP_SDK_DIR)
    find_path(EISKALTDCPP_INCLUDE_DIR
        NAMES dcpp/DCPlusPlus.h
        PATHS "${EISKALTDCPP_SDK_DIR}/include/eiskaltdcpp"
        NO_DEFAULT_PATH
    )
    find_library(EISKALTDCPP_LIBRARY
        NAMES eiskaltdcpp
        PATHS "${EISKALTDCPP_SDK_DIR}/lib" "${EISKALTDCPP_SDK_DIR}/bin"
        NO_DEFAULT_PATH
    )
    if(EISKALTDCPP_INCLUDE_DIR AND EISKALTDCPP_LIBRARY)
        message(STATUS "Using prebuilt eiskaltdcpp SDK: ${EISKALTDCPP_SDK_DIR}")
    else()
        message(WARNING "EISKALTDCPP_SDK_DIR='${EISKALTDCPP_SDK_DIR}' set but no SDK "
                        "found there (expected include/eiskaltdcpp/dcpp/DCPlusPlus.h "
                        "and lib/eiskaltdcpp). Falling back to system/FetchContent.")
    endif()
endif()

# 2) Otherwise try a system-installed libeiskaltdcpp-dev.  The find_* calls are
#    cache-guarded, so they are no-ops if the SDK above already resolved them.
if(USE_SYSTEM_EISKALTDCPP AND NOT (EISKALTDCPP_INCLUDE_DIR AND EISKALTDCPP_LIBRARY))
    find_path(EISKALTDCPP_INCLUDE_DIR
        NAMES dcpp/DCPlusPlus.h
        PATHS
            /usr/include/eiskaltdcpp
            /usr/local/include/eiskaltdcpp
    )

    find_library(EISKALTDCPP_LIBRARY
        NAMES eiskaltdcpp
        PATHS
            /usr/lib
            /usr/lib/x86_64-linux-gnu
            /usr/local/lib
    )
endif()

# 3) If either a prebuilt SDK or a system package supplied the headers and
#    library, wire up the imported target and detect the Lua ABI flag.
if(EISKALTDCPP_INCLUDE_DIR AND EISKALTDCPP_LIBRARY)
    message(STATUS "Found libeiskaltdcpp:")
    message(STATUS "  Include: ${EISKALTDCPP_INCLUDE_DIR}")
    message(STATUS "  Library: ${EISKALTDCPP_LIBRARY}")

    add_library(eiskaltdcpp::dcpp SHARED IMPORTED)
    if(WIN32)
        # On Windows the "library" is the import lib; the DLL is the runtime.
        find_file(EISKALTDCPP_DLL
            NAMES eiskaltdcpp.dll
            PATHS "${EISKALTDCPP_SDK_DIR}/bin" "${EISKALTDCPP_SDK_DIR}/lib"
            NO_DEFAULT_PATH
        )
        set_target_properties(eiskaltdcpp::dcpp PROPERTIES
            IMPORTED_IMPLIB "${EISKALTDCPP_LIBRARY}"
            IMPORTED_LOCATION "${EISKALTDCPP_DLL}"
            INTERFACE_INCLUDE_DIRECTORIES "${EISKALTDCPP_INCLUDE_DIR}"
        )
    else()
        set_target_properties(eiskaltdcpp::dcpp PROPERTIES
            IMPORTED_LOCATION "${EISKALTDCPP_LIBRARY}"
            INTERFACE_INCLUDE_DIRECTORIES "${EISKALTDCPP_INCLUDE_DIR}"
        )
    endif()
    set(EISKALTDCPP_FOUND TRUE)

    # Directory holding the shared library. The SWIG extension records the
    # core lib as a RUNPATH/@rpath dependency, so we add this dir as an RPATH
    # on the extension (see swig/CMakeLists.txt). Without it auditwheel and
    # delocate cannot locate libeiskaltdcpp.so.2.5 / libeiskaltdcpp.2.5.dylib
    # to vendor into the wheel during the repair step.
    get_filename_component(EISKALTDCPP_LIB_DIR "${EISKALTDCPP_LIBRARY}" DIRECTORY)

    # Detect whether the library was compiled with Lua scripting.
    # When LUA_SCRIPT is enabled, Client inherits from ClientScriptInstance
    # which changes the ABI layout (extra vtable pointer).  Our bridge code
    # MUST be compiled with the same setting to match member offsets.
    if(EXISTS "${EISKALTDCPP_INCLUDE_DIR}/dcpp/ScriptManager.h")
        find_package(Lua 5.2)
        if(LUA_FOUND)
            message(STATUS "libeiskaltdcpp has Lua scripting — enabling LUA_SCRIPT for ABI compat")
            set(EISKALTDCPP_HAS_LUA TRUE)
        else()
            message(WARNING "libeiskaltdcpp has Lua support but Lua dev headers not found. "
                            "ABI mismatch may cause crashes!")
            set(EISKALTDCPP_HAS_LUA FALSE)
        endif()

        # WITH_DHT adds a dht_ unique_ptr member to DCContext, changing
        # the memory layout.  Detect it via the DHT header.
        if(EXISTS "${EISKALTDCPP_INCLUDE_DIR}/dht/DHT.h")
            message(STATUS "System libeiskaltdcpp has DHT support — enabling WITH_DHT for ABI compat")
            set(EISKALTDCPP_HAS_DHT TRUE)
        else()
            set(EISKALTDCPP_HAS_DHT FALSE)
        endif()
    else()
        set(EISKALTDCPP_HAS_LUA FALSE)
        set(EISKALTDCPP_HAS_DHT FALSE)
    endif()
endif()

if(NOT EISKALTDCPP_FOUND)
    message(STATUS "System libeiskaltdcpp not found, building from source via FetchContent...")

    FetchContent_Declare(eiskaltdcpp
        GIT_REPOSITORY https://github.com/transfix/eiskaltdcpp.git
        # Pinned to the v2.6.0 release tag (full SHA for reproducible clones)
        GIT_TAG        e3e4a9f37a191b1b11c1c621ba61b17e6bf982e9
    )

    # Disable everything except the core library
    set(NO_UI_DAEMON   ON  CACHE BOOL "" FORCE)
    set(USE_QT6        OFF CACHE BOOL "" FORCE)
    set(USE_QT         OFF CACHE BOOL "" FORCE)
    set(USE_QT5        OFF CACHE BOOL "" FORCE)
    set(USE_GTK        OFF CACHE BOOL "" FORCE)
    set(USE_GTK3       OFF CACHE BOOL "" FORCE)
    set(USE_ASPELL     OFF CACHE BOOL "" FORCE)
    set(JSONRPC_DAEMON OFF CACHE BOOL "" FORCE)
    set(XMLRPC_DAEMON  OFF CACHE BOOL "" FORCE)
    set(WITH_DEV_FILES OFF CACHE BOOL "" FORCE)
    set(WITH_EMOTICONS OFF CACHE BOOL "" FORCE)
    set(WITH_EXAMPLES  OFF CACHE BOOL "" FORCE)
    set(WITH_SOUNDS    OFF CACHE BOOL "" FORCE)
    set(WITH_LUASCRIPTS OFF CACHE BOOL "" FORCE)
    set(LUA_SCRIPT     ON  CACHE BOOL "" FORCE)
    set(USE_CLI_JSONRPC OFF CACHE BOOL "" FORCE)
    set(USE_CLI_XMLRPC  OFF CACHE BOOL "" FORCE)
    set(USE_MINIUPNP   ON  CACHE BOOL "" FORCE)
    set(PERL_REGEX     ON  CACHE BOOL "" FORCE)

    # Use FetchContent_Populate + manual patch instead of FetchContent_MakeAvailable
    # because the eiskaltdcpp CMakeLists.txt uses CMAKE_SOURCE_DIR which resolves
    # to the parent project root under FetchContent. We patch it to use
    # CMAKE_CURRENT_SOURCE_DIR before adding the subdirectory.
    FetchContent_GetProperties(eiskaltdcpp)
    if(NOT eiskaltdcpp_POPULATED)
        FetchContent_Populate(eiskaltdcpp)
        # Patch CMAKE_SOURCE_DIR/CMAKE_BINARY_DIR -> CMAKE_CURRENT_* for FetchContent compat
        # Uses portable CMake commands instead of sed so this works on Windows too.
        file(READ "${eiskaltdcpp_SOURCE_DIR}/CMakeLists.txt" _eiskaltdcpp_cml)
        string(REPLACE "\${CMAKE_SOURCE_DIR}" "\${CMAKE_CURRENT_SOURCE_DIR}" _eiskaltdcpp_cml "${_eiskaltdcpp_cml}")
        string(REPLACE "\${CMAKE_BINARY_DIR}" "\${CMAKE_CURRENT_BINARY_DIR}" _eiskaltdcpp_cml "${_eiskaltdcpp_cml}")
        file(WRITE "${eiskaltdcpp_SOURCE_DIR}/CMakeLists.txt" "${_eiskaltdcpp_cml}")
        # Disable eiskaltdcpp's own tests (BUILD_TESTS leaks from parent)
        set(_save_build_tests "${BUILD_TESTS}")
        set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
        add_subdirectory(${eiskaltdcpp_SOURCE_DIR} ${eiskaltdcpp_BINARY_DIR})
        set(BUILD_TESTS "${_save_build_tests}" CACHE BOOL "" FORCE)
    endif()

    if(TARGET dcpp)
        add_library(eiskaltdcpp::dcpp ALIAS dcpp)
        # Get the include directory from the fetched source
        set(EISKALTDCPP_INCLUDE_DIR "${eiskaltdcpp_SOURCE_DIR}")
    else()
        message(FATAL_ERROR "FetchContent built eiskaltdcpp but 'dcpp' target not found")
    endif()

    # When LUA_SCRIPT is ON, the FetchContent-built dcpp defines -DLUA_SCRIPT
    # globally and links against Lua.  Our bridge code must also compile with
    # LUA_SCRIPT to match the ABI (Client inherits from ClientScriptInstance).
    find_package(Lua 5.2)
    if(LUA_FOUND)
        message(STATUS "FetchContent eiskaltdcpp built with Lua scripting — enabling LUA_SCRIPT for bridge ABI")
        set(EISKALTDCPP_HAS_LUA TRUE)
    else()
        message(WARNING "LUA_SCRIPT enabled but Lua dev headers not found in container — ABI mismatch possible")
        set(EISKALTDCPP_HAS_LUA FALSE)
    endif()

    # WITH_DHT changes DCContext class layout (adds dht_ unique_ptr member).
    # The eiskaltdcpp top-level CMakeLists.txt uses add_definitions(-DWITH_DHT)
    # which only applies within its directory scope, so we must propagate
    # it ourselves for the bridge ABI to match.
    if(WITH_DHT)
        set(EISKALTDCPP_HAS_DHT TRUE)
    else()
        set(EISKALTDCPP_HAS_DHT FALSE)
    endif()

    set(EISKALTDCPP_FOUND TRUE)
endif()

# ===========================================================================
# C++ bridge library (static, linked into SWIG module)
# ===========================================================================

add_subdirectory(src)

# ===========================================================================
# SWIG Python module
# ===========================================================================

add_subdirectory(swig)

# ===========================================================================
# Tests
# ===========================================================================

if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()
