cmake_minimum_required(VERSION 3.25)
project(AetherSDR VERSION 26.6.1.1 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

# Libraries options — opt-in system-package replacements for vendored
# dependencies.  Off by default so existing build hosts (CI containers,
# release builds, contributor laptops) keep using the bundled, version-
# pinned third_party/ snapshots.  Distro packagers turn these ON to
# match their dynamic-linking policy.

option(USE_SYSTEM_ZLIB         "Use system zlib"         OFF)
option(USE_SYSTEM_MSPACK       "Use system libmspack"    OFF)
option(USE_SYSTEM_LIBMOSQUITTO "Use system libmosquitto" OFF)
option(USE_SYSTEM_RTMIDI       "Use system RtMidi"       OFF)

# Build features options

option(REQUIRE_SERIALPORT "Fail if Qt6 SerialPort is not found (use for release builds)" OFF)
option(ENABLE_RADE "Build with RADE digital voice support (uses vendored Opus snapshot)" ON)
option(AETHER_GPU_SPECTRUM "Enable QRhi GPU spectrum rendering" ON)
option(ENABLE_BNR "Enable NVIDIA NIM BNR noise removal (requires grpc++)" OFF)
option(ENABLE_SPECBLEACH "Enable NR4 spectral bleach noise reduction" ON)
option(ENABLE_DFNR "Enable DFNR DeepFilterNet3 noise reduction" ON)
option(ENABLE_MQTT "Enable MQTT client support" ON)
option(MQTT_TLS "Enable MQTT TLS via OpenSSL (disable for AppImage)" ON)
option(LOWER_CASE_BINARY_NAME "Make the output binary lower case. Only affects Linux" OFF)
if(WIN32)
    set(AETHER_EMBED_DFNR_MODEL_DEFAULT ON)
else()
    set(AETHER_EMBED_DFNR_MODEL_DEFAULT OFF)
endif()
option(AETHER_EMBED_DFNR_MODEL
       "Embed the DFNR model payload into application resources instead of deploying a loose archive"
       ${AETHER_EMBED_DFNR_MODEL_DEFAULT})

# macOS: ensure Homebrew lib/include paths are in the search path
# (universal builds with CMAKE_OSX_ARCHITECTURES may not search /opt/homebrew by default)
if(APPLE)
    execute_process(COMMAND brew --prefix OUTPUT_VARIABLE HOMEBREW_PREFIX OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(HOMEBREW_PREFIX)
        link_directories("${HOMEBREW_PREFIX}/lib")
        include_directories("${HOMEBREW_PREFIX}/include")
    endif()
endif()

# Build type
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()

# Qt6 components
# Qt 6.2 minimum: matches Ubuntu 22.04 LTS (our oldest supported distro)
# and is the floor where QWindow::startSystemMove() — used by the
# frameless title-bar drag in TitleBar / ContainerWidget / PanadapterApplet
# — became available.  Binary distributions bundle Qt 6.7+ for QRhiWidget.
find_package(Qt6 6.2 REQUIRED COMPONENTS
    Core
    Widgets
    Network
    Multimedia
    Test
)
# zlib is bundled under third_party/zlib (1.3.1) for parity with the
# libmosquitto bundling pattern (#699) and per the constitution's
# Technology Constraint preferring bundled libraries over package
# managers.  The vcpkg dependency on Windows + system-zlib on Linux/macOS
# is gone — single source-of-truth across all platforms. (#2651)
set(ZLIB_BUILD_EXAMPLES OFF CACHE BOOL "Disable zlib examples" FORCE)
set(SKIP_INSTALL_ALL ON CACHE BOOL "Don't install zlib" FORCE)

if (USE_SYSTEM_ZLIB)
    find_package(PkgConfig REQUIRED)
    if(PkgConfig_FOUND)
        pkg_check_modules(zlib REQUIRED IMPORTED_TARGET zlib)
    endif()
else()
    add_subdirectory(third_party/zlib EXCLUDE_FROM_ALL)
endif()

# Note: on Windows the SerialPortController uses raw Win32 WaitCommEvent for
# DSR/CTS edge detection (FTDI VCP drivers don't refresh
# GetCommModemStatus outside a WaitCommEvent completion).  Qt6::SerialPort
# is still useful on Linux/macOS for pin polling, so the find_package call
# stays platform-agnostic.
if(REQUIRE_SERIALPORT)
    find_package(Qt6 REQUIRED COMPONENTS SerialPort)
else()
    find_package(Qt6 QUIET COMPONENTS SerialPort)
endif()
if(Qt6SerialPort_FOUND)
    message(STATUS "Qt6::SerialPort found — serial PTT/CW support enabled")
else()
    message(STATUS "Qt6::SerialPort not found — serial PTT/CW support disabled")
endif()
find_package(Qt6 QUIET COMPONENTS WebSockets)
if(Qt6WebSockets_FOUND)
    message(STATUS "Qt6::WebSockets found — FreeDV Reporter spot source enabled")
else()
    message(STATUS "Qt6::WebSockets not found — FreeDV Reporter spot source disabled")
endif()
if(UNIX AND NOT APPLE)
    find_package(Qt6 QUIET COMPONENTS DBus)
    if(Qt6DBus_FOUND)
        message(STATUS "Qt6::DBus found — sleep inhibition via D-Bus enabled")
    else()
        message(STATUS "Qt6::DBus not found — sleep inhibition disabled on Linux")
    endif()
endif()
find_package(Qt6Keychain QUIET)
if(Qt6Keychain_FOUND)
    message(STATUS "Qt6Keychain found — SmartLink credential persistence enabled")
else()
    message(STATUS "Qt6Keychain not found — SmartLink credential persistence disabled")
endif()

# PortAudio (optional fallback for audio)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
    pkg_check_modules(PORTAUDIO portaudio-2.0)
endif()

# FFTW3 (required for NR2 spectral noise reduction)
# Windows: run scripts/setup/setup-fftw.ps1 first to download prebuilt DLLs
# Linux:   apt install libfftw3-dev
# macOS:   brew install fftw
if(WIN32)
    set(FFTW3_ROOT "${CMAKE_SOURCE_DIR}/third_party/fftw3")
    if(EXISTS "${FFTW3_ROOT}/include/fftw3.h")
        set(FFTW3_FOUND TRUE)
        set(FFTW3_INCLUDE_DIRS "${FFTW3_ROOT}/include")
        set(FFTW3_LIBRARIES "${FFTW3_ROOT}/lib/fftw3.lib")
        set(FFTW3_DLL "${FFTW3_ROOT}/bin/libfftw3-3.dll")
    else()
        message(WARNING "FFTW3 not found. Run scripts/setup/setup-fftw.ps1 to download it. NR2 will use fallback FFT.")
    endif()
else()
    if(PkgConfig_FOUND)
        pkg_check_modules(FFTW3 fftw3)
    endif()
    if(NOT FFTW3_FOUND)
        find_library(FFTW3_LIB fftw3)
        find_path(FFTW3_INC fftw3.h)
        if(FFTW3_LIB AND FFTW3_INC)
            set(FFTW3_FOUND TRUE)
            set(FFTW3_LIBRARIES ${FFTW3_LIB})
            set(FFTW3_INCLUDE_DIRS ${FFTW3_INC})
        endif()
    endif()
endif()

# Bundled RADE (BSD-2) — FreeDV Radio Autoencoder digital voice codec
# Opus (with FARGAN/LPCNet) is built from a vendored local snapshot via ExternalProject
set(RADE_DIR ${CMAKE_SOURCE_DIR}/third_party/radae)
option(RADE_WAV_TAP "Write diagnostic WAV files at RADE TX tap points (Taps A/B/D/E/F) for offline demod analysis" OFF)
if(ENABLE_RADE AND EXISTS "${RADE_DIR}/src/rade_api.h")
    # macOS universal binary: build Opus for both architectures
    if(APPLE AND CMAKE_OSX_ARCHITECTURES MATCHES "x86_64.*arm64|arm64.*x86_64")
        set(BUILD_OSX_UNIVERSAL ON)
    endif()
    include(${RADE_DIR}/cmake/BuildOpus.cmake)
    set(RADE_SOURCES
        ${RADE_DIR}/src/rade_api_nopy.c
        ${RADE_DIR}/src/rade_dsp.c
        ${RADE_DIR}/src/rade_ofdm.c
        ${RADE_DIR}/src/rade_bpf.c
        ${RADE_DIR}/src/rade_acq.c
        ${RADE_DIR}/src/rade_tx.c
        ${RADE_DIR}/src/rade_rx.c
        ${RADE_DIR}/src/rade_enc.c
        ${RADE_DIR}/src/rade_dec.c
        ${RADE_DIR}/src/rade_enc_data.c
        ${RADE_DIR}/src/rade_dec_data.c
        ${RADE_DIR}/src/kiss_fft.c
        ${RADE_DIR}/src/kiss_fftr.c
        # codec2-derived LDPC + GP-interleaver for EOO callsign (rade_text)
        ${RADE_DIR}/src/mpdecode_core.c
        ${RADE_DIR}/src/gp_interleaver.c
        ${RADE_DIR}/src/HRA_56_56.c
        ${RADE_DIR}/src/ldpc_codes.c
        ${RADE_DIR}/src/rade_text.c
    )
    if(MSVC)
        set(RADE_WARN_FLAG "/w")
    else()
        set(RADE_WARN_FLAG "-w")
    endif()
    set_source_files_properties(${RADE_SOURCES} PROPERTIES
        COMPILE_FLAGS "${RADE_WARN_FLAG} -DIS_BUILDING_RADE_API=1 -DRADE_PYTHON_FREE=1"
        INCLUDE_DIRECTORIES "${RADE_DIR}/src"
    )
    set(RADE_FOUND TRUE)
    message(STATUS "RADE enabled (bundled vendored Opus snapshot)")

    # Standalone offline diagnostic tool: demodulate a WAV file and decode the EOO callsign.
    # Guarded by source-file existence so builds against vendored RADE snapshots that
    # predate rade_demod_wav.c don't hard-fail. Independent of RADE_WAV_TAP.
    # Excluded on WIN32: rade_demod_wav.c uses POSIX getopt.h, unavailable in MSVC's SDK.
    if(EXISTS "${RADE_DIR}/src/rade_demod_wav.c" AND NOT WIN32)
        add_executable(rade_demod_wav
            ${RADE_SOURCES}
            ${RADE_DIR}/src/rade_demod_wav.c
        )
        set_source_files_properties(${RADE_DIR}/src/rade_demod_wav.c PROPERTIES
            COMPILE_FLAGS "${RADE_WARN_FLAG} -DIS_BUILDING_RADE_API=1 -DRADE_PYTHON_FREE=1"
            INCLUDE_DIRECTORIES "${RADE_DIR}/src"
        )
        target_include_directories(rade_demod_wav PRIVATE ${RADE_DIR}/src)
        target_link_libraries(rade_demod_wav PRIVATE opus
            $<$<NOT:$<PLATFORM_ID:Windows>>:m>)
        add_dependencies(rade_demod_wav build_opus)
    endif()

else()
    set(RADE_FOUND FALSE)
    set(RADE_SOURCES "")
    if(ENABLE_RADE)
        message(STATUS "RADE source not found at ${RADE_DIR}. Digital voice disabled.")
    else()
        message(STATUS "RADE disabled by ENABLE_RADE=OFF.")
    endif()
endif()

# Opus codec — required for SmartLink compressed audio, independent of RADE.
# When RADE is enabled, Opus comes from AetherSDR's vendored RADE snapshot
# (with FARGAN/OSCE).
# When RADE is disabled, find system libopus.
# Windows: run scripts/setup/setup-opus.ps1 first to download prebuilt DLLs
# Linux:   apt install libopus-dev
# macOS:   brew install opus
if(RADE_FOUND)
    set(OPUS_FOUND TRUE)
    message(STATUS "Opus: using RADE's bundled build")
elseif(WIN32)
    set(OPUS_ROOT "${CMAKE_SOURCE_DIR}/third_party/opus")
    if(EXISTS "${OPUS_ROOT}/include/opus/opus.h")
        set(OPUS_FOUND TRUE)
        set(OPUS_INCLUDE_DIRS "${OPUS_ROOT}/include/opus")
        set(OPUS_LIBRARIES "${OPUS_ROOT}/lib/opus.lib")
        message(STATUS "Opus: using prebuilt Windows library (static)")
    else()
        set(OPUS_FOUND FALSE)
        message(WARNING "Opus not found. Run scripts/setup/setup-opus.ps1 to download it. "
                        "SmartLink compressed audio will be unavailable.")
    endif()
else()
    if(PkgConfig_FOUND)
        pkg_check_modules(OPUS opus)
    endif()
    if(NOT OPUS_FOUND)
        find_library(OPUS_LIB opus)
        find_path(OPUS_INC opus/opus.h)
        if(OPUS_LIB AND OPUS_INC)
            set(OPUS_FOUND TRUE)
            set(OPUS_LIBRARIES ${OPUS_LIB})
            set(OPUS_INCLUDE_DIRS "${OPUS_INC}/opus")
        endif()
    endif()
    if(OPUS_FOUND)
        message(STATUS "Opus: using system libopus")
    else()
        message(WARNING "Opus not found — SmartLink compressed audio will be unavailable. "
                        "Install libopus-dev (Debian/Ubuntu), opus (Arch/vcpkg), or brew install opus (macOS).")
    endif()
endif()

# GPU-accelerated spectrum/waterfall rendering via QRhi (#391)
# Requires: Qt 6.7+ (QRhiWidget), Qt6::ShaderTools (build-time shader compilation)
# Defaults to ON but auto-disables on Qt < 6.7 (e.g. Ubuntu 24.04 ships Qt 6.4).
if(AETHER_GPU_SPECTRUM)
    if(Qt6_VERSION VERSION_LESS "6.7")
        set(AETHER_GPU_SPECTRUM OFF)
        message(STATUS "GPU spectrum rendering disabled (requires Qt 6.7+, found ${Qt6_VERSION} — pass -DCMAKE_PREFIX_PATH=/path/to/Qt/6.7.x/gcc_64 to use a newer Qt installation)")
    else()
        find_package(Qt6 REQUIRED COMPONENTS ShaderTools)
        # Qt6GuiPrivate provides QRhi headers needed for GPU rendering.
        find_package(Qt6GuiPrivate QUIET)

        # Detect Debian paths early (support any multiarch tuple, e.g. aarch64-linux-gnu)
        if(NOT Qt6GuiPrivate_FOUND)
            message(STATUS "Qt6GuiPrivate not found, checking Debian multi-arch paths...")
            set(DEB_HOST_MULTIARCH "")
            find_program(DPKG_ARCH dpkg-architecture)
            if(DPKG_ARCH)
                execute_process(COMMAND ${DPKG_ARCH} -qDEB_HOST_MULTIARCH
                    OUTPUT_VARIABLE DEB_HOST_MULTIARCH OUTPUT_STRIP_TRAILING_WHITESPACE
                    ERROR_QUIET)
            endif()
            find_path(DEBIAN_PRIVATE_INC
                NAMES "QtGui/${Qt6_VERSION}/QtGui/private/qhighdpiscaling_p.h"
                PATHS "/usr/include/${DEB_HOST_MULTIARCH}/qt6" "/usr/include/qt6"
                NO_DEFAULT_PATH
            )
            if(DEBIAN_PRIVATE_INC)
                set(Qt6GuiPrivate_FOUND TRUE)
                set(DEBIAN_GPU_FIX_REQUIRED TRUE) # Mark for Step 2
            endif()
        endif()

        if(Qt6GuiPrivate_FOUND)
            message(STATUS "GPU spectrum rendering enabled (QRhi, Qt ${Qt6_VERSION})")
        else()
            set(AETHER_GPU_SPECTRUM OFF)
            message(STATUS "GPU spectrum rendering disabled — Qt6GuiPrivate not found "
                           "(install qt6-base-private-dev / qt6-qtbase-private-devel)")
        endif()
    endif()
else()
    message(STATUS "GPU spectrum rendering disabled (use -DAETHER_GPU_SPECTRUM=ON to enable)")
endif()

# NVIDIA NIM BNR (Background Noise Removal) — GPU-accelerated neural denoising
# Requires: grpc++, protobuf, NVIDIA RTX 4000+ GPU, Docker container
if(ENABLE_BNR)
    find_package(PkgConfig REQUIRED)
    pkg_check_modules(GRPC REQUIRED grpc++)
    pkg_check_modules(PROTOBUF REQUIRED protobuf)
    find_program(PROTOC protoc REQUIRED)
    find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED)

    # Generate C++ stubs from bnr.proto
    set(BNR_PROTO "${CMAKE_SOURCE_DIR}/src/core/proto/bnr.proto")
    set(BNR_GEN_DIR "${CMAKE_BINARY_DIR}/bnr_gen")
    file(MAKE_DIRECTORY ${BNR_GEN_DIR})

    set(BNR_PB_CC "${BNR_GEN_DIR}/bnr.pb.cc")
    set(BNR_PB_H  "${BNR_GEN_DIR}/bnr.pb.h")
    set(BNR_GRPC_CC "${BNR_GEN_DIR}/bnr.grpc.pb.cc")
    set(BNR_GRPC_H  "${BNR_GEN_DIR}/bnr.grpc.pb.h")

    add_custom_command(
        OUTPUT ${BNR_PB_CC} ${BNR_PB_H} ${BNR_GRPC_CC} ${BNR_GRPC_H}
        COMMAND ${PROTOC}
            --proto_path=${CMAKE_SOURCE_DIR}/src/core/proto
            --cpp_out=${BNR_GEN_DIR}
            --grpc_out=${BNR_GEN_DIR}
            --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN}
            ${BNR_PROTO}
        DEPENDS ${BNR_PROTO}
        COMMENT "Generating BNR gRPC stubs from bnr.proto"
    )

    set(BNR_SOURCES ${BNR_PB_CC} ${BNR_GRPC_CC})
    message(STATUS "NVIDIA BNR enabled (gRPC ${GRPC_VERSION})")
else()
    set(BNR_SOURCES "")
    message(STATUS "NVIDIA BNR disabled (use -DENABLE_BNR=ON to enable)")
endif()

# libspecbleach NR4 — bundled spectral noise reduction (LGPL-2.1)
# MSVC: libspecbleach uses C99 VLAs and __attribute__ which MSVC doesn't support.
# When clang-cl is available, build as a separate static lib using it.
if(MSVC)
    find_program(CLANG_CL clang-cl HINTS "C:/Program Files/LLVM/bin")
    if(NOT CLANG_CL)
        set(ENABLE_SPECBLEACH OFF CACHE BOOL "Enable NR4 spectral bleach noise reduction" FORCE)
    endif()
endif()
if(ENABLE_SPECBLEACH)
    file(GLOB_RECURSE SPECBLEACH_SOURCES third_party/libspecbleach/src/*.c)
    if(MSVC AND CLANG_CL)
        # Pre-build specbleach.lib with clang-cl (supports VLAs and MSVC ABI)
        set(SPECBLEACH_BUILD_DIR "${CMAKE_BINARY_DIR}/specbleach")
        file(MAKE_DIRECTORY ${SPECBLEACH_BUILD_DIR})
        set(SPECBLEACH_STATIC_LIB "${SPECBLEACH_BUILD_DIR}/specbleach.lib")
        # When -T ClangCL is used CMAKE_C_COMPILER is the VS-bundled clang-cl which
        # auto-detects the Windows SDK and MSVC include paths. Prefer it over the
        # standalone LLVM found by find_program (which may not locate stdint.h in a
        # MSBuild custom-command environment).
        if(CMAKE_C_COMPILER_ID STREQUAL "Clang")
            set(_specbleach_cc "${CMAKE_C_COMPILER}")
        else()
            set(_specbleach_cc "${CLANG_CL}")
        endif()
        add_custom_command(
            OUTPUT ${SPECBLEACH_STATIC_LIB}
            COMMAND ${_specbleach_cc} -w --target=x86_64-pc-windows-msvc
                -DFFTW_DLL
                -I${CMAKE_SOURCE_DIR}/third_party/libspecbleach/include
                -I${CMAKE_SOURCE_DIR}/third_party/libspecbleach/src
                -I${CMAKE_SOURCE_DIR}/third_party/fftw3/include
                -c ${SPECBLEACH_SOURCES}
            COMMAND lib /nologo /out:specbleach.lib *.obj
            WORKING_DIRECTORY ${SPECBLEACH_BUILD_DIR}
            DEPENDS ${SPECBLEACH_SOURCES}
            COMMENT "Building libspecbleach with clang-cl"
        )
        add_custom_target(specbleach_build DEPENDS ${SPECBLEACH_STATIC_LIB})
        set(SPECBLEACH_SOURCES "")
        message(STATUS "NR4 (libspecbleach) enabled — building with clang-cl")
    else()
        message(STATUS "NR4 (libspecbleach) enabled — ${CMAKE_SOURCE_DIR}/third_party/libspecbleach")
    endif()
else()
    set(SPECBLEACH_SOURCES "")
    if(MSVC AND NOT CLANG_CL)
        message(STATUS "NR4 (libspecbleach) disabled — install LLVM for clang-cl VLA support")
    else()
        message(STATUS "NR4 (libspecbleach) disabled")
    endif()
endif()

# DeepFilterNet3 DFNR — bundled neural noise reduction (MIT/Apache-2.0)
# Pre-built libdf library from the Rust crate; model payload is handled below.
if(ENABLE_DFNR)
    set(DEEPFILTER_DIR ${CMAKE_SOURCE_DIR}/third_party/deepfilter)
    set(DFNR_MODEL "${DEEPFILTER_DIR}/models/DeepFilterNet3_onnx.tar.gz")
    set(DFNR_EMBEDDED_MODEL_NAME "DeepFilterNet3_onnx.dfmodel")
    if(WIN32)
        set(DFNR_LIB_DIR "${DEEPFILTER_DIR}/lib/windows-x86_64")
        if(MINGW)
            set(DFNR_LIB "${DFNR_LIB_DIR}/libdeepfilter.dll.a")
            set(DFNR_DLL "${DFNR_LIB_DIR}/deepfilter.dll")
        else()
            # MSVC: prefer .dll.lib import library, fall back to .lib
            if(EXISTS "${DFNR_LIB_DIR}/deepfilter.dll.lib")
                set(DFNR_LIB "${DFNR_LIB_DIR}/deepfilter.dll.lib")
            else()
                set(DFNR_LIB "${DFNR_LIB_DIR}/deepfilter.lib")
            endif()
            set(DFNR_DLL "${DFNR_LIB_DIR}/deepfilter.dll")
        endif()
    elseif(APPLE)
        if(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64")
            set(DFNR_LIB_DIR "${DEEPFILTER_DIR}/lib/darwin-arm64")
        else()
            set(DFNR_LIB_DIR "${DEEPFILTER_DIR}/lib/darwin-x86_64")
        endif()
        set(DFNR_LIB "${DFNR_LIB_DIR}/libdeepfilter.a")
    else()
        if(CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64")
            set(DFNR_LIB_DIR "${DEEPFILTER_DIR}/lib/linux-aarch64")
        else()
            set(DFNR_LIB_DIR "${DEEPFILTER_DIR}/lib/linux-x86_64")
        endif()
        set(DFNR_LIB "${DFNR_LIB_DIR}/libdeepfilter.a")
    endif()
    if(EXISTS ${DFNR_LIB})
        message(STATUS "DFNR (DeepFilterNet3) enabled — ${DFNR_LIB}")
    else()
        set(ENABLE_DFNR OFF)
        message(STATUS "DFNR (DeepFilterNet3) disabled — library not found at ${DFNR_LIB} "
                       "(run ./scripts/setup/setup-deepfilter.sh before cmake to enable)")
    endif()
endif()

# MQTT client support — bundled libmosquitto (#699)
if(ENABLE_MQTT)
    if (USE_SYSTEM_LIBMOSQUITTO)
        find_package(PkgConfig REQUIRED)
        if(PkgConfig_FOUND)
            pkg_check_modules(libmosquitto REQUIRED IMPORTED_TARGET libmosquitto)
        endif()
    else()
        if(MQTT_TLS)
            find_package(OpenSSL)
        endif()
        set(MOSQUITTO_DIR ${CMAKE_SOURCE_DIR}/third_party/mosquitto)
        file(GLOB MOSQUITTO_SOURCES ${MOSQUITTO_DIR}/src/*.c)
        # Remove broker-only and optional files we don't need
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "socks_mosq\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "http_client\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "picohttpparser\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "extended_auth\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "cjson_common\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "password_common\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "base64_common\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "json_help\\.c$")
        list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "srv_mosq\\.c$")
        if(OpenSSL_FOUND)
            message(STATUS "MQTT client support enabled (bundled libmosquitto, TLS via OpenSSL ${OPENSSL_VERSION})")
        else()
            # tls_mosq.c and file_common.c (cert file access) require OpenSSL — exclude when not available
            list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "tls_mosq\\.c$")
            list(FILTER MOSQUITTO_SOURCES EXCLUDE REGEX "file_common\\.c$")
            message(STATUS "MQTT client support enabled (bundled libmosquitto, TLS disabled — OpenSSL not found)")
        endif()
    endif()
endif()

# Sources
set(CORE_SOURCES
    src/core/AppSettings.cpp
    src/core/SettingsHelpers.cpp
    src/core/ThemeManager.cpp
    src/core/BandStackSettings.cpp
    src/core/RadioDiscovery.cpp
    src/core/RadioConnection.cpp
    src/core/NetworkPathResolver.cpp
    src/core/TgxlConnection.cpp
    src/core/CommandParser.cpp
    src/core/AudioSummaryLogger.cpp
    src/core/AudioEngine.cpp
    src/core/TxMicChannelNormalizer.cpp
    src/core/ChannelStripPresets.cpp
    src/core/Biquad.cpp
    src/core/StereoBiquad.cpp
    src/core/ClientEq.cpp
    src/core/ClientComp.cpp
    src/core/ClientGate.cpp
    src/core/ClientDeEss.cpp
    src/core/ClientTube.cpp
    src/core/ClientPudu.cpp
    src/core/ClientPuduMonitor.cpp
    src/core/ClientReverb.cpp
    src/core/ClientPhaseRotator.cpp
    src/core/ClientFinalLimiter.cpp
    src/core/ClientTxTestTone.cpp
    src/core/ClientQuindarTone.cpp
    src/core/QuindarLocalSink.cpp
    src/core/CwSidetoneGenerator.cpp
    src/core/CwSidetoneQAudioSink.cpp
    src/core/CwxLocalKeyer.cpp
    src/core/IambicKeyer.cpp
    src/core/SpectralNR.cpp
    src/core/PanadapterStream.cpp
    src/core/PacketLossConcealment.cpp
    src/core/PerfTelemetry.cpp
    src/core/RigctlProtocol.cpp
    src/core/RigctlPty.cpp
    src/core/SmartCatProtocol.cpp
    src/core/SmartCatSession.cpp
    src/core/CatPort.cpp
    src/core/SmartLinkClient.cpp
    src/core/WanConnection.cpp
    src/core/DxClusterClient.cpp
    src/core/WsjtxClient.cpp
    src/core/SpotCollectorClient.cpp
    src/core/PotaClient.cpp
    src/core/PropForecastClient.cpp
    src/core/MqttAntennaAlias.cpp
    src/core/MqttSettings.cpp
    src/core/SpotCommandPolicy.cpp
    src/core/SpotModeResolver.cpp
    src/core/TciServer.cpp
    src/core/TciProtocol.cpp
    src/core/MqttClient.cpp
    src/core/PgxlConnection.cpp
    src/core/FirmwareUploader.cpp
    src/core/DvkWavTransfer.cpp
    src/core/ZipArchive.cpp
    src/core/ProfileTransfer.cpp
    src/core/QsoRecorder.cpp
    src/core/FirmwareStager.cpp
    src/core/OleCompoundFile.cpp
    src/core/CabExtractor.cpp
    src/core/RNNoiseFilter.cpp
    src/core/SpecbleachFilter.cpp
    src/core/CwDecoder.cpp
    src/core/VoiceSignalDetector.cpp
    src/core/SpectrogramBuffer.cpp
    src/core/SignalClassifier.cpp
    src/core/Resampler.cpp
    src/core/NvidiaBnrFilter.cpp
    src/core/DeepFilterFilter.cpp
    src/core/RADEEngine.cpp
    src/core/AsyncLogWriter.cpp
    src/core/LogManager.cpp
    src/core/ShortcutManager.cpp
    src/core/SupportBundle.cpp
    src/core/DeviceDiagnostics.cpp
    src/core/SerialPortController.cpp
    src/core/FlexControlManager.cpp
    src/core/OpusCodec.cpp
    src/core/CtyDatParser.cpp
    src/core/AdifParser.cpp
    src/core/DxccWorkedStatus.cpp
    src/core/DxccColorProvider.cpp
    src/core/SleepInhibitor.cpp
    src/core/MemoryCsvCompat.cpp
    src/core/MemoryRecallPolicy.cpp
    src/core/tnc/AetherAx25LibmodemShim.cpp
    src/core/tnc/Ax25FrameFormatter.cpp
)

if(APPLE)
    list(APPEND CORE_SOURCES src/core/VirtualAudioBridge.cpp src/core/MacMicPermission.mm)
elseif(UNIX)
    # Linux DAX uses PulseAudio pipe modules via pactl (works with PipeWire too).
    # When libpipewire-0.3 dev headers are present we additionally compile a
    # native pw_stream-based RX source for sub-100ms DAX RX latency.
    list(APPEND CORE_SOURCES src/core/PipeWireAudioBridge.cpp)
    set(HAVE_PIPEWIRE TRUE)
    pkg_check_modules(PIPEWIRE_NATIVE libpipewire-0.3)
    if(PIPEWIRE_NATIVE_FOUND)
        list(APPEND CORE_SOURCES
            src/core/PipeWireNativeContext.cpp
            src/core/PipeWireNativeRxSource.cpp
        )
        set(HAVE_PIPEWIRE_NATIVE TRUE)
    endif()
endif()

set(MODEL_SOURCES
    src/models/RadioModel.cpp
    src/models/ModelCapabilities.cpp
    src/models/AntennaAliasStore.cpp
    src/models/SliceModel.cpp
    src/models/PanadapterModel.cpp
    src/models/MeterModel.cpp
    src/models/TunerModel.cpp
    src/models/TransmitModel.cpp
    src/models/EqualizerModel.cpp
    src/models/TnfModel.cpp
    src/models/UsbCableModel.cpp
    src/models/DaxIqModel.cpp
    src/models/SpotModel.cpp
    src/models/CwxModel.cpp
    src/models/DvkModel.cpp
    src/models/NavtexModel.cpp
    src/models/FlexWaveformModel.cpp
    src/models/BandSettings.cpp
    src/models/BandPlanManager.cpp
    src/models/XvtrPolicy.cpp
    src/models/AntennaGeniusModel.cpp
)

set(GUI_SOURCES
    src/gui/MainWindow.cpp
    src/gui/AudioDeviceChangeDialog.cpp
    src/gui/ConnectionPanel.cpp
    src/gui/ClientDisconnectDialog.cpp
    src/gui/ConnectedStationsDialog.cpp
    src/gui/SpectrumWidget.cpp
    src/gui/SpectrumOverlayMenu.cpp
    src/gui/FrequencyEntryParser.cpp
    src/gui/SliceColorManager.cpp
    src/gui/SliceLabel.cpp
    src/gui/VfoWidget.cpp
    src/gui/RadioSetupDialog.cpp
    src/gui/NetworkDiagnosticsDialog.cpp
    src/gui/Ax25HfPacketDecodeDialog.cpp
    src/gui/FlexControlDialog.cpp
    src/gui/PropDashboardDialog.cpp
    src/gui/MemoryCommands.cpp
    src/gui/MemoryBrowsePanel.cpp
    src/gui/MemoryDialog.cpp
    src/gui/SpotSettingsDialog.cpp
    src/gui/AetherDspDialog.cpp
    src/gui/AetherDspWidget.cpp
    src/gui/WaveformsDialog.cpp
    src/gui/ClientRxDspApplet.cpp
    src/gui/DragValuePopup.cpp
    src/gui/DspParamPopup.cpp
    src/gui/DxClusterDialog.cpp
    src/gui/DxClusterStartupCommandsDialog.cpp
    src/gui/CwxPanel.cpp
    src/gui/BandStackPanel.cpp
    src/gui/FramelessWindowTitleBar.cpp
    src/gui/FramelessResizer.cpp
    src/gui/PanFloatingWindow.cpp
    src/gui/DvkPanel.cpp
    src/gui/AmpApplet.cpp
    src/gui/MeterApplet.cpp
    src/gui/HealthApplet.cpp
    src/gui/PersistentDialog.cpp
    src/gui/ProfileManagerDialog.cpp
    src/gui/ProfileImportExportDialog.cpp
    src/gui/TxBandDialog.cpp
    src/gui/PanadapterApplet.cpp
    src/gui/PanadapterStack.cpp
    src/gui/PanLayoutDialog.cpp
    src/gui/AppletPanel.cpp
    src/gui/FavoritesPickerDialog.cpp
    src/gui/RxApplet.cpp
    src/gui/FilterPassbandWidget.cpp
    src/gui/SMeterWidget.cpp
    src/gui/TunerApplet.cpp
    src/gui/TxApplet.cpp
    src/gui/AtuPreTuneDialog.cpp
    src/gui/SwrSweepLicenseDialog.cpp
    src/gui/PhoneCwApplet.cpp
    src/gui/PhoneApplet.cpp
    src/gui/EqApplet.cpp
    src/gui/WaveApplet.cpp
    src/gui/WaveformWidget.cpp
    src/gui/ClientEqApplet.cpp
    src/gui/ClientEqCurveWidget.cpp
    src/gui/ClientEqEditor.cpp
    src/gui/ClientEqEditorCanvas.cpp
    src/gui/StripEqPanel.cpp
    src/gui/ClientEqFftAnalyzer.cpp
    src/gui/ClientEqIconRow.cpp
    src/gui/ClientEqOutputFader.cpp
    src/gui/ClientLevelMeter.cpp
    src/gui/ClientEqParamRow.cpp
    src/gui/ClientChainApplet.cpp
    src/gui/ClientChainWidget.cpp
    src/gui/StripChainWidget.cpp
    src/gui/StripRxChainWidget.cpp
    src/gui/StripRxOutputPanel.cpp
    src/gui/ClientRxChainWidget.cpp
    src/gui/EditorFramelessTitleBar.cpp
    src/gui/containers/ContainerManager.cpp
    src/gui/containers/ContainerTitleBar.cpp
    src/gui/containers/ContainerWidget.cpp
    src/gui/containers/FloatingContainerWindow.cpp
    src/gui/ClientCompApplet.cpp
    src/gui/ClientCompCurveWidget.cpp
    src/gui/ClientCompKnob.cpp
    src/gui/ClientGateApplet.cpp
    src/gui/ClientGateCurveWidget.cpp
    src/gui/ClientGateEditor.cpp
    src/gui/StripGatePanel.cpp
    src/gui/ClientGateLevelView.cpp
    src/gui/ClientDeEssApplet.cpp
    src/gui/ClientDeEssCurveWidget.cpp
    src/gui/StripDeEssPanel.cpp
    src/gui/ClientTubeApplet.cpp
    src/gui/ClientTubeCurveWidget.cpp
    src/gui/ClientTubeEditor.cpp
    src/gui/StripTubePanel.cpp
    src/gui/ClientPuduApplet.cpp
    src/gui/ClientPuduEditor.cpp
    src/gui/StripPuduPanel.cpp
    src/gui/ClientReverbApplet.cpp
    src/gui/StripReverbPanel.cpp
    src/gui/StripWaveform.cpp
    src/gui/StripWaveformPanel.cpp
    src/gui/StripFinalOutputPanel.cpp
    src/gui/AetherialAudioStrip.cpp
    src/gui/PooDooLogo.cpp
    src/gui/ClientCompLimiterButton.cpp
    src/gui/ClientCompMeter.cpp
    src/gui/ClientCompThresholdFader.cpp
    src/gui/ClientCompEditor.cpp
    src/gui/ClientCompEditorCanvas.cpp
    src/gui/StripCompPanel.cpp
    src/gui/CatControlApplet.cpp
    src/gui/DaxApplet.cpp
    src/gui/TciApplet.cpp
    src/gui/DaxIqApplet.cpp
    src/gui/MqttApplet.cpp
    src/gui/MqttSettingsDialog.cpp
    src/gui/MeterSlider.cpp
    src/gui/PhaseKnob.cpp
    src/gui/AntennaGeniusApplet.cpp
    src/gui/ShackSwitchApplet.cpp
    src/gui/TitleBar.cpp
    src/gui/SupportDialog.cpp
    src/gui/SliceTroubleshootingDialog.cpp
    src/gui/KeyboardMapWidget.cpp
    src/gui/ShortcutDialog.cpp
    src/gui/MultiFlexDialog.cpp
    src/gui/HelpDialog.cpp
    src/gui/WhatsNewDialog.cpp
    src/gui/ThemeEditorDialog.cpp
    src/gui/ThemeInspector.cpp
    src/gui/GradientEditorDialog.cpp
    src/gui/TokenEditorWidget.cpp
    src/gui/CompactColorPicker.cpp
)

set(ALL_SOURCES
    src/main.cpp
    ${CORE_SOURCES}
    ${MODEL_SOURCES}
    ${GUI_SOURCES}
)

# Bundled RNNoise (Mozilla/Xiph BSD-3) — client-side neural noise suppression
set(RNNOISE_DIR ${CMAKE_SOURCE_DIR}/third_party/rnnoise)
set(RNNOISE_SOURCES
    ${RNNOISE_DIR}/src/denoise.c
    ${RNNOISE_DIR}/src/celt_lpc.c
    ${RNNOISE_DIR}/src/kiss_fft.c
    ${RNNOISE_DIR}/src/pitch.c
    ${RNNOISE_DIR}/src/rnn.c
    ${RNNOISE_DIR}/src/nnet.c
    ${RNNOISE_DIR}/src/nnet_default.c
    ${RNNOISE_DIR}/src/rnnoise_data.c
    ${RNNOISE_DIR}/src/rnnoise_tables.c
    ${RNNOISE_DIR}/src/parse_lpcnet_weights.c
)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|i[3-6]86")
    list(APPEND RNNOISE_SOURCES
        ${RNNOISE_DIR}/src/x86/x86cpu.c
        ${RNNOISE_DIR}/src/x86/x86_dnn_map.c
        ${RNNOISE_DIR}/src/x86/nnet_sse4_1.c
        ${RNNOISE_DIR}/src/x86/nnet_avx2.c
    )
endif()

# Suppress warnings and set include paths for bundled C code
set_source_files_properties(${RNNOISE_SOURCES} PROPERTIES
    INCLUDE_DIRECTORIES "${RNNOISE_DIR}/include;${RNNOISE_DIR}/src;${RNNOISE_DIR}/src/x86"
)
# x86 RTCD (runtime CPU detection) + per-file SIMD flags
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|i[3-6]86")
    if(MSVC)
        # MSVC: uses _MSC_VER path in x86cpu.c (intrin.h), no CPU_INFO_BY_C needed
        # MSVC doesn't need -msse4.1/-mavx flags — intrinsics are always available
        set_source_files_properties(${RNNOISE_SOURCES} PROPERTIES
            COMPILE_FLAGS "/w /DRNN_ENABLE_X86_RTCD=1"
        )
        set_source_files_properties(${RNNOISE_DIR}/src/x86/nnet_sse4_1.c PROPERTIES
            COMPILE_FLAGS "/w /DRNN_ENABLE_X86_RTCD=1 /D__SSE4_1__"
        )
        set_source_files_properties(${RNNOISE_DIR}/src/x86/nnet_avx2.c PROPERTIES
            COMPILE_FLAGS "/w /DRNN_ENABLE_X86_RTCD=1 /arch:AVX2 /D__AVX2__"
        )
    else()
        # GCC/Clang: use cpuid.h intrinsic and per-file ISA flags
        set_source_files_properties(${RNNOISE_SOURCES} PROPERTIES
            COMPILE_FLAGS "-w -DRNN_ENABLE_X86_RTCD=1 -DCPU_INFO_BY_C=1"
        )
        set_source_files_properties(${RNNOISE_DIR}/src/x86/nnet_sse4_1.c PROPERTIES
            COMPILE_FLAGS "-w -DRNN_ENABLE_X86_RTCD=1 -DCPU_INFO_BY_C=1 -msse4.1"
        )
        set_source_files_properties(${RNNOISE_DIR}/src/x86/nnet_avx2.c PROPERTIES
            COMPILE_FLAGS "-w -DRNN_ENABLE_X86_RTCD=1 -DCPU_INFO_BY_C=1 -mavx -mfma -mavx2"
        )
    endif()
else()
    set_source_files_properties(${RNNOISE_SOURCES} PROPERTIES
        COMPILE_FLAGS "-w"
    )
endif()

# Bundled ggmorse (MIT) — CW Morse code decoder
set(GGMORSE_DIR ${CMAKE_SOURCE_DIR}/third_party/ggmorse)
set(GGMORSE_SOURCES
    ${GGMORSE_DIR}/src/ggmorse.cpp
    ${GGMORSE_DIR}/src/resampler.cpp
)
set_source_files_properties(${GGMORSE_SOURCES} PROPERTIES
    INCLUDE_DIRECTORIES "${GGMORSE_DIR}/include;${GGMORSE_DIR}/src"
)
if(MSVC)
    set_source_files_properties(${GGMORSE_SOURCES} PROPERTIES COMPILE_FLAGS "/w")
else()
    set_source_files_properties(${GGMORSE_SOURCES} PROPERTIES COMPILE_FLAGS "-w")
endif()

add_library(aether_libmodem_core STATIC
    third_party/libmodem_core/bitstream.cpp
    third_party/libmodem_core/demodulator.cpp
)
target_include_directories(aether_libmodem_core PUBLIC
    ${CMAKE_SOURCE_DIR}/third_party/libmodem_core
)
target_compile_features(aether_libmodem_core PUBLIC cxx_std_20)
target_compile_definitions(aether_libmodem_core PUBLIC
    LIBMODEM_NAMESPACE=aether_libmodem_core
    "LIBMODEM_NAMESPACE_REFERENCE=aether_libmodem_core::"
)
if(MSVC)
    target_compile_options(aether_libmodem_core PRIVATE /w)
else()
    target_compile_options(aether_libmodem_core PRIVATE -w)
endif()

# Bundled liquid-dsp (MIT) — comprehensive DSP toolkit covering modems
# (PSK/QAM/FSK/GMSK/OFDM), FEC (Hamming/Golay/RS/convolutional), adaptive
# filters (LMS/RLS), AGC, NCO, polyphase resampling, and equalizers.
# Vendored proactively under #3043 as foundation infrastructure for future
# digital-mode work (e.g. native FT8/FT4 — #85 Phase 4 — or PSK31 / RTTY).
#
# Linux + macOS only — liquid-dsp upstream targets GCC/Clang/MinGW; its
# include/liquid.h:473 uses C99 `float _Complex` typedefs which the MSVC C
# compiler doesn't accept (syntax error: identifier 'liquid_float_complex').
# Upstream has no first-class MSVC support and patching the header for
# `_Fcomplex`/`_Dcomplex` would be invasive against future upstream syncs.
# Tracked as a follow-up; today the Windows build skips this vendor and
# any future consumer that needs liquid-dsp on Windows needs to land MSVC
# support first.
#
# Built via the upstream CMakeLists.txt with examples / tests / benchmarks /
# sandbox / docs disabled to keep the build slim. FFTW lookup is disabled
# (FIND_FFTW=OFF) so liquid-dsp uses its own internal FFT — wiring it to our
# bundled third_party/fftw3 can be a follow-up if a future consumer wants
# FFTW-accelerated transforms.
#
# AetherSDR links against liquid-static (conditionally on non-MSVC) so the
# build verifies clean on Linux + macOS even though no module currently
# consumes liquid symbols. Standard linker dead-code elimination
# (--gc-sections on ELF) drops the unused static lib from the final binary.
if(NOT MSVC)
    set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
    set(BUILD_AUTOTESTS OFF CACHE BOOL "" FORCE)
    set(BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE)
    set(BUILD_SANDBOX OFF CACHE BOOL "" FORCE)
    set(BUILD_DOC OFF CACHE BOOL "" FORCE)
    set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE)
    set(FIND_FFTW OFF CACHE BOOL "" FORCE)
    set(ENABLE_LOGGING OFF CACHE BOOL "" FORCE)
# liquid-dsp's cmake_minimum_required(VERSION 3.10) puts its option() calls
# under CMP0077 OLD behavior — option() with that policy does NOT honor a
# same-name *normal* variable. We must use CACHE BOOL FORCE so the override
# takes effect. Without this, BUILD_SHARED_LIBS stays at the option's ON
# default. Linux silently ships both libliquid.so and libliquid.a (different
# extensions); the OUTPUT_NAME `liquid` at upstream CMakeLists.txt:444
# renames the STATIC target's archive to drop the -static suffix.
#
# Cache pollution is undone with unset(CACHE) after add_subdirectory so
# this doesn't leak into the rest of the project's BUILD_SHARED_LIBS
# semantics.
    set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
    add_subdirectory(third_party/liquid-dsp EXCLUDE_FROM_ALL)
    unset(BUILD_SHARED_LIBS CACHE)
# Suppress warnings from vendored code (same pattern as other third_party
# integrations — we don't want -Werror surfaces on code we don't own).
    if(TARGET liquid-static)
        target_compile_options(liquid-static PRIVATE -w)
    endif()
endif()  # NOT MSVC

set(DFNR_RESOURCES "")
if(ENABLE_DFNR AND AETHER_EMBED_DFNR_MODEL)
    if(EXISTS "${DFNR_MODEL}")
        file(TO_CMAKE_PATH "${DFNR_MODEL}" DFNR_MODEL_QRC_PATH)
        set(DFNR_MODEL_QRC "${CMAKE_CURRENT_BINARY_DIR}/dfnr_model.qrc")
        file(WRITE "${DFNR_MODEL_QRC}"
"<RCC>
    <qresource prefix=\"/models\">
        <file alias=\"${DFNR_EMBEDDED_MODEL_NAME}\">${DFNR_MODEL_QRC_PATH}</file>
    </qresource>
</RCC>
")
        qt_add_resources(DFNR_RESOURCES "${DFNR_MODEL_QRC}")
        message(STATUS "DFNR model will be embedded in Qt resources")
    else()
        message(WARNING "AETHER_EMBED_DFNR_MODEL is ON, but DFNR model not found at ${DFNR_MODEL}")
    endif()
endif()

qt_add_resources(RESOURCES resources/resources.qrc)

add_executable(AetherSDR ${ALL_SOURCES} ${RNNOISE_SOURCES} ${GGMORSE_SOURCES} ${RADE_SOURCES} ${BNR_SOURCES} ${SPECBLEACH_SOURCES} ${RESOURCES} ${DFNR_RESOURCES})

if (USE_SYSTEM_ZLIB)
    target_link_libraries(AetherSDR PRIVATE PkgConfig::zlib)
else()
    target_link_libraries(AetherSDR PRIVATE zlibstatic) # bundled third_party/zlib 1.3.1
endif()

if (USE_SYSTEM_MSPACK)
    target_link_libraries(AetherSDR PRIVATE PkgConfig::libmspack)
else()
    target_link_libraries(AetherSDR PRIVATE mspack_static)
endif()

# Link the system libmosquitto target only when MQTT is enabled — the
# pkg_check_modules() call that defines PkgConfig::libmosquitto lives
# inside the ENABLE_MQTT block above, so without this guard the combo
# (ENABLE_MQTT=OFF + USE_SYSTEM_LIBMOSQUITTO=ON) would configure cleanly
# then fail at link time with a missing target.
if (ENABLE_MQTT AND USE_SYSTEM_LIBMOSQUITTO)
    target_link_libraries(AetherSDR PRIVATE PkgConfig::libmosquitto)
endif()

# Optionally change binary name to lower case on Linux
if (LINUX AND LOWER_CASE_BINARY_NAME)
    set_target_properties(AetherSDR PROPERTIES OUTPUT_NAME aethersdr)

    set(AETHERSDR_OUTPUT_BINARY_NAME aethersdr)
else()
    set(AETHERSDR_OUTPUT_BINARY_NAME AetherSDR)
endif()
configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/packaging/linux/AetherSDR.desktop.in
    ${CMAKE_CURRENT_BINARY_DIR}/packaging/linux/AetherSDR.desktop
    @ONLY
)

# Capture the git short SHA at configure time and surface it in the About
# dialog so dev/test builds are identifiable.  Falls back to "unknown" for
# source-tarball builds where .git is absent.
find_package(Git QUIET)
set(AETHER_GIT_SHA "unknown")
if(Git_FOUND)
    execute_process(
        COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        OUTPUT_VARIABLE _aether_git_sha_out
        OUTPUT_STRIP_TRAILING_WHITESPACE
        ERROR_QUIET
        RESULT_VARIABLE _aether_git_sha_rv
    )
    if(_aether_git_sha_rv EQUAL 0 AND NOT "${_aether_git_sha_out}" STREQUAL "")
        set(AETHER_GIT_SHA "${_aether_git_sha_out}")
    endif()
endif()
target_compile_definitions(AetherSDR PRIVATE AETHER_GIT_SHA="${AETHER_GIT_SHA}")
if(Git_FOUND AND _aether_git_sha_rv EQUAL 0 AND NOT "${_aether_git_sha_out}" STREQUAL "")
    message(STATUS "Build SHA: ${AETHER_GIT_SHA}")
else()
    message(STATUS "Build SHA: ${AETHER_GIT_SHA} (git unavailable or not a git checkout)")
endif()

# Reproducible-builds.org SOURCE_DATE_EPOCH support.  When a distro
# packager (Debian, Arch, NixOS, openSUSE, Fedora, etc.) sets
#   export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)
# before invoking cmake, GCC 7.2+ and Clang automatically substitute
# __DATE__ and __TIME__ with that fixed timestamp.  Many archive tools
# (ar, tar, zip, objcopy) honour the same variable, so the built binary
# becomes byte-reproducible across hosts.
#
# No source-tree changes needed — this block just surfaces a configure-
# time status line so packagers can confirm at a glance that their
# env-var actually reached the build.  Originally proposed as
# https://github.com/aethersdr/AetherSDR/pull/3139 (closed in favour
# of this ecosystem-standard approach).
if(DEFINED ENV{SOURCE_DATE_EPOCH})
    message(STATUS "Reproducible build: honoring SOURCE_DATE_EPOCH=$ENV{SOURCE_DATE_EPOCH}")
endif()

# Windows: GUI app (no console window) + icon resource
if(WIN32)
    set_target_properties(AetherSDR PROPERTIES WIN32_EXECUTABLE TRUE)
endif()

# macOS app bundle
if(APPLE)
    set_target_properties(AetherSDR PROPERTIES
        MACOSX_BUNDLE TRUE
        MACOSX_BUNDLE_GUI_IDENTIFIER "com.aethersdr.AetherSDR"
        MACOSX_BUNDLE_BUNDLE_NAME "AetherSDR"
        MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
        MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
        MACOSX_BUNDLE_ICON_FILE "AetherSDR.icns"
        MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/packaging/macos/Info.plist.in"
    )
    target_link_libraries(AetherSDR PRIVATE "-framework AVFoundation" "-framework Accelerate")
    target_sources(AetherSDR PRIVATE src/core/MacNRFilter.cpp)

    # Generate AetherSDR.icns at build time from docs/assets/logo-circle.png so local
    # builds get an app icon without any extra setup (sips and iconutil are
    # part of macOS).
    set(ICON_SOURCE "${CMAKE_SOURCE_DIR}/docs/assets/logo-circle.png")
    set(ICNS_PATH "${CMAKE_BINARY_DIR}/AetherSDR.icns")
    set(ICONSET_PATH "${CMAKE_BINARY_DIR}/AetherSDR.iconset")
    add_custom_command(
        OUTPUT "${ICNS_PATH}"
        COMMAND ${CMAKE_COMMAND} -E make_directory "${ICONSET_PATH}"
        COMMAND sips -z 16  16  "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_16x16.png"
        COMMAND sips -z 32  32  "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_16x16@2x.png"
        COMMAND sips -z 32  32  "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_32x32.png"
        COMMAND sips -z 64  64  "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_32x32@2x.png"
        COMMAND sips -z 128 128 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_128x128.png"
        COMMAND sips -z 256 256 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_128x128@2x.png"
        COMMAND sips -z 256 256 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_256x256.png"
        COMMAND sips -z 512 512 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_256x256@2x.png"
        COMMAND sips -z 512 512 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_512x512.png"
        COMMAND sips -z 1024 1024 "${ICON_SOURCE}" --out "${ICONSET_PATH}/icon_512x512@2x.png"
        COMMAND iconutil -c icns "${ICONSET_PATH}" -o "${ICNS_PATH}"
        DEPENDS "${ICON_SOURCE}"
        COMMENT "Generating AetherSDR.icns"
    )
    target_sources(AetherSDR PRIVATE "${ICNS_PATH}")
    set_source_files_properties("${ICNS_PATH}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
elseif(WIN32)
    # Windows application icon (taskbar, Start Menu, Alt-Tab)
    set(WIN_RC "${CMAKE_SOURCE_DIR}/packaging/windows/AetherSDR.rc")
    if(EXISTS "${WIN_RC}")
        target_sources(AetherSDR PRIVATE "${WIN_RC}")
    endif()
    if(MSVC)
        # /MANIFESTINPUT requires /MANIFEST:EMBED per MSVC docs.  Newer MSVC
        # toolsets (VS 18.x / 14.50+) enforce this strictly with LNK1220;
        # older ones were lenient.  Be explicit.
        target_link_options(AetherSDR PRIVATE
            "/MANIFEST:EMBED"
            "/MANIFESTINPUT:${CMAKE_SOURCE_DIR}/packaging/windows/AetherSDR.exe.manifest")
    endif()
endif()

target_include_directories(AetherSDR PRIVATE
    src/
    ${RNNOISE_DIR}/include
    ${RNNOISE_DIR}/src
    ${GGMORSE_DIR}/include
    ${CMAKE_SOURCE_DIR}/third_party/r8brain
)

target_link_libraries(AetherSDR PRIVATE
    Qt6::Core
    Qt6::Gui
    Qt6::Widgets
    Qt6::Network
    Qt6::Multimedia
    aether_libmodem_core
    # bundled third_party/liquid-dsp (#3043) — non-MSVC only; see CMakeLists
    # block above for the liquid-dsp/MSVC C99 _Complex incompatibility.
    $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:liquid-static>
    ${CMAKE_DL_LIBS}   # dlopen/dlsym for tolerant X11 error handler (#1839)
)

if(Qt6SerialPort_FOUND)
    target_compile_definitions(AetherSDR PRIVATE HAVE_SERIALPORT)
    target_link_libraries(AetherSDR PRIVATE Qt6::SerialPort)
endif()

if(Qt6WebSockets_FOUND)
    target_compile_definitions(AetherSDR PRIVATE HAVE_WEBSOCKETS)
    target_sources(AetherSDR PRIVATE src/core/FreeDvClient.cpp)
    target_link_libraries(AetherSDR PRIVATE Qt6::WebSockets)
endif()

if(Qt6Keychain_FOUND)
    target_compile_definitions(AetherSDR PRIVATE HAVE_KEYCHAIN)
    target_link_libraries(AetherSDR PRIVATE Qt6Keychain::Qt6Keychain)
endif()

if(Qt6DBus_FOUND)
    target_compile_definitions(AetherSDR PRIVATE HAVE_DBUS)
    target_link_libraries(AetherSDR PRIVATE Qt6::DBus)
endif()

if(AETHER_GPU_SPECTRUM)
    target_compile_definitions(AetherSDR PRIVATE AETHER_GPU_SPECTRUM)

    # Apply the Debian include paths now that AetherSDR exists
    if(DEBIAN_GPU_FIX_REQUIRED)
        message(STATUS "Applying Debian GPU include paths to AetherSDR")
        target_include_directories(AetherSDR PRIVATE
            "${DEBIAN_PRIVATE_INC}/QtGui/${Qt6_VERSION}"
            "${DEBIAN_PRIVATE_INC}/QtGui/${Qt6_VERSION}/QtGui"
        )
    endif()

    if(TARGET Qt6::GuiPrivate)
        target_link_libraries(AetherSDR PRIVATE Qt6::GuiPrivate)
    endif()

    qt_add_shaders(AetherSDR "aether_shaders"
        PREFIX "/shaders"
        FILES
            resources/shaders/texturedquad.vert
            resources/shaders/texturedquad.frag
            resources/shaders/overlay.vert
            resources/shaders/overlay.frag
            resources/shaders/spectrum.vert
            resources/shaders/spectrum.frag
    )
endif()

# Bundled libmspack (LGPL-2.1) — CAB+LZX decompression for the v4.2+ MSI
# firmware-installer extraction path. See third_party/libmspack/README.md.
if (USE_SYSTEM_MSPACK)
    find_package(PkgConfig REQUIRED)
    if(PkgConfig_FOUND)
        pkg_check_modules(libmspack REQUIRED IMPORTED_TARGET libmspack)
    endif()
else()
    add_subdirectory(third_party/libmspack)
endif()

# Bundled RtMidi (MIT license) — MIDI controller support on all platforms
message(STATUS "MIDI controller support enabled (RtMidi)")
target_compile_definitions(AetherSDR PRIVATE HAVE_MIDI)
target_sources(AetherSDR PRIVATE
    src/core/MidiControlManager.cpp
    src/core/MidiSettings.cpp
    src/gui/MidiMappingDialog.cpp)
if (USE_SYSTEM_RTMIDI)
    find_package(PkgConfig REQUIRED)
    if(PkgConfig_FOUND)
        pkg_check_modules(rtmidi REQUIRED IMPORTED_TARGET rtmidi)
    endif()
    target_link_libraries(AetherSDR PRIVATE PkgConfig::rtmidi)
else()
    target_sources(AetherSDR PRIVATE third_party/rtmidi/RtMidi.cpp)
    target_include_directories(AetherSDR PRIVATE third_party/rtmidi)
endif()

if(APPLE)
    target_compile_definitions(AetherSDR PRIVATE __MACOSX_CORE__)
    target_link_libraries(AetherSDR PRIVATE "-framework CoreMIDI" "-framework CoreAudio" "-framework CoreFoundation" "-framework IOKit")
elseif(WIN32)
    target_compile_definitions(AetherSDR PRIVATE __WINDOWS_MM__)
    target_link_libraries(AetherSDR PRIVATE winmm)
else()
    target_compile_definitions(AetherSDR PRIVATE __LINUX_ALSA__)
    find_package(PkgConfig REQUIRED)
    if(PkgConfig_FOUND)
        pkg_check_modules(alsa REQUIRED IMPORTED_TARGET alsa)
    endif()
    target_link_libraries(AetherSDR PRIVATE PkgConfig::alsa)
endif()

# hidapi — USB HID encoder support (Stream Deck, Icom RC-28, Griffin PowerMate, Contour Shuttle)
# Windows: run scripts/setup/setup-hidapi.ps1 first to download and build hidapi
# Linux:   apt install libhidapi-dev
# macOS:   brew install hidapi
if(WIN32)
    set(HIDAPI_ROOT "${CMAKE_SOURCE_DIR}/third_party/hidapi")
    if(EXISTS "${HIDAPI_ROOT}/include/hidapi/hidapi.h")
        set(HIDAPI_FOUND TRUE)
        set(HIDAPI_INCLUDE_DIRS "${HIDAPI_ROOT}/include")
        set(HIDAPI_LIBRARIES "${HIDAPI_ROOT}/lib/hidapi.lib")
        set(HIDAPI_DLL "${HIDAPI_ROOT}/bin/hidapi.dll")
    else()
        message(WARNING "hidapi not found. Run scripts/setup/setup-hidapi.ps1 to download it. "
                        "USB HID device support (Stream Deck, etc.) will be disabled.")
    endif()
else()
    if(PkgConfig_FOUND)
        pkg_check_modules(HIDAPI hidapi-hidraw)
        if(NOT HIDAPI_FOUND)
            pkg_check_modules(HIDAPI hidapi-libusb)
        endif()
        if(NOT HIDAPI_FOUND)
            pkg_check_modules(HIDAPI hidapi)
        endif()
    endif()
endif()
# Ulanzi Dial backend — one per platform.  All three implementations
# expose the same Qt signal contract (see UlanziDialBackend.h); the
# mapper dialog and MainWindow dispatcher are unchanged across
# platforms.  See #3232 for the design.
target_sources(AetherSDR PRIVATE
    src/core/UlanziDialBackend.h
    src/gui/UlanziDialMapperDialog.cpp
    src/gui/UlanziDialMapperDialog.h)
if(UNIX AND NOT APPLE)
    target_sources(AetherSDR PRIVATE
        src/core/EvdevEncoderManager.cpp
        src/core/EvdevEncoderManager.h)
endif()
if(WIN32)
    target_sources(AetherSDR PRIVATE
        src/core/UlanziDialWindowsManager.cpp
        src/core/UlanziDialWindowsManager.h)
endif()
if(APPLE)
    target_sources(AetherSDR PRIVATE
        src/core/UlanziDialMacOSManager.cpp
        src/core/UlanziDialMacOSManager.h)
    target_link_libraries(AetherSDR PRIVATE
        "-framework IOKit"
        "-framework CoreFoundation")
endif()

if(HIDAPI_FOUND)
    message(STATUS "hidapi found — USB HID encoder support enabled")
    target_compile_definitions(AetherSDR PRIVATE HAVE_HIDAPI)
    target_sources(AetherSDR PRIVATE
        src/core/HidEncoderManager.cpp
        src/core/HidEncoderManager.h
        src/core/HidDeviceParser.cpp
        src/core/HidDeviceParser.h)
    set(HIDAPI_NORMALIZED_INCLUDE_DIRS ${HIDAPI_INCLUDE_DIRS})
    foreach(hidapi_dir IN LISTS HIDAPI_INCLUDE_DIRS)
        if(EXISTS "${hidapi_dir}/hidapi.h")
            get_filename_component(hidapi_leaf "${hidapi_dir}" NAME)
            if(hidapi_leaf STREQUAL "hidapi")
                get_filename_component(hidapi_parent "${hidapi_dir}" DIRECTORY)
                list(APPEND HIDAPI_NORMALIZED_INCLUDE_DIRS "${hidapi_parent}")
            endif()
        endif()
    endforeach()
    list(REMOVE_DUPLICATES HIDAPI_NORMALIZED_INCLUDE_DIRS)
    target_include_directories(AetherSDR PRIVATE ${HIDAPI_NORMALIZED_INCLUDE_DIRS})
    target_link_libraries(AetherSDR PRIVATE ${HIDAPI_LIBRARIES})
    # Copy DLL to build dir on Windows
    if(WIN32 AND HIDAPI_DLL)
        add_custom_command(TARGET AetherSDR POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${HIDAPI_DLL}" "$<TARGET_FILE_DIR:AetherSDR>"
            COMMENT "Copying hidapi.dll to build directory"
        )
    endif()
else()
    message(STATUS "hidapi not found — USB HID encoder support disabled")
endif()

# ONNX Runtime — optional CNN signal classifier for S-History v2
# Windows: run scripts/setup/setup-onnxruntime.ps1 first to download prebuilt DLLs
# Linux:   apt install libonnxruntime-dev  (or build from source)
# macOS:   brew install onnxruntime
if(WIN32)
    set(ORT_ROOT "${CMAKE_SOURCE_DIR}/third_party/onnxruntime")
    if(EXISTS "${ORT_ROOT}/include/onnxruntime_cxx_api.h")
        set(ORT_FOUND TRUE)
        set(ORT_INCLUDE_DIRS "${ORT_ROOT}/include")
        set(ORT_LIBRARIES "${ORT_ROOT}/lib/onnxruntime.lib")
        file(GLOB ORT_DLLS "${ORT_ROOT}/bin/*.dll")
    else()
        message(STATUS "ONNX Runtime not found — CNN signal classifier disabled. "
                       "Run scripts/setup/setup-onnxruntime.ps1 to enable.")
    endif()
else()
    if(PkgConfig_FOUND)
        pkg_check_modules(ORT libonnxruntime)
    endif()
    if(NOT ORT_FOUND)
        find_library(ORT_LIB onnxruntime)
        find_path(ORT_INC onnxruntime_cxx_api.h)
        if(ORT_LIB AND ORT_INC)
            set(ORT_FOUND TRUE)
            set(ORT_LIBRARIES ${ORT_LIB})
            set(ORT_INCLUDE_DIRS ${ORT_INC})
        endif()
    endif()
endif()
if(ORT_FOUND)
    message(STATUS "ONNX Runtime found — CNN signal classifier enabled")
else()
    message(STATUS "ONNX Runtime not found — CNN signal classifier disabled")
endif()

if(PORTAUDIO_FOUND)
    target_sources(AetherSDR PRIVATE src/core/CwSidetonePortAudioSink.cpp)
    target_compile_definitions(AetherSDR PRIVATE HAVE_PORTAUDIO)
    target_include_directories(AetherSDR PRIVATE ${PORTAUDIO_INCLUDE_DIRS})
    target_link_directories(AetherSDR PRIVATE ${PORTAUDIO_LIBRARY_DIRS})
    target_link_libraries(AetherSDR PRIVATE ${PORTAUDIO_LIBRARIES})
endif()

if(FFTW3_FOUND)
    target_compile_definitions(AetherSDR PRIVATE HAVE_FFTW3
        # On Windows lld-link (used by ClangCL) requires explicit dllimport
        # declarations; define FFTW_DLL so fftw3.h emits __declspec(dllimport)
        # and the linker can resolve __imp_fftwf_* symbols from the import lib.
        $<$<BOOL:${WIN32}>:FFTW_DLL>)
    target_include_directories(AetherSDR PRIVATE ${FFTW3_INCLUDE_DIRS})
    target_link_directories(AetherSDR PRIVATE ${FFTW3_LIBRARY_DIRS})
    target_link_libraries(AetherSDR PRIVATE ${FFTW3_LIBRARIES})
    # Copy DLL to build dir on Windows
    if(WIN32 AND FFTW3_DLL)
        add_custom_command(TARGET AetherSDR POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${FFTW3_DLL}" "$<TARGET_FILE_DIR:AetherSDR>"
            COMMENT "Copying libfftw3-3.dll to build directory"
        )
    endif()
endif()

if(ORT_FOUND)
    target_compile_definitions(AetherSDR PRIVATE HAVE_ONNX)
    target_include_directories(AetherSDR PRIVATE ${ORT_INCLUDE_DIRS})
    target_link_libraries(AetherSDR PRIVATE ${ORT_LIBRARIES})
    if(WIN32 AND ORT_DLLS)
        foreach(_ort_dll IN LISTS ORT_DLLS)
            add_custom_command(TARGET AetherSDR POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    "${_ort_dll}" "$<TARGET_FILE_DIR:AetherSDR>"
                COMMENT "Copying ONNX Runtime DLL to build directory")
        endforeach()
    endif()
endif()

if(RADE_FOUND)
    target_compile_definitions(AetherSDR PRIVATE HAVE_RADE HAVE_OPUS IS_BUILDING_RADE_API=1)
    if(RADE_WAV_TAP)
        set(RADE_TAP_DIR "${CMAKE_BINARY_DIR}/rade_taps"
            CACHE PATH "Output directory for RADE WAV tap files")
        target_compile_definitions(AetherSDR PRIVATE RADE_WAV_TAP
            RADE_TAP_DIR="${RADE_TAP_DIR}")
    endif()
    target_include_directories(AetherSDR PRIVATE ${RADE_DIR}/src)
    if(WIN32)
        target_link_libraries(AetherSDR PRIVATE opus)
    else()
        target_link_libraries(AetherSDR PRIVATE opus m)
    endif()
elseif(OPUS_FOUND)
    target_compile_definitions(AetherSDR PRIVATE HAVE_OPUS)
    target_include_directories(AetherSDR PRIVATE ${OPUS_INCLUDE_DIRS})
    target_link_libraries(AetherSDR PRIVATE ${OPUS_LIBRARIES})
endif()

if(ENABLE_BNR)
    target_compile_definitions(AetherSDR PRIVATE HAVE_BNR)
    target_include_directories(AetherSDR PRIVATE
        ${BNR_GEN_DIR}
        ${GRPC_INCLUDE_DIRS}
        ${PROTOBUF_INCLUDE_DIRS})
    target_link_libraries(AetherSDR PRIVATE ${GRPC_LIBRARIES} ${PROTOBUF_LIBRARIES})
endif()

if(ENABLE_SPECBLEACH)
    target_compile_definitions(AetherSDR PRIVATE HAVE_SPECBLEACH)
    target_include_directories(AetherSDR PRIVATE
        ${CMAKE_SOURCE_DIR}/third_party/libspecbleach/include
        ${CMAKE_SOURCE_DIR}/third_party/libspecbleach/src)
    if(MSVC AND SPECBLEACH_STATIC_LIB)
        # Link pre-built clang-cl static lib
        add_dependencies(AetherSDR specbleach_build)
        target_link_libraries(AetherSDR PRIVATE ${SPECBLEACH_STATIC_LIB})
        # fftw3f (float precision) for Windows
        set(FFTW3F_LIB "${CMAKE_SOURCE_DIR}/third_party/fftw3/lib/fftw3f.lib")
        if(EXISTS ${FFTW3F_LIB})
            target_link_libraries(AetherSDR PRIVATE ${FFTW3F_LIB})
            set(FFTW3F_DLL "${CMAKE_SOURCE_DIR}/third_party/fftw3/bin/libfftw3f-3.dll")
            add_custom_command(TARGET AetherSDR POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    "${FFTW3F_DLL}" "$<TARGET_FILE_DIR:AetherSDR>"
                COMMENT "Copying libfftw3f-3.dll to build directory")
        else()
            message(WARNING "fftw3f.lib not found — run scripts/setup/setup-fftw.ps1 and gen-fftw3f-lib.bat")
        endif()
    else()
        # libspecbleach C sources include <fftw3.h> directly — ensure it's findable
        if(FFTW3_INCLUDE_DIRS)
            target_include_directories(AetherSDR PRIVATE ${FFTW3_INCLUDE_DIRS})
        else()
            find_path(FFTW3_H_DIR fftw3.h HINTS ${CMAKE_SOURCE_DIR}/third_party/fftw3/include)
            if(FFTW3_H_DIR)
                target_include_directories(AetherSDR PRIVATE ${FFTW3_H_DIR})
            endif()
        endif()
        # libspecbleach uses fftwf (float precision FFTW3)
        find_library(FFTW3F_LIB fftw3f HINTS /opt/homebrew/lib /usr/local/lib ${CMAKE_SOURCE_DIR}/third_party/fftw3/lib)
        if(FFTW3F_LIB)
            target_link_libraries(AetherSDR PRIVATE ${FFTW3F_LIB})
        else()
            message(WARNING "fftw3f not found — NR4 will fail to link")
        endif()
        # Suppress warnings from third-party C code
        set_source_files_properties(${SPECBLEACH_SOURCES} PROPERTIES COMPILE_FLAGS "-w")
    endif()
endif()

if(ENABLE_DFNR)
    target_compile_definitions(AetherSDR PRIVATE HAVE_DFNR)
    target_include_directories(AetherSDR PRIVATE ${DEEPFILTER_DIR}/include)
    target_link_libraries(AetherSDR PRIVATE ${DFNR_LIB})
    if(WIN32)
        target_link_libraries(AetherSDR PRIVATE ws2_32 bcrypt userenv ntdll)
    elseif(APPLE)
        # Rust runtime deps on macOS
        target_link_libraries(AetherSDR PRIVATE "-framework Security" "-framework CoreFoundation")
    else()
        # Rust runtime deps on Linux
        target_link_libraries(AetherSDR PRIVATE pthread dl m)
    endif()
    # Copy DLL and model to build directory
    if(DFNR_DLL AND EXISTS ${DFNR_DLL})
        add_custom_command(TARGET AetherSDR POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${DFNR_DLL}" "$<TARGET_FILE_DIR:AetherSDR>"
            COMMENT "Copying deepfilter.dll to build directory")
    endif()
    if(EXISTS "${DFNR_MODEL}" AND NOT AETHER_EMBED_DFNR_MODEL)
        if(APPLE)
            # macOS: place in Resources/ so notarization doesn't reject a
            # non-Mach-O file inside Contents/MacOS/
            add_custom_command(TARGET AetherSDR POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E make_directory
                    "$<TARGET_BUNDLE_DIR:AetherSDR>/Contents/Resources"
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    "${DFNR_MODEL}" "$<TARGET_BUNDLE_DIR:AetherSDR>/Contents/Resources/"
                COMMENT "Copying DeepFilterNet3 model to app bundle Resources")
        else()
            add_custom_command(TARGET AetherSDR POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    "${DFNR_MODEL}" "$<TARGET_FILE_DIR:AetherSDR>"
                COMMENT "Copying DeepFilterNet3 model to build directory")
        endif()
    endif()
    # Install the model alongside the binary for cmake --install
    if(NOT APPLE AND NOT AETHER_EMBED_DFNR_MODEL)
        install(FILES "${DFNR_MODEL}"
            DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/AetherSDR"
            OPTIONAL)
    endif()
endif()

if(ENABLE_MQTT)
    # HAVE_MQTT gates the project's MQTT client code (MqttApplet,
    # RadioModel plumbing) — must be set regardless of whether
    # libmosquitto is the bundled or the system copy.
    target_compile_definitions(AetherSDR PRIVATE HAVE_MQTT)
    if (USE_SYSTEM_LIBMOSQUITTO)
        # System libmosquitto is conventionally built with TLS support on
        # distro packages — assume HAVE_MQTT_TLS unless the packager
        # explicitly opted out via -DMQTT_TLS=OFF.
        if(MQTT_TLS)
            target_compile_definitions(AetherSDR PRIVATE HAVE_MQTT_TLS)
        endif()
    else()
        target_include_directories(AetherSDR PRIVATE
            ${MOSQUITTO_DIR}/include
            ${MOSQUITTO_DIR}/src)
        target_sources(AetherSDR PRIVATE ${MOSQUITTO_SOURCES})
        if(OpenSSL_FOUND)
            set(MQTT_TLS_FLAG " -DWITH_TLS")
            target_compile_definitions(AetherSDR PRIVATE HAVE_MQTT_TLS)
            target_link_libraries(AetherSDR PRIVATE OpenSSL::SSL OpenSSL::Crypto)
        else()
            set(MQTT_TLS_FLAG "")
        endif()
        if(WIN32)
            set_source_files_properties(${MOSQUITTO_SOURCES} PROPERTIES
                COMPILE_FLAGS "-w -DLIBMOSQUITTO_STATIC -DLIBMOSQCOMMON_STATIC${MQTT_TLS_FLAG}")
            target_compile_definitions(AetherSDR PRIVATE LIBMOSQUITTO_STATIC LIBMOSQCOMMON_STATIC)
        else()
            set_source_files_properties(${MOSQUITTO_SOURCES} PROPERTIES
                COMPILE_FLAGS "-w -DWITH_THREADING${MQTT_TLS_FLAG}")
        endif()
    endif()
    if(NOT WIN32)
        target_link_libraries(AetherSDR PRIVATE pthread)
    endif()
endif()

if(AETHER_GPU_SPECTRUM)
    target_compile_definitions(AetherSDR PRIVATE AETHER_GPU_SPECTRUM)
    if(TARGET Qt6::GuiPrivate)
        target_link_libraries(AetherSDR PRIVATE Qt6::GuiPrivate)
    else()
        # Qt6::GuiPrivate not available (macOS/skipped) — find rhi/ headers manually
        get_target_property(_qt_gui_inc Qt6::Gui INTERFACE_INCLUDE_DIRECTORIES)
        foreach(_dir IN LISTS _qt_gui_inc)
            if(EXISTS "${_dir}/rhi/qrhi.h")
                # Already in the include path via Qt6::Gui
                break()
            endif()
            # Check versioned private header directory (e.g. QtGui/6.7.3/QtGui/rhi/)
            file(GLOB _priv_dirs "${_dir}/${Qt6_VERSION}/QtGui")
            foreach(_pd IN LISTS _priv_dirs)
                if(EXISTS "${_pd}/rhi/qrhi.h")
                    target_include_directories(AetherSDR PRIVATE "${_pd}/..")
                    message(STATUS "QRhi headers found at ${_pd}/rhi/")
                    break()
                endif()
            endforeach()
        endforeach()
    endif()
    qt_add_shaders(AetherSDR "aether_shaders"
        PREFIX "/shaders"
        FILES
            resources/shaders/texturedquad.vert
            resources/shaders/texturedquad.frag
            resources/shaders/overlay.vert
            resources/shaders/overlay.frag
            resources/shaders/spectrum.vert
            resources/shaders/spectrum.frag
    )
endif()

if(HAVE_PIPEWIRE)
    target_compile_definitions(AetherSDR PRIVATE HAVE_PIPEWIRE)
    message(STATUS "Linux DAX bridge enabled (PulseAudio pipe modules)")
endif()

if(HAVE_PIPEWIRE_NATIVE)
    target_compile_definitions(AetherSDR PRIVATE HAVE_PIPEWIRE_NATIVE)
    target_include_directories(AetherSDR PRIVATE ${PIPEWIRE_NATIVE_INCLUDE_DIRS})
    target_link_libraries(AetherSDR PRIVATE ${PIPEWIRE_NATIVE_LIBRARIES})
    target_compile_options(AetherSDR PRIVATE ${PIPEWIRE_NATIVE_CFLAGS_OTHER})
    message(STATUS "Linux DAX RX uses native pw_stream (libpipewire-0.3 ${PIPEWIRE_NATIVE_VERSION})")
endif()

# Pass project version to code
target_compile_definitions(AetherSDR PRIVATE
    AETHERSDR_VERSION="${PROJECT_VERSION}"
)

# Compiler warnings
if(MSVC)
    target_compile_options(AetherSDR PRIVATE /W3 /Zc:__cplusplus /permissive- /utf-8 /bigobj)
else()
    target_compile_options(AetherSDR PRIVATE
        -Wall -Wextra -Wpedantic
        $<$<CONFIG:Debug>:-g3 -fsanitize=address>
        $<$<CONFIG:Debug>:-fno-omit-frame-pointer>
    )
    target_link_options(AetherSDR PRIVATE
        $<$<CONFIG:Debug>:-fsanitize=address>
    )
endif()


# ── Unit test harnesses ──────────────────────────────────────────────────────
# Standalone DSP smoke tests. Built alongside the main target so they share
# the same toolchain and warning flags. Run manually with ./build/<target>.

add_executable(client_eq_test
    tests/client_eq_test.cpp
    src/core/ClientEq.cpp
)
target_include_directories(client_eq_test PRIVATE src)

# Fractional-octave smoothing — exercises the static helper on
# ClientEqCurveWidget with no live widget required.
add_executable(client_eq_smoothing_test
    tests/client_eq_smoothing_test.cpp
    src/gui/ClientEqCurveWidget.cpp
    src/core/ClientEq.cpp
)
target_include_directories(client_eq_smoothing_test PRIVATE src)
target_link_libraries(client_eq_smoothing_test PRIVATE Qt6::Widgets)
set_target_properties(client_eq_smoothing_test PROPERTIES AUTOMOC ON)

add_executable(client_comp_test
    tests/client_comp_test.cpp
    src/core/ClientComp.cpp
    src/core/ClientPhaseRotator.cpp
)
target_include_directories(client_comp_test PRIVATE src)

add_executable(slice_label_test
    tests/slice_label_test.cpp
    src/gui/SliceLabel.cpp
    src/core/AppSettings.cpp
)
target_include_directories(slice_label_test PRIVATE src)
target_link_libraries(slice_label_test PRIVATE Qt6::Gui)
add_test(NAME slice_label_test COMMAND slice_label_test)

add_executable(slice_model_letter_test
    tests/slice_model_letter_test.cpp
    src/models/SliceModel.cpp
)
target_include_directories(slice_model_letter_test PRIVATE src)
target_link_libraries(slice_model_letter_test PRIVATE Qt6::Core Qt6::Test)
add_test(NAME slice_model_letter_test COMMAND slice_model_letter_test)

# ThemeManager — RFC #3076 Phase 1.  Verifies the built-in default-dark
# theme loads from Qt resources, scalar tokens resolve, missing tokens
# don't crash, and the stylesheet template resolver substitutes correctly.
qt_add_resources(THEME_TEST_RESOURCES resources/resources.qrc)
add_executable(theme_manager_test
    tests/theme_manager_test.cpp
    src/core/ThemeManager.cpp
    src/core/AppSettings.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    ${THEME_TEST_RESOURCES}
)
target_include_directories(theme_manager_test PRIVATE src)
target_link_libraries(theme_manager_test PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Test)
add_test(NAME theme_manager_test COMMAND theme_manager_test)

add_executable(panadapter_model_rx_antenna_test
    tests/panadapter_model_rx_antenna_test.cpp
    src/models/PanadapterModel.cpp
    src/core/PerfTelemetry.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(panadapter_model_rx_antenna_test PRIVATE src)
target_link_libraries(panadapter_model_rx_antenna_test PRIVATE Qt6::Core Qt6::Test)
add_test(NAME panadapter_model_rx_antenna_test COMMAND panadapter_model_rx_antenna_test)

add_executable(packet_loss_concealment_test
    tests/packet_loss_concealment_test.cpp
    src/core/PacketLossConcealment.cpp
)
target_include_directories(packet_loss_concealment_test PRIVATE src)
target_link_libraries(packet_loss_concealment_test PRIVATE Qt6::Core)
add_test(NAME packet_loss_concealment_test COMMAND packet_loss_concealment_test)

add_executable(model_capabilities_test
    tests/model_capabilities_test.cpp
    src/models/ModelCapabilities.cpp
)
target_include_directories(model_capabilities_test PRIVATE src)
target_link_libraries(model_capabilities_test PRIVATE Qt6::Core)
add_test(NAME model_capabilities_test COMMAND model_capabilities_test)

add_executable(client_quindar_test
    tests/client_quindar_test.cpp
    src/core/ClientQuindarTone.cpp
)
target_include_directories(client_quindar_test PRIVATE src)
target_link_libraries(client_quindar_test PRIVATE Qt6::Core)

add_executable(tx_mic_channel_normalizer_test
    tests/tx_mic_channel_normalizer_test.cpp
    src/core/TxMicChannelNormalizer.cpp
    src/core/Resampler.cpp
)
target_include_directories(tx_mic_channel_normalizer_test PRIVATE
    src
    ${CMAKE_SOURCE_DIR}/third_party/r8brain
)
target_link_libraries(tx_mic_channel_normalizer_test PRIVATE Qt6::Core)
add_test(NAME tx_mic_channel_normalizer_test COMMAND tx_mic_channel_normalizer_test)

add_executable(profile_transfer_test
    tests/profile_transfer_test.cpp
)
target_include_directories(profile_transfer_test PRIVATE src)
target_link_libraries(profile_transfer_test PRIVATE Qt6::Core)
add_test(NAME profile_transfer_test COMMAND profile_transfer_test)

add_executable(zip_archive_test
    tests/zip_archive_test.cpp
    src/core/ZipArchive.cpp
)
target_include_directories(zip_archive_test PRIVATE src)
target_link_libraries(zip_archive_test PRIVATE Qt6::Core)
if (USE_SYSTEM_ZLIB)
    target_link_libraries(zip_archive_test PRIVATE PkgConfig::zlib)
else()
    target_link_libraries(zip_archive_test PRIVATE zlibstatic)
endif()
add_test(NAME zip_archive_test COMMAND zip_archive_test)

# Pins the filter-before-merge invariant on the license-class-aware overload
# of BandPlanManager::contiguousRegionsForBand (PR #3050, closing #2649). (#3060)
add_executable(band_plan_license_filter_test
    tests/band_plan_license_filter_test.cpp
    src/models/BandPlanManager.cpp
    src/core/AppSettings.cpp
)
target_include_directories(band_plan_license_filter_test PRIVATE src)
target_link_libraries(band_plan_license_filter_test PRIVATE Qt6::Core Qt6::Gui)
add_test(NAME band_plan_license_filter_test COMMAND band_plan_license_filter_test)

add_executable(biquad_test
    tests/biquad_test.cpp
    src/core/Biquad.cpp
    src/core/StereoBiquad.cpp
)
target_include_directories(biquad_test PRIVATE src)
add_test(NAME biquad_test COMMAND biquad_test)

add_executable(spectral_nr_test
    tests/spectral_nr_test.cpp
    src/core/SpectralNR.cpp
)
target_include_directories(spectral_nr_test PRIVATE src)
target_link_libraries(spectral_nr_test PRIVATE Qt6::Core)
add_test(NAME spectral_nr_test COMMAND spectral_nr_test)

add_executable(client_gate_test
    tests/client_gate_test.cpp
    src/core/ClientGate.cpp
)
target_include_directories(client_gate_test PRIVATE src)

add_executable(client_deess_test
    tests/client_deess_test.cpp
    src/core/ClientDeEss.cpp
)
target_include_directories(client_deess_test PRIVATE src)

add_executable(client_tube_test
    tests/client_tube_test.cpp
    src/core/ClientTube.cpp
)
target_include_directories(client_tube_test PRIVATE src)

add_executable(client_pudu_test
    tests/client_pudu_test.cpp
    src/core/ClientPudu.cpp
)
target_include_directories(client_pudu_test PRIVATE src)

add_executable(client_reverb_test
    tests/client_reverb_test.cpp
    src/core/ClientReverb.cpp
)
target_include_directories(client_reverb_test PRIVATE src)

add_executable(iambic_keyer_test
    tests/iambic_keyer_test.cpp
    src/core/IambicKeyer.cpp
)
target_include_directories(iambic_keyer_test PRIVATE src)
if(UNIX)
    target_link_libraries(iambic_keyer_test PRIVATE pthread)
endif()

add_executable(passive_spots_policy_test
    tests/passive_spots_policy_test.cpp
    src/core/AppSettings.cpp
    src/core/SpotCommandPolicy.cpp
)
target_include_directories(passive_spots_policy_test PRIVATE src)
target_link_libraries(passive_spots_policy_test PRIVATE Qt6::Core)

add_executable(spot_mode_resolver_test
    tests/spot_mode_resolver_test.cpp
    src/core/SpotModeResolver.cpp
)
target_include_directories(spot_mode_resolver_test PRIVATE src)
target_link_libraries(spot_mode_resolver_test PRIVATE Qt6::Core)
add_test(NAME spot_mode_resolver_test COMMAND spot_mode_resolver_test)

add_executable(navtex_model_test
    tests/navtex_model_test.cpp
    src/models/NavtexModel.cpp
)
target_include_directories(navtex_model_test PRIVATE src)
target_link_libraries(navtex_model_test PRIVATE Qt6::Core Qt6::Test)

add_executable(flex_waveform_model_test
    tests/flex_waveform_model_test.cpp
    src/models/FlexWaveformModel.cpp
)
target_include_directories(flex_waveform_model_test PRIVATE src)
target_link_libraries(flex_waveform_model_test PRIVATE Qt6::Core Qt6::Test)

add_executable(ole_compound_file_test
    tests/ole_compound_file_test.cpp
    src/core/OleCompoundFile.cpp
    src/core/CabExtractor.cpp
    src/core/AsyncLogWriter.cpp
    src/core/LogManager.cpp
    src/core/AppSettings.cpp
)
target_include_directories(ole_compound_file_test PRIVATE src)
if (USE_SYSTEM_MSPACK)
    target_link_libraries(ole_compound_file_test PRIVATE Qt6::Core PkgConfig::libmspack)
else()
    target_link_libraries(ole_compound_file_test PRIVATE Qt6::Core mspack_static)
endif()
if(UNIX)
    target_link_libraries(ole_compound_file_test PRIVATE pthread)
endif()

add_executable(xvtr_policy_test
    tests/xvtr_policy_test.cpp
    src/models/XvtrPolicy.cpp
)
target_include_directories(xvtr_policy_test PRIVATE src)
target_link_libraries(xvtr_policy_test PRIVATE Qt6::Core)

add_executable(frequency_entry_parser_test
    tests/frequency_entry_parser_test.cpp
    src/gui/FrequencyEntryParser.cpp
)
target_include_directories(frequency_entry_parser_test PRIVATE src)
target_link_libraries(frequency_entry_parser_test PRIVATE Qt6::Core)
add_test(NAME frequency_entry_parser_test COMMAND frequency_entry_parser_test)

add_executable(radio_status_ownership_test
    tests/radio_status_ownership_test.cpp
    src/core/CommandParser.cpp
)
target_include_directories(radio_status_ownership_test PRIVATE src)
target_link_libraries(radio_status_ownership_test PRIVATE Qt6::Core)
enable_testing()
add_test(NAME radio_status_ownership_test COMMAND radio_status_ownership_test)

add_executable(shortcut_manager_test
    tests/shortcut_manager_test.cpp
    src/core/ShortcutManager.cpp
    src/core/AppSettings.cpp
)
target_include_directories(shortcut_manager_test PRIVATE src)
target_link_libraries(shortcut_manager_test PRIVATE Qt6::Core Qt6::Widgets)
add_test(NAME shortcut_manager_test COMMAND shortcut_manager_test)

add_executable(antenna_alias_test
    tests/antenna_alias_test.cpp
    src/models/AntennaAliasStore.cpp
    src/models/SliceModel.cpp
    src/core/AppSettings.cpp
)
target_include_directories(antenna_alias_test PRIVATE src)
target_link_libraries(antenna_alias_test PRIVATE Qt6::Core)
set_target_properties(antenna_alias_test PROPERTIES AUTOMOC ON)
add_test(NAME antenna_alias_test COMMAND antenna_alias_test)

add_executable(mqtt_antenna_alias_test
    tests/mqtt_antenna_alias_test.cpp
    src/core/MqttAntennaAlias.cpp
)
target_include_directories(mqtt_antenna_alias_test PRIVATE src)
target_link_libraries(mqtt_antenna_alias_test PRIVATE Qt6::Core)
add_test(NAME mqtt_antenna_alias_test COMMAND mqtt_antenna_alias_test)

add_executable(mqtt_settings_test
    tests/mqtt_settings_test.cpp
    src/core/MqttSettings.cpp
    src/core/MqttAntennaAlias.cpp
    src/core/AppSettings.cpp
)
target_include_directories(mqtt_settings_test PRIVATE src)
target_link_libraries(mqtt_settings_test PRIVATE Qt6::Core)
add_test(NAME mqtt_settings_test COMMAND mqtt_settings_test)

# Pure-math test for the ATU pre-tune center-frequency calculator.
# Locks in the IARU R1 reference table from issue #2624 so future edits
# to computeCenters() can't silently regress the per-band point counts.
add_executable(atu_pretune_centers_test
    tests/atu_pretune_centers_test.cpp
)
target_include_directories(atu_pretune_centers_test PRIVATE src)
target_link_libraries(atu_pretune_centers_test PRIVATE Qt6::Core)
add_test(NAME atu_pretune_centers_test COMMAND atu_pretune_centers_test)

add_executable(cw_sidetone_test
    tests/cw_sidetone_test.cpp
    src/core/CwSidetoneGenerator.cpp
)
target_include_directories(cw_sidetone_test PRIVATE src)
target_link_libraries(cw_sidetone_test PRIVATE Qt6::Core)

add_executable(cwx_local_keyer_drift_test
    tests/cwx_local_keyer_drift_test.cpp
    src/core/CwxLocalKeyer.cpp
    src/core/CwxLocalKeyer.h
)
target_include_directories(cwx_local_keyer_drift_test PRIVATE src)
target_link_libraries(cwx_local_keyer_drift_test PRIVATE Qt6::Core)
add_test(NAME cwx_local_keyer_drift_test COMMAND cwx_local_keyer_drift_test)

add_executable(ax25_frame_formatter_test
    tests/ax25_frame_formatter_test.cpp
    src/core/tnc/Ax25FrameFormatter.cpp
)
target_include_directories(ax25_frame_formatter_test PRIVATE src)
target_link_libraries(ax25_frame_formatter_test PRIVATE Qt6::Core)
add_test(NAME ax25_frame_formatter_test COMMAND ax25_frame_formatter_test)

add_executable(ax25_libmodem_shim_test
    tests/ax25_libmodem_shim_test.cpp
    src/core/tnc/AetherAx25LibmodemShim.cpp
    src/core/tnc/Ax25FrameFormatter.cpp
    # LogManager.cpp provides lcAx25 (the shim's qCDebug category, #2763);
    # LogManager.cpp depends on AsyncLogWriter via the m_writer member, so
    # AppSettings + AsyncLogWriter come along to satisfy the constructor /
    # destructor chain at link time.
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(ax25_libmodem_shim_test PRIVATE src)
target_link_libraries(ax25_libmodem_shim_test PRIVATE Qt6::Core aether_libmodem_core)
add_test(NAME ax25_libmodem_shim_test COMMAND ax25_libmodem_shim_test)

add_executable(cwx_panel_test
    tests/cwx_panel_test.cpp
    src/gui/CwxPanel.cpp
    src/gui/CwxPanel.h
    src/models/CwxModel.cpp
    src/models/CwxModel.h
    # CwxPanel.cpp calls ThemeManager::resolve() post-Phase-2 migration;
    # pull in the manager + its logging deps so the test links.
    src/core/ThemeManager.cpp
    src/core/AppSettings.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(cwx_panel_test PRIVATE src)
target_link_libraries(cwx_panel_test PRIVATE
    Qt6::Core Qt6::Widgets
)

add_executable(meter_model_test
    tests/meter_model_test.cpp
    src/models/MeterModel.cpp
    src/core/AsyncLogWriter.cpp
    src/core/LogManager.cpp
    src/core/AppSettings.cpp
)
target_include_directories(meter_model_test PRIVATE src)
target_link_libraries(meter_model_test PRIVATE Qt6::Core)
if(UNIX)
    target_link_libraries(meter_model_test PRIVATE pthread)
endif()
set_target_properties(meter_model_test PROPERTIES AUTOMOC ON)

add_executable(async_log_writer_test
    tests/async_log_writer_test.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(async_log_writer_test PRIVATE src)
target_link_libraries(async_log_writer_test PRIVATE Qt6::Core)
if(UNIX)
    target_link_libraries(async_log_writer_test PRIVATE pthread)
endif()
set_target_properties(async_log_writer_test PROPERTIES AUTOMOC ON)

add_executable(perf_telemetry_test
    tests/perf_telemetry_test.cpp
    src/core/PerfTelemetry.cpp
    # LogManager.cpp owns the lcPerf Q_LOGGING_CATEGORY definition (per
    # Principle III consolidation, #2770); LogManager has AsyncLogWriter
    # as a member which transitively needs AppSettings, so all three .cpp
    # files come along to satisfy the ctor/dtor chain at link time.
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
    src/core/AppSettings.cpp
)
target_include_directories(perf_telemetry_test PRIVATE src)
target_link_libraries(perf_telemetry_test PRIVATE Qt6::Core)
if(UNIX)
    target_link_libraries(perf_telemetry_test PRIVATE pthread)
endif()
set_target_properties(perf_telemetry_test PROPERTIES AUTOMOC ON)
add_test(NAME perf_telemetry_test COMMAND perf_telemetry_test)

add_executable(memory_recall_policy_test
    tests/memory_recall_policy_test.cpp
    src/core/MemoryRecallPolicy.cpp
)
target_include_directories(memory_recall_policy_test PRIVATE src)
target_link_libraries(memory_recall_policy_test PRIVATE Qt6::Core)

add_executable(transmit_model_apd_test
    tests/transmit_model_apd_test.cpp
    src/models/TransmitModel.cpp
    src/core/ClientQuindarTone.cpp
    src/core/AsyncLogWriter.cpp
    src/core/LogManager.cpp
    src/core/AppSettings.cpp
)
target_include_directories(transmit_model_apd_test PRIVATE src)
target_link_libraries(transmit_model_apd_test PRIVATE Qt6::Core Qt6::Test)
if(UNIX)
    target_link_libraries(transmit_model_apd_test PRIVATE pthread)
endif()
set_target_properties(transmit_model_apd_test PROPERTIES AUTOMOC ON)

# Help guide search tests - needs QApplication + Widgets.
add_executable(help_dialog_test
    tests/help_dialog_test.cpp
    src/gui/HelpDialog.cpp
    # HelpDialog.cpp calls ThemeManager::resolve() post-Phase-2 migration;
    # pull in the manager + its logging deps so the test links.
    src/core/ThemeManager.cpp
    src/core/AppSettings.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(help_dialog_test PRIVATE src)
target_link_libraries(help_dialog_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(help_dialog_test PROPERTIES AUTOMOC ON)

add_executable(device_diagnostics_test
    tests/device_diagnostics_test.cpp
)
target_include_directories(device_diagnostics_test PRIVATE src)
target_link_libraries(device_diagnostics_test PRIVATE Qt6::Core)

add_executable(midi_settings_test
    tests/midi_settings_test.cpp
    src/core/MidiSettings.cpp
)
target_compile_definitions(midi_settings_test PRIVATE HAVE_MIDI)

if (USE_SYSTEM_RTMIDI)
    target_link_libraries(midi_settings_test PRIVATE PkgConfig::rtmidi)
else()
    target_include_directories(midi_settings_test PRIVATE src third_party/rtmidi)
endif()
target_link_libraries(midi_settings_test PRIVATE Qt6::Core)
add_test(NAME midi_settings_test COMMAND midi_settings_test)

add_executable(transmit_model_test
    tests/transmit_model_test.cpp
    src/models/TransmitModel.cpp
    src/core/ClientQuindarTone.cpp
    src/core/AppSettings.cpp
    src/core/AsyncLogWriter.cpp
    src/core/LogManager.cpp
)
target_include_directories(transmit_model_test PRIVATE src)
target_link_libraries(transmit_model_test PRIVATE Qt6::Core)
if(UNIX)
    target_link_libraries(transmit_model_test PRIVATE pthread)
endif()

# Container system Phase 1 tests — needs QApplication + Widgets.
add_executable(container_widget_test
    tests/container_widget_test.cpp
    src/gui/FramelessResizer.cpp
    src/gui/containers/ContainerTitleBar.cpp
    src/gui/containers/ContainerWidget.cpp
    src/gui/containers/FloatingContainerWindow.cpp
    src/core/AppSettings.cpp
    # FloatingContainerWindow now calls applyAppTheme() which routes
    # through ThemeManager::applyStyleSheet — pull in the manager + its
    # logging deps so the test links.  ThemeManager's compiled-in
    # seedBuiltinDefaults() means we don't need the theme resource here.
    src/core/ThemeManager.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(container_widget_test PRIVATE src)
target_link_libraries(container_widget_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(container_widget_test PROPERTIES AUTOMOC ON)

add_executable(container_manager_test
    tests/container_manager_test.cpp
    src/gui/FramelessResizer.cpp
    src/gui/containers/ContainerManager.cpp
    src/gui/containers/ContainerTitleBar.cpp
    src/gui/containers/ContainerWidget.cpp
    src/gui/containers/FloatingContainerWindow.cpp
    src/core/AppSettings.cpp
    src/core/ThemeManager.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(container_manager_test PRIVATE src)
target_link_libraries(container_manager_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(container_manager_test PROPERTIES AUTOMOC ON)

add_executable(container_nesting_test
    tests/container_nesting_test.cpp
    src/gui/FramelessResizer.cpp
    src/gui/containers/ContainerManager.cpp
    src/gui/containers/ContainerTitleBar.cpp
    src/gui/containers/ContainerWidget.cpp
    src/gui/containers/FloatingContainerWindow.cpp
    src/core/AppSettings.cpp
    src/core/ThemeManager.cpp
    src/core/LogManager.cpp
    src/core/AsyncLogWriter.cpp
)
target_include_directories(container_nesting_test PRIVATE src)
target_link_libraries(container_nesting_test PRIVATE
    Qt6::Core Qt6::Widgets Qt6::Test
)
set_target_properties(container_nesting_test PROPERTIES AUTOMOC ON)

# Integration test — requires a running AetherSDR instance.
# Not added to ctest; run manually:
#   ./build/rigctld_test [--host HOST] [--port PORT] [--ptt] [--cw]
add_executable(rigctld_test
    tests/rigctld_test.cpp
)
target_include_directories(rigctld_test PRIVATE src)
target_link_libraries(rigctld_test PRIVATE Qt6::Core Qt6::Network)

# Integration tests — require a running AetherSDR instance with CAT ports enabled.
# Not added to ctest; run manually:
#   ./build/CAT_TS-2000_test  [--host HOST] [--port PORT] [--ptt] [--cw] [--pty PATH]
#   ./build/CAT_Flex_test     [--host HOST] [--port PORT] [--ptt] [--cw] [--pty PATH]
add_executable(CAT_TS-2000_test
    tests/CAT_TS-2000_test.cpp
)
target_include_directories(CAT_TS-2000_test PRIVATE src)
target_link_libraries(CAT_TS-2000_test PRIVATE Qt6::Core Qt6::Network)

add_executable(CAT_Flex_test
    tests/CAT_Flex_test.cpp
)
target_include_directories(CAT_Flex_test PRIVATE src)
target_link_libraries(CAT_Flex_test PRIVATE Qt6::Core Qt6::Network)


# ── Install rules ────────────────────────────────────────────────────────────
include(GNUInstallDirs)

if(APPLE)
    install(TARGETS AetherSDR
        BUNDLE DESTINATION .
    )
else()
    install(TARGETS AetherSDR
        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    )
    install(FILES ${CMAKE_CURRENT_BINARY_DIR}/packaging/linux/AetherSDR.desktop
        DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
    )
    install(FILES docs/assets/logo-circle-256.png
        DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps
        RENAME aethersdr.png
    )
endif()
