cmake_minimum_required(VERSION 3.24)
project(udpscope VERSION 0.1.0 LANGUAGES C CXX)

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

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()

find_package(Threads REQUIRED)

# --- libadwaita: the shortcut overview ---------------------------------------
# GTK4's own `GtkShortcutsWindow` is deprecated as of 4.18 and libadwaita's
# `AdwShortcutsDialog` (1.8) is its replacement, so the overview is built from the
# platform widget rather than from a hand-rolled grid. This is the only thing libadwaita
# is used for; `adw_init()` is called for it, which also means the app picks up
# libadwaita's stylesheet.
find_package(PkgConfig REQUIRED)
pkg_check_modules(ADWAITA REQUIRED IMPORTED_TARGET libadwaita-1>=1.8)

# --- deps/plotter: built from the submodule, GTK4 adapter only ----------------
set(PLOT_BUILD_TESTS     OFF CACHE BOOL "" FORCE)
set(PLOT_BUILD_EXAMPLES  OFF CACHE BOOL "" FORCE)
set(PLOT_BUILD_GLFW      OFF CACHE BOOL "" FORCE)
set(PLOT_BUILD_IMGUI     OFF CACHE BOOL "" FORCE)
set(PLOT_BUILD_QT6       OFF CACHE BOOL "" FORCE)
set(PLOT_BUILD_GTK4      ON  CACHE BOOL "" FORCE)
set(PLOT_INSTALL         OFF CACHE BOOL "" FORCE)
add_subdirectory(deps/plotter EXCLUDE_FROM_ALL)

# --- deps/udpsclient: vendored C99 receiver ----------------------------------
add_library(udpsclient STATIC deps/udpsclient/udps_client.c)
target_include_directories(udpsclient PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/deps/udpsclient)
set_target_properties(udpsclient PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
  target_compile_options(udpsclient PRIVATE -Wall -Wextra)
endif()

# --- icons: six SVGs compiled into the binary --------------------------------
# There is no stock icon for an oscilloscope trigger, a pair of cursors or a measurement
# ruler, so this app draws its own. They go in a GResource rather than beside the binary
# because udpscope is run straight out of the build tree and has no install step: a
# search path would have to be located at runtime, and the failure mode of not locating
# it is a silent missing-image placeholder.
find_program(GLIB_COMPILE_RESOURCES NAMES glib-compile-resources REQUIRED)
set(UDPSCOPE_ICON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/icons)
file(GLOB UDPSCOPE_ICON_FILES CONFIGURE_DEPENDS
     ${UDPSCOPE_ICON_DIR}/scalable/actions/*.svg
     ${UDPSCOPE_ICON_DIR}/scalable/apps/*.svg)
add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
  COMMAND ${GLIB_COMPILE_RESOURCES}
          --target=${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
          --sourcedir=${UDPSCOPE_ICON_DIR}
          --generate-source
          ${UDPSCOPE_ICON_DIR}/udpscope.gresource.xml
  DEPENDS ${UDPSCOPE_ICON_DIR}/udpscope.gresource.xml ${UDPSCOPE_ICON_FILES}
  COMMENT "Compiling udpscope icon resources"
  VERBATIM)

# --- help pages: Pango markup in a GResource ----------------------------------
# A second bundle beside the icons'. --c-name is given explicitly so the two generated
# sources cannot collide on symbol names.
set(UDPSCOPE_HELP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/help)
file(GLOB UDPSCOPE_HELP_FILES CONFIGURE_DEPENDS ${UDPSCOPE_HELP_DIR}/*.txt)
add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/udpscope_help_resources.c
  COMMAND ${GLIB_COMPILE_RESOURCES}
          --target=${CMAKE_CURRENT_BINARY_DIR}/udpscope_help_resources.c
          --sourcedir=${UDPSCOPE_HELP_DIR}
          --c-name=udpscope_help
          --generate-source
          ${UDPSCOPE_HELP_DIR}/udpscope-help.gresource.xml
  DEPENDS ${UDPSCOPE_HELP_DIR}/udpscope-help.gresource.xml ${UDPSCOPE_HELP_FILES}
  COMMENT "Compiling udpscope help resources"
  VERBATIM)

# --- UI files: GtkBuilder XML in a GResource ----------------------------------
# The third bundle, for the same reason as the other two: `.ui` files are data, so they are compiled
# in rather than looked for at runtime -- and `preprocess="xml-stripblanks"` makes a malformed one a
# build failure with a file and line number, instead of a window that silently refuses to open.
set(UDPSCOPE_UI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/ui)
file(GLOB UDPSCOPE_UI_FILES CONFIGURE_DEPENDS ${UDPSCOPE_UI_DIR}/*.ui)
add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
  COMMAND ${GLIB_COMPILE_RESOURCES}
          --target=${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
          --sourcedir=${UDPSCOPE_UI_DIR}
          --c-name=udpscope_ui
          --generate-source
          ${UDPSCOPE_UI_DIR}/udpscope-ui.gresource.xml
  DEPENDS ${UDPSCOPE_UI_DIR}/udpscope-ui.gresource.xml ${UDPSCOPE_UI_FILES}
  COMMENT "Compiling udpscope UI resources"
  VERBATIM)
# The generated source carries each file as one C string literal, and a `.ui` is big enough to pass the
# 4095-character minimum the standard requires a compiler to support: `-Woverlength-strings` fires on
# generated *data*, not on anything anyone wrote. Silenced for this one generated file (the icon and
# help bundles are small enough per file never to need it), so the build stays at its four warnings.
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
                            PROPERTIES COMPILE_OPTIONS "-Wno-overlength-strings")

# --- the UI files: an opt-in check, never part of `cmake --build` ---------------
# The interface is hand-written GtkBuilder XML, and nothing in the build sees a mistake in it: a
# property a class does not have is a `Gtk-ERROR` when the window is built, and `Gtk-ERROR` *aborts
# the process* -- so a typo takes the application down rather than failing to open a dialog.
# `xml-stripblanks` catches malformed XML and nothing else, and the probes catch the rest only for the
# windows they open.
#
# blueprint-compiler type-checks every property and child type against the GIR, and it reads XML as
# well as blueprint (`decompile` turns one into the other), so `tools/check_ui.py` round-trips each
# file through it. The XML stays the source of truth and stays hand-written; nothing here is needed to
# build the application, which is the whole point of not converting the files.
#
# No ALL: this runs only when it is named, so `cmake --build build` is unaffected and
# blueprint-compiler is not a build dependency.
find_program(UDPSCOPE_PYTHON NAMES python3)
if(UDPSCOPE_PYTHON)
  add_custom_target(test-ui
    COMMAND ${UDPSCOPE_PYTHON} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_ui.py ${UDPSCOPE_UI_DIR}
    DEPENDS ${UDPSCOPE_UI_FILES} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_ui.py
    COMMENT "Checking the UI files with blueprint-compiler"
    VERBATIM)
else()
  message(WARNING "python3 was not found: the test-ui target is not available")
endif()

# --- build info: version, commit and timestamp compiled into the binary -------
# Written at configure time so add_executable can name the file, and again on every
# build so the commit and the timestamp belong to the binary that is running rather
# than to whenever the tree was last configured.
set(UDPSCOPE_BUILD_INFO ${CMAKE_CURRENT_BINARY_DIR}/build_info.cpp)
set(UDPSCOPE_BUILD_INFO_ARGS
    -DSRC=${CMAKE_CURRENT_SOURCE_DIR}
    -DOUT=${UDPSCOPE_BUILD_INFO}
    -DVERSION=${PROJECT_VERSION}
    -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/write_build_info.cmake)
execute_process(COMMAND ${CMAKE_COMMAND} ${UDPSCOPE_BUILD_INFO_ARGS})
add_custom_target(udpscope_build_info
  BYPRODUCTS ${UDPSCOPE_BUILD_INFO}
  COMMAND ${CMAKE_COMMAND} ${UDPSCOPE_BUILD_INFO_ARGS}
  COMMENT "Writing udpscope build info"
  VERBATIM)
set_source_files_properties(${UDPSCOPE_BUILD_INFO} PROPERTIES GENERATED TRUE)

# --- the application ----------------------------------------------------------
add_executable(udpscope
  ${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
  ${CMAKE_CURRENT_BINARY_DIR}/udpscope_help_resources.c
  ${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
  ${UDPSCOPE_BUILD_INFO}
  src/main.cpp
  src/config.cpp
  src/channel.cpp
  src/model.cpp
  src/condition.cpp
  src/trigger.cpp
  src/capture.cpp
  src/waveform.cpp
  src/event_store.cpp
  src/acquisition.cpp
  src/plot_host.cpp
  src/export.cpp
  src/search.cpp
  src/layout_store.cpp
  src/source_dialog.cpp
  src/measure.cpp
  src/math_channel.cpp
  src/status_text.cpp
  src/shortcuts.cpp
  src/help_window.cpp
  src/settings.cpp
  src/ui_prefs.cpp
  src/ui.cpp
  src/ui_probe.cpp
  src/ui_widgets.cpp
  src/ui_events.cpp
  src/ui_status.cpp
  src/ui_measure.cpp
  src/ui_cursors.cpp
  src/ui_math.cpp
  src/ui_layout.cpp
  src/ui_trigger.cpp
  src/ui_signals.cpp
)

target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads
                                      PkgConfig::ADWAITA)
add_dependencies(udpscope udpscope_build_info)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
  target_compile_options(udpscope PRIVATE -Wall -Wextra -Wpedantic)
endif()

# --- the desktop entry: opt-in, never part of `cmake --build` ------------------
# `Exec` is the built binary's absolute path in the build tree, so the entry always launches
# exactly the binary that was last built -- and moving or deleting the build directory breaks
# it. That is the price of having no install step, and it is the right one here: an install()
# rule would create a second copy of the app that can diverge from the build tree, and a
# documented `cp` would need the binary on PATH, which it is not.
#
# configure_file() cannot evaluate a generator expression and file(GENERATE) cannot substitute
# an @VAR@, so the two run in sequence: the first writes the genex out literally, the second
# evaluates it. This is the documented idiom.
set(UDPSCOPE_EXEC "$<TARGET_FILE:udpscope>")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/data/org.udpscope.app.desktop.in
               ${CMAKE_CURRENT_BINARY_DIR}/desktop-entry.in @ONLY)
file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/org.udpscope.app.desktop
     INPUT ${CMAKE_CURRENT_BINARY_DIR}/desktop-entry.in)

# No ALL: this runs only when it is named, so `cmake --build build` is unaffected.
add_custom_target(desktop-entry
  COMMAND ${CMAKE_COMMAND}
          -DENTRY=${CMAKE_CURRENT_BINARY_DIR}/org.udpscope.app.desktop
          -DICON=${UDPSCOPE_ICON_DIR}/scalable/apps/org.udpscope.app.svg
          -DSYMBOLIC=${UDPSCOPE_ICON_DIR}/scalable/apps/org.udpscope.app-symbolic.svg
          -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/install_desktop_entry.cmake
  COMMENT "Installing the udpscope desktop entry into XDG_DATA_HOME"
  VERBATIM)
add_dependencies(desktop-entry udpscope)
