# vim: set ts=4:sw=4:expandtab:
cmake_minimum_required(VERSION 3.14)

if (POLICY CMP0048)
    cmake_policy(SET CMP0048 NEW) # Allow project(xxx VERSION a.b.c)
endif()

if (POLICY CMP0074)
    cmake_policy(SET CMP0074 NEW) # find_package uses <PackageName>_ROOT variables
endif()

if (POLICY CMP0110)
    cmake_policy(SET CMP0110 NEW) # add_test() supports arbitrary characters in test names
endif()

# Get version from git-version-gen
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.tarball-version")
    file(READ "${CMAKE_CURRENT_SOURCE_DIR}/.tarball-version" PROJECT_VERSION)
    string(STRIP "${PROJECT_VERSION}" PROJECT_VERSION)
else()
    execute_process(
        COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/build-aux/git-version-gen" "${CMAKE_CURRENT_SOURCE_DIR}/.tarball-version"
        OUTPUT_VARIABLE PROJECT_VERSION
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )
endif()

# Parse version components
string(REGEX MATCH "^([0-9]+)(\\.([0-9]+)(\\.([0-9]+))?)?(-(.*))?$" _ "${PROJECT_VERSION}")
set(VERSION_MAJOR "${CMAKE_MATCH_1}")
if(CMAKE_MATCH_3)
    set(VERSION_MINOR "${CMAKE_MATCH_3}")
else()
    set(VERSION_MINOR "0")
endif()
if(CMAKE_MATCH_5)
    set(VERSION_PATCH "${CMAKE_MATCH_5}")
else()
    set(VERSION_PATCH "0")
endif()
if(CMAKE_MATCH_7)
    set(VERSION_EXTRA "${CMAKE_MATCH_7}")
else()
    set(VERSION_EXTRA "")
endif()

# Optional MSVC cross-compilation support via msvc-wine
# Usage: cmake -DMSVC_WINE_DIR=/path/to/msvc [-DMSVC_WINE_COMPILER=cl|clang-cl|clang] ..
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/MSVCCrossCompile.cmake OPTIONAL)

project(libfyaml LANGUAGES C ASM VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH})

# Export compile commands by default for clangd LSP support
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Must use GNUInstallDirs to install libraries into correct locations on all platforms
include(GNUInstallDirs)
include(CheckIncludeFile)
include(CheckFunctionExists)
include(CheckCSourceCompiles)
include(CheckCCompilerFlag)
include(CheckLibraryExists)
include(CheckSymbolExists)
include(CMakeDependentOption)
include(CTest)
include(TestBigEndian)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/CheckLibClang.cmake)
include(FetchContent)

# does this CMake version understand generator expressions?
if(${CMAKE_VERSION} VERSION_LESS "3.27")
    set(HAVE_CMAKE_GEXPR 0)
else()
    set(HAVE_CMAKE_GEXPR 1)
endif()

# Macro to add libclang support to a target
macro(add_libclang_to_target target_name link_scope)
    if(HAVE_LIBCLANG)
        # Split LIBCLANG_CFLAGS into list and add as compile options
        string(REPLACE " " ";" LIBCLANG_CFLAGS_LIST "${LIBCLANG_CFLAGS}")
        target_compile_options(${target_name} PRIVATE ${LIBCLANG_CFLAGS_LIST})

        # Split LIBCLANG_LDFLAGS into list and add as link directories/options
        # LIBCLANG_LDFLAGS typically contains -L/path/to/lib and -Wl,flags
        # Extract only the -L paths
        string(REGEX MATCHALL "-L([^ ]+)" LIBCLANG_L_FLAGS "${LIBCLANG_LDFLAGS}")
        set(LIBCLANG_LINK_DIRS_LOCAL "")
        foreach(flag ${LIBCLANG_L_FLAGS})
            string(REGEX REPLACE "^-L" "" dir "${flag}")
            list(APPEND LIBCLANG_LINK_DIRS_LOCAL "${dir}")
        endforeach()
        if(LIBCLANG_LINK_DIRS_LOCAL)
            target_link_directories(${target_name} ${link_scope} ${LIBCLANG_LINK_DIRS_LOCAL})
        endif()

        # Add include directories (LIBCLANG_INCLUDES covers cases where the
        # path is not embedded as -I flags in LIBCLANG_CFLAGS, e.g. direct path)
        if(LIBCLANG_INCLUDES)
            target_include_directories(${target_name} PRIVATE ${LIBCLANG_INCLUDES})
        endif()

        # Add libclang libraries
        target_link_libraries(${target_name} ${link_scope} ${LIBCLANG_LIBS})
    endif()
endmacro()

function(link_whole_archive_static target_name static_target)
    if(MSVC)
        target_link_libraries(${target_name} PRIVATE ${static_target})
    elseif(APPLE)
        target_link_libraries(${target_name} PRIVATE
            -Wl,-force_load,$<TARGET_FILE:${static_target}>
        )
    elseif(WIN32)
        target_link_libraries(${target_name} PRIVATE ${static_target})
        target_link_options(${target_name} PRIVATE
            "LINKER:/WHOLEARCHIVE:$<TARGET_FILE:${static_target}>"
        )
    else()
        target_link_libraries(${target_name} PRIVATE
            -Wl,--whole-archive ${static_target} -Wl,--no-whole-archive
        )
    endif()
endfunction()

# Options
option(BUILD_SHARED_LIBS "Build shared libraries" ON)
option(ENABLE_PORTABLE_TARGET "Enable portable mode (disable per-target optimizations)" OFF)
option(ENABLE_STATIC_TOOLS "Tools will be compiled as static executables" OFF)
option(ENABLE_ASAN "Enable ASAN support" OFF)
option(ENABLE_NETWORK "Enable tests requiring network access" ON)
option(ENABLE_DEVMODE "Enable development mode only debugging" OFF)
option(BUILD_TESTING "Build tests" ON)
option(ENABLE_GENERIC "Enable generic subsystem (requires statement expressions)" ON)
option(ENABLE_REFLECTION "Enable reflection subsystem (requires statement expressions)" ON)
option(ENABLE_LIBCLANG "Enable libclang support for reflection" ON)

# Path to LLVM installation (optional, overrides automatic detection)
set(LLVM_ROOT "" CACHE PATH "Path to LLVM installation directory (e.g., /usr/lib/llvm-15)")

# Read libtool version
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/.libtool-version" LIBTOOL_VERSION)
string(STRIP "${LIBTOOL_VERSION}" LIBTOOL_VERSION)

# Parse libtool version components (format is current:revision:age)
string(REGEX REPLACE "^([0-9]+):.*" "\\1" LIBTOOL_CURRENT "${LIBTOOL_VERSION}")
string(REGEX REPLACE "^[0-9]+:([0-9]+):.*" "\\1" LIBTOOL_REVISION "${LIBTOOL_VERSION}")
string(REGEX REPLACE "^[0-9]+:[0-9]+:([0-9]+)" "\\1" LIBTOOL_AGE "${LIBTOOL_VERSION}")

# Calculate SO version (current - age)
math(EXPR SO_VERSION "${LIBTOOL_CURRENT} - ${LIBTOOL_AGE}")
set(SO_VERSION_FULL "${SO_VERSION}.${LIBTOOL_AGE}.${LIBTOOL_REVISION}")

# Platform detection
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
    set(TARGET_CPU_X86_64 TRUE)
    set(TARGET_CPU_ANY_X86 TRUE)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "i.86|x86")
    set(TARGET_CPU_X86 TRUE)
    set(TARGET_CPU_ANY_X86 TRUE)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
    set(TARGET_CPU_ARM64 TRUE)
    set(TARGET_CPU_ANY_ARM TRUE)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm")
    set(TARGET_CPU_ARM TRUE)
    set(TARGET_CPU_ANY_ARM TRUE)
endif()

if(CMAKE_CROSSCOMPILING)
    set(CROSS_COMPILING 1)
    message(STATUS "Cross-compiling detected:")
    message(STATUS "  Build system: ${CMAKE_HOST_SYSTEM_NAME} ${CMAKE_HOST_SYSTEM_PROCESSOR}")
    message(STATUS "  Target system: ${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_PROCESSOR}")
else()
    set(CROSS_COMPILING 0)
endif()

# When cross-compiling for Windows, look for wine to run target executables
set(HAVE_WINE 0)
if(CROSS_COMPILING AND CMAKE_SYSTEM_NAME STREQUAL "Windows")
    find_program(WINE_EXECUTABLE NAMES wine64 wine)
    if(WINE_EXECUTABLE)
        set(HAVE_WINE 1)
        set(CMAKE_CROSSCOMPILING_EMULATOR "${WINE_EXECUTABLE}"
            CACHE STRING "Wine emulator for Windows cross-compilation tests" FORCE)
        message(STATUS "Found wine: ${WINE_EXECUTABLE} (tests will run under wine)")
    else()
        message(STATUS "wine not found; tests disabled for Windows cross-compilation")
    endif()
endif()

# Check for required dependencies
if(NOT WIN32)
    set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
    set(THREADS_PREFER_PTHREAD_FLAG TRUE)
    find_package(Threads REQUIRED)
    if(NOT CMAKE_USE_PTHREADS_INIT)
        message(FATAL_ERROR "Missing required pthread support")
    endif()
else()
    # Windows uses native threading APIs
    set(CMAKE_USE_WIN32_THREADS_INIT TRUE)
endif()

# detect if we're on clang-cl
if(WIN32 AND MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
    set(CLANG_CL TRUE)
endif()

# Check for headers and functions
check_include_file("alloca.h" HAVE_ALLOCA_H)
check_include_file("byteswap.h" HAVE_BYTESWAP_H)
check_function_exists(__builtin_bswap16 HAVE___BUILTIN_BSWAP16)
check_function_exists(__builtin_bswap32 HAVE___BUILTIN_BSWAP32)
check_function_exists(__builtin_bswap64 HAVE___BUILTIN_BSWAP64)
check_function_exists(qsort_r HAVE_QSORT_R)
check_function_exists(mremap HAVE_MREMAP)

# Check for environment variables
check_c_source_compiles("
    extern char **environ;
    int main() { return 0; }
" HAVE_DECL_ENVIRON)

# find PkgConfig
if(NOT CMAKE_CROSSCOMPILING)
    find_package(PkgConfig)
endif()

# Check for optional dependencies

# libyaml detection - try CMake config first, then pkg-config
set(HAVE_LIBYAML 0)
find_package(yaml QUIET CONFIG)
if(yaml_FOUND)
    set(HAVE_LIBYAML 1)
    # CMake config found, use imported target
    set(LIBYAML_LIBRARIES yaml)
    message(STATUS "Found libyaml via CMake config")
else()
    if(PKG_CONFIG_FOUND)
        pkg_check_modules(LIBYAML yaml-0.1)
        if(LIBYAML_FOUND)
            set(HAVE_LIBYAML 1)
            message(STATUS "Found libyaml via pkg-config")
        else()
            message(WARNING "failed to find libyaml; compatibility disabled")
        endif()
    endif()
endif()

# check framework detection - try CMake config first, then pkg-config, then FetchContent
# When cross-compiling with wine we skip find_package/pkg-config (those would
# find host libraries) and go straight to FetchContent so check is built for
# the target platform.
function(fy_target_compile_definition_if_exists _target_name)
    if(NOT TARGET ${_target_name})
        return()
    endif()

    get_target_property(_aliased_target ${_target_name} ALIASED_TARGET)
    if(_aliased_target)
        set(_target_name ${_aliased_target})
    endif()

    target_compile_definitions(${_target_name} PRIVATE _CRT_SECURE_NO_WARNINGS)
endfunction()

set(HAVE_CHECK 0)
set(HAVE_COMPATIBLE_CHECK 0)
if(BUILD_TESTING AND (NOT CMAKE_CROSSCOMPILING OR HAVE_WINE))
    if(NOT CMAKE_CROSSCOMPILING)
        find_package(check QUIET CONFIG)
        if(check_FOUND)
            set(HAVE_CHECK 1)
            set(CHECK_LIBRARIES check)
            if(TARGET Check::check)
                set(CHECK_TARGET Check::check)
            else()
                set(CHECK_TARGET check)
            endif()
            get_target_property(CHECK_INCLUDE_DIRS ${CHECK_TARGET} INTERFACE_INCLUDE_DIRECTORIES)
            # Get library location to extract directory
            get_target_property(CHECK_LIB_LOCATION ${CHECK_TARGET} IMPORTED_LOCATION)
            if(NOT CHECK_LIB_LOCATION)
                get_target_property(CHECK_LIB_LOCATION ${CHECK_TARGET} LOCATION)
            endif()
            if(CHECK_LIB_LOCATION)
                get_filename_component(CHECK_LIBRARY_DIRS "${CHECK_LIB_LOCATION}" DIRECTORY)
            endif()
            message(STATUS "Found check framework via CMake config")
        elseif(PKG_CONFIG_FOUND)
            # Fall back to pkg-config
            pkg_check_modules(CHECK check)
            if(CHECK_FOUND)
                set(HAVE_CHECK 1)
                message(STATUS "Found check framework via pkg-config")
            endif()
        endif()
    endif()

    # Fall back to FetchContent if check not found.
    # Always used when cross-compiling (skips host detection above) so that
    # check is compiled for the target platform and can run under wine.
    if(NOT HAVE_CHECK)
        message(STATUS "check framework not found, fetching via FetchContent...")
        FetchContent_Declare(
            check
            GIT_REPOSITORY https://github.com/libcheck/check.git
            GIT_TAG        0.15.2
        )
        # Disable check's own tests and docs
        set(CHECK_ENABLE_TESTS OFF CACHE BOOL "" FORCE)
        set(CHECK_ENABLE_GCOV OFF CACHE BOOL "" FORCE)
        if(WIN32)
            # check's itimerspec/clock_gettime detection is unreliable on
            # Windows (both MinGW and MSVC/Clang): probe them explicitly.
            include(CheckTypeSize)
            check_type_size("struct itimerspec" ITIMERSPEC LANGUAGE C)
            if(NOT HAVE_ITIMERSPEC)
                set(STRUCT_ITIMERSPEC_DEFINITION_MISSING 1 CACHE INTERNAL "" FORCE)
                add_compile_definitions(STRUCT_ITIMERSPEC_DEFINITION_MISSING=1)
            endif()
            # Windows always provides clock_gettime (MinGW via winpthread,
            # MSVC via Windows SDK), but check_function_exists may fail to
            # detect it. Force it so check's configure_file emits
            # #define HAVE_CLOCK_GETTIME 1 and skips its own stub.
            set(HAVE_CLOCK_GETTIME 1 CACHE INTERNAL "" FORCE)
        endif()
        FetchContent_MakeAvailable(check)
        if(WIN32)
            fy_target_compile_definition_if_exists(check)
            fy_target_compile_definition_if_exists(Check::check)
            fy_target_compile_definition_if_exists(checkShared)
            fy_target_compile_definition_if_exists(Check::checkShared)
        endif()
        if(TARGET checkShared)
            # check unconditionally creates a checkShared SHARED target alongside
            # the static check target. We only need the static library; exclude the
            # shared target and give it a distinct output name so that Ninja never
            # sees two rules generating the same check.lib import library.
            set_target_properties(checkShared PROPERTIES
                EXCLUDE_FROM_ALL TRUE
                OUTPUT_NAME checkDynamic)
        endif()

        set(HAVE_CHECK 1)
        set(CHECK_LIBRARIES check)
        set(CHECK_INCLUDE_DIRS "${check_SOURCE_DIR}/src" "${check_BINARY_DIR}" "${check_BINARY_DIR}/src")
        set(HAVE_SRUNNER_SET_TAP 1)  # 0.15.2 has srunner_set_tap
        set(HAVE_COMPATIBLE_CHECK 1)
        message(STATUS "Found check framework via FetchContent")
    endif()

    # Check if libcheck has srunner_set_tap (jessie has outdated libcheck).
    # FetchContent always has it (set above); only probe when using a system library.
    if(HAVE_CHECK AND NOT HAVE_COMPATIBLE_CHECK)
        set(_SAVED_CMAKE_REQUIRED_LIBRARIES "${CMAKE_REQUIRED_LIBRARIES}")
        set(_SAVED_CMAKE_REQUIRED_INCLUDES  "${CMAKE_REQUIRED_INCLUDES}")
        set(_SAVED_CMAKE_REQUIRED_LINK_OPTIONS "${CMAKE_REQUIRED_LINK_OPTIONS}")
        set(CMAKE_REQUIRED_LIBRARIES ${CHECK_LIBRARIES})
        set(CMAKE_REQUIRED_INCLUDES ${CHECK_INCLUDE_DIRS})
        foreach(libdir ${CHECK_LIBRARY_DIRS})
            list(APPEND CMAKE_REQUIRED_LINK_OPTIONS "-L${libdir}")
        endforeach()
        check_symbol_exists(srunner_set_tap "check.h" HAVE_SRUNNER_SET_TAP)
        set(CMAKE_REQUIRED_LIBRARIES "${_SAVED_CMAKE_REQUIRED_LIBRARIES}")
        set(CMAKE_REQUIRED_INCLUDES  "${_SAVED_CMAKE_REQUIRED_INCLUDES}")
        set(CMAKE_REQUIRED_LINK_OPTIONS "${_SAVED_CMAKE_REQUIRED_LINK_OPTIONS}")
        unset(_SAVED_CMAKE_REQUIRED_LIBRARIES)
        unset(_SAVED_CMAKE_REQUIRED_INCLUDES)
        unset(_SAVED_CMAKE_REQUIRED_LINK_OPTIONS)
        if(HAVE_SRUNNER_SET_TAP)
            set(HAVE_COMPATIBLE_CHECK 1)
        endif()
    endif()
endif()

# Check for libclang (for reflection support)
set(HAVE_LIBCLANG 0)
set(LIBCLANG_CFLAGS "")
set(LIBCLANG_LDFLAGS "")
set(LIBCLANG_LIBS "")
set(LIBCLANG_FOUND FALSE)
set(LIBCLANG_DETECTION_METHOD "")

if(NOT ENABLE_REFLECTION)
    message(STATUS "libclang support disabled (ENABLE_REFLECTION=OFF)")
    set(ENABLE_LIBCLANG OFF CACHE BOOL "Disable libclang" FORCE)
endif()

if(NOT HAVE_CMAKE_GEXPR)
    message(STATUS "libclang support disabled (CMake version is too old (<=3.27)")
    set(ENABLE_LIBCLANG OFF CACHE BOOL "Disable libclang" FORCE)
endif()

if(NOT ENABLE_LIBCLANG)
    message(STATUS "libclang support disabled (ENABLE_LIBCLANG=OFF)")
else()

    # If user specified LLVM_ROOT, add it to CMAKE_PREFIX_PATH for this search
    if(LLVM_ROOT)
        message(STATUS "Using user-specified LLVM_ROOT: ${LLVM_ROOT}")
        list(PREPEND CMAKE_PREFIX_PATH "${LLVM_ROOT}")
    endif()

    # Try CMake config mode first
    find_package(LLVM QUIET CONFIG)

    # If LLVM was found, help CMake find Clang in the same installation
    if(LLVM_FOUND)
        # Clang's config is typically in ${LLVM_INSTALL_PREFIX}/lib/cmake/clang
        set(Clang_DIR "${LLVM_INSTALL_PREFIX}/lib/cmake/clang")
    endif()

    # Avoid find_package(Clang CONFIG): some distros ship LLVM packages where
    # ClangTargets.cmake references missing static libs (e.g. libclangBasic.a)
    # and emits a CMake FATAL_ERROR that QUIET cannot suppress. Find libclang
    # directly instead — we only need the shared lib and the include path.
    if(LLVM_FOUND)
        find_library(_LIBCLANG_LIB NAMES clang libclang
                     PATHS "${LLVM_LIBRARY_DIRS}" NO_DEFAULT_PATH)
        if(_LIBCLANG_LIB)
            set(Clang_FOUND TRUE)
            set(CLANG_INCLUDE_DIRS "${LLVM_INCLUDE_DIRS}")
            set(LIBCLANG_FULL_PATH "${_LIBCLANG_LIB}")
        else()
            message(STATUS "LLVM ${LLVM_PACKAGE_VERSION}: libclang not found in ${LLVM_LIBRARY_DIRS}, skipping")
            set(LLVM_FOUND FALSE)
        endif()
        unset(_LIBCLANG_LIB CACHE)
    endif()

    if(LLVM_FOUND AND Clang_FOUND)

        message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION} via CMake config")
        message(STATUS "  LLVM_INSTALL_PREFIX: ${LLVM_INSTALL_PREFIX}")
        message(STATUS "  LLVM_INCLUDE_DIRS: ${LLVM_INCLUDE_DIRS}")

        # Use LLVM's CMake variables directly
        set(LIBCLANG_CFLAGS_LIST ${LLVM_DEFINITIONS})
        list(APPEND LIBCLANG_CFLAGS_LIST "-I${LLVM_INCLUDE_DIRS}")
        list(APPEND LIBCLANG_CFLAGS_LIST "-I${CLANG_INCLUDE_DIRS}")

        # Set up library directories
        set(LIBCLANG_LINK_DIRS ${LLVM_LIBRARY_DIRS})

        # Link against libclang (C API) and LLVM core libs.
        # Use the full path found by find_library to avoid -lclang resolution
        # issues when the lib dir isn't in the default search path.
        if(TARGET libclang)
            set(LIBCLANG_LIBS_LIST libclang LLVM)
        else()
            set(LIBCLANG_LIBS_LIST "${LIBCLANG_FULL_PATH}" LLVM)
        endif()

        set(LIBCLANG_INCLUDES "${LLVM_INCLUDE_DIRS};${CLANG_INCLUDE_DIRS}")
        set(LIBCLANG_FOUND TRUE)
        set(LIBCLANG_DETECTION_METHOD "CMake config")
    elseif(NOT CROSS_COMPILING)
        # Fallback to llvm-config method (only when not cross-compiling)
        message(STATUS "LLVM CMake config not found, trying llvm-config")

        # Try to find llvm-config (versions 20 down to 10)
        set(LLVM_CONFIG_NAMES llvm-config)
        foreach(ver RANGE 20 10 -1)
            list(APPEND LLVM_CONFIG_NAMES llvm-config-${ver})
        endforeach()

        find_program(LLVM_CONFIG NAMES ${LLVM_CONFIG_NAMES})

        if(LLVM_CONFIG)
            execute_process(COMMAND ${LLVM_CONFIG} --cflags OUTPUT_VARIABLE LIBCLANG_CFLAGS_TEMP OUTPUT_STRIP_TRAILING_WHITESPACE)
            execute_process(COMMAND ${LLVM_CONFIG} --ldflags OUTPUT_VARIABLE LIBCLANG_LDFLAGS_TEMP OUTPUT_STRIP_TRAILING_WHITESPACE)
            execute_process(COMMAND ${LLVM_CONFIG} --libs OUTPUT_VARIABLE LLVM_LIBS_TEMP OUTPUT_STRIP_TRAILING_WHITESPACE)
            execute_process(COMMAND ${LLVM_CONFIG} --system-libs OUTPUT_VARIABLE LLVM_SYSLIBS_TEMP OUTPUT_STRIP_TRAILING_WHITESPACE)

            set(LIBCLANG_CFLAGS_LIST "${LIBCLANG_CFLAGS_TEMP} ${LIBCLANG_LDFLAGS_TEMP}")
            set(LIBCLANG_LIBS_LIST "-lclang;${LLVM_LIBS_TEMP};${LLVM_SYSLIBS_TEMP}")
            set(LIBCLANG_CFLAGS "${LIBCLANG_CFLAGS_TEMP}")
            set(LIBCLANG_LDFLAGS "${LIBCLANG_LDFLAGS_TEMP}")
            set(LIBCLANG_FOUND TRUE)
            set(LIBCLANG_DETECTION_METHOD "llvm-config: ${LLVM_CONFIG}")
        else()
            # Last resort: no LLVMConfig.cmake and no llvm-config.
            # Try to find libclang directly under LLVM_ROOT (if set) or under
            # well-known platform-specific default install locations.
            set(_LLVM_SEARCH_ROOTS)
            if(LLVM_ROOT)
                list(APPEND _LLVM_SEARCH_ROOTS "${LLVM_ROOT}")
            endif()
            if(WIN32)
                # Chocolatey installs to Program Files/LLVM by default
                list(APPEND _LLVM_SEARCH_ROOTS
                    "C:/Program Files/LLVM"
                    "C:/LLVM")
            else()
                # Homebrew (macOS) and common Linux package manager paths
                list(APPEND _LLVM_SEARCH_ROOTS
                    "/opt/homebrew/opt/llvm"
                    "/usr/local/opt/llvm")
                foreach(ver RANGE 20 10 -1)
                    list(APPEND _LLVM_SEARCH_ROOTS
                        "/usr/lib/llvm-${ver}"
                        "/usr/local/lib/llvm-${ver}")
                endforeach()
            endif()
            foreach(_root IN LISTS _LLVM_SEARCH_ROOTS)
                if(NOT LIBCLANG_FOUND)
                    find_library(_LIBCLANG_LIB_DIRECT NAMES clang libclang
                                 PATHS "${_root}/lib" NO_DEFAULT_PATH)
                    if(_LIBCLANG_LIB_DIRECT AND EXISTS "${_root}/include/clang-c/Index.h")
                        set(LIBCLANG_CFLAGS_LIST "")
                        set(LIBCLANG_LINK_DIRS "${_root}/lib")
                        set(LIBCLANG_LIBS_LIST "${_LIBCLANG_LIB_DIRECT}")
                        set(LIBCLANG_INCLUDES "${_root}/include")
                        set(LIBCLANG_FOUND TRUE)
                        set(LIBCLANG_DETECTION_METHOD "direct path: ${_root}")
                        message(STATUS "Found libclang via direct path: ${_LIBCLANG_LIB_DIRECT}")
                    endif()
                    unset(_LIBCLANG_LIB_DIRECT CACHE)
                endif()
            endforeach()
        endif()
    else()
        message(STATUS "Cross-compiling: LLVM CMake config not found")
        message(STATUS "  Set LLVM_ROOT to LLVM install directory (e.g., /usr/lib/llvm-18)")
        message(STATUS "  Or set CMAKE_PREFIX_PATH or LLVM_DIR to the directory containing LLVMConfig.cmake")
    endif()

    # Test if libclang actually works (single test for all detection methods)
    if(LIBCLANG_FOUND)
        if(NOT CROSS_COMPILING)
            check_libclang_works(LIBCLANG_WORKS
                CFLAGS "${LIBCLANG_CFLAGS_LIST}"
                INCLUDES "${LIBCLANG_INCLUDES}"
                LINK_DIRS "${LIBCLANG_LINK_DIRS}"
                LIBRARIES "${LIBCLANG_LIBS_LIST}"
            )
        else()
            # When cross-compiling, disable libclang by default to avoid linking host libraries
            # User can set LLVM_ROOT, CMAKE_PREFIX_PATH, or LLVM_DIR to point to target-specific LLVM installation
            message(STATUS "Cross-compiling: disabling libclang")
            message(STATUS "  To enable, set LLVM_ROOT or CMAKE_PREFIX_PATH to target LLVM installation")
            set(LIBCLANG_WORKS FALSE)
        endif()

        if(LIBCLANG_WORKS)
            set(HAVE_LIBCLANG 1)
            # Convert list to string for LIBCLANG_CFLAGS if needed (CMake config path)
            if(LIBCLANG_DETECTION_METHOD STREQUAL "CMake config")
                string(REPLACE ";" " " LIBCLANG_CFLAGS "${LIBCLANG_CFLAGS_LIST}")
            endif()
            set(LIBCLANG_LIBS "${LIBCLANG_LIBS_LIST}")
            message(STATUS "Found libclang via ${LIBCLANG_DETECTION_METHOD}")
            # Check for llvm-c/Core.h (absent in stripped installs e.g. Chocolatey)
            set(_SAVED_CMAKE_REQUIRED_INCLUDES "${CMAKE_REQUIRED_INCLUDES}")
            set(CMAKE_REQUIRED_INCLUDES "${LIBCLANG_INCLUDES}")
            include(CheckIncludeFile)
            check_include_file("llvm-c/Core.h" HAVE_LLVM_C_CORE_H)
            set(CMAKE_REQUIRED_INCLUDES "${_SAVED_CMAKE_REQUIRED_INCLUDES}")
            if(HAVE_LLVM_C_CORE_H)
                list(APPEND COMMON_C_DEFINITIONS HAVE_LLVM_C_CORE_H)
            endif()
        else()
            message(STATUS "libclang not found or not working")
            message(STATUS "  Reflection support will be disabled")
            message(STATUS "  To specify libclang location: cmake -DLLVM_ROOT=/path/to/llvm")
            message(STATUS "  To disable this check: cmake -DENABLE_LIBCLANG=OFF")
        endif()
    else()
        if(NOT LIBCLANG_FOUND)
            message(STATUS "libclang not found")
            message(STATUS "  Reflection support will be disabled")
            message(STATUS "  To specify libclang location: cmake -DLLVM_ROOT=/path/to/llvm")
            message(STATUS "  To disable this check: cmake -DENABLE_LIBCLANG=OFF")
        endif()
    endif()

endif() # ENABLE_LIBCLANG

# check for bash
find_program(BASH_EXECUTABLE bash)
if(BASH_EXECUTABLE)
    set(HAVE_BASH 1)
else()
    set(HAVE_BASH 0)
endif()

# Check for tools
find_program(GIT_EXECUTABLE git)
if(GIT_EXECUTABLE)
    set(HAVE_GIT 1)
else()
    set(HAVE_GIT 0)
endif()

find_program(JQ_EXECUTABLE jq)
if(JQ_EXECUTABLE)
    set(HAVE_JQ 1)
else()
    set(HAVE_JQ 0)
endif()

find_program(DOCKER_EXECUTABLE docker)
if(DOCKER_EXECUTABLE)
    set(HAVE_DOCKER 1)
else()
    set(HAVE_DOCKER 0)
endif()

# Test suite URLs and commits
if(NOT TESTSUITEURL)
    set(TESTSUITEURL "https://github.com/yaml/yaml-test-suite")
endif()
if(NOT TESTSUITECHECKOUT)
    set(TESTSUITECHECKOUT "6e6c296ae9c9d2d5c4134b4b64d01b29ac19ff6f")
endif()
if(NOT JSONTESTSUITEURL)
    set(JSONTESTSUITEURL "https://github.com/nst/JSONTestSuite")
endif()
if(NOT JSONTESTSUITECHECKOUT)
    set(JSONTESTSUITECHECKOUT "d64aefb55228d9584d3e5b2433f720ea8fd00c82")
endif()

# FetchContent for test suites (downloaded at configure time when network is available)
if(ENABLE_NETWORK)
    FetchContent_Declare(
        yaml_test_suite
        GIT_REPOSITORY ${TESTSUITEURL}
        GIT_TAG        ${TESTSUITECHECKOUT}
        GIT_SHALLOW    TRUE
    )
    FetchContent_Declare(
        json_test_suite
        GIT_REPOSITORY ${JSONTESTSUITEURL}
        GIT_TAG        ${JSONTESTSUITECHECKOUT}
        GIT_SHALLOW    FALSE
    )
endif()

# SIMD capability detection
set(TARGET_HAS_SSE2 FALSE)
set(TARGET_HAS_SSE41 FALSE)
set(TARGET_HAS_AVX2 FALSE)
set(TARGET_HAS_AVX512 FALSE)
set(TARGET_HAS_NEON FALSE)

# Disable SIMD assembly on Windows (uses Unix assembly syntax)
# Use portable implementations instead
if(WIN32)
    message(STATUS "Windows detected: using portable SIMD implementations")
elseif(TARGET_CPU_X86_64 AND NOT ENABLE_PORTABLE_TARGET)
    check_c_source_compiles("
        #include <emmintrin.h>
        int main() { __m128i a = _mm_setzero_si128(); return 0; }
    " COMPILER_SUPPORTS_SSE2)
    if(COMPILER_SUPPORTS_SSE2)
        set(TARGET_HAS_SSE2 TRUE)
    endif()

    check_c_source_compiles("
        #include <smmintrin.h>
        int main() { __m128i a = _mm_setzero_si128(); return 0; }
    " COMPILER_SUPPORTS_SSE41)
    if(COMPILER_SUPPORTS_SSE41)
        set(TARGET_HAS_SSE41 TRUE)
    endif()

    set(CMAKE_REQUIRED_FLAGS "-mavx2")
    check_c_source_compiles("
        #include <immintrin.h>
        int main() { __m256i a = _mm256_setzero_si256(); return 0; }
    " COMPILER_SUPPORTS_AVX2)
    set(CMAKE_REQUIRED_FLAGS "")
    if(COMPILER_SUPPORTS_AVX2)
        set(TARGET_HAS_AVX2 TRUE)
    endif()

    set(CMAKE_REQUIRED_FLAGS "-mavx512f -mavx512vl")
    check_c_source_compiles("
        #include <immintrin.h>
        int main() { __m512i a = _mm512_setzero_si512(); return 0; }
    " COMPILER_SUPPORTS_AVX512)
    set(CMAKE_REQUIRED_FLAGS "")
    if(COMPILER_SUPPORTS_AVX512)
        set(TARGET_HAS_AVX512 TRUE)
    endif()
endif()

if(TARGET_CPU_ANY_ARM AND NOT ENABLE_PORTABLE_TARGET)
    if(TARGET_CPU_ARM64)
        set(TARGET_HAS_NEON TRUE)
    else()
        check_c_source_compiles("
            #include <arm_neon.h>
            int main() { uint8x16_t a = vdupq_n_u8(0); return 0; }
        " COMPILER_SUPPORTS_NEON)
        if(COMPILER_SUPPORTS_NEON)
            set(TARGET_HAS_NEON TRUE)
        endif()
    endif()
endif()

# ASAN support
set(HAVE_ASAN 0)
if(ENABLE_ASAN)
    check_c_source_compiles("
        int main() { return 0; }
    " ASAN_WORKS)
    if(ASAN_WORKS)
        set(HAVE_ASAN 1)
    endif()
endif()

set(ASAN_C_FLAGS)
if(HAVE_ASAN)
  set(ASAN_C_FLAGS "-fsanitize=address,signed-integer-overflow,undefined" "-fno-omit-frame-pointer")
endif()

# Some older toolchains need libatomic for 8-byte atomics.
set(ATOMIC_LIBS "")
if(NOT WIN32 AND NOT MSVC)
    set(CMAKE_REQUIRED_LIBRARIES_SAVE "${CMAKE_REQUIRED_LIBRARIES}")
    set(CMAKE_REQUIRED_LIBRARIES "")
    check_c_source_compiles("
        volatile unsigned long long v = 1;
        int main(void) {
            return (int)__atomic_fetch_sub(&v, 1, __ATOMIC_SEQ_CST);
        }
    " ATOMIC_LINKS_WITHOUT_LIBATOMIC)
    if(NOT ATOMIC_LINKS_WITHOUT_LIBATOMIC)
        set(CMAKE_REQUIRED_LIBRARIES "atomic")
        check_c_source_compiles("
            volatile unsigned long long v = 1;
            int main(void) {
                return (int)__atomic_fetch_sub(&v, 1, __ATOMIC_SEQ_CST);
            }
        " ATOMIC_LINKS_WITH_LIBATOMIC)
        if(ATOMIC_LINKS_WITH_LIBATOMIC)
            set(ATOMIC_LIBS "-latomic")
        else()
            message(FATAL_ERROR "compiler supports __atomic builtins but linking 8-byte atomics failed, even with -latomic")
        endif()
    endif()
    set(CMAKE_REQUIRED_LIBRARIES "${CMAKE_REQUIRED_LIBRARIES_SAVE}")
endif()

# Build common compiler flags for all targets
set(COMMON_C_FLAGS)
if(WIN32 AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
    # MSVC compat mode (enabled by default when clang targets x86_64-windows-msvc)
    # breaks __VA_OPT__ preprocessing — empty __VA_ARGS__ causes completely wrong
    # macro expansion. Apply only to libfyaml targets (not FetchContent deps).
    list(APPEND COMMON_C_FLAGS -fno-ms-compatibility)
endif()
if(WIN32)
    set(COMMON_C_DEFINITIONS WIN32_LEAN_AND_MEAN _CRT_SECURE_NO_WARNINGS)
else()
    set(COMMON_C_DEFINITIONS _GNU_SOURCE)
endif()

# MSVC requires C11 for stdatomic.h
if(MSVC)
    # Use /std:c17 for better C11/C17 support in MSVC
    set(CMAKE_C_STANDARD 17)
    set(CMAKE_C_STANDARD_REQUIRED ON)

    # Enable experimental C11 atomics in MSVC (only)
    if(NOT CLANG_CL)
        add_compile_options(/experimental:c11atomics)
    endif()
endif()

if(NOT MSVC)
    list(APPEND COMMON_C_FLAGS -Wall -Wsign-compare)
    if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
        list(APPEND COMMON_C_FLAGS -Wextra)
    endif()

    set(USE_STD 0)

    if(NOT USE_STD)
        check_c_compiler_flag(-std=c2x COMPILER_SUPPORTS_GNU2X)
        if(COMPILER_SUPPORTS_GNU2X)
            list(APPEND COMMON_C_FLAGS -std=gnu2x)
            set(USE_STD 1)
        endif()
    endif()

    if(NOT USE_STD)
        check_c_compiler_flag(-std=c2x COMPILER_SUPPORTS_C2X)
        if(COMPILER_SUPPORTS_C2X)
            list(APPEND COMMON_C_FLAGS -std=c2x)
            set(USE_STD 1)
        endif()
    endif()

    if(NOT USE_STD)
        check_c_compiler_flag(-std=gnu11 COMPILER_SUPPORTS_GNU11)
        if(COMPILER_SUPPORTS_GNU11)
            list(APPEND COMMON_C_FLAGS -std=gnu11)
            set(USE_STD 1)
        endif()
    endif()

    # GCC heap trampolines support (for nested functions)
    # Heap trampolines are more secure than executable stack trampolines
    # Available in GCC 14+ with -ftrampoline-impl=heap
    # Supported targets: x86_64/i386/aarch64 Linux, x86_64/i386 Darwin
    # Note: we link-test a nested function to catch platforms (e.g. Cygwin)
    # where the compiler accepts the flag but the runtime support is missing.
    set(HAVE_HEAP_TRAMPOLINES 0)
    if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
        set(CMAKE_REQUIRED_FLAGS_SAVE "${CMAKE_REQUIRED_FLAGS}")
        set(CMAKE_REQUIRED_FLAGS "-ftrampoline-impl=heap")
        check_c_source_compiles("
typedef void (*fn_t)(void);
fn_t make_trampoline(int x) {
    void nested(void) { (void)x; }
    return nested;
}
int main(void) {
    fn_t f = make_trampoline(42);
    f();
    return 0;
}
" COMPILER_SUPPORTS_HEAP_TRAMPOLINES)
        set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS_SAVE}")
        if(COMPILER_SUPPORTS_HEAP_TRAMPOLINES)
            set(HAVE_HEAP_TRAMPOLINES 1)
            list(APPEND COMMON_C_FLAGS -ftrampoline-impl=heap)
            message(STATUS "GCC heap trampolines enabled (-ftrampoline-impl=heap)")
        else()
            message(STATUS "GCC heap trampolines not available or runtime missing, using stack trampolines")
        endif()
    endif()

endif()

# Check for GCC statement expressions ({ ... }) support
# MSVC does not support these; GCC and Clang do.
check_c_source_compiles("
int main(void) {
    int x = ({ int y = 1; y + 1; });
    return x - 2;
}
" HAVE_STATEMENT_EXPRESSIONS)
test_big_endian(IS_BIG_ENDIAN)
if(NOT IS_BIG_ENDIAN)
    set(HAVE_GENERIC_PLATFORM 1)
else()
    set(HAVE_GENERIC_PLATFORM 0)
endif()

if(HAVE_STATEMENT_EXPRESSIONS AND HAVE_GENERIC_PLATFORM)
    list(APPEND COMMON_C_DEFINITIONS HAVE_STATEMENT_EXPRESSIONS)

    if(ENABLE_GENERIC)
        set(HAVE_GENERIC 1)
        list(APPEND COMMON_C_DEFINITIONS HAVE_GENERIC)
    else()
        set(HAVE_GENERIC 0)
    endif()

    if(ENABLE_REFLECTION)
        set(HAVE_REFLECTION 1)
        list(APPEND COMMON_C_DEFINITIONS HAVE_REFLECTION)
    else()
        set(HAVE_REFLECTION 0)
    endif()
elseif(HAVE_STATEMENT_EXPRESSIONS)
    list(APPEND COMMON_C_DEFINITIONS HAVE_STATEMENT_EXPRESSIONS)

    set(HAVE_GENERIC 0)
    if(ENABLE_GENERIC)
        message(STATUS "Generic subsystem disabled (requires little-endian targets)")
    endif()

    if(ENABLE_REFLECTION)
        set(HAVE_REFLECTION 1)
        list(APPEND COMMON_C_DEFINITIONS HAVE_REFLECTION)
    else()
        set(HAVE_REFLECTION 0)
    endif()
else()
    set(HAVE_GENERIC 0)
    set(HAVE_REFLECTION 0)
    if(ENABLE_GENERIC OR ENABLE_REFLECTION)
        message(STATUS "Statement expressions not supported; generic and reflection disabled")
    endif()
endif()

# the options for the libs
set(LIB_OPTS "")
if(NOT MSVC AND NOT WIN32)
    if(BUILD_SHARED_LIBS)
        list(APPEND LIB_OPTS "-fPIC")
    endif()
endif()

# Network tests
if(ENABLE_NETWORK)
    set(HAVE_NETWORK 1)
else()
    set(HAVE_NETWORK 0)
endif()

# Dev mode
if(ENABLE_DEVMODE)
    set(HAVE_DEVMODE 1)
else()
    set(HAVE_DEVMODE 0)
endif()

# Static support
if(NOT BUILD_SHARED_LIBS)
    set(HAVE_STATIC 1)
else()
    set(HAVE_STATIC 0)
endif()

# Library sources
set(LIBHDRS
    src/lib/fy-accel.h
    src/lib/fy-atom.h
    src/lib/fy-composer.h
    src/lib/fy-cache.h
    src/lib/fy-diag.h
    src/lib/fy-doc.h
    src/lib/fy-docbuilder.h
    src/lib/fy-docstate.h
    src/lib/fy-dump.h
    src/lib/fy-emit.h
    src/lib/fy-emit-accum.h
    src/lib/fy-event.h
    src/lib/fy-input.h
    src/lib/fy-parse.h
    src/lib/fy-path.h
    src/lib/fy-token.h
    src/lib/fy-types.h
    src/lib/fy-walk.h
)

set(UTILHDRS
    src/util/fy-blob.h
    src/util/fy-ctype.h
    src/util/fy-endian.h
    src/util/fy-id.h
    src/util/fy-list.h
    src/util/fy-typelist.h
    src/util/fy-utf8.h
    src/util/fy-utils.h
    src/util/fy-align.h
    src/util/fy-bit64.h
    src/util/fy-vlsize.h
    src/util/fy-win32.h
)

set(LIBSRCS
    src/lib/fy-accel.c
    src/lib/fy-atom.c
    src/lib/fy-composer.c
    src/lib/fy-cache.c
    src/lib/fy-diag.c
    src/lib/fy-doc.c
    src/lib/fy-docbuilder.c
    src/lib/fy-docstate.c
    src/lib/fy-dump.c
    src/lib/fy-emit.c
    src/lib/fy-event.c
    src/lib/fy-input.c
    src/lib/fy-parse.c
    src/lib/fy-path.c
    src/lib/fy-token.c
    src/lib/fy-types.c
    src/lib/fy-walk.c
    src/util/fy-blob.c
    src/util/fy-ctype.c
    src/util/fy-utf8.c
    src/util/fy-utils.c
    src/xxhash/xxhash.c
    src/thread/fy-thread.c
    src/allocator/fy-allocator.c
    src/allocator/fy-allocator-linear.c
    src/allocator/fy-allocator-malloc.c
    src/allocator/fy-allocator-mremap.c
    src/allocator/fy-allocator-dedup.c
    src/allocator/fy-allocator-auto.c
    src/blake3/blake3_host_state.c
    src/blake3/blake3_backend.c
    src/blake3/blake3_be_cpusimd.c
    src/blake3/fy-blake3.c
    src/lib/fy-composer-diag.c
    src/lib/fy-doc-diag.c
    src/lib/fy-docbuilder-diag.c
    src/lib/fy-input-diag.c
    src/lib/fy-parse-diag.c
)

if(HAVE_GENERIC)
    list(APPEND LIBSRCS
        src/generic/fy-generic.c
        src/generic/fy-generic-docbuilder.c
        src/generic/fy-generic-decoder.c
        src/generic/fy-generic-encoder.c
        src/generic/fy-generic-op.c
        src/generic/fy-generic-iter.c
    )
endif()

if(HAVE_REFLECTION)
    list(APPEND LIBSRCS
        src/reflection/fy-reflection.c
        src/reflection/fy-packed-backend.c
        src/reflection/fy-null-backend.c
        src/reflection/fy-registry.c
        src/reflection/fy-type-meta.c
        src/reflection/fy-meta-type-system.c
        src/reflection/fy-type-context.c
        src/reflection/fy-meta-serdes.c
        src/reflection/fy-reflection-util.c
    )
    # Conditionally add clang backend if libclang is available
    if(HAVE_LIBCLANG)
        list(APPEND LIBSRCS src/reflection/fy-clang-backend.c)
    endif()
endif()

set(LIBHDRSPUB
    include/libfyaml/libfyaml-util.h
    include/libfyaml/libfyaml-core.h
    include/libfyaml/libfyaml-path-exec.h
    include/libfyaml/libfyaml-dociter.h
    include/libfyaml/libfyaml-docbuild.h
    include/libfyaml/libfyaml-composer.h
    include/libfyaml/libfyaml-allocator.h
    include/libfyaml/libfyaml-thread.h
    include/libfyaml/libfyaml-blake3.h
    include/libfyaml/libfyaml-align.h
    include/libfyaml/libfyaml-endian.h
    include/libfyaml/libfyaml-vlsize.h
    include/libfyaml/libfyaml-atomics.h
)

if(HAVE_REFLECTION)
    list(APPEND LIBHDRSPUB include/libfyaml/libfyaml-reflection.h)
endif()
if(HAVE_GENERIC)
    list(APPEND LIBHDRSPUB include/libfyaml/libfyaml-generic.h)
endif()

# BLAKE3 portable library
add_library(b3portable OBJECT
    src/blake3/blake3_portable.c
    src/blake3/blake3.c
)
target_include_directories(b3portable PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
    ${CMAKE_CURRENT_SOURCE_DIR}/src/util
    ${CMAKE_CURRENT_SOURCE_DIR}/src/thread
    ${CMAKE_CURRENT_SOURCE_DIR}/src/blake3
)
target_compile_definitions(b3portable PRIVATE
    ${COMMON_C_DEFINITIONS}
    HASHER_SUFFIX=portable
    SIMD_DEGREE=1
)
target_compile_options(b3portable PRIVATE ${COMMON_C_FLAGS} ${LIB_OPTS})

# BLAKE3 SIMD libraries
set(BLAKE3_TARGETS b3portable)

if(TARGET_HAS_SSE2)
    add_library(b3sse2 OBJECT
        src/blake3/blake3_sse2.c
        src/blake3/blake3_sse2_x86-64_unix.S
        src/blake3/blake3.c
    )
    target_include_directories(b3sse2 PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/include
        ${CMAKE_CURRENT_SOURCE_DIR}/src/util
        ${CMAKE_CURRENT_SOURCE_DIR}/src/thread
        ${CMAKE_CURRENT_SOURCE_DIR}/src/blake3
    )
    target_compile_definitions(b3sse2 PRIVATE
        ${COMMON_C_DEFINITIONS}
        HASHER_SUFFIX=sse2
        SIMD_DEGREE=4
    )
    target_compile_options(b3sse2 PRIVATE ${COMMON_C_FLAGS} -msse2 ${LIB_OPTS})
    list(APPEND BLAKE3_TARGETS b3sse2)
endif()

if(TARGET_HAS_SSE41)
    add_library(b3sse41 OBJECT
        src/blake3/blake3_sse41.c
        src/blake3/blake3_sse41_x86-64_unix.S
        src/blake3/blake3.c
    )
    target_include_directories(b3sse41 PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/include
        ${CMAKE_CURRENT_SOURCE_DIR}/src/util
        ${CMAKE_CURRENT_SOURCE_DIR}/src/thread
        ${CMAKE_CURRENT_SOURCE_DIR}/src/blake3
    )
    target_compile_definitions(b3sse41 PRIVATE
        ${COMMON_C_DEFINITIONS}
        HASHER_SUFFIX=sse41
        SIMD_DEGREE=4
    )
    target_compile_options(b3sse41 PRIVATE ${COMMON_C_FLAGS} -msse4.1 ${LIB_OPTS})
    list(APPEND BLAKE3_TARGETS b3sse41)
endif()

if(TARGET_HAS_AVX2)
    add_library(b3avx2 OBJECT
        src/blake3/blake3_avx2.c
        src/blake3/blake3_avx2_x86-64_unix.S
        src/blake3/blake3.c
    )
    target_include_directories(b3avx2 PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/include
        ${CMAKE_CURRENT_SOURCE_DIR}/src/util
        ${CMAKE_CURRENT_SOURCE_DIR}/src/thread
        ${CMAKE_CURRENT_SOURCE_DIR}/src/blake3
    )
    target_compile_definitions(b3avx2 PRIVATE
        ${COMMON_C_DEFINITIONS}
        HASHER_SUFFIX=avx2
        SIMD_DEGREE=8
    )
    target_compile_options(b3avx2 PRIVATE ${COMMON_C_FLAGS} -mavx2 ${LIB_OPTS})
    list(APPEND BLAKE3_TARGETS b3avx2)
endif()

if(TARGET_HAS_AVX512)
    add_library(b3avx512 OBJECT
        src/blake3/blake3_avx512.c
        src/blake3/blake3_avx512_x86-64_unix.S
        src/blake3/blake3.c
    )
    target_include_directories(b3avx512 PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/include
        ${CMAKE_CURRENT_SOURCE_DIR}/src/util
        ${CMAKE_CURRENT_SOURCE_DIR}/src/thread
        ${CMAKE_CURRENT_SOURCE_DIR}/src/blake3
    )
    target_compile_definitions(b3avx512 PRIVATE
        ${COMMON_C_DEFINITIONS}
        HASHER_SUFFIX=avx512
        SIMD_DEGREE=16
    )
    target_compile_options(b3avx512 PRIVATE ${COMMON_C_FLAGS} -mavx512f -mavx512vl ${LIB_OPTS})
    list(APPEND BLAKE3_TARGETS b3avx512)
endif()

if(TARGET_HAS_NEON)
    add_library(b3neon OBJECT
        src/blake3/blake3_neon.c
        src/blake3/blake3.c
    )
    target_include_directories(b3neon PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/include
        ${CMAKE_CURRENT_SOURCE_DIR}/src/util
        ${CMAKE_CURRENT_SOURCE_DIR}/src/thread
        ${CMAKE_CURRENT_SOURCE_DIR}/src/blake3
    )
    target_compile_definitions(b3neon PRIVATE
        ${COMMON_C_DEFINITIONS}
        HASHER_SUFFIX=neon
        SIMD_DEGREE=4
    )
    target_compile_options(b3neon PRIVATE ${COMMON_C_FLAGS} ${LIB_OPTS})
    if(TARGET_CPU_ARM)
        target_compile_options(b3neon PRIVATE -mfpu=neon)
    endif()
    list(APPEND BLAKE3_TARGETS b3neon)
endif()

foreach(blake3_target ${BLAKE3_TARGETS})
    set_property(TARGET ${blake3_target} PROPERTY
        POSITION_INDEPENDENT_CODE "${CMAKE_POSITION_INDEPENDENT_CODE}"
    )
endforeach()

# Main library
add_library(fyaml
    ${LIBHDRS}
    ${UTILHDRS}
    ${LIBSRCS}
    ${LIBHDRSPUB}
)

# Link BLAKE3 object libraries
foreach(blake3_target ${BLAKE3_TARGETS})
    target_sources(fyaml PRIVATE $<TARGET_OBJECTS:${blake3_target}>)
endforeach()

# On Windows, use a .def file to export only the public API symbols
# This is needed because libfyaml uses GCC-style suffix attributes for FY_EXPORT
# which are incompatible with MSVC's __declspec prefix requirement
if(WIN32 AND BUILD_SHARED_LIBS)
    # Generate the .def file from all public headers during build
    set(GENERATED_DEF_FILE "${CMAKE_CURRENT_BINARY_DIR}/fyaml.def")
    # Build a list of absolute paths for all public headers
    set(ABS_LIBHDRSPUB)
    foreach(HDR ${LIBHDRSPUB})
        list(APPEND ABS_LIBHDRSPUB "${CMAKE_CURRENT_SOURCE_DIR}/${HDR}")
    endforeach()
    # Write header list to a response file (one path per line) to avoid
    # issues with passing semicolon-separated lists via -D on native Windows.
    # On cmd.exe, backslash-escaped semicolons (\;) are passed literally,
    # corrupting the paths and causing "Invalid argument" from file(READ).
    set(HEADER_LIST_FILE "${CMAKE_CURRENT_BINARY_DIR}/def-header-list.txt")
    string(REPLACE ";" "\n" HEADER_LIST_CONTENT "${ABS_LIBHDRSPUB}")
    file(WRITE "${HEADER_LIST_FILE}" "${HEADER_LIST_CONTENT}\n")
    add_custom_command(
        OUTPUT ${GENERATED_DEF_FILE}
        COMMAND ${CMAKE_COMMAND}
            -DHEADER_LIST_FILE=${HEADER_LIST_FILE}
            -DOUTPUT_DEF=${GENERATED_DEF_FILE}
            -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/generate-def.cmake
        DEPENDS
            ${HEADER_LIST_FILE}
            ${ABS_LIBHDRSPUB}
            ${CMAKE_CURRENT_SOURCE_DIR}/cmake/generate-def.cmake
        COMMENT "Generating fyaml.def from public headers"
    )
    target_sources(fyaml PRIVATE "${GENERATED_DEF_FILE}")
endif()

add_library(libfyaml::libfyaml ALIAS fyaml)

# Always build a static library for tests (they need access to internal APIs)
# This is separate from BUILD_SHARED_LIBS and matches autotools behavior
add_library(fyaml_static STATIC
    ${LIBHDRS}
    ${UTILHDRS}
    ${LIBSRCS}
    ${LIBHDRSPUB}
)

# Link BLAKE3 object libraries to static version
foreach(blake3_target ${BLAKE3_TARGETS})
    target_sources(fyaml_static PRIVATE $<TARGET_OBJECTS:${blake3_target}>)
endforeach()

set_target_properties(fyaml_static PROPERTIES
    OUTPUT_NAME fyaml_static
    POSITION_INDEPENDENT_CODE ON
    FOLDER "Libraries"
)

# Configure static library with same settings as main library
target_include_directories(fyaml_static
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src/lib
        ${CMAKE_CURRENT_SOURCE_DIR}/src/util
        ${CMAKE_CURRENT_SOURCE_DIR}/src/xxhash
        ${CMAKE_CURRENT_SOURCE_DIR}/src/thread
        ${CMAKE_CURRENT_SOURCE_DIR}/src/allocator
        ${CMAKE_CURRENT_SOURCE_DIR}/src/blake3
        ${CMAKE_CURRENT_SOURCE_DIR}/src/generic
        ${CMAKE_CURRENT_SOURCE_DIR}/src/reflection
        ${CMAKE_CURRENT_BINARY_DIR}
)

if(NOT WIN32)
    target_link_libraries(fyaml_static
        PUBLIC
            Threads::Threads
    )
endif()

# Add libclang support if available
add_libclang_to_target(fyaml_static INTERFACE)

# check if blocks are available, and if so link them
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/CheckClangBlocks.cmake)

# Apply flags if available
if (HAVE_CLANG_BLOCKS)
    # Restrict the global -fblocks flag to C targets only: C++ targets inherit
    # add_compile_options() entries but -fblocks is a Clang/Apple extension
    # that GCC's C++ frontend rejects.  C targets also receive it via
    # COMMON_C_FLAGS below, so there is no change in behaviour for them.
    add_compile_options($<$<COMPILE_LANGUAGE:C>:-fblocks>)
    list(APPEND COMMON_C_FLAGS -fblocks)

    if (CLANG_BLOCKS_LIB)
        link_libraries(${CLANG_BLOCKS_LIB})
    endif()
endif()

target_compile_definitions(fyaml_static PRIVATE
    HAVE_CONFIG_H
    VERSION="${PROJECT_VERSION}"
    $<$<NOT:$<CONFIG:DEBUG>>:NDEBUG>
)

target_compile_definitions(fyaml_static PRIVATE ${COMMON_C_DEFINITIONS})
target_compile_options(fyaml_static PRIVATE ${COMMON_C_FLAGS})

if(NOT MSVC)
    if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
        target_compile_options(fyaml_static PRIVATE -O2)
    endif()

    if(HAVE_ASAN)
        target_compile_options(fyaml_static PRIVATE ${ASAN_C_FLAGS})
        # Don't add link options here - executables will link dynamically against libasan
    endif()
endif()

set_target_properties(fyaml PROPERTIES
    OUTPUT_NAME fyaml
    # Keep package/project versioning on PROJECT_VERSION, but derive the
    # shared-library ABI version from .libtool-version for CMake builds too.
    VERSION ${SO_VERSION_FULL}
    SOVERSION ${SO_VERSION}
    FOLDER "Libraries"
    PUBLIC_HEADER "include/libfyaml.h"
)

target_include_directories(fyaml
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src/lib
        ${CMAKE_CURRENT_SOURCE_DIR}/src/util
        ${CMAKE_CURRENT_SOURCE_DIR}/src/xxhash
        ${CMAKE_CURRENT_SOURCE_DIR}/src/thread
        ${CMAKE_CURRENT_SOURCE_DIR}/src/allocator
        ${CMAKE_CURRENT_SOURCE_DIR}/src/blake3
        ${CMAKE_CURRENT_SOURCE_DIR}/src/generic
        ${CMAKE_CURRENT_SOURCE_DIR}/src/reflection
)

if(NOT WIN32)
    target_link_libraries(fyaml
        PUBLIC
            Threads::Threads
    )
else()
    # Windows needs no extra libraries for threading (uses kernel32)
endif()

# Link libm where it exists as a separate library (Linux, FreeBSD, etc.).
# macOS includes math in libSystem; Windows has no libm.
find_library(LIBM m)
if(LIBM)
    target_link_libraries(fyaml PUBLIC ${LIBM})
    target_link_libraries(fyaml_static PUBLIC ${LIBM})
endif()
if(ATOMIC_LIBS)
    target_link_libraries(fyaml PUBLIC ${ATOMIC_LIBS})
    target_link_libraries(fyaml_static PUBLIC ${ATOMIC_LIBS})
endif()

# Add libclang support if available
add_libclang_to_target(fyaml PRIVATE)

# Generate config.h
configure_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.h.in"
    "${CMAKE_CURRENT_BINARY_DIR}/config.h"
)
target_include_directories(fyaml PRIVATE ${CMAKE_CURRENT_BINARY_DIR})

target_compile_definitions(fyaml PRIVATE
    HAVE_CONFIG_H
    VERSION="${PROJECT_VERSION}"
    $<$<NOT:$<CONFIG:DEBUG>>:NDEBUG>
)

target_compile_definitions(fyaml PRIVATE ${COMMON_C_DEFINITIONS})
target_compile_options(fyaml PRIVATE ${COMMON_C_FLAGS})

# Shared options between GCC and CLANG (skip for Windows cross-compilation)
if(NOT MSVC AND NOT WIN32)
    target_compile_options(fyaml PRIVATE -fvisibility=hidden)

    if(BUILD_SHARED_LIBS)
        target_compile_options(fyaml PRIVATE -fPIC)
    endif()

    if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
        target_compile_options(fyaml PRIVATE -O2)
    endif()

    if(HAVE_ASAN)
        target_compile_options(fyaml PRIVATE ${ASAN_C_FLAGS} -fno-omit-frame-pointer)
        # Use INTERFACE to propagate link flags to executables linking against this library
        target_link_options(fyaml PRIVATE ${ASAN_C_FLAGS} INTERFACE ${ASAN_C_FLAGS})
    endif()
endif()

# clang-cl: disable MSVC-compatible preprocessor so that statement expressions
# in the generic API headers work correctly for both the library build and for
# consumers that include libfyaml headers via find_package(libfyaml).
# CMAKE_C_SIMULATE_ID is "MSVC" only for clang-cl, not for regular Clang.
if(CMAKE_C_COMPILER_ID STREQUAL "Clang" AND CMAKE_C_SIMULATE_ID STREQUAL "MSVC")
    foreach(_tgt fyaml fyaml_static)
        target_compile_options(${_tgt} PUBLIC -fno-ms-compatibility)
    endforeach()
    message(STATUS "clang-cl detected: adding -fno-ms-compatibility to fyaml targets")
endif()

# on windows we need a fall-back getopt
if(WIN32)
    set(GETOPT_SOURCES src/getopt/getopt.c)
    set(GETOPT_INCLUDES "${CMAKE_CURRENT_SOURCE_DIR}/src/getopt")
else()
    set(GETOPT_SOURCES)
    set(GETOPT_INCLUDES)
endif()

# Build fy-tool
set(FY_TOOL_SOURCES
    src/tool/fy-tool.c
    src/tool/fy-tool-dump.c
    ${GETOPT_SOURCES}
)

set(FY_TOOL_INCLUDE_DIRS
    ${CMAKE_CURRENT_SOURCE_DIR}/src/valgrind
    ${CMAKE_CURRENT_SOURCE_DIR}/src/reflection
    ${CMAKE_CURRENT_SOURCE_DIR}/src/lib
    ${CMAKE_CURRENT_SOURCE_DIR}/src/util
    ${CMAKE_CURRENT_SOURCE_DIR}/src/xxhash
    ${CMAKE_CURRENT_SOURCE_DIR}/src/thread
    ${CMAKE_CURRENT_SOURCE_DIR}/src/allocator
    ${CMAKE_CURRENT_SOURCE_DIR}/src/blake3
    ${CMAKE_CURRENT_SOURCE_DIR}/src/generic
    ${CMAKE_CURRENT_BINARY_DIR}
    ${GETOPT_INCLUDES}
)

if(WIN32)
    list(APPEND FY_TOOL_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/src/util")
endif()

add_executable(fy-tool ${FY_TOOL_SOURCES})
target_include_directories(fy-tool PRIVATE ${FY_TOOL_INCLUDE_DIRS})
target_compile_definitions(fy-tool PRIVATE ${COMMON_C_DEFINITIONS})

target_link_libraries(fy-tool PRIVATE fyaml)

if(ENABLE_STATIC_TOOLS AND NOT MSVC)
    target_link_options(fy-tool PRIVATE -static)
endif()

if(BUILD_TESTING AND HAVE_BASH AND HAVE_CHECK AND (NOT CROSS_COMPILING OR HAVE_WINE))
    set(CAN_TEST 1)
else()
    set(CAN_TEST 0)
endif()

# Testing
if(CAN_TEST)
    # Include TAP subtest registration functions
    include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/add-tap-subtests.cmake)

    # Create test directory
    file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/test)

    # Create src directory for compatibility with test scripts
    # Tests expect tools at build/src/
    file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src)

    # Build libfyaml-test if check is available
    if(HAVE_COMPATIBLE_CHECK)

        add_executable(libfyaml-test
            test/libfyaml-test.c
            test/libfyaml-test-core.c
            test/libfyaml-test-meta.c
            test/libfyaml-test-emit.c
            test/libfyaml-test-atom-analysis.c
            test/libfyaml-test-emit-bugs.c
            test/libfyaml-test-parse-bugs.c
            test/libfyaml-test-allocator.c
            test/libfyaml-test-fuzzing.c
            test/libfyaml-test-private.c
            test/libfyaml-test-private-id.c
            test/libfyaml-test-parser.c
            test/libfyaml-test-thread.c
            $<$<BOOL:${HAVE_GENERIC}>:test/libfyaml-test-generic.c>
            $<$<BOOL:${HAVE_GENERIC}>:test/libfyaml-test-generic-scalars.c>
            $<$<BOOL:${HAVE_REFLECTION}>:test/libfyaml-test-reflection.c>
            ${GETOPT_SOURCES}
        )

        target_compile_options(libfyaml-test PRIVATE ${COMMON_C_FLAGS})
        target_compile_definitions(libfyaml-test PRIVATE
            HAVE_CONFIG_H
            ${COMMON_C_DEFINITIONS}
        )

        target_include_directories(libfyaml-test PRIVATE
            ${CMAKE_CURRENT_SOURCE_DIR}/include
            ${CMAKE_CURRENT_SOURCE_DIR}/src/check
            ${CMAKE_CURRENT_SOURCE_DIR}/src/valgrind
            ${CMAKE_CURRENT_SOURCE_DIR}/src/lib
            ${CMAKE_CURRENT_SOURCE_DIR}/src/util
            ${CMAKE_CURRENT_SOURCE_DIR}/src/allocator
            ${CMAKE_CURRENT_SOURCE_DIR}/src/generic
            ${CMAKE_CURRENT_SOURCE_DIR}/src/reflection
            ${CMAKE_CURRENT_BINARY_DIR}
            ${CHECK_INCLUDE_DIRS}
            ${GETOPT_INCLUDES}
        )

        # Link against static library to access private symbols
        # Use same pattern as internal tools
        link_whole_archive_static(libfyaml-test fyaml_static)
        target_link_libraries(libfyaml-test PRIVATE
            ${CHECK_LIBRARIES}
            $<$<BOOL:${HAVE_LIBCLANG}>:${LIBCLANG_LIBS}>
        )
        if(NOT MSVC)
            if(HAVE_ASAN)
                target_link_options(libfyaml-test PRIVATE ${ASAN_C_FLAGS})
            endif()
        endif()
        if(CHECK_LIBRARY_DIRS)
            target_link_directories(libfyaml-test PRIVATE ${CHECK_LIBRARY_DIRS})
        endif()
        if(HAVE_LIBCLANG)
            target_link_directories(libfyaml-test PRIVATE ${LIBCLANG_LINK_DIRS})
        endif()

        if (HAVE_CLANG_BLOCKS)
            target_compile_options(libfyaml-test PRIVATE -fblocks)
            if (CLANG_BLOCKS_LIB)
                target_link_libraries(libfyaml-test PRIVATE ${CLANG_BLOCKS_LIB})
            endif()
        endif()

        add_dependencies(libfyaml-test fyaml_static)

        # Set output directory to match autoconf behavior
        set_target_properties(libfyaml-test PROPERTIES
            RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/test
        )

        add_libfyaml_tests()
    endif()

    # Error tests - register individual subtests
    add_testerrors_tests()

    # Emitter tests - register individual subtests
    add_testemitter_tests(testemitter "")
    add_testemitter_tests(testemitter-streaming "--streaming")
    add_testemitter_tests(testemitter-restreaming "--streaming --recreating")
    if(HAVE_GENERIC)
        add_testemitter_tests(testemitter-generic-failsafe ""
            SUITE_NAME testemitter
            EXTRA_ENV "EMITTER_DUMP_ARGS=--generic --keep-style --schema yaml1.2-failsafe"
        )
    endif()

    # Network-dependent tests using FetchContent
    if(HAVE_NETWORK)
        # Download test suites at configure time via FetchContent
        message(STATUS "Fetching YAML test suite...")
        FetchContent_MakeAvailable(yaml_test_suite)
        message(STATUS "Fetching JSON test suite...")
        FetchContent_MakeAvailable(json_test_suite)

        # YAML test suite tests - register individual subtests
        # Two modes (handled automatically by add_testsuite_tests):
        # 1. If test-suite-data exists (via FetchContent symlink): scan filesystem
        # 2. If cmake/testsuite-tests.cmake exists: use pre-generated list (offline builds)
        add_testsuite_tests(testsuite testsuite.test)
        add_testsuite_tests(testsuite-evstream testsuite-evstream.test)
        add_testsuite_tests(testsuite-resolution testsuite-resolution.test)
        if(HAVE_GENERIC)
            add_testsuite_tests(testsuite-generic testsuite-generic.test)
        endif()
        if(HAVE_JQ)
            add_testsuite_tests(testsuite-json testsuite-json.test)
        endif()

        # JSON test suite - register individual subtests
        add_jsontestsuite_tests()
    endif()

    # Reflection tests - only if libclang is available and reflection is enabled
    if(HAVE_REFLECTION AND HAVE_LIBCLANG)
        add_testreflection_tests()
        add_testreflection_packed_tests()
    endif()

    # Build all test dependencies as part of default 'all' target
    # Note: CMake's 'make test' target doesn't trigger builds by design.
    # Use 'make all test' or 'make check-am' or 'make build_and_test' for complete workflow.
    set(TEST_DEPENDENCIES fy-tool fy-thread fy-b3sum fy-allocators)
    if(HAVE_COMPATIBLE_CHECK)
        list(APPEND TEST_DEPENDENCIES libfyaml-test)
    endif()
    if(HAVE_LIBYAML)
        list(APPEND TEST_DEPENDENCIES libfyaml-parser)
    endif()

    set(TEST_SUITES
        testsuite.test
        jsontestsuite.test
        testsuite-evstream.test
        testsuite-resolution.test
        testerrors.test
        testemitter.test
        testemitter-streaming.test
        testemitter-restreaming.test
    )

    if(HAVE_GENERIC)
        list(APPEND TEST_SUITES
            testsuite-generic.test
            testemitter-generic-failsafe.test
        )
    endif()
    if(HAVE_JQ)
        list(APPEND TEST_SUITES testsuite-json.test)
    endif()

endif()

# C++ compatibility tests
# These executables are built as C++ translation units and verify that the
# public libfyaml headers (in particular libfyaml-atomics.h) can be included
# and used from C++ code — the usage pattern in projects such as MRPT.
# They require only a C++ compiler and the shared fyaml target; no check/bash.
include(CheckLanguage)
check_language(CXX)
if(CMAKE_CXX_COMPILER)
    enable_language(CXX)

    # Pick a C++ standard high enough for libfyaml-atomics.h to find a usable
    # atomic backend on every supported compiler:
    #
    #   * GCC / Clang: any C++ standard (>= 17) works — they expose <stdatomic.h>
    #     or fall back to GCC-extension macros that are accepted in C++.
    #   * MSVC:        requires C++23 (/std:c++latest), since MSVC's
    #                  <stdatomic.h> is a stub before C++23 (warning STL4038)
    #                  and _Atomic is not provided as an extension keyword.
    if(MSVC)
        set(_FYAML_CPP_TEST_STANDARD 23)
    else()
        set(_FYAML_CPP_TEST_STANDARD 17)
    endif()

    # Test 1: libfyaml-atomics.h usable from a C++ TU
    add_executable(libfyaml-test-cpp-atomics
        test/libfyaml-test-cpp-atomics.cpp
    )
    set_target_properties(libfyaml-test-cpp-atomics PROPERTIES
        CXX_STANDARD ${_FYAML_CPP_TEST_STANDARD}
        CXX_STANDARD_REQUIRED ON
    )
    target_include_directories(libfyaml-test-cpp-atomics PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/include
    )
    target_link_libraries(libfyaml-test-cpp-atomics PRIVATE fyaml)
    if(BUILD_TESTING)
        add_test(NAME cpp-atomics COMMAND libfyaml-test-cpp-atomics)
    endif()

    # Test 2: libfyaml-core.h (and therefore the full public API) usable
    # from a pure C++ translation unit — mirrors the MRPT yaml.cpp use-case.
    add_executable(libfyaml-test-cpp-include-tu
        test/libfyaml-test-cpp-include-tu.cpp
    )
    set_target_properties(libfyaml-test-cpp-include-tu PROPERTIES
        CXX_STANDARD ${_FYAML_CPP_TEST_STANDARD}
        CXX_STANDARD_REQUIRED ON
    )
    target_include_directories(libfyaml-test-cpp-include-tu PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/include
    )
    target_link_libraries(libfyaml-test-cpp-include-tu PRIVATE fyaml)
    if(BUILD_TESTING)
        add_test(NAME cpp-include-tu COMMAND libfyaml-test-cpp-include-tu)
    endif()
endif()

# Documentation
find_program(SPHINX_EXECUTABLE NAMES sphinx-build)
find_program(LATEXMK_EXECUTABLE NAMES latexmk)
find_program(KPSEWHICH_EXECUTABLE NAMES kpsewhich)

set(HAVE_XCOLOR_STY FALSE)
if(KPSEWHICH_EXECUTABLE)
    execute_process(
        COMMAND ${KPSEWHICH_EXECUTABLE} xcolor.sty
        RESULT_VARIABLE KPSEWHICH_XCOLOR_RESULT
        OUTPUT_QUIET
        ERROR_QUIET
    )
    if(KPSEWHICH_XCOLOR_RESULT EQUAL 0)
        set(HAVE_XCOLOR_STY TRUE)
    endif()
endif()
# all of these to be able to build doc-latexpdf
set(HAVE_DOC_LATEXPDF (SPHINX_EXECUTABLE AND LATEXMK_EXECUTABLE AND HAVE_XCOLOR_STY))

if(SPHINX_EXECUTABLE)
    set(HAVE_SPHINX TRUE)
    set(SPHINX_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/doc")
    set(SPHINX_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/doc/_build")

    # Add custom targets for documentation
    add_custom_target(doc-html
        COMMAND ${SPHINX_EXECUTABLE} -M html "${SPHINX_SOURCE_DIR}" "${SPHINX_BUILD_DIR}"
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
        COMMENT "Building HTML documentation with Sphinx"
        VERBATIM
    )

    add_custom_target(doc-man
        COMMAND ${SPHINX_EXECUTABLE} -M man "${SPHINX_SOURCE_DIR}" "${SPHINX_BUILD_DIR}"
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
        COMMENT "Building man pages with Sphinx"
        VERBATIM
    )

    add_custom_target(doc-text
        COMMAND ${SPHINX_EXECUTABLE} -M text "${SPHINX_SOURCE_DIR}" "${SPHINX_BUILD_DIR}"
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
        COMMENT "Building plain-text documentation with Sphinx"
        VERBATIM
    )

    if(HAVE_DOC_LATEXPDF)
        add_custom_target(doc-latexpdf
            COMMAND ${SPHINX_EXECUTABLE} -M latexpdf "${SPHINX_SOURCE_DIR}" "${SPHINX_BUILD_DIR}"
            WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
            COMMENT "Building PDF documentation with Sphinx"
            VERBATIM
        )
    endif()

    add_custom_target(doc-clean
        COMMAND ${CMAKE_COMMAND} -E remove_directory "${SPHINX_BUILD_DIR}"
        COMMENT "Cleaning documentation build directory"
    )

    add_custom_target(doc-canned-man
        DEPENDS doc-man
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${SPHINX_BUILD_DIR}/man/fy-tool.1"
            "${CMAKE_CURRENT_SOURCE_DIR}/doc/canned-man/fy-tool.1"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${SPHINX_BUILD_DIR}/man/libfyaml.3"
            "${CMAKE_CURRENT_SOURCE_DIR}/doc/canned-man/libfyaml.3"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${SPHINX_BUILD_DIR}/man/libfyaml-core.3"
            "${CMAKE_CURRENT_SOURCE_DIR}/doc/canned-man/libfyaml-core.3"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${SPHINX_BUILD_DIR}/man/libfyaml-misc.3"
            "${CMAKE_CURRENT_SOURCE_DIR}/doc/canned-man/libfyaml-misc.3"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${SPHINX_BUILD_DIR}/man/libfyaml-generics.3"
            "${CMAKE_CURRENT_SOURCE_DIR}/doc/canned-man/libfyaml-generics.3"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${SPHINX_BUILD_DIR}/man/libfyaml-reflection.3"
            "${CMAKE_CURRENT_SOURCE_DIR}/doc/canned-man/libfyaml-reflection.3"
        COMMENT "Updating canned man pages from the built Sphinx man pages"
        VERBATIM
    )

    message(STATUS "Sphinx found: ${SPHINX_EXECUTABLE}")
    message(STATUS "  make doc-html     - Build HTML documentation")
    message(STATUS "  make doc-man      - Build man pages")
    message(STATUS "  make doc-canned-man - Refresh canned man pages from doc-man")
    message(STATUS "  make doc-text     - Build plain-text documentation")
    if(HAVE_DOC_LATEXPDF)
        message(STATUS "  make doc-latexpdf - Build PDF documentation")
    endif()
else()
    set(HAVE_SPHINX FALSE)
    message(STATUS "Sphinx not found - documentation targets disabled")
    message(STATUS "  Install: pip3 install sphinx sphinx_rtd_theme sphinx-markdown-builder linuxdoc")
endif()

install(FILES doc/canned-man/fy-tool.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES
    doc/canned-man/libfyaml.3
    doc/canned-man/libfyaml-core.3
    doc/canned-man/libfyaml-misc.3
    doc/canned-man/libfyaml-generics.3
    doc/canned-man/libfyaml-reflection.3
    DESTINATION ${CMAKE_INSTALL_MANDIR}/man3
)
if(NOT WIN32)
    install(CODE "
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool.1 fy-dump.1 WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_MANDIR}/man1)
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool.1 fy-filter.1 WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_MANDIR}/man1)
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool.1 fy-testsuite.1 WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_MANDIR}/man1)
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool.1 fy-join.1 WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_MANDIR}/man1)
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool.1 fy-ypath.1 WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_MANDIR}/man1)
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool.1 fy-compose.1 WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_MANDIR}/man1)
    ")
endif()

# Install targets
install(TARGETS fyaml fy-tool
    EXPORT libfyaml-targets
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

# Install sub-headers into <prefix>/include/libfyaml/
install(FILES ${LIBHDRSPUB}
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/libfyaml
)

# Create symlinks for fy-tool (not on Windows - symlinks require special privileges)
if(NOT WIN32)
    install(CODE "
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool fy-dump WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR})
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool fy-filter WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR})
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool fy-testsuite WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR})
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool fy-join WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR})
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool fy-ypath WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR})
        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink fy-tool fy-compose WORKING_DIRECTORY \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR})
    ")
endif()

install(EXPORT libfyaml-targets
    FILE libfyaml-targets.cmake
    NAMESPACE libfyaml::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libfyaml
)

export(
    TARGETS fyaml
    FILE "${CMAKE_CURRENT_BINARY_DIR}/cmake/libfyaml-targets.cmake"
    NAMESPACE libfyaml::
)

# Package config
include(CMakePackageConfigHelpers)
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/libfyaml-config.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/cmake/libfyaml-config.cmake
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libfyaml
)

write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/cmake/libfyaml-config-version.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY AnyNewerVersion
)

install(
    FILES
        ${CMAKE_CURRENT_BINARY_DIR}/cmake/libfyaml-config.cmake
        ${CMAKE_CURRENT_BINARY_DIR}/cmake/libfyaml-config-version.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libfyaml
)

# pkg-config file
configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/libfyaml.pc.in
    ${CMAKE_CURRENT_BINARY_DIR}/libfyaml.pc
    @ONLY
)

install(
    FILES ${CMAKE_CURRENT_BINARY_DIR}/libfyaml.pc
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
)

# tags and cscope support
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/tags.cmake)

# Python core bindings
if(NOT CMAKE_CROSSCOMPILING)
    # Development.SABIModule is needed for Python3_add_library(USE_SABI ...);
    # it was added in CMake 3.26. Request it conditionally so older CMake
    # (which still works for non-abi3 fallback builds) doesn't error out.
    if(NOT CMAKE_VERSION VERSION_LESS "3.26")
        find_package(Python3 COMPONENTS Interpreter Development.Module Development.SABIModule)
    else()
        find_package(Python3 COMPONENTS Interpreter Development.Module)
    endif()
endif()
if(CMAKE_CROSSCOMPILING)
    set(_ENABLE_PYTHON_BINDINGS_DEFAULT OFF)
elseif(Python3_FOUND)
    set(_ENABLE_PYTHON_BINDINGS_DEFAULT ON)
else()
    set(_ENABLE_PYTHON_BINDINGS_DEFAULT OFF)
endif()
option(ENABLE_PYTHON_BINDINGS "Build Python core bindings (_libfyaml extension)" ${_ENABLE_PYTHON_BINDINGS_DEFAULT})

if(CMAKE_CROSSCOMPILING AND ENABLE_PYTHON_BINDINGS)
    message(STATUS "Python bindings disabled (cross-compiling requires a target Python SDK, which is not auto-detected)")
    set(HAVE_PYTHON_BINDINGS 0)
elseif(WIN32 AND ENABLE_PYTHON_BINDINGS AND NOT CMAKE_C_COMPILER_ID STREQUAL "Clang")
    message(STATUS "Python bindings disabled (windows requires clang/clang-cl)")
    set(HAVE_PYTHON_BINDINGS 0)
elseif(ENABLE_PYTHON_BINDINGS AND NOT HAVE_GENERIC)
    message(STATUS "Python bindings disabled (requires statement expressions / generic support)")
    set(HAVE_PYTHON_BINDINGS 0)
elseif(ENABLE_PYTHON_BINDINGS)
    if(NOT Python3_FOUND)
        message(FATAL_ERROR "Python3 development headers not found (required for ENABLE_PYTHON_BINDINGS)")
    endif()
    if(WIN32
            AND CMAKE_BUILD_TYPE STREQUAL "Debug"
            AND (NOT DEFINED Python3_LIBRARY_DEBUG OR Python3_LIBRARY_DEBUG MATCHES "-NOTFOUND$"))
        message(STATUS "Python bindings disabled (windows Debug build requires matching Python debug import library)")
        set(HAVE_PYTHON_BINDINGS 0)
    else()

        # Build the extension against the Python Stable ABI (abi3) so a
        # single cp310 wheel loads on every newer CPython. USE_SABI is a
        # CMake 3.26 feature; on older CMake fall back to a non-abi3 build
        # (the abi3 wheels then come from non-CMake build paths).
        if(NOT CMAKE_VERSION VERSION_LESS "3.26")
            Python3_add_library(_libfyaml MODULE USE_SABI 3.10 WITH_SOABI
                "${CMAKE_CURRENT_SOURCE_DIR}/python-libfyaml/libfyaml/_libfyaml.c"
            )
        else()
            Python3_add_library(_libfyaml MODULE WITH_SOABI
                "${CMAKE_CURRENT_SOURCE_DIR}/python-libfyaml/libfyaml/_libfyaml.c"
            )
            target_compile_definitions(_libfyaml PRIVATE Py_LIMITED_API=0x030A0000)
        endif()
        target_include_directories(_libfyaml PRIVATE
            "${CMAKE_CURRENT_SOURCE_DIR}/include"
            "${CMAKE_CURRENT_BINARY_DIR}"
        )
        target_link_libraries(_libfyaml PRIVATE fyaml_static)
        target_compile_options(_libfyaml PRIVATE ${COMMON_C_FLAGS})
        if(HAVE_ASAN)
            target_compile_options(_libfyaml PRIVATE ${ASAN_C_FLAGS})
            target_link_options(_libfyaml PRIVATE ${ASAN_C_FLAGS})
        endif()
        # Place the .so in the build tree, not the source tree
        set(_PY_PKG_SRC "${CMAKE_CURRENT_SOURCE_DIR}/python-libfyaml/libfyaml")
        set(_PY_PKG_DST "${CMAKE_CURRENT_BINARY_DIR}/python-libfyaml/libfyaml")
        set_target_properties(_libfyaml PROPERTIES
            LIBRARY_OUTPUT_DIRECTORY "${_PY_PKG_DST}"
            RUNTIME_OUTPUT_DIRECTORY "${_PY_PKG_DST}"
            PREFIX ""
        )
        # Link/copy source files into the build staging dir so the complete
        # package is importable directly from the build tree.
        # COPY_ON_ERROR makes this work on Windows where symlinks may be
        # unavailable (requires Developer Mode or admin rights).
        file(MAKE_DIRECTORY "${_PY_PKG_DST}")
        file(GLOB _py_srcs "${_PY_PKG_SRC}/*.py" "${_PY_PKG_SRC}/*.pyi")
        foreach(_src ${_py_srcs})
            get_filename_component(_name "${_src}" NAME)
            file(CREATE_LINK "${_src}" "${_PY_PKG_DST}/${_name}" SYMBOLIC COPY_ON_ERROR)
        endforeach()
        set(HAVE_PYTHON_BINDINGS 1)
    endif()
else()
    set(HAVE_PYTHON_BINDINGS 0)
endif()

# Python binding tests (pytest) - one CTest entry per test function/method
# ASan + dlopen of an ASan-instrumented extension crashes on macOS and Windows,
# so skip Python tests in those configurations.
if(HAVE_PYTHON_BINDINGS AND BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING
        AND NOT (HAVE_ASAN AND (APPLE OR WIN32)))
    add_python_tests()
endif()

# Print configuration summary
message(STATUS "")
message(STATUS "---{ ${PROJECT_NAME} ${PROJECT_VERSION} }---")
message(STATUS "")
message(STATUS "VERSION:               ${PROJECT_VERSION}")
message(STATUS "MAJOR.MINOR:           ${VERSION_MAJOR}.${VERSION_MINOR}")
message(STATUS "PATCH:                 ${VERSION_PATCH}")
message(STATUS "EXTRA:                 ${VERSION_EXTRA}")
message(STATUS "LIBTOOL_VERSION:       ${LIBTOOL_VERSION}")
message(STATUS "prefix:                ${CMAKE_INSTALL_PREFIX}")
message(STATUS "Build system:          ${CMAKE_HOST_SYSTEM_NAME} ${CMAKE_HOST_SYSTEM_PROCESSOR}")
message(STATUS "Target system:         ${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_PROCESSOR}")
message(STATUS "Cross compiling:       ${CROSS_COMPILING}")
message(STATUS "C compiler:            ${CMAKE_C_COMPILER}")
message(STATUS "Build type:            ${CMAKE_BUILD_TYPE}")
message(STATUS "HAVE_BASH:             ${HAVE_BASH}")
message(STATUS "HAVE_CHECK:            ${HAVE_CHECK}")
message(STATUS "HAVE_COMPATIBLE_CHECK: ${HAVE_COMPATIBLE_CHECK}")
message(STATUS "HAVE_LIBYAML:          ${HAVE_LIBYAML}")
message(STATUS "HAVE_CMAKE_GEXPR:      ${HAVE_CMAKE_GEXPR}")
message(STATUS "HAVE_LIBCLANG:         ${HAVE_LIBCLANG}")
message(STATUS "HAVE_GENERIC:          ${HAVE_GENERIC}")
message(STATUS "HAVE_REFLECTION:       ${HAVE_REFLECTION}")
message(STATUS "HAVE_PYTHON_BINDINGS:  ${HAVE_PYTHON_BINDINGS}")
message(STATUS "HAVE_CLANG_BLOCKS:     ${HAVE_CLANG_BLOCKS}")
message(STATUS "HAVE_NETWORK:          ${HAVE_NETWORK}")
message(STATUS "HAVE_DEVMODE:          ${HAVE_DEVMODE}")
message(STATUS "HAVE_GIT:              ${HAVE_GIT}")
message(STATUS "HAVE_JQ:               ${HAVE_JQ}")
message(STATUS "HAVE_DOCKER:           ${HAVE_DOCKER}")
message(STATUS "HAVE_SPHINX:           ${HAVE_SPHINX}")
message(STATUS "HAVE_ASAN:             ${HAVE_ASAN}")
message(STATUS "TARGET_HAS_SSE2:       ${TARGET_HAS_SSE2}")
message(STATUS "TARGET_HAS_SSE41:      ${TARGET_HAS_SSE41}")
message(STATUS "TARGET_HAS_AVX2:       ${TARGET_HAS_AVX2}")
message(STATUS "TARGET_HAS_AVX512:     ${TARGET_HAS_AVX512}")
message(STATUS "TARGET_HAS_NEON:       ${TARGET_HAS_NEON}")
message(STATUS "HAVE_HEAP_TRAMPOLINES: ${HAVE_HEAP_TRAMPOLINES}")
message(STATUS "TESTSUITEURL:          ${TESTSUITEURL}")
message(STATUS "TESTSUITECHECKOUT:     ${TESTSUITECHECKOUT}")
message(STATUS "JSONTESTSUITEURL:      ${JSONTESTSUITEURL}")
message(STATUS "JSONTESTSUITECHECKOUT: ${JSONTESTSUITECHECKOUT}")
message(STATUS "CAN_TEST:              ${CAN_TEST}")
message(STATUS "")
if(CROSS_COMPILING AND NOT HAVE_WINE)
    message(STATUS "Tests disabled when cross-compiling (target binaries cannot execute on build host)")
    message(STATUS "")
elseif(CROSS_COMPILING AND HAVE_WINE)
    message(STATUS "Tests enabled via wine: ${WINE_EXECUTABLE}")
    message(STATUS "")
endif()
