cmake_minimum_required(VERSION 3.30)

project(cua_hyprland_plugin VERSION 0.1.0 LANGUAGES CXX)

option(CUA_HYPRLAND_BUILD_PLUGIN "Build the ABI-pinned Hyprland plugin" ON)
option(CUA_HYPRLAND_INPUT "Build trusted-local per-action input protocol v3 candidate" OFF)
option(CUA_HYPRLAND_INPUT_TRACE "Instrument v3 input for native certification only" OFF)
option(CUA_HYPRLAND_TEST_INPUT "Build nonshipping isolated-input VM experiment" OFF)
if(CUA_HYPRLAND_INPUT AND CUA_HYPRLAND_TEST_INPUT)
    message(FATAL_ERROR "Production v3 and signed experimental input are mutually exclusive")
endif()
if(CUA_HYPRLAND_INPUT_TRACE AND NOT CUA_HYPRLAND_INPUT)
    message(FATAL_ERROR "Input trace instrumentation requires CUA_HYPRLAND_INPUT")
endif()
set(CUA_HYPRLAND_TEST_OPERATOR_KEY "" CACHE STRING "Test operator Ed25519 public key (hex); never a private key")
set(CUA_HYPRLAND_EXPECTED_VERSION "" CACHE STRING
    "Require an exact hyprland pkg-config version when building the plugin")

include(CTest)
include(cmake/DetectHyprlandAPI.cmake)

function(cua_hyprland_harden target)
    if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
        target_compile_definitions(${target} PRIVATE
            $<$<CONFIG:Release,RelWithDebInfo,MinSizeRel>:_FORTIFY_SOURCE=3>
            _GLIBCXX_ASSERTIONS)
        target_compile_options(${target} PRIVATE -fstack-protector-strong)
    endif()
    target_compile_options(${target} PRIVATE -fno-lto)
endfunction()

add_library(cua_hyprland_protocol STATIC
    src/protocol.cpp
    src/session.cpp
    src/status.cpp)
target_compile_features(cua_hyprland_protocol PUBLIC cxx_std_26)
target_include_directories(cua_hyprland_protocol PUBLIC include)
target_compile_options(cua_hyprland_protocol PRIVATE -Wall -Wextra -Wpedantic -Werror)
cua_hyprland_harden(cua_hyprland_protocol)
set_target_properties(cua_hyprland_protocol PROPERTIES POSITION_INDEPENDENT_CODE ON)

if(BUILD_TESTING)
    find_package(Python3 REQUIRED COMPONENTS Interpreter)
    add_test(NAME cua_hyprland_desktop_fault_policy_test
        COMMAND ${Python3_EXECUTABLE} -B
            ${CMAKE_CURRENT_SOURCE_DIR}/tests/desktop_fault_policy_test.py)
    set_tests_properties(cua_hyprland_desktop_fault_policy_test PROPERTIES
        ENVIRONMENT "CXX=${CMAKE_CXX_COMPILER}")

    add_executable(cua_hyprland_foreground_route_test tests/foreground_route_test.cpp)
    target_compile_features(cua_hyprland_foreground_route_test PRIVATE cxx_std_26)
    target_include_directories(cua_hyprland_foreground_route_test PRIVATE src)
    target_compile_options(cua_hyprland_foreground_route_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
    add_test(NAME cua_hyprland_foreground_route_test COMMAND cua_hyprland_foreground_route_test)

    add_executable(cua_hyprland_passive_pointer_target_test tests/passive_pointer_target_test.cpp)
    target_compile_features(cua_hyprland_passive_pointer_target_test PRIVATE cxx_std_26)
    target_include_directories(cua_hyprland_passive_pointer_target_test PRIVATE src)
    target_compile_options(cua_hyprland_passive_pointer_target_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
    add_test(NAME cua_hyprland_passive_pointer_target_test COMMAND cua_hyprland_passive_pointer_target_test)

    add_executable(cua_hyprland_input_client_deadline_test tests/input_client_deadline_test.cpp)
    target_compile_features(cua_hyprland_input_client_deadline_test PRIVATE cxx_std_26)
    target_include_directories(cua_hyprland_input_client_deadline_test PRIVATE src)
    target_compile_options(cua_hyprland_input_client_deadline_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
    add_test(NAME cua_hyprland_input_client_deadline_test COMMAND cua_hyprland_input_client_deadline_test)

    add_executable(cua_hyprland_input_grant_test tests/input_grant_test.cpp)
    target_compile_features(cua_hyprland_input_grant_test PRIVATE cxx_std_26)
    target_include_directories(cua_hyprland_input_grant_test PRIVATE src)
    target_compile_options(cua_hyprland_input_grant_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
    add_test(NAME cua_hyprland_input_grant_test COMMAND cua_hyprland_input_grant_test)

    add_executable(cua_hyprland_drag_geometry_test tests/drag_geometry_test.cpp)
    target_compile_features(cua_hyprland_drag_geometry_test PRIVATE cxx_std_26)
    target_include_directories(cua_hyprland_drag_geometry_test PRIVATE src)
    target_compile_options(cua_hyprland_drag_geometry_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
    add_test(NAME cua_hyprland_drag_geometry_test COMMAND cua_hyprland_drag_geometry_test)

    add_test(NAME cua_hyprland_cmake_api_test
        COMMAND ${CMAKE_COMMAND}
            -S ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake-api
            -B ${CMAKE_CURRENT_BINARY_DIR}/cmake-api-test
            -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER})
    add_executable(cua_hyprland_protocol_test tests/protocol_test.cpp)
    target_link_libraries(cua_hyprland_protocol_test PRIVATE cua_hyprland_protocol)
    target_compile_options(cua_hyprland_protocol_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
    add_test(NAME cua_hyprland_protocol_test COMMAND cua_hyprland_protocol_test)

    add_executable(cua_hyprland_status_test tests/status_test.cpp)
    target_link_libraries(cua_hyprland_status_test PRIVATE cua_hyprland_protocol)
    target_compile_options(cua_hyprland_status_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
    add_test(NAME cua_hyprland_status_test COMMAND cua_hyprland_status_test)

    foreach(api_variant IN ITEMS socket1 legacy)
        if(api_variant STREQUAL "socket1")
            set(api_value 1)
        else()
            set(api_value 0)
        endif()
        add_library(cua_hyprland_plugin_api_${api_variant}_compile_test
            OBJECT src/plugin.cpp)
        target_compile_features(cua_hyprland_plugin_api_${api_variant}_compile_test
            PRIVATE cxx_std_26)
        target_compile_definitions(cua_hyprland_plugin_api_${api_variant}_compile_test
            PRIVATE
            CUA_HYPRLAND_PLUGIN_VERSION="${PROJECT_VERSION}"
            CUA_HYPRLAND_SOCKET1_API=${api_value})
        target_compile_options(cua_hyprland_plugin_api_${api_variant}_compile_test
            PRIVATE -Wall -Wextra -Wpedantic -Werror -Wno-return-type-c-linkage)
        target_include_directories(cua_hyprland_plugin_api_${api_variant}_compile_test
            PRIVATE tests/mock-hyprland include src)
    endforeach()
endif()

if(UNIX)
    find_package(Threads REQUIRED)
    add_library(cua_hyprland_transport STATIC src/inject_server.cpp)
    target_compile_features(cua_hyprland_transport PUBLIC cxx_std_26)
    target_compile_options(cua_hyprland_transport PRIVATE -Wall -Wextra -Wpedantic -Werror)
    if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
        target_compile_definitions(cua_hyprland_transport PRIVATE _GNU_SOURCE)
    endif()
    target_include_directories(cua_hyprland_transport PUBLIC include src)
    target_link_libraries(cua_hyprland_transport PUBLIC
        cua_hyprland_protocol
        Threads::Threads)
    cua_hyprland_harden(cua_hyprland_transport)
    set_target_properties(cua_hyprland_transport PROPERTIES POSITION_INDEPENDENT_CODE ON)

    if(BUILD_TESTING)
        foreach(lifetime_test IN ITEMS owned_socket_path seat_lifetime)
            add_executable(cua_hyprland_${lifetime_test}_test tests/${lifetime_test}_test.cpp)
            target_compile_features(cua_hyprland_${lifetime_test}_test PRIVATE cxx_std_26)
            target_include_directories(cua_hyprland_${lifetime_test}_test PRIVATE src)
            target_compile_options(cua_hyprland_${lifetime_test}_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
            add_test(NAME cua_hyprland_${lifetime_test}_test COMMAND cua_hyprland_${lifetime_test}_test)
        endforeach()
        add_executable(cua_hyprland_plugin_input_lifetime_test
            tests/plugin_input_lifetime_test.cpp src/plugin.cpp)
        target_compile_features(cua_hyprland_plugin_input_lifetime_test PRIVATE cxx_std_26)
        target_include_directories(cua_hyprland_plugin_input_lifetime_test PRIVATE tests tests/mock-hyprland include src)
        target_compile_definitions(cua_hyprland_plugin_input_lifetime_test PRIVATE
            CUA_HYPRLAND_PLUGIN_VERSION="${PROJECT_VERSION}" CUA_HYPRLAND_SOCKET1_API=0 CUA_HYPRLAND_TEST_INPUT=1)
        target_compile_options(cua_hyprland_plugin_input_lifetime_test PRIVATE
            -Wall -Wextra -Wpedantic -Werror -Wno-return-type-c-linkage)
        target_link_libraries(cua_hyprland_plugin_input_lifetime_test PRIVATE cua_hyprland_transport)
        add_test(NAME cua_hyprland_plugin_input_lifetime_test COMMAND cua_hyprland_plugin_input_lifetime_test)
        add_executable(cua_hyprland_plugin_v3_lifetime_test
            tests/plugin_input_lifetime_test.cpp src/plugin.cpp)
        target_compile_features(cua_hyprland_plugin_v3_lifetime_test PRIVATE cxx_std_26)
        target_include_directories(cua_hyprland_plugin_v3_lifetime_test PRIVATE tests tests/mock-hyprland include src)
        target_compile_definitions(cua_hyprland_plugin_v3_lifetime_test PRIVATE
            CUA_HYPRLAND_PLUGIN_VERSION="${PROJECT_VERSION}" CUA_HYPRLAND_SOCKET1_API=0 CUA_HYPRLAND_INPUT=1)
        target_compile_options(cua_hyprland_plugin_v3_lifetime_test PRIVATE
            -Wall -Wextra -Wpedantic -Werror -Wno-return-type-c-linkage)
        target_link_libraries(cua_hyprland_plugin_v3_lifetime_test PRIVATE cua_hyprland_transport)
        add_test(NAME cua_hyprland_plugin_v3_lifetime_test COMMAND cua_hyprland_plugin_v3_lifetime_test)

        add_executable(cua_hyprland_transport_test tests/transport_test.cpp)
        target_compile_options(cua_hyprland_transport_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
        target_link_libraries(cua_hyprland_transport_test PRIVATE cua_hyprland_transport)
        add_test(NAME cua_hyprland_transport_test COMMAND cua_hyprland_transport_test)

        foreach(api_variant IN ITEMS socket1 legacy)
            if(api_variant STREQUAL "socket1")
                set(api_value 1)
            else()
                set(api_value 0)
            endif()
            add_executable(cua_hyprland_plugin_api_${api_variant}_test
                tests/plugin_api_test.cpp
                $<TARGET_OBJECTS:cua_hyprland_plugin_api_${api_variant}_compile_test>)
            target_compile_features(cua_hyprland_plugin_api_${api_variant}_test
                PRIVATE cxx_std_26)
            target_compile_definitions(cua_hyprland_plugin_api_${api_variant}_test
                PRIVATE CUA_HYPRLAND_SOCKET1_API=${api_value})
            target_compile_options(cua_hyprland_plugin_api_${api_variant}_test PRIVATE
                -Wall -Wextra -Wpedantic -Werror -Wno-return-type-c-linkage)
            target_include_directories(cua_hyprland_plugin_api_${api_variant}_test PRIVATE
                tests tests/mock-hyprland include src)
            target_link_libraries(cua_hyprland_plugin_api_${api_variant}_test PRIVATE
                cua_hyprland_transport)
            add_test(NAME cua_hyprland_plugin_api_${api_variant}_test
                COMMAND cua_hyprland_plugin_api_${api_variant}_test)
        endforeach()
    endif()
endif()

if(CUA_HYPRLAND_BUILD_PLUGIN)
    if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
        message(FATAL_ERROR "The Hyprland plugin can only be built on Linux")
    endif()

    find_package(PkgConfig REQUIRED)
    if(CUA_HYPRLAND_EXPECTED_VERSION)
        pkg_check_modules(HYPRLAND REQUIRED IMPORTED_TARGET
            "hyprland=${CUA_HYPRLAND_EXPECTED_VERSION}")
    else()
        pkg_check_modules(HYPRLAND REQUIRED IMPORTED_TARGET hyprland)
    endif()
    message(STATUS "Building against Hyprland headers ${HYPRLAND_VERSION}")

    cua_detect_hyprland_api(cua_hyprland_socket1_api
        "${HYPRLAND_INCLUDE_DIRS}" "${HYPRLAND_CFLAGS_OTHER}")
    if(cua_hyprland_socket1_api STREQUAL "1")
        message(STATUS "Using Hyprland Socket1 status-command API")
    elseif(cua_hyprland_socket1_api STREQUAL "0")
        message(STATUS "Using Hyprland legacy status-command API")
    else()
        message(FATAL_ERROR
            "Hyprland headers expose no supported hyprctl plugin command API")
    endif()

    add_library(cua_hyprland_plugin MODULE src/plugin.cpp)
    if(CUA_HYPRLAND_TEST_INPUT OR CUA_HYPRLAND_INPUT)
        if(NOT HYPRLAND_VERSION STREQUAL "0.56.2")
            message(FATAL_ERROR "Input is pinned to Hyprland 0.56.2")
        endif()
        target_sources(cua_hyprland_plugin PRIVATE src/input_experiment.cpp)
    endif()
    if(CUA_HYPRLAND_INPUT)
        target_compile_definitions(cua_hyprland_plugin PRIVATE CUA_HYPRLAND_INPUT=1)
    endif()
    if(CUA_HYPRLAND_INPUT_TRACE OR CUA_HYPRLAND_TEST_INPUT)
        target_sources(cua_hyprland_plugin PRIVATE src/primary_trace.cpp)
    endif()
    if(CUA_HYPRLAND_INPUT_TRACE)
        target_compile_definitions(cua_hyprland_plugin PRIVATE CUA_HYPRLAND_INPUT_TRACE=1)
    endif()
    if(CUA_HYPRLAND_TEST_INPUT)
        if(NOT CUA_HYPRLAND_TEST_OPERATOR_KEY MATCHES "^[0-9a-f]+$")
            message(FATAL_ERROR "Test input requires a lowercase Ed25519 public key")
        endif()
        string(LENGTH "${CUA_HYPRLAND_TEST_OPERATOR_KEY}" key_length)
        if(NOT key_length EQUAL 64)
            message(FATAL_ERROR "Test operator public key must be 32 bytes")
        endif()
        find_package(OpenSSL REQUIRED)
        target_compile_definitions(cua_hyprland_plugin PRIVATE
            CUA_HYPRLAND_TEST_INPUT=1
            CUA_HYPRLAND_TEST_OPERATOR_KEY="${CUA_HYPRLAND_TEST_OPERATOR_KEY}")
        target_link_libraries(cua_hyprland_plugin PRIVATE OpenSSL::Crypto)
    endif()
    target_compile_features(cua_hyprland_plugin PRIVATE cxx_std_26)
    target_compile_definitions(cua_hyprland_plugin PRIVATE
        CUA_HYPRLAND_PLUGIN_VERSION="${PROJECT_VERSION}"
        CUA_HYPRLAND_SOCKET1_API=${cua_hyprland_socket1_api}
        _GNU_SOURCE)
    target_compile_options(cua_hyprland_plugin PRIVATE
        -Wall -Wextra -Wpedantic -Werror -Wno-return-type-c-linkage)
    cua_hyprland_harden(cua_hyprland_plugin)
    target_link_options(cua_hyprland_plugin PRIVATE
        -Wl,-z,relro
        -Wl,-z,now
        -Wl,-z,noexecstack)
    target_link_libraries(cua_hyprland_plugin PRIVATE
        cua_hyprland_transport
        PkgConfig::HYPRLAND
        Threads::Threads)
    set_target_properties(cua_hyprland_plugin PROPERTIES
        CXX_VISIBILITY_PRESET hidden
        PREFIX ""
        OUTPUT_NAME "cua-hyprland-plugin")
    if(CUA_HYPRLAND_TEST_INPUT OR CUA_HYPRLAND_INPUT)
        # Hyprland exports inline manager globals. Hidden visibility would bind
        # a private null copy instead of the running compositor's managers.
        # GNU-unique definitions also pin a module in memory across dlclose.
        set_target_properties(cua_hyprland_plugin PROPERTIES CXX_VISIBILITY_PRESET default)
        target_compile_options(cua_hyprland_plugin PRIVATE -fno-gnu-unique)
        # Retired seat resources must accept late client release requests after
        # global removal. Keep their inert callbacks mapped until process exit.
        target_link_options(cua_hyprland_plugin PRIVATE -Wl,-z,nodelete)
    endif()

    if(NOT CMAKE_READELF)
        message(FATAL_ERROR "readelf is required to verify the plugin C++ runtime")
    endif()
    add_custom_command(TARGET cua_hyprland_plugin POST_BUILD
        COMMAND ${CMAKE_COMMAND}
            -DREADELF=${CMAKE_READELF}
            -DMODULE=$<TARGET_FILE:cua_hyprland_plugin>
            -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyRuntime.cmake
        VERBATIM)

    install(TARGETS cua_hyprland_plugin
        LIBRARY DESTINATION lib/cua/hyprland)
endif()
