cmake_minimum_required(VERSION 3.24)

project(Alvin
    VERSION 0.1.0
    DESCRIPTION "Agent Leveraging Vision and Input: cross-platform input synthesis and screen capture"
    LANGUAGES C CXX)  # C only for wayland-scanner output

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

include(GNUInstallDirs)

# ---------------------------------------------------------------------------
# Options. "AUTO" enables a backend when its dependencies are found.
# ---------------------------------------------------------------------------
set(ALVIN_WITH_WAYLAND AUTO CACHE STRING "Wayland virtual pointer/keyboard input + monitor listing (ON/OFF/AUTO)")
set(ALVIN_WITH_LIBEI   AUTO CACHE STRING "libei input via the RemoteDesktop portal (ON/OFF/AUTO)")
set(ALVIN_WITH_PORTAL  AUTO CACHE STRING "ScreenCast portal + PipeWire capture (ON/OFF/AUTO)")
set(ALVIN_WITH_UINPUT  ON   CACHE BOOL   "/dev/uinput input backend")
set(ALVIN_WITH_VISION  AUTO CACHE STRING "OpenCV-based alvin::vision library (ON/OFF/AUTO)")
option(ALVIN_BUILD_EXAMPLES "Build the alvin command-line tool" ON)
option(ALVIN_BUILD_TESTS "Build unit tests" ${PROJECT_IS_TOP_LEVEL})

# Resolves an AUTO/ON/OFF option against whether its dependencies were found.
function(alvin_resolve option found)
    if(${option} STREQUAL "AUTO")
        set(${option}_ENABLED ${found} PARENT_SCOPE)
    elseif(${option})
        if(NOT ${found})
            message(FATAL_ERROR "${option}=ON but its dependencies were not found")
        endif()
        set(${option}_ENABLED TRUE PARENT_SCOPE)
    else()
        set(${option}_ENABLED FALSE PARENT_SCOPE)
    endif()
endfunction()

add_library(alvin
    src/common/core.cpp
    src/common/frame.cpp
    src/common/json.cpp
    src/common/keys.cpp
)
add_library(alvin::alvin ALIAS alvin)
target_include_directories(alvin
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_features(alvin PUBLIC cxx_std_23)
if(MSVC)
    target_compile_options(alvin PRIVATE /W4 /utf-8)
else()
    target_compile_options(alvin PRIVATE -Wall -Wextra -Wpedantic)
endif()

if(WIN32)
    # No export macros in the headers; let CMake export every symbol from the DLL.
    set_target_properties(alvin PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()

set(ALVIN_ENABLED_BACKENDS "")

# Links a pkg-config dependency privately and records how AlvinConfig.cmake can
# find it again: a static libalvin carries no record of its own dependencies.
set(ALVIN_CONFIG_DEPENDENCIES "")
macro(alvin_private_pkg var)
    target_link_libraries(alvin PRIVATE PkgConfig::${var})
    string(APPEND ALVIN_CONFIG_DEPENDENCIES "    pkg_check_modules(${var} REQUIRED QUIET IMPORTED_TARGET ${ARGN})\n")
endmacro()

# ---------------------------------------------------------------------------
# Linux
# ---------------------------------------------------------------------------
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    find_package(PkgConfig REQUIRED)
    pkg_check_modules(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon)
    pkg_check_modules(WAYLAND IMPORTED_TARGET wayland-client)
    pkg_check_modules(WAYLAND_PROTOCOLS wayland-protocols)
    find_program(WAYLAND_SCANNER wayland-scanner)
    pkg_check_modules(GIO IMPORTED_TARGET gio-2.0 gio-unix-2.0)
    pkg_check_modules(PIPEWIRE IMPORTED_TARGET libpipewire-0.3)
    pkg_check_modules(LIBEI IMPORTED_TARGET libei-1.0)

    set(_wayland_found FALSE)
    if(WAYLAND_FOUND AND WAYLAND_PROTOCOLS_FOUND AND WAYLAND_SCANNER)
        set(_wayland_found TRUE)
    endif()
    set(_portal_found FALSE)
    if(GIO_FOUND AND PIPEWIRE_FOUND)
        set(_portal_found TRUE)
    endif()
    set(_libei_found FALSE)
    if(GIO_FOUND AND LIBEI_FOUND)
        set(_libei_found TRUE)
    endif()
    alvin_resolve(ALVIN_WITH_WAYLAND _wayland_found)
    alvin_resolve(ALVIN_WITH_PORTAL _portal_found)
    alvin_resolve(ALVIN_WITH_LIBEI _libei_found)

    target_sources(alvin PRIVATE
        src/linux/platform_linux.cpp
        src/linux/hyprland_windows.cpp
        src/linux/keycodes.cpp
        src/linux/xkb.cpp)
    alvin_private_pkg(XKBCOMMON xkbcommon)

    if(ALVIN_WITH_WAYLAND_ENABLED)
        pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
        set(_proto_out ${CMAKE_CURRENT_BINARY_DIR}/protocols)
        file(MAKE_DIRECTORY ${_proto_out})
        set(_protocols
            ${CMAKE_CURRENT_SOURCE_DIR}/protocols/wlr-virtual-pointer-unstable-v1.xml
            ${CMAKE_CURRENT_SOURCE_DIR}/protocols/virtual-keyboard-unstable-v1.xml
            ${WAYLAND_PROTOCOLS_DIR}/unstable/xdg-output/xdg-output-unstable-v1.xml)
        foreach(xml ${_protocols})
            get_filename_component(name ${xml} NAME_WE)
            add_custom_command(
                OUTPUT ${_proto_out}/${name}-client-protocol.h ${_proto_out}/${name}-protocol.c
                COMMAND ${WAYLAND_SCANNER} client-header ${xml} ${_proto_out}/${name}-client-protocol.h
                COMMAND ${WAYLAND_SCANNER} private-code ${xml} ${_proto_out}/${name}-protocol.c
                DEPENDS ${xml}
                VERBATIM)
            list(APPEND _proto_sources ${_proto_out}/${name}-protocol.c)
            list(APPEND _proto_headers ${_proto_out}/${name}-client-protocol.h)
        endforeach()
        # wayland-scanner emits C; build it as a small C object library.
        add_library(alvin_wayland_protocols OBJECT ${_proto_sources} ${_proto_headers})
        target_include_directories(alvin_wayland_protocols PUBLIC ${_proto_out})
        target_link_libraries(alvin_wayland_protocols PUBLIC PkgConfig::WAYLAND)
        set_target_properties(alvin_wayland_protocols PROPERTIES POSITION_INDEPENDENT_CODE ON)

        target_sources(alvin PRIVATE
            src/linux/wayland.cpp
            src/linux/wlr_input.cpp
            $<TARGET_OBJECTS:alvin_wayland_protocols>)
        target_include_directories(alvin PRIVATE ${_proto_out})
        alvin_private_pkg(WAYLAND wayland-client)
        add_dependencies(alvin alvin_wayland_protocols)
        target_compile_definitions(alvin PRIVATE ALVIN_HAVE_WAYLAND)
        list(APPEND ALVIN_ENABLED_BACKENDS "wlr-input" "wayland-monitors")
    endif()

    if(ALVIN_WITH_PORTAL_ENABLED OR ALVIN_WITH_LIBEI_ENABLED)
        target_sources(alvin PRIVATE src/linux/portal.cpp)
        alvin_private_pkg(GIO gio-2.0 gio-unix-2.0)
    endif()

    if(ALVIN_WITH_PORTAL_ENABLED)
        target_sources(alvin PRIVATE src/linux/pipewire_capture.cpp)
        alvin_private_pkg(PIPEWIRE libpipewire-0.3)
        target_compile_definitions(alvin PRIVATE ALVIN_HAVE_PORTAL)
        list(APPEND ALVIN_ENABLED_BACKENDS "portal-capture")
    endif()

    if(ALVIN_WITH_LIBEI_ENABLED)
        target_sources(alvin PRIVATE src/linux/libei_input.cpp)
        alvin_private_pkg(LIBEI libei-1.0)
        target_compile_definitions(alvin PRIVATE ALVIN_HAVE_LIBEI)
        if(LIBEI_VERSION VERSION_GREATER_EQUAL 1.6)
            target_compile_definitions(alvin PRIVATE ALVIN_LIBEI_HAS_TEXT)
        endif()
        list(APPEND ALVIN_ENABLED_BACKENDS "libei-input")
    endif()

    if(ALVIN_WITH_UINPUT)
        target_sources(alvin PRIVATE src/linux/uinput_input.cpp)
        target_compile_definitions(alvin PRIVATE ALVIN_HAVE_UINPUT)
        list(APPEND ALVIN_ENABLED_BACKENDS "uinput-input")
    endif()

    list(APPEND ALVIN_ENABLED_BACKENDS "hyprland-windows")

# ---------------------------------------------------------------------------
# Windows
# ---------------------------------------------------------------------------
elseif(WIN32)
    target_sources(alvin PRIVATE
        src/windows/platform_windows.cpp
        src/windows/sendinput_input.cpp
        src/windows/gdi_capture.cpp
        src/windows/win32_windows.cpp)
    target_compile_definitions(alvin PRIVATE UNICODE _UNICODE)
    target_link_libraries(alvin PRIVATE user32 gdi32 dwmapi)
    list(APPEND ALVIN_ENABLED_BACKENDS "sendinput-input" "gdi-capture" "win32-windows")
else()
    message(FATAL_ERROR "Alvin does not support ${CMAKE_SYSTEM_NAME} yet")
endif()

# ---------------------------------------------------------------------------
# Optional OpenCV integration
# ---------------------------------------------------------------------------
find_package(OpenCV QUIET COMPONENTS core imgproc imgcodecs)
alvin_resolve(ALVIN_WITH_VISION OpenCV_FOUND)
if(ALVIN_WITH_VISION_ENABLED)
    add_library(alvin_vision src/vision/vision.cpp)
    add_library(alvin::vision ALIAS alvin_vision)
    target_link_libraries(alvin_vision PUBLIC alvin ${OpenCV_LIBS})
    set_target_properties(alvin_vision PROPERTIES EXPORT_NAME vision)
    if(WIN32)
        set_target_properties(alvin_vision PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
    endif()
    list(APPEND ALVIN_ENABLED_BACKENDS "vision(OpenCV ${OpenCV_VERSION})")
endif()

message(STATUS "Alvin backends: ${ALVIN_ENABLED_BACKENDS}")

if(ALVIN_BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

if(ALVIN_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

# ---------------------------------------------------------------------------
# Install + find_package(Alvin) support
# ---------------------------------------------------------------------------
include(CMakePackageConfigHelpers)
set(ALVIN_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/Alvin)

set(_alvin_install_targets alvin)
if(ALVIN_WITH_VISION_ENABLED)
    list(APPEND _alvin_install_targets alvin_vision)
endif()
install(TARGETS ${_alvin_install_targets} EXPORT AlvinTargets
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT AlvinTargets NAMESPACE alvin:: DESTINATION ${ALVIN_CMAKE_DIR})

# Values baked into AlvinConfig.cmake.
get_target_property(_alvin_type alvin TYPE)
if(_alvin_type STREQUAL "STATIC_LIBRARY" AND NOT ALVIN_CONFIG_DEPENDENCIES STREQUAL "")
    set(ALVIN_NEEDS_PKGCONFIG TRUE)
else()
    set(ALVIN_NEEDS_PKGCONFIG FALSE)
endif()
if(ALVIN_WITH_VISION_ENABLED)
    set(ALVIN_HAS_VISION TRUE)
else()
    set(ALVIN_HAS_VISION FALSE)
endif()

configure_package_config_file(cmake/AlvinConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/AlvinConfig.cmake
    INSTALL_DESTINATION ${ALVIN_CMAKE_DIR})
write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/AlvinConfigVersion.cmake
    COMPATIBILITY SameMinorVersion)  # pre-1.0: minor versions may break the API
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/AlvinConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/AlvinConfigVersion.cmake
    DESTINATION ${ALVIN_CMAKE_DIR})
