cmake_minimum_required(VERSION 3.15)

include(CheckCCompilerFlag)

get_filename_component(root ${CMAKE_CURRENT_LIST_DIR} ABSOLUTE)
# The `binroot` path includes a platform+profile segment
get_filename_component(binroot ${CMAKE_CURRENT_BINARY_DIR}/.. ABSOLUTE)
# Rust takes care of the different platform+profiles on its own,
# therefore we don't need to use separate target directories for different
# platform+profiles combinations.
get_filename_component(rust_binroot ${binroot}/.. ABSOLUTE)

# Platform detection
if(APPLE)
    set(OS "macos")
elseif(UNIX)
    set(OS "linux")
endif()
message(STATUS "OS detected: ${OS}")

# Suppress 'has no symbols' warnings from ranlib on macOS.
# Cross-platform deps (cpu_features, hiredis) compile platform stubs that are
# empty on irrelevant architectures, producing harmless noise.
# CMake may pick up LLVM's llvm-ranlib from PATH, which doesn't support
# -no_warning_for_no_symbols. Use xcrun to locate Apple's native ranlib.
if(APPLE)
    execute_process(
        COMMAND xcrun --find ranlib
        OUTPUT_VARIABLE APPLE_RANLIB
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    set(CMAKE_C_ARCHIVE_FINISH "${APPLE_RANLIB} -no_warning_for_no_symbols <TARGET>")
    set(CMAKE_CXX_ARCHIVE_FINISH "${APPLE_RANLIB} -no_warning_for_no_symbols <TARGET>")
endif()

# Always use .so extension even on macOS
set(CMAKE_SHARED_LIBRARY_SUFFIX ".so")
# Export the underlying compiler invocations in a compile_commands.json, located in the bin directory.
# It'll be picked up by clangd to provide LSP support in editors like Zed and VSCode
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Define compiler setup function
function(setup_cc_options)
    message("# CMAKE_C_COMPILER_ID: " ${CMAKE_C_COMPILER_ID})

    # Accumulate C/CXX flags locally and publish once. set(... PARENT_SCOPE)
    # does NOT update the function-local variable, so a later read of
    # ${CMAKE_C_FLAGS} would miss earlier additions (MOD-14916).
    set(_common_c_flags "${CMAKE_C_FLAGS} -fPIC -g -pthread -fno-strict-aliasing -Wno-unused-function -Wno-unused-variable -Wno-sign-compare -fcommon -funsigned-char")
    set(_common_cxx_flags "${CMAKE_CXX_FLAGS} -fPIC -g -pthread -fno-strict-aliasing -Wno-unused-function -Wno-unused-variable -Wno-sign-compare")
    set(CMAKE_CXX_STANDARD 20)

    # MOD-14916: inline LSE atomics on Linux AArch64 -- avoid libgcc
    # __aarch64_* outline-atomics helpers on every atomic RMW.
    #   -march=armv8-a+lse   adds LSE without raising -march, so deps that
    #                        probe `-march=armv8-a` (e.g. VecSim's CXX_ARMV8A
    #                        gate on NEON_HP.cpp) still pass.
    #   -mno-outline-atomics canonical disable spelling (the "=no" form is
    #                        silently ignored by GCC).
    # Apple excluded: Apple clang rejects -mno-outline-atomics and Apple
    # Silicon already emits inline LSE.
    if(NOT APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$" AND INLINE_LSE_ATOMICS)
        include(CheckCCompilerFlag)
        include(CheckCXXCompilerFlag)
        set(_aarch64_lse_flags "-march=armv8-a+lse -mtune=neoverse-n1 -mno-outline-atomics")
        check_c_compiler_flag("${_aarch64_lse_flags}" _c_lse_ok)
        check_cxx_compiler_flag("${_aarch64_lse_flags}" _cxx_lse_ok)
        if(_c_lse_ok AND _cxx_lse_ok)
            set(_common_c_flags   "${_common_c_flags} ${_aarch64_lse_flags}")
            set(_common_cxx_flags "${_common_cxx_flags} ${_aarch64_lse_flags}")
        else()
            message(WARNING "AArch64 LSE build flags not accepted by compiler; "
                            "outline atomics will remain in the .so")
        endif()
    endif()

    set(CMAKE_C_FLAGS   "${_common_c_flags}"   PARENT_SCOPE)
    set(CMAKE_CXX_FLAGS "${_common_cxx_flags}" PARENT_SCOPE)

    # Config-specific flags only — CMake appends these to CMAKE_C/CXX_FLAGS automatically
    if(CMAKE_BUILD_TYPE STREQUAL "Debug")
        set(CMAKE_C_FLAGS_DEBUG "-O0 -fno-omit-frame-pointer -ggdb" PARENT_SCOPE)
        set(CMAKE_CXX_FLAGS_DEBUG "-O0 -fno-omit-frame-pointer -ggdb" PARENT_SCOPE)
    elseif(PROFILE)
        set(CMAKE_C_FLAGS_RELWITHDEBINFO "-O2 -fno-omit-frame-pointer" PARENT_SCOPE)
        set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -fno-omit-frame-pointer" PARENT_SCOPE)
    else()
        set(CMAKE_C_FLAGS_RELWITHDEBINFO "-O3" PARENT_SCOPE)
        set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O3" PARENT_SCOPE)
    endif()
endfunction()

# Define shared object setup function
function(setup_shared_object_target target)
    if(APPLE)
        set_target_properties(${target} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
        # Force .so extension on macOS instead of .dylib
        set_target_properties(${target} PROPERTIES SUFFIX ".so")
    else()
        # We are building a shared library and want to verify that any reference to a symbol within the library will resolve to
        # the library's own definition, rather than to a definition in another shared library or the main executable.
        set_target_properties(${target} PROPERTIES LINK_FLAGS "-pthread -shared -Wl,-Bsymbolic,-Bsymbolic-functions")
    endif()
    set_target_properties(${target} PROPERTIES PREFIX "")
endfunction()

# Define debug symbols extraction function
function(extract_debug_symbols target)
    if(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo" AND NOT APPLE)
        add_custom_command(TARGET ${target} POST_BUILD
        COMMAND cp $<TARGET_FILE:${target}> $<TARGET_FILE:${target}>.debug
        COMMAND objcopy --add-gnu-debuglink=$<TARGET_FILE:${target}>.debug $<TARGET_FILE:${target}>
        COMMAND strip -g $<TARGET_FILE:${target}>
        COMMENT "Extracting debug symbols from ${target}"
    )
    endif()
endfunction()

#----------------------------------------------------------------------------------------------
# Command line options with default values
option(USE_REDIS_ALLOCATOR "Use redis allocator" ON)
option(BUILD_SEARCH_UNIT_TESTS "Build unit tests" OFF)
option(VERBOSE_UTESTS "Enable verbose unit tests" OFF)
option(ENABLE_ASSERT "Enable assertions" OFF)
set(MAX_WORKER_THREADS "" CACHE STRING "Override the maximum parallel worker threads allowed in thread-pool")
option(BUILD_TESTING "Enable testing for cpu-features dep" OFF)
# Inline LSE atomics on Linux AArch64 (Armv8.1-a+). Disable for pre-Armv8.1-a
# cores (Cortex-A72, AWS Graviton1, Raspberry Pi 4) to avoid SIGILL on load.
option(INLINE_LSE_ATOMICS "Inline LSE atomics on Linux AArch64 (Armv8.1-a+)" ON)

# Regenerate Rust→C FFI headers via cheadergen during the build.
# Default: ON when RediSearch is the top-level project, OFF when consumed as a
# subdirectory. Downstream consumers pulling a master commit don't need to
# regenerate — checked-in headers are guaranteed fresh by CI — and cheadergen
# may not be installed in their environment.
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
    set(_redisearch_generate_headers_default ON)
else()
    set(_redisearch_generate_headers_default OFF)
endif()
option(REDISEARCH_GENERATE_HEADERS
    "Regenerate Rust→C FFI headers via cheadergen during the build"
    ${_redisearch_generate_headers_default})


#----------------------------------------------------------------------------------------------
project(redisearch)

# Configure output paths based on build configuration
set(MODULE_NAME "search" CACHE STRING "Module name" FORCE)
if(NOT DEFINED COORD_TYPE)
    set(COORD_TYPE "oss")
endif()

set(RUST_BINROOT "${rust_binroot}")
if(COORD_TYPE STREQUAL "oss")
    set(BINDIR "${binroot}/search-community")
elseif(COORD_TYPE STREQUAL "rlec")
    set(BINDIR "${binroot}/search-enterprise")
    add_compile_definitions(PRIVATE RS_CLUSTER_ENTERPRISE)
else()
    message(FATAL_ERROR "Invalid COORD_TYPE (='${COORD_TYPE}'). Should be either 'oss' or 'rlec'")
endif()

#----------------------------------------------------------------------------------------------

# Configure compiler options
setup_cc_options()

# Sanitizer settings
message(STATUS "SAN: ${SAN}")
if(SAN)
    if(SAN STREQUAL "address")
        set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-omit-frame-pointer -fsanitize=address -fsanitize-recover=all")
        set(CMAKE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS} -fsanitize=address")
        set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address")
        set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
        message(STATUS "CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}")
        message(STATUS "CMAKE_LINKER_FLAGS: ${CMAKE_LINKER_FLAGS}")
        message(STATUS "CMAKE_SHARED_LINKER_FLAGS: ${CMAKE_SHARED_LINKER_FLAGS}")
    endif()
endif()

# Coverage settings
message(STATUS "COV: ${COV}")
if (COV)
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --coverage")
    add_compile_definitions(COVERAGE=1)
endif()

# Get Git version info - to be printed in log upon loading the module
execute_process(
  COMMAND git describe --abbrev=7 --always
  WORKING_DIRECTORY ${root}
  OUTPUT_VARIABLE GIT_VERSPEC
  OUTPUT_STRIP_TRAILING_WHITESPACE
  ERROR_QUIET
)

execute_process(
  COMMAND git rev-parse HEAD
  WORKING_DIRECTORY ${root}
  OUTPUT_VARIABLE GIT_SHA
  OUTPUT_STRIP_TRAILING_WHITESPACE
  ERROR_QUIET
)

# ugly hack for cpu_features::list_cpu_features coming from VecSim
set(CMAKE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS} ${CMAKE_LD_FLAGS}")

# Treat all format-string warnings as errors
set(CMAKE_C_FLAGS   "${CMAKE_C_FLAGS} -Werror=format")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=format")

# Detect available compiler flags for subsequent use. These flags are global and available
# throughout the build. Some flags below form GCC/Clang equivalence pairs (only one will be
# available on a given compiler), others stand alone.
check_c_compiler_flag("-Werror=discarded-qualifiers" HAS_WERROR_DISCARDED_QUALIFIERS)
check_c_compiler_flag("-Werror=implicit-function-declaration" HAS_WERROR_IMPLICIT_FUNCTION_DECLARATION)
check_c_compiler_flag("-Werror=incompatible-pointer-types-discards-qualifiers" HAS_WERROR_INCOMPATIBLE_POINTER_TYPES_DISCARDS_QUALIFIERS)
check_c_compiler_flag("-Werror=jump-misses-init" HAS_WERROR_JUMP_MISSES_INIT)
check_c_compiler_flag("-Werror=sometimes-uninitialized" HAS_WERROR_SOMETIMES_UNINITIALIZED)

# Homebrew silently removes '-Werror' flags which breaks the regular form of detection.
# Instead we specifically check for the '-Wno-error' variants here separately.
check_c_compiler_flag("-Wno-error=discarded-qualifiers" HAS_WNO_ERROR_DISCARDED_QUALIFIERS)
check_c_compiler_flag("-Wno-error=incompatible-pointer-types-discards-qualifiers" HAS_WNO_ERROR_INCOMPATIBLE_POINTER_TYPES_DISCARDS_QUALIFIERS)

# Promote specific warnings to errors. These use add_compile_options() which
# propagates to ALL subdirectories, including deps (libuv, VectorSimilarity, etc.).
# Each flag is compiler-specific (gcc vs clang), so if a dep needs an override
# (e.g. -Wno-error=X via target_compile_options), guard it with the same
# compiler check or a $<C_COMPILER_ID:...> generator expression.

# Promote dropped const/volatile qualifier warnings to errors
if(HAS_WERROR_DISCARDED_QUALIFIERS)
    add_compile_options($<$<COMPILE_LANGUAGE:C>:-Werror=discarded-qualifiers>)
endif()
if(HAS_WERROR_INCOMPATIBLE_POINTER_TYPES_DISCARDS_QUALIFIERS)
    add_compile_options($<$<COMPILE_LANGUAGE:C>:-Werror=incompatible-pointer-types-discards-qualifiers>)
endif()

# Promote jumps/paths that skip a variable's initialization to errors
if(HAS_WERROR_JUMP_MISSES_INIT)
    add_compile_options($<$<COMPILE_LANGUAGE:C>:-Werror=jump-misses-init>)
endif()
if(HAS_WERROR_SOMETIMES_UNINITIALIZED)
    add_compile_options($<$<COMPILE_LANGUAGE:C>:-Werror=sometimes-uninitialized>)
endif()

# Promote implicit function declarations to errors
if(HAS_WERROR_IMPLICIT_FUNCTION_DECLARATION)
    add_compile_options($<$<COMPILE_LANGUAGE:C>:-Werror=implicit-function-declaration>)
endif()

# NOTE: GIT_VERSPEC and GIT_SHA are intentionally NOT defined globally. They
# change on every commit, so a global -D...  would be part of every C/C++
# compile command and bust the sccache key for the whole tree on every push.
# They are scoped to the only two TUs that read them in src/CMakeLists.txt.
add_compile_definitions(
    "REDISEARCH_MODULE_NAME=\"${MODULE_NAME}\""
    REDISMODULE_SDK_RLEC
    _GNU_SOURCE)

if(BUILD_INTEL_SVS_OPT)
    add_compile_definitions("BUILD_INTEL_SVS_OPT=1")
endif()

if(USE_REDIS_ALLOCATOR)
    add_compile_definitions(REDIS_MODULE_TARGET)
endif()

if(VERBOSE_UTESTS)
    add_compile_definitions(VERBOSE_UTESTS=1)
endif()

# Platform-specific settings
if(APPLE)
    # Find OpenSSL on macOS. Prefer explicit paths, then known Homebrew prefixes.
    if(NOT OPENSSL_ROOT_DIR AND DEFINED LIBSSL_DIR)
        set(OPENSSL_ROOT_DIR "${LIBSSL_DIR}")
    endif()
    if(NOT OPENSSL_ROOT_DIR)
        foreach(_openssl_prefix
            "/opt/homebrew/opt/openssl@3"
            "/usr/local/opt/openssl@3"
            "/opt/homebrew/opt/openssl"
            "/usr/local/opt/openssl")
            if(EXISTS "${_openssl_prefix}")
                set(OPENSSL_ROOT_DIR "${_openssl_prefix}")
                break()
            endif()
        endforeach()
    endif()
    find_package(OpenSSL REQUIRED)
    include_directories(${OPENSSL_INCLUDE_DIR})

    if(DEFINED LIBSSL_DIR)
        include_directories(${LIBSSL_DIR}/include)
        set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -L${LIBSSL_DIR}/lib")
    endif()

    set(SSL_LIBS ${OPENSSL_LIBRARIES})
    set(CMAKE_LD_FLAGS "${CMAKE_LD_FLAGS} -dynamiclib")
else()
    set(SSL_LIBS crypto crypt ssl)
endif()

set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${CMAKE_LD_FLAGS}")

# On debug artifacts, enable assertions
if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR ENABLE_ASSERT)
    add_compile_definitions(ENABLE_ASSERT=1)
endif()

#----------------------------------------------------------------------------------------------
# Include external dependencies (hiredis and boost - needed before src/)
include(${root}/build/hiredis/hiredis.cmake)

if (NOT DEFINED BOOST_DIR)
    set(BOOST_DIR "${root}/.install/boost")
endif()

include(${root}/build/boost/boost.cmake)
if (NOT IS_DIRECTORY ${BOOST_DIR})
    message(FATAL_ERROR "BOOST_DIR is not defined or does not point to a valid directory ${BOOST_DIR}")
endif()

message(STATUS "BOOST_DIR: ${BOOST_DIR}")
set(BOOST_ROOT ${BOOST_DIR})
set(Boost_NO_WARN_NEW_VERSIONS ON)

#----------------------------------------------------------------------------------------------
# Include directories (needed by both src/ and tests/)
include_directories(
    ${root}/src
    ${root}/src/buffer
    ${root}/src/dict
    ${root}/src/coord
    ${root}/src/redisearch_rs/headers
    ${root}/deps/libuv/include
    ${root}/deps
    ${root}/deps/VectorSimilarity/src
    ${root}/deps/rmalloc
    ${BOOST_DIR}
    ${root})

#----------------------------------------------------------------------------------------------
# Build the C code and its dependencies
add_subdirectory(src)

# Build the Rust code
add_subdirectory(src/redisearch_rs)

# C/C++ code includes headers generated by cheadergen from Rust FFI crates.
# Ensure header generation completes before any C/C++ compilation begins.
# Every target that transitively includes generated headers (via redisearch.h etc.)
# must wait for cheadergen.
add_dependencies(rscore cheadergen_generate)
add_dependencies(redisearch-geometry cheadergen_generate)
add_dependencies(iterators cheadergen_generate)
add_dependencies(index_result cheadergen_generate)
add_dependencies(coordinator-core cheadergen_generate)
add_dependencies(rmr cheadergen_generate)

#----------------------------------------------------------------------------------------------
# Build the final shared library by merging C and Rust static libraries

# Default behavior: SHARED when main project, STATIC when subdirectory
if(NOT DEFINED REDISEARCH_BUILD_SHARED)
    if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
        set(REDISEARCH_BUILD_SHARED ON)
    else()
        set(REDISEARCH_BUILD_SHARED OFF)
    endif()
endif()

option(REDISEARCH_BUILD_SHARED "Build RediSearch as shared library" OFF)

# Create library with determined type
if(REDISEARCH_BUILD_SHARED)
    add_library(redisearch SHARED ${REDISEARCH_C_FINAL_OBJECTS})
    message(STATUS "Building RediSearch as SHARED library")
else()
    add_library(redisearch STATIC ${REDISEARCH_C_FINAL_OBJECTS})
    message(STATUS "Building RediSearch as STATIC library")
endif()

if(COORD_TYPE STREQUAL "rlec")
    set_target_properties(redisearch PROPERTIES OUTPUT_NAME "module-enterprise")
endif()

set_target_properties(redisearch
    PROPERTIES
    LINKER_LANGUAGE CXX
    C_STANDARD 17
    C_STANDARD_REQUIRED ON
    POSITION_INDEPENDENT_CODE ON)
setup_shared_object_target(redisearch "")

# Link the final library - redisearch_c brings in all C dependencies transitively
target_link_libraries(redisearch
    redisearch_c
    redisearch_rs
    ${SSL_LIBS}
    ${CMAKE_LD_LIBS})

extract_debug_symbols(redisearch)
add_dependencies(redisearch redisearch_rs)

#----------------------------------------------------------------------------------------------
# Unit tests configuration
if(BUILD_SEARCH_UNIT_TESTS)
    enable_testing()

    # Disable coverage on C++ tests/benchmarks to avoid geninfo mismatch errors
    if (COV)
        string(REPLACE "--coverage" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
    endif()

    add_subdirectory(tests/cpptests/redismock)

    set(BUILD_GTEST ON CACHE BOOL "enable gtest" FORCE)
    set(BUILD_GMOCK OFF CACHE BOOL "disable gmock" FORCE)

    add_subdirectory(deps/googletest)
    add_subdirectory(tests/cpptests)
    add_subdirectory(tests/cpptests/micro-benchmarks)
    add_subdirectory(tests/ctests)
    add_subdirectory(tests/ctests/ext-example example_extension)
    add_subdirectory(tests/ctests/coord_tests)
endif()
