cmake_minimum_required(VERSION 3.20)

# Driver files install into the Wine arch layout under this root. The default
# follows the install prefix; set an absolute path when the target Wine keeps
# its libraries elsewhere (Debian/Ubuntu: /usr/lib/x86_64-linux-gnu/wine,
# WineHQ: /opt/wine-<branch>/lib/wine).
# Validated before project(): the guard must fire even where toolchain probing
# cannot run (the wine_install_root_relative CTest configures in a bare
# environment, so no generator or compiler may exist yet).
# STRING, not PATH: CMake would normalize a relative PATH value to absolute
# against the cwd, hiding the mistake from the IS_ABSOLUTE guard below.
set(PIPEASIO_WINE_INSTALL_ROOT "" CACHE STRING
    "absolute install root for driver files (default <prefix>/lib/wine)")
if(PIPEASIO_WINE_INSTALL_ROOT)
    # A relative value would split the install: install(FILES) would resolve
    # it under the prefix while install(CODE) resolved it against the install
    # process, landing symlinks and manifest entries elsewhere.
    if(NOT IS_ABSOLUTE "${PIPEASIO_WINE_INSTALL_ROOT}")
        message(FATAL_ERROR
            "PIPEASIO_WINE_INSTALL_ROOT must be an absolute path "
            "(got '${PIPEASIO_WINE_INSTALL_ROOT}'; e.g. "
            "/usr/lib/x86_64-linux-gnu/wine).")
    endif()
    set(PA_WINE_DEST "${PIPEASIO_WINE_INSTALL_ROOT}")
    set(PA_WINE_DEST_ABS "${PIPEASIO_WINE_INSTALL_ROOT}")
else()
    set(PA_WINE_DEST "lib/wine")
    # Stays a deferred reference: install(CODE) expands it at install time.
    set(PA_WINE_DEST_ABS "\${CMAKE_INSTALL_PREFIX}/lib/wine")
endif()
project(pipeasio C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_EXTENSIONS ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
                 Debug Release RelWithDebInfo)
endif()

list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")

# PipeWire headers compile into driver objects before winegcc links them.
# Steam Runtime 4 ships the supported 1.4.2 floor.
find_package(PkgConfig REQUIRED)
pkg_check_modules(PIPEWIRE REQUIRED IMPORTED_TARGET libpipewire-0.3>=1.4.2)
message(STATUS "Found libpipewire-0.3 ${PIPEWIRE_VERSION} (minimum 1.4.2)")

# Ahead of WineDLL: the SDK probe requires unixlib.h only when this is on.
option(BUILD_WOW64_32 "Build the experimental 32-bit WoW64 PE front end + unixlib" OFF)


include(WineDLL)

add_wine_dll(
    NAME      pipeasio64
    SPEC      ${CMAKE_SOURCE_DIR}/pipeasio.dll.spec
    SOURCES   src/asio.c
              src/audio.c
              src/config.c
              src/main.c
              src/regsvr.c
    INCLUDES  ${CMAKE_SOURCE_DIR}/include
              ${PIPEWIRE_INCLUDE_DIRS}
    LIBS      odbc32 ole32 uuid winmm user32 kernelbase
    LDFLAGS   ${PIPEWIRE_LINK_LIBRARIES} ${PIPEWIRE_LDFLAGS_OTHER}
)

# Optional 32-bit WoW64 front end.
# - pipeasio32.dll: i386 PE stub, installed under lib/wine/i386-windows
# - pipeasio32.so:  x86_64 unixlib, installed under lib/wine/x86_64-unix
if(BUILD_WOW64_32)
    find_program(I686_W64_MINGW_GCC i686-w64-mingw32-gcc)
    find_program(I686_W64_MINGW_RANLIB i686-w64-mingw32-ranlib)
    set(WINE_LIB_ROOT "/usr/lib/wine" CACHE PATH
        "Wine lib/wine directory holding the i386-windows import libraries")
    if(NOT I686_W64_MINGW_GCC OR NOT I686_W64_MINGW_RANLIB)
        message(FATAL_ERROR "BUILD_WOW64_32=ON requires i686-w64-mingw32-gcc and "
                            "i686-w64-mingw32-ranlib (install mingw-w64-gcc).")
    endif()
    if(NOT EXISTS "${WINE_LIB_ROOT}/i386-windows/libwinecrt0.a")
        message(FATAL_ERROR "BUILD_WOW64_32=ON: ${WINE_LIB_ROOT}/i386-windows/libwinecrt0.a "
                            "not found; pass -DWINE_LIB_ROOT=<lib/wine>.")
    endif()

    set(PA32_DLL  "${CMAKE_BINARY_DIR}/pipeasio32.dll")
    set(PA32_SO   "${CMAKE_BINARY_DIR}/pipeasio32.so")
    set(PA32_DEF  "${CMAKE_SOURCE_DIR}/src/wow64/pipeasio32.def")
    set(PA32_SPEC "${CMAKE_SOURCE_DIR}/src/wow64/pipeasio32_unixlib.spec")
    file(GLOB PA32_SHARED_HEADERS CONFIGURE_DEPENDS
         "${CMAKE_SOURCE_DIR}/include/*.h"
         "${CMAKE_SOURCE_DIR}/src/wow64/*.h")

    # The i386 Wine import libs must be copied, made writable, and re-indexed
    # with the i686 ranlib before the mingw linker accepts them.
    set(_pa32_imp_names winecrt0 ntdll ole32 uuid kernelbase)
    set(_pa32_imp_libs "")
    set(_pa32_imp_cmds "")
    set(_pa32_imp_deps "")
    foreach(_n ${_pa32_imp_names})
        set(_src "${WINE_LIB_ROOT}/i386-windows/lib${_n}.a")
        set(_dst "${CMAKE_BINARY_DIR}/lib${_n}-i386.a")
        list(APPEND _pa32_imp_libs "${_dst}")
        list(APPEND _pa32_imp_deps "${_src}")
        list(APPEND _pa32_imp_cmds
            COMMAND ${CMAKE_COMMAND} -E copy "${_src}" "${_dst}"
            COMMAND chmod u+w "${_dst}"
            COMMAND "${I686_W64_MINGW_RANLIB}" "${_dst}")
    endforeach()
    add_custom_command(
        OUTPUT  ${_pa32_imp_libs}
        ${_pa32_imp_cmds}
        DEPENDS ${_pa32_imp_deps}
        VERBATIM
        COMMENT "index i386 Wine import libraries")

    set(_pa32_inc -I "${CMAKE_SOURCE_DIR}/include" -I "${CMAKE_SOURCE_DIR}/src/wow64")
    foreach(_d ${WINE_INCLUDE_DIRS})
        list(APPEND _pa32_inc -I "${_d}")
    endforeach()
    set(_pa32_cflags -Wall -Wextra -Werror=implicit-function-declaration -msse3
        $<$<CONFIG:Release>:-O2> $<$<CONFIG:Release>:-DNDEBUG>
        $<$<CONFIG:RelWithDebInfo>:-O2> $<$<CONFIG:RelWithDebInfo>:-g>
        $<$<CONFIG:RelWithDebInfo>:-DNDEBUG>
        $<$<CONFIG:Debug>:-O0> $<$<CONFIG:Debug>:-g3> $<$<CONFIG:Debug>:-DDEBUG>
        $<$<CONFIG:Debug>:-fno-omit-frame-pointer>)

    # i386 PE front end.  The shared ASIO sources link against the WoW64
    # proxy instead of src/audio.c.
    # -lmingw32 first: its tlssup.o must own _tls_index, or Wine 11.16+'s winecrt0 tls.o collides.
    add_custom_command(
        OUTPUT  "${PA32_DLL}"
        COMMAND "${I686_W64_MINGW_GCC}" -shared
                "${CMAKE_SOURCE_DIR}/src/asio.c"
                "${CMAKE_SOURCE_DIR}/src/main.c"
                "${CMAKE_SOURCE_DIR}/src/regsvr.c"
                "${CMAKE_SOURCE_DIR}/src/config.c"
                "${CMAKE_SOURCE_DIR}/src/wow64/audio_proxy.c"
                "${PA32_DEF}"
                -lmingw32
                "${CMAKE_BINARY_DIR}/libwinecrt0-i386.a"
                -DPIPEASIO_WOW64_PE
                ${_pa32_inc} ${_pa32_cflags} -static -static-libgcc
                -o "${PA32_DLL}"
                "${CMAKE_BINARY_DIR}/libntdll-i386.a"
                "${CMAKE_BINARY_DIR}/libole32-i386.a"
                "${CMAKE_BINARY_DIR}/libuuid-i386.a"
                "${CMAKE_BINARY_DIR}/libkernelbase-i386.a"
        COMMAND "${WINEBUILD}" -m32 --builtin "${PA32_DLL}"
        DEPENDS "${CMAKE_SOURCE_DIR}/src/asio.c"
                "${CMAKE_SOURCE_DIR}/src/main.c"
                "${CMAKE_SOURCE_DIR}/src/regsvr.c"
                "${CMAKE_SOURCE_DIR}/src/config.c"
                "${CMAKE_SOURCE_DIR}/src/wow64/audio_proxy.c"
                ${PA32_SHARED_HEADERS}
                "${PA32_DEF}" ${_pa32_imp_libs}
        VERBATIM COMMAND_EXPAND_LISTS
        COMMENT "mingw pipeasio32.dll (32-bit WoW64 PE front end)")

    # 64-bit unixlib hosting the real PipeWire backend.
    # -fno-lto: same winebuild ld -r / .spec export hazard as add_wine_dll (issue #6).
    add_library(pipeasio32_unix_objs OBJECT
        src/wow64/audio_unix.c src/wow64/handle_table.c src/audio.c src/config.c)
    set_target_properties(pipeasio32_unix_objs PROPERTIES POSITION_INDEPENDENT_CODE ON)
    target_include_directories(pipeasio32_unix_objs PRIVATE
        ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/src/wow64
        ${PIPEWIRE_INCLUDE_DIRS} ${WINE_INCLUDE_DIRS})
    target_compile_options(pipeasio32_unix_objs PRIVATE
        -DPIPEASIO_AUDIO_UNIXLIB -D_REENTRANT
        -Wall -fno-strict-aliasing -Werror=implicit-function-declaration
        -fno-lto
        $<$<CONFIG:Release>:-O2> $<$<CONFIG:Release>:-DNDEBUG>
        $<$<CONFIG:Release>:-fvisibility=hidden>
        $<$<CONFIG:RelWithDebInfo>:-O2> $<$<CONFIG:RelWithDebInfo>:-g>
        $<$<CONFIG:RelWithDebInfo>:-DNDEBUG> $<$<CONFIG:RelWithDebInfo>:-fvisibility=hidden>
        $<$<CONFIG:Debug>:-O0> $<$<CONFIG:Debug>:-g3> $<$<CONFIG:Debug>:-DDEBUG>
        $<$<CONFIG:Debug>:-fno-omit-frame-pointer>)
    set(_pa32_sanitize_libs "")
    if(PIPEASIO_ASAN)
        target_compile_options(pipeasio32_unix_objs PRIVATE
            -fsanitize=address -fsanitize=undefined)
        list(APPEND _pa32_sanitize_libs -lasan -lubsan)
    endif()
    add_custom_command(
        OUTPUT  "${PA32_SO}"
        COMMAND "${WINEGCC}" -shared
                "${PA32_SPEC}"
                $<TARGET_OBJECTS:pipeasio32_unix_objs>
                -lpthread -ldl ${PIPEWIRE_LINK_LIBRARIES} ${PIPEWIRE_LDFLAGS_OTHER}
                ${_pa32_sanitize_libs}
                -o "${PA32_SO}"
        DEPENDS pipeasio32_unix_objs "${PA32_SPEC}"
                $<TARGET_OBJECTS:pipeasio32_unix_objs>
        VERBATIM COMMAND_EXPAND_LISTS
        COMMENT "winegcc pipeasio32.so (WoW64 unixlib)")

    add_custom_target(pipeasio32_unix ALL DEPENDS "${PA32_SO}")
    add_custom_target(pipeasio32 ALL DEPENDS "${PA32_DLL}")
    add_dependencies(pipeasio32 pipeasio32_unix)

    install(FILES "${PA32_DLL}" DESTINATION "${PA_WINE_DEST}/i386-windows")
    install(FILES "${PA32_SO}" DESTINATION "${PA_WINE_DEST}/x86_64-unix"
            PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
                        GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
endif()

include(GNUInstallDirs)

install(PROGRAMS ${CMAKE_SOURCE_DIR}/pipeasio-register
        DESTINATION ${CMAKE_INSTALL_BINDIR})

# Optional native Qt6 settings panel; not used inside Wine/Proton.
option(BUILD_SETTINGS_PANEL "Build the Qt6 settings panel" ON)
if(BUILD_SETTINGS_PANEL)
    include(CheckLanguage)
    check_language(CXX)
    if(CMAKE_CXX_COMPILER)
        enable_language(CXX)
        set(CMAKE_CXX_STANDARD 17)
        find_package(Qt6 QUIET COMPONENTS Widgets)
        if(Qt6_FOUND)
            add_subdirectory(gui)
        else()
            message(WARNING "Qt6 Widgets not found - skipping the settings panel "
                            "(set -DBUILD_SETTINGS_PANEL=OFF to silence).")
        endif()
    else()
        message(WARNING "No C++ compiler - skipping the settings panel.")
    endif()
endif()

# Test rig: standalone Wine probe host + Linux-native unit tests.
option(BUILD_TESTS "Build the asio_probe Wine host and unit tests" ON)
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests/asio_probe)
    add_subdirectory(tests/asio_loopback)
    add_subdirectory(tests/unit)
    add_subdirectory(tests/pw_probe)
    add_subdirectory(tests/wow64)

    # pipeasio-register discovery/warning logic; self-contained (stubs wine).
    find_program(BASH_EXECUTABLE bash REQUIRED)
    add_test(NAME register_script
             COMMAND ${BASH_EXECUTABLE}
                     ${CMAKE_SOURCE_DIR}/tests/register/run.sh)
    set_tests_properties(register_script PROPERTIES
        TIMEOUT 30
        SKIP_RETURN_CODE 77)

    # A relative PIPEASIO_WINE_INSTALL_ROOT must fail at configure time.
    add_test(NAME wine_install_root_relative
             COMMAND ${CMAKE_COMMAND}
                     -S ${CMAKE_SOURCE_DIR}
                     -B ${CMAKE_BINARY_DIR}/test-wine-root-relative
                     -DPIPEASIO_WINE_INSTALL_ROOT=lib64/wine)
    set_tests_properties(wine_install_root_relative PROPERTIES
        TIMEOUT 60
        PASS_REGULAR_EXPRESSION "PIPEASIO_WINE_INSTALL_ROOT must be an absolute")
    if(BUILD_SETTINGS_PANEL AND Qt6_FOUND)
        add_subdirectory(tests/gui)
    elseif(BUILD_SETTINGS_PANEL)
        add_test(NAME test_panel_prerequisite
                 COMMAND ${CMAKE_COMMAND} -P
                         ${CMAKE_SOURCE_DIR}/tests/gui/check_prerequisite.cmake)
        set_tests_properties(test_panel_prerequisite PROPERTIES
            TIMEOUT 10
            SKIP_REGULAR_EXPRESSION "SKIP:")
    endif()
    if(PIPEASIO_ASAN)
        find_program(BASH_EXECUTABLE bash REQUIRED)
        set(_pipeasio_sanitizer_artifacts
            ${CMAKE_BINARY_DIR}/pipeasio64.dll.so
            ${CMAKE_BINARY_DIR}/pipeasio64_stop_test.dll.so)
        if(BUILD_WOW64_32)
            list(APPEND _pipeasio_sanitizer_artifacts ${CMAKE_BINARY_DIR}/pipeasio32.so)
        endif()
        foreach(_target test_offsets test_config test_admission_gate
                         test_pw_buffer_region test_handle_table pw_filter_probe
                         pw_delivery_probe pw_default_probe pipeasio-settings test_panel)
            if(TARGET ${_target})
                list(APPEND _pipeasio_sanitizer_artifacts $<TARGET_FILE:${_target}>)
            endif()
        endforeach()
        add_test(NAME sanitizer_artifacts
                 COMMAND ${BASH_EXECUTABLE}
                         ${CMAKE_SOURCE_DIR}/tests/check_sanitizer_artifacts.sh
                         ${_pipeasio_sanitizer_artifacts})
        set_tests_properties(sanitizer_artifacts PROPERTIES
            TIMEOUT 10
            LABELS "pipeasio-sanitize"
            FIXTURES_SETUP sanitizer_artifacts)
    endif()
endif()
