# src/CMakeLists.txt
# Builds the C code and all its dependencies as a static library

#----------------------------------------------------------------------------------------------
# Define paths for the script and output files (command info generation)
set(GEN_SCRIPT "${root}/srcutil/gen_command_info.py")
set(COMMAND_JSON "${root}/commands.json")
set(COMMAND_INFO_FILE_NAME "command_info")
set(COMMAND_INFO_FOLDER_NAME "command_info")
set(COMMAND_OUTPUT_FOLDER "${CMAKE_CURRENT_SOURCE_DIR}/${COMMAND_INFO_FOLDER_NAME}")
set(COMMAND_OUTPUT_FILE "${COMMAND_OUTPUT_FOLDER}/${COMMAND_INFO_FILE_NAME}")
set(COMMAND_OUTPUT_H "${COMMAND_OUTPUT_FILE}.h")
set(COMMAND_OUTPUT_C "${COMMAND_OUTPUT_FILE}.c")

# Add custom command to run the Python script
add_custom_command(
    OUTPUT ${COMMAND_OUTPUT_H} ${COMMAND_OUTPUT_C}
    COMMAND python3 ${GEN_SCRIPT} -j ${COMMAND_JSON} -f ${COMMAND_OUTPUT_FILE} -i ${COMMAND_INFO_FOLDER_NAME}
    DEPENDS ${GEN_SCRIPT} ${COMMAND_JSON}
    COMMENT "Generating ${COMMAND_INFO_FILE_NAME}.h, ${COMMAND_INFO_FILE_NAME}.c"
    VERBATIM
)

# Create a custom target that relies on the generated files
add_custom_target(generate_command_info ALL DEPENDS ${COMMAND_OUTPUT_H} ${COMMAND_OUTPUT_C})

#----------------------------------------------------------------------------------------------
# Add subdirectories for dependencies (from deps/)
add_subdirectory(${root}/deps/rmutil ${CMAKE_CURRENT_BINARY_DIR}/rmutil)
add_subdirectory(${root}/deps/friso ${CMAKE_CURRENT_BINARY_DIR}/friso)
include(${root}/cmake/snowball.cmake)
add_subdirectory(${root}/deps/phonetics ${CMAKE_CURRENT_BINARY_DIR}/phonetics)
add_subdirectory(${root}/deps/fast_float ${CMAKE_CURRENT_BINARY_DIR}/fast_float)

# Configure libuv options
set(LIBUV_BUILD_TESTS OFF CACHE BOOL "Build libuv tests" FORCE)
set(LIBUV_BUILD_BENCH OFF CACHE BOOL "Build libuv benchmarks" FORCE)
set(LIBUV_BUILD_SHARED OFF CACHE BOOL "Build shared libuv library" FORCE)
add_subdirectory(${root}/deps/libuv ${CMAKE_CURRENT_BINARY_DIR}/libuv)

# libuv has const-correctness issues that trigger warnings-as-errors on newer compilers.
# Turn off flags set in the top-level CMakeLists.txt.
if(HAS_WNO_ERROR_DISCARDED_QUALIFIERS)
    target_compile_options(uv_a PRIVATE $<$<COMPILE_LANGUAGE:C>:-Wno-error=discarded-qualifiers>)
endif()
if(HAS_WNO_ERROR_INCOMPATIBLE_POINTER_TYPES_DISCARDS_QUALIFIERS)
    target_compile_options(uv_a PRIVATE $<$<COMPILE_LANGUAGE:C>:-Wno-error=incompatible-pointer-types-discards-qualifiers>)
endif()

option(VECSIM_BUILD_TESTS "Build vecsim tests" OFF)
add_subdirectory(${root}/deps/VectorSimilarity ${CMAKE_CURRENT_BINARY_DIR}/VectorSimilarity)

# Workaround: fmt 11.2.0 is missing <cstdlib> include, which is needed for clang 21+ on macOS
# This was fixed in fmt 12.0.0, but that requires patching several levels upstream.
if(TARGET fmt)
    target_compile_options(fmt PRIVATE "-include" "cstdlib")
endif()

#----------------------------------------------------------------------------------------------
# Add subdirectories for src/ components
add_subdirectory(geometry)
add_subdirectory(buffer)
add_subdirectory(iterators)
add_subdirectory(index_result)
add_subdirectory(util/mempool)
add_subdirectory(util/dict)
add_subdirectory(util/hash)
add_subdirectory(coord)

#----------------------------------------------------------------------------------------------
# Source files for the core library
file(GLOB SOURCES CONFIGURE_DEPENDS
    "${CMAKE_CURRENT_SOURCE_DIR}/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/pipeline/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/aggregate/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/aggregate/expr/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/aggregate/functions/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/aggregate/reducers/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/command_info/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/hybrid/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/hybrid/parse/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/ext/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/fork_gc/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/hll/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/query_parser/v1/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/query_parser/v2/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/util/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/util/arr/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/wildcard/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/trie/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/info/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/info/info_redis/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/info/info_redis/threads/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/info/info_redis/types/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/module-init/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/obfuscation/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/profile/*.c"

    "${root}/deps/cndict/cndict_data.c"
    "${root}/deps/libnu/*.c"
    "${root}/deps/miniz/*.c"
    "${root}/deps/fast_float/*.c"
    "${root}/deps/thpool/*.c")

#----------------------------------------------------------------------------------------------
# GIT_VERSPEC / GIT_SHA (computed in the top-level CMakeLists) change on every
# commit. Scope them to the only two TUs that read them — debug_commands.c
# (FT.DEBUG GIT_SHA) and module-init.c (version string) — instead of defining
# them globally, so every other TU keeps a stable compile command line and
# stays cacheable (sccache) across commits.
set_source_files_properties(
    "${CMAKE_CURRENT_SOURCE_DIR}/debug_commands.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/module-init/module-init.c"
    PROPERTIES COMPILE_DEFINITIONS "GIT_VERSPEC=\"${GIT_VERSPEC}\";GIT_SHA=\"${GIT_SHA}\"")

#----------------------------------------------------------------------------------------------
# Create the rscore object library
add_library(rscore OBJECT ${SOURCES})
add_dependencies(rscore generate_command_info)
if(MAX_WORKER_THREADS)
    target_compile_definitions(rscore PRIVATE MAX_WORKER_THREADS=${MAX_WORKER_THREADS})
endif()

# Collect all object files
set(FINAL_OBJECTS
    $<TARGET_OBJECTS:buffer>
    $<TARGET_OBJECTS:dict>
    $<TARGET_OBJECTS:iterators>
    $<TARGET_OBJECTS:index_result>
    $<TARGET_OBJECTS:mempool>
    $<TARGET_OBJECTS:rscore>
    $<TARGET_OBJECTS:rmutil>
    $<TARGET_OBJECTS:friso>
    $<TARGET_OBJECTS:snowball>
    $<TARGET_OBJECTS:metaphone>
    $<TARGET_OBJECTS:fast_float_strtod>
    $<TARGET_OBJECTS:redisearch-coord>
)

# Export FINAL_OBJECTS to parent scope for the top-level redisearch target
set(REDISEARCH_C_FINAL_OBJECTS ${FINAL_OBJECTS} PARENT_SCOPE)

#----------------------------------------------------------------------------------------------
# Build a static library from the C object files
add_library(redisearch_c STATIC ${FINAL_OBJECTS})
set_target_properties(redisearch_c PROPERTIES LINKER_LANGUAGE CXX)

# Declare dependencies for transitive linking
target_link_libraries(redisearch_c
    redisearch-geometry
    redisearch-hash
    VectorSimilarity
    redisearch-coord
    uv_a
    ${HIREDIS_LIBS})

add_dependencies(redisearch_c VectorSimilarity)
add_dependencies(redisearch_c generate_command_info)

#----------------------------------------------------------------------------------------------
# Build a static library that combines all symbols defined in C/C++,
# by RediSearch or by one of its dependencies (either direct or transitive).
# This static library will be linked by Rust tests and benchmarks whenever
# they have to invoke a foreign symbol.

# A helper function to recursively collect all static library dependencies from a target.
# Accumulates results in _COLLECT_LIBS (library paths) and _COLLECT_TARGETS (target names).
function(_collect_static_libs_recurse target)
    # Skip if already visited or not a valid target
    if(target IN_LIST _COLLECT_TARGETS OR NOT TARGET ${target})
        return()
    endif()

    # Mark as visited immediately to avoid cycles
    list(APPEND _COLLECT_TARGETS ${target})
    set(_COLLECT_TARGETS ${_COLLECT_TARGETS} PARENT_SCOPE)

    # Get target properties
    get_target_property(target_type ${target} TYPE)
    get_target_property(is_imported ${target} IMPORTED)

    # Try to get library file path for static libraries
    set(lib_path "")
    if(target_type STREQUAL "STATIC_LIBRARY")
        if(is_imported)
            # Try various IMPORTED_LOCATION properties
            foreach(loc_prop IMPORTED_LOCATION IMPORTED_LOCATION_RELEASE IMPORTED_LOCATION_NOCONFIG)
                get_target_property(lib_path ${target} ${loc_prop})
                if(lib_path)
                    break()
                endif()
            endforeach()
        else()
            set(lib_path $<TARGET_FILE:${target}>)
        endif()
    elseif(target_type STREQUAL "UNKNOWN_LIBRARY")
        # IMPORTED target with unknown type - check if it's a static library
        foreach(loc_prop IMPORTED_LOCATION IMPORTED_LOCATION_RELEASE)
            get_target_property(lib_path ${target} ${loc_prop})
            if(lib_path)
                break()
            endif()
        endforeach()
    endif()

    # Add to collection if we found a static library
    if(lib_path AND (lib_path MATCHES "\\.(a|lib)$" OR NOT is_imported))
        list(APPEND _COLLECT_LIBS ${lib_path})
        set(_COLLECT_LIBS ${_COLLECT_LIBS} PARENT_SCOPE)
    endif()

    # Recurse into link dependencies
    foreach(prop LINK_LIBRARIES INTERFACE_LINK_LIBRARIES)
        get_target_property(deps ${target} ${prop})
        if(NOT deps)
            continue()
        endif()
        foreach(dep IN LISTS deps)
            # Skip generator expressions
            if(dep MATCHES "^\\$<")
                continue()
            endif()
            # Recurse if it's a valid target
            if(TARGET ${dep})
                # Resolve ALIAS targets to their real name
                get_target_property(aliased ${dep} ALIASED_TARGET)
                if(aliased)
                    set(dep ${aliased})
                endif()
                _collect_static_libs_recurse(${dep})
                set(_COLLECT_LIBS ${_COLLECT_LIBS} PARENT_SCOPE)
                set(_COLLECT_TARGETS ${_COLLECT_TARGETS} PARENT_SCOPE)
            endif()
        endforeach()
    endforeach()
endfunction()

macro(collect_static_libs target out_libs out_targets)
    set(_COLLECT_LIBS "")
    set(_COLLECT_TARGETS "")
    _collect_static_libs_recurse(${target})
    set(${out_libs} ${_COLLECT_LIBS})
    set(${out_targets} ${_COLLECT_TARGETS})
endmacro()

set(REDISEARCH_ALL_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libredisearch_all.a")

# Collect all static library dependencies from redisearch_c
set(LIBS_TO_MERGE "")
set(VISITED_TARGETS "")
collect_static_libs(redisearch_c LIBS_TO_MERGE VISITED_TARGETS)

# On Linux, SVS may be precompiled and fetched via FetchContent.
# As a consequence, its IMPORTED targets aren't visible from this scope and won't
# be picked up by `collect_static_libs`, causing undefined symbol errors at link
# time when building Rust tests.
# To fix the issue, we add SVS and its dependencies using their known path.
set(_svs_lib_dir "${CMAKE_BINARY_DIR}/_deps/svs-src/lib")
if(EXISTS "${_svs_lib_dir}")
    file(GLOB _svs_static_libs "${_svs_lib_dir}/*.a")
    # Exclude MKL from the combined archive — its ~42K object files overflow
    # the u16 archive member index in rustc's ar_archive_writer.
    # MKL is linked separately by the Rust build (see build_utils).
    list(FILTER _svs_static_libs EXCLUDE REGEX "libmkl_static_library\\.a$")
    list(APPEND LIBS_TO_MERGE ${_svs_static_libs})
    message(STATUS "SVS static libraries found: ${_svs_static_libs}")
else()
    message(STATUS "SVS lib directory not found: ${_svs_lib_dir}")
endif()

list(REMOVE_DUPLICATES LIBS_TO_MERGE)
list(REMOVE_DUPLICATES VISITED_TARGETS)
message(STATUS "Static libraries for libredisearch_all.a: ${LIBS_TO_MERGE}")

# Combine targets and library paths for DEPENDS
set(MERGE_DEPENDS ${VISITED_TARGETS} ${LIBS_TO_MERGE})
list(REMOVE_DUPLICATES MERGE_DEPENDS)

# Create the combined library using platform-specific tools
if(APPLE)
    # macOS: use libtool to merge static libraries
    add_custom_command(
        OUTPUT ${REDISEARCH_ALL_OUTPUT}
        COMMAND libtool -static -no_warning_for_no_symbols -o ${REDISEARCH_ALL_OUTPUT} ${LIBS_TO_MERGE}
        DEPENDS ${MERGE_DEPENDS}
        COMMENT "Creating unified libredisearch_all.a with libtool"
        VERBATIM
        COMMAND_EXPAND_LISTS
    )
else()
    # Linux: use ar with MRI script to merge static libraries
    set(MRI_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/merge_libs.mri")
    string(REPLACE ";" "\nADDLIB " ADDLIB_COMMANDS "${LIBS_TO_MERGE}")
    file(GENERATE OUTPUT ${MRI_SCRIPT} CONTENT
"CREATE ${REDISEARCH_ALL_OUTPUT}
ADDLIB ${ADDLIB_COMMANDS}
SAVE
END
")
    add_custom_command(
        OUTPUT ${REDISEARCH_ALL_OUTPUT}
        COMMAND ${CMAKE_COMMAND} -E rm -f ${REDISEARCH_ALL_OUTPUT}
        COMMAND ar -M < ${MRI_SCRIPT}
        DEPENDS ${MERGE_DEPENDS} ${MRI_SCRIPT}
        COMMENT "Creating unified libredisearch_all.a with ar"
        VERBATIM
    )
endif()

add_custom_target(redisearch_all ALL DEPENDS ${REDISEARCH_ALL_OUTPUT})
