cmake_minimum_required(VERSION 3.24)
project(gamecult_mesh_quic_native LANGUAGES CXX)

# Two platforms, one source. Windows takes MsQuic from the pinned OpenSSL NuGet
# layout (MSQUIC_ROOT); Linux takes it from the extracted libmsquic deb, whose
# headers and library land in unrelated places, so they are named separately.
if(NOT WIN32 AND NOT (CMAKE_SYSTEM_NAME STREQUAL "Linux"))
  message(FATAL_ERROR "The CultMesh native QUIC bridge supports Windows x64 and Linux x64")
endif()

add_library(gamecult_mesh_quic_native SHARED cultmesh_quic_native.cpp)
target_compile_features(gamecult_mesh_quic_native PRIVATE cxx_std_20)
# include/cultmesh_quic_native.h is the host-facing contract, not an internal
# detail: the bridge includes it, and so does any consumer that can.
target_include_directories(gamecult_mesh_quic_native PUBLIC include)

if(WIN32)
  if(NOT DEFINED MSQUIC_ROOT)
    message(FATAL_ERROR "MSQUIC_ROOT must point at the extracted Microsoft.Native.Quic.MsQuic.OpenSSL package build/native directory")
  endif()
  # Static CRT. A Node or Unity host without the Visual C++ redistributable
  # installed cannot load a DLL that imports vcruntime140.dll, and the failure
  # arrives as "the specified module could not be found" with nothing naming the
  # missing runtime. The bridge carries its own instead.
  set_property(TARGET gamecult_mesh_quic_native PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded")
  target_compile_definitions(gamecult_mesh_quic_native PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
  target_include_directories(gamecult_mesh_quic_native PRIVATE "${MSQUIC_ROOT}/include")
  target_link_directories(gamecult_mesh_quic_native PRIVATE "${MSQUIC_ROOT}/lib/x64")
  # crypt32 and bcrypt belong to the v1 shim alone: CryptHashCertificate2
  # compares the Unity pin. Nothing in the v2 path needs them. ntdll carries
  # RtlIpv4/6StringToAddressEx, which msquic_winuser.h's inline QuicAddrFromString
  # calls to parse the listener host; the Linux header parses it in-tree instead.
  target_link_libraries(gamecult_mesh_quic_native PRIVATE msquic crypt32 bcrypt ntdll)
  # The DLL is committed beside its source, so a rebuild must reproduce it: /Brepro drops the link timestamps.
  target_compile_options(gamecult_mesh_quic_native PRIVATE /Brepro)
  target_link_options(gamecult_mesh_quic_native PRIVATE /Brepro)
else()
  if(NOT DEFINED MSQUIC_INCLUDE_DIR OR NOT DEFINED MSQUIC_LIB_DIR)
    message(FATAL_ERROR "MSQUIC_INCLUDE_DIR and MSQUIC_LIB_DIR must point at the msquic v2.5.9 headers and the extracted libmsquic library directory")
  endif()
  target_include_directories(gamecult_mesh_quic_native PRIVATE "${MSQUIC_INCLUDE_DIR}")
  target_link_directories(gamecult_mesh_quic_native PRIVATE "${MSQUIC_LIB_DIR}")
  target_link_libraries(gamecult_mesh_quic_native PRIVATE msquic pthread)
  # Only the CULTMESH_API exports leave the .so; everything else stays internal.
  target_compile_options(gamecult_mesh_quic_native PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden)
  # libmsquic.so.2 ships beside the bridge rather than being installed
  # system-wide, so the loader is pointed at the bridge's own directory.
  set_target_properties(gamecult_mesh_quic_native PROPERTIES
    BUILD_WITH_INSTALL_RPATH TRUE
    INSTALL_RPATH "$ORIGIN")
endif()

set_target_properties(gamecult_mesh_quic_native PROPERTIES
  OUTPUT_NAME "gamecult_mesh_quic_native"
  RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
  LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")

# The runtime-lifetime scenarios. Off by default: the shipped artifact is the
# library alone, and a delivery build has no business producing an executable.
# CULTMESH_QUIC_DEBUG_ASSERTS goes with them, never into a shipped binary — it
# makes the bridge state the quiesce invariant instead of leaving it to whatever
# a sanitizer happens to catch.
option(CULTMESH_QUIC_BUILD_TESTS "Build the runtime-lifetime scenario runner" OFF)
option(CULTMESH_QUIC_DEBUG_ASSERTS "Abort the bridge on a violated internal invariant" OFF)

if(CULTMESH_QUIC_DEBUG_ASSERTS)
  target_compile_definitions(gamecult_mesh_quic_native PRIVATE CULTMESH_QUIC_DEBUG_ASSERTS)
endif()

if(CULTMESH_QUIC_BUILD_TESTS)
  add_executable(cultmesh_quic_native_tests tests/cultmesh_quic_native_tests.cpp)
  target_compile_features(cultmesh_quic_native_tests PRIVATE cxx_std_20)
  # The header only. The scenarios drive the bridge as a host does, so they get
  # no more of it than a host has.
  target_link_libraries(cultmesh_quic_native_tests PRIVATE gamecult_mesh_quic_native)
  # The scenarios see the development seam (section 7 of the header) exactly when
  # the library has one, so `holdclose` compiles out of the same builds the
  # bridge's own assertions compile out of.
  if(CULTMESH_QUIC_DEBUG_ASSERTS)
    target_compile_definitions(cultmesh_quic_native_tests PRIVATE CULTMESH_QUIC_DEBUG_ASSERTS)
  endif()
  set_target_properties(cultmesh_quic_native_tests PROPERTIES
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
  if(WIN32)
    # gamecult_mesh_quic_native.dll imports msquic.dll, and the scenario
    # runner loads it from its own directory (or PATH) at process start, not
    # from MSQUIC_ROOT. build-quic-native.ps1's packaged output gets a copy
    # from its own recipe; a test build has no packaging step, so without this
    # the loader falls through to whatever msquic.dll happens to already be on
    # PATH — silently running the scenarios against an unpinned, wrong-version
    # MsQuic instead of failing to start. CMake already knows MSQUIC_ROOT, so
    # it places the pinned DLL itself instead of leaving the reader to guess
    # why a scenario passed against the wrong binary.
    add_custom_command(TARGET cultmesh_quic_native_tests POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy
        "${MSQUIC_ROOT}/bin/x64/msquic.dll"
        "$<TARGET_FILE_DIR:cultmesh_quic_native_tests>/msquic.dll")
  else()
    target_link_libraries(cultmesh_quic_native_tests PRIVATE pthread)
    # The linker resolves the bridge's own NEEDED libmsquic.so.2 while linking
    # this executable, and the runtime copy only lands beside the binaries after
    # the build; point it at the extracted package instead.
    target_link_options(cultmesh_quic_native_tests PRIVATE "-Wl,-rpath-link,${MSQUIC_LIB_DIR}")
    set_target_properties(cultmesh_quic_native_tests PROPERTIES
      BUILD_WITH_INSTALL_RPATH TRUE
      INSTALL_RPATH "$ORIGIN")
    # Both binaries carry INSTALL_RPATH "$ORIGIN" and expect libmsquic.so.2
    # beside them at load time. build-quic-native.sh's packaged output gets
    # that copy from its own recipe; a test build has no packaging step, so
    # the runner would otherwise die at once with "cannot open shared object
    # file". CMake already knows where MSQUIC_LIB_DIR is, so it places the
    # file itself instead of leaving the reader to run an extra cp by hand.
    add_custom_command(TARGET cultmesh_quic_native_tests POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy
        "${MSQUIC_LIB_DIR}/libmsquic.so.2"
        "$<TARGET_FILE_DIR:cultmesh_quic_native_tests>/libmsquic.so.2")
  endif()
endif()
