
cmake_minimum_required(VERSION 3.18)

project(xiapl VERSION 0.1.0 LANGUAGES CXX)

set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
include(GNUInstallDirs)

# Use C++20
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Always emit compile_commands.json so editors / clangd can find include
# paths and the C++ standard. The file is regenerated by any cmake configure
# step; the project root symlink is created manually once (see README).
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# =========================================
# Common compile options (apply to every target below, not just one)
# =========================================
if (MSVC)
  # All sources in this project are UTF-8. Without /utf-8, MSVC interprets
  # them using the active ANSI code page (e.g. CP932 on a Japanese-locale
  # build machine) instead of UTF-8, which floods the build with C4819
  # ("file contains a character that cannot be represented in the current
  # code page") on every file that has a non-ASCII byte (comments included).
  # Set globally via add_compile_options rather than per-target -Wall/-Wextra
  # style below, since it must apply uniformly to every target (library,
  # tests, apps) that compiles these sources.
  add_compile_options(/utf-8)
endif()

# =========================================
# Build options
# =========================================
option(XIAPL_BUILD_CORE "Build xiapl core library" ON)
option(XIAPL_BUILD_FFI  "Build the xiapl C API library (C ABI, for FFI)" ON)
option(XIAPL_BUILD_TESTS "Build C++ unit tests (doctest)" OFF)
option(XIAPL_BUILD_EXAMPLES "Build xiapl_core usage examples" OFF)
option(XIAPL_BUILD_BENCHMARKS "Build xiapl_core benchmarks (chrono-based)" OFF)

# OFF by default; turning it ON only ADDS a target (xiapl_c_api_shared) built
# from the same XIAPL_C_API_SOURCES as the static xiapl_c_api target -- no
# existing target's flags or install rules are conditioned on this option. The
# export decoration itself (XIAPL_API -> hidden by default, visible only for
# xiapl_* symbols) is already handled by <xiapl/c_api.h>; this option only
# supplies the two macros (XIAPL_BUILD_SHARED, XIAPL_C_API_EXPORTS) that
# activate it and the -fvisibility=hidden compiler flag that makes "hidden by
# default" true at the object-file level (without it every symbol, including
# ones from statically-linked xiapl_core, would still default to visible).
option(XIAPL_BUILD_SHARED
       "Also build xiapl_c_api_shared, the C ABI as a shared library"
       OFF)

# =========================================
# Shell-driven guards (tests/check_binding_includes.sh, tests/check_exports.sh)
#
# Both are bash scripts, so they are registered through an explicitly located
# interpreter rather than by executing the script path: on Windows a .sh file
# is not executable by itself, and ctest would fail the test for a reason that
# has nothing to do with what it checks. When no bash is found the guards are
# skipped with a status line -- they are correctness checks on the source tree,
# not on the build, so a platform that cannot run them loses no coverage of the
# library itself. (GitHub's windows-latest runner ships Git Bash on PATH, so in
# practice this locates one on all three CI platforms.)
# =========================================
find_program(XIAPL_BASH NAMES bash)

# =========================================
# Binding include-purity guard (tests/check_binding_includes.sh)
#
# Registered UNCONDITIONALLY -- no build option, no target dependency, and
# independent of XIAPL_BUILD_TESTS: every CMake configure of this project
# registers this test. It reads binding/ sources and builds nothing, and the
# property it guards (the Python binding reaches the library only through
# <xiapl/c_api.h>) has no compile-time enforcement of its own: re-adding a C++
# core header there would build cleanly and pass every behavioural test while
# undoing the decoupling. Note that `pip install .` never invokes CMake at all
# (it drives setup.py directly), so this guard does not run as part of that
# flow -- ctest (or a plain CMake configure + `ctest`) is what exercises it.
# =========================================
enable_testing()
# NOT WIN32: this is a bash script, and on Windows "bash" tends to resolve
# to the System32 bash.exe stub (the WSL launcher), which cannot interpret
# a Windows path and always fails. The ubuntu/macos CI legs already run
# this guard, so coverage is not lost.
if (XIAPL_BASH AND NOT WIN32)
  add_test(NAME xiapl_binding_include_purity
    COMMAND ${XIAPL_BASH} ${CMAKE_SOURCE_DIR}/tests/check_binding_includes.sh
            ${CMAKE_SOURCE_DIR})
else()
  message(STATUS "bash not found: skipping the xiapl_binding_include_purity test")
endif()

# =========================================
# Common include dirs
# =========================================
set(XIAPL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/include)

set(XIAPL_CORE_HEADERS
  ${CMAKE_SOURCE_DIR}/include/xiapl/card.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/canonicalize.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/deck.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/eval.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/game_type.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/hand_value.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/xiapl.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/range.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/simulation.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/street.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/utils.h
  ${CMAKE_SOURCE_DIR}/include/xiapl/version.h
)

# No private detail headers are installed.

set(XIAPL_C_API_HEADERS
  ${CMAKE_SOURCE_DIR}/include/xiapl/c_api.h
)

# =========================================
# Core library sources (explicit list)
# =========================================
set(XIAPL_CORE_SOURCES
  ${CMAKE_SOURCE_DIR}/src/core/card.cpp
  ${CMAKE_SOURCE_DIR}/src/core/deck.cpp
  ${CMAKE_SOURCE_DIR}/src/core/eval.cpp
  ${CMAKE_SOURCE_DIR}/src/core/hand_value.cpp
  ${CMAKE_SOURCE_DIR}/src/core/equity_hands.cpp
  ${CMAKE_SOURCE_DIR}/src/core/equity_range.cpp
  ${CMAKE_SOURCE_DIR}/src/core/utils.cpp
  ${CMAKE_SOURCE_DIR}/src/core/canonicalize.cpp
  ${CMAKE_SOURCE_DIR}/src/core/range_holdem.cpp
  ${CMAKE_SOURCE_DIR}/src/core/range.cpp
  ${CMAKE_SOURCE_DIR}/src/core/range_plo.cpp
  ${CMAKE_SOURCE_DIR}/src/core/range_setops.cpp
  ${CMAKE_SOURCE_DIR}/src/core/preflop_rank.cpp
)

# FFI / C-ABI adapter -- the frozen contract in include/xiapl/c_api.h.
# The scalar half (version / errors / card / masks / eval / options /
# canonicalization) lives in waist_scalar.cpp; the handle half (range / deck /
# equity / result handles) in waist_handles.cpp. Both share the boundary
# conventions in src/api/waist_internal.h and the error plumbing in
# src/api/waist_error.h.
set(XIAPL_C_API_SOURCES
  ${CMAKE_SOURCE_DIR}/src/api/waist_error.cpp
  ${CMAKE_SOURCE_DIR}/src/api/waist_scalar.cpp
  ${CMAKE_SOURCE_DIR}/src/api/waist_handles.cpp
)

# =========================================
# Core library (pure C++ engine)
# =========================================
if (XIAPL_BUILD_CORE)
  add_library(xiapl_core STATIC
    ${XIAPL_CORE_SOURCES}
  )

  target_include_directories(xiapl_core
    PUBLIC
      $<BUILD_INTERFACE:${XIAPL_INCLUDE_DIR}>
      $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
  )

  target_link_libraries(xiapl_core
    PUBLIC
      Threads::Threads
  )

  # Keep warnings reasonable; adjust as you like
  if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU")
    target_compile_options(xiapl_core PRIVATE -Wall -Wextra)
  endif()
endif()

# =========================================
# C API library (C ABI; link this from FFI hosts, not xiapl_core)
# =========================================
if (XIAPL_BUILD_FFI)
  if (NOT TARGET xiapl_core)
    message(FATAL_ERROR "XIAPL_BUILD_FFI requires XIAPL_BUILD_CORE=ON")
  endif()

  add_library(xiapl_c_api STATIC
    ${XIAPL_C_API_SOURCES}
  )

  target_include_directories(xiapl_c_api
    PUBLIC
      $<BUILD_INTERFACE:${XIAPL_INCLUDE_DIR}>
      $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
  )

  target_link_libraries(xiapl_c_api
    PUBLIC
      xiapl_core
  )

  # Same warning level as xiapl_core: the C ABI is the file every binding is
  # generated from, so a signedness or unused-parameter slip here reaches every
  # language at once.
  if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU")
    target_compile_options(xiapl_c_api PRIVATE -Wall -Wextra)
  endif()

  # Keep the static archive usable when embedded into shared FFI wrappers.
  set_target_properties(xiapl_c_api PROPERTIES POSITION_INDEPENDENT_CODE ON)

  # -----------------------------------------------------------------------
  # xiapl_c_api_shared (XIAPL_BUILD_SHARED=ON only)
  #
  # Same sources as xiapl_c_api above, built as a second, independent target
  # rather than a property flip on xiapl_c_api itself: nothing here may touch
  # the static target, so the default (OFF) configuration stays byte-for-byte
  # identical. XIAPL_C_API_EXPORTS + XIAPL_BUILD_SHARED select the dllexport /
  # visibility("default") branch of the XIAPL_API macro in <xiapl/c_api.h> for
  # the object files that make up this target ONLY; consumers of the static
  # target still see XIAPL_API expand to nothing, exactly as before.
  #
  # -fvisibility=hidden is what makes "the header exports only xiapl_*"
  # ACTUALLY true for this target's OWN object files: without it, every
  # symbol XIAPL_API does not explicitly mark defaults to visible. It is NOT
  # enough by itself, though: xiapl_core (statically linked in below) is
  # compiled with its own default visibility -- unconditionally, since it is
  # also linked into plain executables (xiapl_tests, examples/) where hiding
  # its symbols would serve no purpose -- so its C++ symbols would still leak
  # into this library's dynamic export table (measured: 173 exported symbols,
  # mostly mangled xiapl:: internals, before the export-filter fix below was
  # added). The linker-level export filter is therefore the mechanism that
  # actually enforces "only xiapl_*", regardless of what any linked-in .o's
  # own visibility says; it is spelled differently per linker (ld64's
  # -exported_symbols_list on macOS, GNU ld's / lld's --version-script on
  # Linux -- see cmake/xiapl_c_api_exports.txt and
  # cmake/xiapl_c_api_exports.map), but both express the identical rule, so
  # the .dylib and the .so end up with the same export surface. The
  # compile-time hidden visibility above is still kept because it also
  # shrinks the library (hidden symbols can be dead-stripped) and is the
  # documented cross-platform half of the story in <xiapl/c_api.h>.
  # tests/check_exports.sh is the machine check that both halves hold.
  # -----------------------------------------------------------------------
  if (XIAPL_BUILD_SHARED)
    # PIC for the statically-linked xiapl_core. On ELF/x86-64, linking a
    # non-PIC archive into a shared object is a hard link error ("relocation
    # R_X86_64_32S against `...' can not be used when making a shared object;
    # recompile with -fPIC"); macOS only gets away with it today because
    # clang defaults to PIC there. CMake has no per-consumer notion of PIC, so
    # this is necessarily a property of the ARCHIVE, and therefore also
    # applies to the plain executables that link it (xiapl_tests, examples/)
    # in this same configure -- behaviour-identical, marginally different
    # codegen.
    #
    # Set here, inside if (XIAPL_BUILD_SHARED), rather than unconditionally
    # next to add_library(xiapl_core): -fPIC costs a GOT indirection on global
    # access on x86-64, and the default (OFF) configuration is what the
    # benchmarks and every plain executable are built from -- their codegen
    # must not change merely because an unrelated option exists. The
    # alternative (a second, PIC-only copy of the core as an OBJECT library)
    # was rejected: it doubles core compile time in every SHARED=ON build, CI
    # included, to buy PIC-purity for targets whose behaviour is unaffected
    # either way.
    set_target_properties(xiapl_core PROPERTIES POSITION_INDEPENDENT_CODE ON)

    add_library(xiapl_c_api_shared SHARED
      ${XIAPL_C_API_SOURCES}
    )

    target_include_directories(xiapl_c_api_shared
      PUBLIC
        $<BUILD_INTERFACE:${XIAPL_INCLUDE_DIR}>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
    )

    target_link_libraries(xiapl_c_api_shared
      PRIVATE
        xiapl_core
    )

    target_compile_definitions(xiapl_c_api_shared
      PRIVATE
        XIAPL_BUILD_SHARED
        XIAPL_C_API_EXPORTS
    )

    set_target_properties(xiapl_c_api_shared PROPERTIES
      POSITION_INDEPENDENT_CODE ON
      OUTPUT_NAME "xiapl_c_api"
      C_VISIBILITY_PRESET hidden
      CXX_VISIBILITY_PRESET hidden
      VISIBILITY_INLINES_HIDDEN ON
    )

    if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU")
      target_compile_options(xiapl_c_api_shared PRIVATE -Wall -Wextra -fvisibility=hidden)
    endif()

    # The link-time export filter (see the comment above the target), in each
    # linker's own format. Restricted to the two platforms that have actually
    # been exercised: macOS/ld64 and Linux (GNU ld or lld, both of which
    # accept --version-script). Other ELF hosts (FreeBSD etc.) fall through
    # unfiltered -- the status quo for every non-Apple platform before this,
    # so no regression -- and extending the branch there is a one-line change
    # once someone can test it.
    #
    # LINK_DEPENDS on both branches: the -Wl, flags are opaque strings to
    # CMake, so without it CMake has no way to know the target must be
    # re-linked when the export file itself changes (only the sources it does
    # track trigger a relink). Editing the export list without a relink would
    # leave the library exporting a stale symbol set that
    # tests/check_exports.sh would then wrongly pass or fail against.
    if (APPLE)
      target_link_options(xiapl_c_api_shared PRIVATE
        -Wl,-exported_symbols_list,${CMAKE_SOURCE_DIR}/cmake/xiapl_c_api_exports.txt)

      set_target_properties(xiapl_c_api_shared PROPERTIES
        LINK_DEPENDS "${CMAKE_SOURCE_DIR}/cmake/xiapl_c_api_exports.txt")
    elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
      # --no-undefined does not tighten anything relative to macOS; it
      # RESTORES parity. ld64 already treats an unresolved symbol in a dylib
      # as a link error by default, whereas GNU ld happily produces a .so
      # with dangling references that only fail once a host process dlopen()s
      # it. Since the identical source set links clean under ld64's default,
      # the link closure is known-complete and this flag simply moves the
      # Linux failure mode from run time back to build time.
      target_link_options(xiapl_c_api_shared PRIVATE
        -Wl,--version-script=${CMAKE_SOURCE_DIR}/cmake/xiapl_c_api_exports.map
        -Wl,--no-undefined)

      set_target_properties(xiapl_c_api_shared PROPERTIES
        LINK_DEPENDS "${CMAKE_SOURCE_DIR}/cmake/xiapl_c_api_exports.map")
    endif()

    # tests/check_exports.sh -- the export-surface check described above
    # (both halves: nothing outside the ABI is exported, and nothing the
    # header declares is missing). Registered on exactly the platforms whose
    # export filter is configured above, since on any other platform the
    # shared target links unfiltered and would legitimately export
    # xiapl_core's C++ internals -- better not registered than registered and
    # failing for a reason the build never promised to prevent.
    # AND NOT WIN32 is belt-and-suspenders against a Windows host running
    # ctest against a CMAKE_SYSTEM_NAME=Linux cross-build (see the bash/WSL
    # note on the include-purity test above).
    if ((APPLE OR CMAKE_SYSTEM_NAME STREQUAL "Linux") AND XIAPL_BASH AND NOT WIN32)
      add_test(NAME xiapl_c_api_shared_exports
        COMMAND ${XIAPL_BASH} ${CMAKE_SOURCE_DIR}/tests/check_exports.sh
                $<TARGET_FILE:xiapl_c_api_shared>)
    endif()
  endif()
endif()

# =========================================
# Offline table generator (built on demand: `make gen_preflop_rank`)
#
# EXCLUDE_FROM_ALL keeps it out of the default build -- it is an offline
# one-shot that regenerates src/core/preflop_rank_table.inc (a checked-in
# artifact), not part of the library or of CI. Always -O3: the enumeration is
# ~2.8e9 hand evaluations and the default (empty) CMAKE_BUILD_TYPE would run
# it unoptimized.
# =========================================
if (TARGET xiapl_core)
  add_executable(gen_preflop_rank EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/apps/gen_preflop_rank.cpp
  )

  target_include_directories(gen_preflop_rank
    PRIVATE
      ${XIAPL_INCLUDE_DIR}
  )

  if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU")
    target_compile_options(gen_preflop_rank PRIVATE -O3 -g0)
  elseif (MSVC)
    target_compile_options(gen_preflop_rank PRIVATE /O2)
  endif()

  target_link_libraries(gen_preflop_rank
    PRIVATE
      xiapl_core
      Threads::Threads
  )

  if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU")
    target_compile_options(gen_preflop_rank PRIVATE -Wall -Wextra)
  endif()
endif()

# =========================================
# C++ unit tests (doctest)
# =========================================
if (XIAPL_BUILD_TESTS)
  if (NOT TARGET xiapl_core)
    message(FATAL_ERROR "XIAPL_BUILD_TESTS requires XIAPL_BUILD_CORE=ON")
  endif()

  set(XIAPL_TEST_SOURCES
    ${CMAKE_SOURCE_DIR}/tests/test_main.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_card.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_deck.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_eval.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_eval_exhaustive.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_eval_core.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_hand_value.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_mc_chunking.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_simulation.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_simulation_options.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_equity_threading.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_equity_agreement.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_validation.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_range_equity.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_canonicalize.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_range.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_range_gametype.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_range_setops.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_parse_locale.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_utils.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_fast_rng.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_alias_picker.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_plo_pattern.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_plo_parser.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_plo_range_equity.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_preflop_rank_table.cpp
    ${CMAKE_SOURCE_DIR}/tests/test_preflop_rank.cpp
  )

  add_executable(xiapl_tests
    ${XIAPL_TEST_SOURCES}
  )

  target_include_directories(xiapl_tests
    PRIVATE
      ${CMAKE_SOURCE_DIR}/tests
      ${XIAPL_INCLUDE_DIR}
  )

  target_link_libraries(xiapl_tests
    PRIVATE
      xiapl_core
      Threads::Threads
  )

  # C5285 ("explicit specialization requires 'template<>' syntax") fires from
  # vendored tests/doctest.h's std::tuple template-test-case machinery on
  # newer MSVC toolsets -- it is doctest upstream's issue, not ours, and
  # benign (the specialization compiles and behaves correctly either way).
  # Scoped to this test target only, not the global MSVC block above, since
  # only translation units that include doctest.h hit it.
  if (MSVC)
    target_compile_options(xiapl_tests PRIVATE /wd5285)
  endif()

  # The exhaustive / high-trial-count cases live in doctest's "slow" test
  # suite, so a quick edit-compile-test loop can run `ctest -LE slow`.
  add_test(NAME xiapl_tests_fast COMMAND xiapl_tests --test-suite-exclude=slow)
  add_test(NAME xiapl_tests_slow COMMAND xiapl_tests --test-suite=slow)
  set_tests_properties(xiapl_tests_slow PROPERTIES LABELS "slow")

  # ---- the C ABI's own two lanes -------------------------------------------
  # xiapl_c_api_tests   behaviour: every waist entry point, its error
  #                     classification and its ownership rules, driven from C++.
  # xiapl_c_api_abi_check  compilability: the same header consumed by a plain
  #                     C11 translation unit with -Wpedantic, which is the only
  #                     way to catch a C++-only construct sneaking into the ABI
  #                     header (a C++ consumer would never notice).
  if (TARGET xiapl_c_api)
    add_executable(xiapl_c_api_tests
      ${CMAKE_SOURCE_DIR}/tests/test_main.cpp
      ${CMAKE_SOURCE_DIR}/tests/test_c_api_waist.cpp
    )

    # See the xiapl_tests /wd5285 comment above: vendored tests/doctest.h,
    # benign upstream C5285, scoped to this doctest-consuming target only.
    if (MSVC)
      target_compile_options(xiapl_c_api_tests PRIVATE /wd5285)
    endif()

    target_include_directories(xiapl_c_api_tests
      PRIVATE
        ${CMAKE_SOURCE_DIR}/tests
        ${XIAPL_INCLUDE_DIR}
        # src/ carries the C ABI's private error plumbing, which the test
        # includes as <api/waist_error.h> to pin the exception -> status
        # classification directly (std::out_of_range and std::bad_alloc have
        # no public entry point that raises them).
        ${CMAKE_SOURCE_DIR}/src
    )

    target_link_libraries(xiapl_c_api_tests
      PRIVATE
        xiapl_c_api
    )

    add_test(NAME xiapl_c_api_tests COMMAND xiapl_c_api_tests)

    enable_language(C)
    add_executable(xiapl_c_api_abi_check tests/test_c_api_abi.c)
    target_compile_features(xiapl_c_api_abi_check PRIVATE c_std_11)
    # -Wpedantic is the load-bearing flag here: it is what rejects a C++-only
    # construct that slipped into the ABI header. Guarded by compiler id
    # because MSVC does not understand the GNU spellings (it accepts -Wall as
    # /Wall and merely warns about the other two, which would make the check
    # look like it ran when it did not).
    if (CMAKE_C_COMPILER_ID MATCHES "Clang|AppleClang|GNU")
      target_compile_options(xiapl_c_api_abi_check PRIVATE -Wall -Wextra -Wpedantic)
    elseif (MSVC)
      target_compile_options(xiapl_c_api_abi_check PRIVATE /W4 /permissive-)
    endif()
    target_include_directories(xiapl_c_api_abi_check PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
    target_link_libraries(xiapl_c_api_abi_check PRIVATE xiapl_c_api)
    set_target_properties(xiapl_c_api_abi_check PROPERTIES LINKER_LANGUAGE CXX)
    add_test(NAME xiapl_c_api_abi_check COMMAND xiapl_c_api_abi_check)
  endif()
endif()

# =========================================
# Examples (XIAPL_BUILD_EXAMPLES=ON)
# Small executables demonstrating the stable xiapl_core API.
# =========================================
if (XIAPL_BUILD_EXAMPLES)
  if (NOT TARGET xiapl_core)
    message(FATAL_ERROR "XIAPL_BUILD_EXAMPLES requires XIAPL_BUILD_CORE=ON")
  endif()

  set(XIAPL_EXAMPLES
    evaluate_hand
    calc_equity
    range_equity
    weighted_range
  )

  foreach (ex IN LISTS XIAPL_EXAMPLES)
    add_executable(${ex} ${CMAKE_SOURCE_DIR}/examples/${ex}.cpp)
    target_link_libraries(${ex} PRIVATE xiapl_core)
    set_target_properties(${ex} PROPERTIES
      RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/examples
    )
    if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU")
      target_compile_options(${ex} PRIVATE -Wall -Wextra)
    endif()
  endforeach()
endif()

# =========================================
# Benchmarks (XIAPL_BUILD_BENCHMARKS=ON)
# std::chrono-based throughput measurements. Built at -O3 -DNDEBUG
# regardless of CMAKE_BUILD_TYPE so numbers are meaningful even when
# the rest of the project is built in Debug.
# =========================================
if (XIAPL_BUILD_BENCHMARKS)
  if (NOT TARGET xiapl_core)
    message(FATAL_ERROR "XIAPL_BUILD_BENCHMARKS requires XIAPL_BUILD_CORE=ON")
  endif()

  set(XIAPL_BENCHMARKS
    bench_eval
    bench_equity
  )

  foreach (bn IN LISTS XIAPL_BENCHMARKS)
    add_executable(${bn} ${CMAKE_SOURCE_DIR}/benchmarks/${bn}.cpp)
    target_link_libraries(${bn} PRIVATE xiapl_core Threads::Threads)
    set_target_properties(${bn} PROPERTIES
      RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/benchmarks
    )
    if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU")
      target_compile_options(${bn} PRIVATE -O3 -DNDEBUG -Wall -Wextra)
    endif()
  endforeach()
endif()

# =========================================
# Install rules
# =========================================
include(CMakePackageConfigHelpers)

set(XIAPL_HAS_INSTALL_TARGETS FALSE)

if (TARGET xiapl_core)
  set(XIAPL_HAS_INSTALL_TARGETS TRUE)
  install(TARGETS xiapl_core EXPORT xiaplTargets
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  )

  install(FILES ${XIAPL_CORE_HEADERS}
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/xiapl
  )
endif()

if (TARGET xiapl_c_api)
  set(XIAPL_HAS_INSTALL_TARGETS TRUE)
  install(TARGETS xiapl_c_api EXPORT xiaplTargets
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  )

  install(FILES ${XIAPL_C_API_HEADERS}
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/xiapl
  )
endif()

if (XIAPL_HAS_INSTALL_TARGETS)
  install(EXPORT xiaplTargets
    FILE xiaplTargets.cmake
    NAMESPACE xiapl::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/xiapl
  )

  configure_package_config_file(
    ${CMAKE_SOURCE_DIR}/cmake/xiaplConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/xiaplConfig.cmake
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/xiapl
  )

  write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/xiaplConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion
  )

  install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/xiaplConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/xiaplConfigVersion.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/xiapl
  )
endif()

# =========================================
# Notes:
# - FFI hosts (Flutter / iOS / Android / Node ...) should link against
#   `xiapl_c_api` (C ABI), not `xiapl_core`.
# - `xiapl_c_api` is STATIC by default; XIAPL_BUILD_SHARED=ON additionally
#   builds `xiapl_c_api_shared` for hosts that need a .so / .dylib.
# =========================================
