cmake_minimum_required(VERSION 3.19)
project(fzf-native C)

# UTF8PROC Configuration. The vendored utf8proc library provides the Unicode
# case-folding / classification used by fzf.c's UTF-8 matching variants.  Only
# the runtime source, generated data, public header, and license are vendored.
set(UTF8PROC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/utf8proc-2.10.0")
set(UTF8PROC_INCLUDE_DIR "${UTF8PROC_DIR}")

# Check compilers
message(STATUS ">>>>>>>> ${CMAKE_CXX_COMPILER_ID}")
message(STATUS ">>>>>>>> ${CMAKE_SYSTEM_PROCESSOR}")
message(STATUS ">>>>>>>> UTF8PROC_DIR: ${UTF8PROC_DIR}")

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  message(
    STATUS "Setting build type to 'RelWithDebInfo' as none was specified.")
  set(CMAKE_BUILD_TYPE
      "RelWithDebInfo"
      CACHE STRING "Choose the type of build." FORCE)
endif()

# Option to enable AddressSanitizer (catches segfaults, heap overflows,
# use-after-free, etc.)
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)

# Option to enable UndefinedBehaviorSanitizer
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)

find_program(EMACS_PROGRAM emacs)
if(EMACS_PROGRAM)
  get_filename_component(EMACS_PROGRAM ${EMACS_PROGRAM} REALPATH)
  get_filename_component(EMACS_PROGRAM_DIR ${EMACS_PROGRAM} DIRECTORY)
  get_filename_component(EMACS_PROGRAM_DIR ${EMACS_PROGRAM_DIR} DIRECTORY)
endif()

if(NOT DEFINED EMACS_INCLUDE_DIR)
  set(EMACS_INCLUDE_DIR "")
endif()

# See
# https://gitlab.kitware.com/cmake/community/-/wikis/doc/tutorials/How-To-Write-Platform-Checks
set(PLATFORM_DIR ${CMAKE_SYSTEM_NAME})

if(NOT DEFINED FZF_NATIVE_MODULE_OUTPUT_DIR)
  if((CMAKE_SYSTEM_NAME STREQUAL "Darwin") AND (CMAKE_SYSTEM_PROCESSOR STREQUAL
                                                "arm64"))
    # Assume Apple Silicon.
    set(FZF_NATIVE_MODULE_OUTPUT_DIR bin/${PLATFORM_DIR}/arm64)
  else()
    set(FZF_NATIVE_MODULE_OUTPUT_DIR bin/${PLATFORM_DIR})
  endif()
endif()

# Build the minimal vendored runtime directly so the project does not depend on
# utf8proc's upstream build system or carry its tests and generated binaries.
add_library(utf8proc STATIC ${UTF8PROC_DIR}/utf8proc.c)
set_target_properties(
  utf8proc
  PROPERTIES C_STANDARD 11
             POSITION_INDEPENDENT_CODE ON
             C_VISIBILITY_PRESET hidden)
target_compile_definitions(utf8proc PUBLIC UTF8PROC_STATIC)
target_include_directories(utf8proc PUBLIC ${UTF8PROC_INCLUDE_DIR})

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
  # Assume compiler MSVC.
  add_library(
    fzf-native-module SHARED
    fzf-native-module.c
    fzf.c
    fzf.h
    fzf-private.h
    fzf-score-input.inc
    fzf-additions.c
    fzf-additions.h
    fzf-native-module.def)
else()
  add_library(
    fzf-native-module MODULE
    fzf-native-module.c
    fzf.c
    fzf.h
    fzf-private.h
    fzf-score-input.inc
    fzf-additions.c
    fzf-additions.h)
endif()

# Pull in utf8proc headers and link the static runtime.
target_include_directories(fzf-native-module PRIVATE ${UTF8PROC_INCLUDE_DIR})
target_link_libraries(fzf-native-module PRIVATE utf8proc)

# ELF startup objects can add globally visible _init and _fini symbols even when
# the target uses hidden visibility.  Restrict the dynamic ABI to the two
# symbols that Emacs modules require.  Darwin uses source visibility, and the
# Windows target uses fzf-native-module.def.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
  set(FZF_NATIVE_EXPORT_MAP "${CMAKE_CURRENT_SOURCE_DIR}/fzf-native-module.map")
  target_link_options(fzf-native-module PRIVATE
                      "LINKER:--version-script=${FZF_NATIVE_EXPORT_MAP}")
  set_property(
    TARGET fzf-native-module
    APPEND
    PROPERTY LINK_DEPENDS "${FZF_NATIVE_EXPORT_MAP}")
endif()

if(IS_ABSOLUTE "${FZF_NATIVE_MODULE_OUTPUT_DIR}")
  set(OUTPUT_DIR "${FZF_NATIVE_MODULE_OUTPUT_DIR}")
else()
  set(OUTPUT_DIR "${CMAKE_SOURCE_DIR}/${FZF_NATIVE_MODULE_OUTPUT_DIR}")
endif()
set_target_properties(
  fzf-native-module
  PROPERTIES C_STANDARD 11
             POSITION_INDEPENDENT_CODE ON
             C_VISIBILITY_PRESET hidden
             PREFIX ""
             LIBRARY_OUTPUT_DIRECTORY ${OUTPUT_DIR})
target_include_directories(fzf-native-module PUBLIC ${EMACS_INCLUDE_DIR})

# Set output directory for the library. See
# https://github.com/RoukaVici/LibRoukaVici
set_target_properties(
  fzf-native-module
  PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${OUTPUT_DIR}
             LIBRARY_OUTPUT_DIRECTORY ${OUTPUT_DIR}
             RUNTIME_OUTPUT_DIRECTORY ${OUTPUT_DIR})

# Compile-time logging gate. fzf_log() is compiled out of the module by default.
# Enable with: FZF_NATIVE_DEBUG=1 cmake -B build When toggling, run `make clean`
# first so CMake reconfigures cleanly.
if(DEFINED ENV{FZF_NATIVE_DEBUG})
  target_compile_definitions(fzf-native-module PRIVATE FZF_NATIVE_DEBUG)
  message(STATUS "FZF_NATIVE_DEBUG enabled — file logging compiled in")
endif()

# --- Debug / Sanitizer flags (GCC and Clang-family compilers) ---
# Keep these options compiler-based instead of Linux-only.  The interactive
# Emacs ABI is exercised on Darwin too, and silently producing an
# uninstrumented module for -DENABLE_ASAN=ON defeats the option's contract.
if(CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")

  # Full debug info: -g3 includes macro definitions, -fno-omit-frame-pointer
  # keeps stack frames intact for better backtraces, -O0 disables optimizations
  # so gdb sees exactly what the source says.
  if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    target_compile_options(
      fzf-native-module
      PRIVATE -g3 -O0 -fno-omit-frame-pointer
              -fno-optimize-sibling-calls # improves call-stack accuracy in gdb
              -Wall -Wextra)
    target_link_options(fzf-native-module PRIVATE -g -fno-omit-frame-pointer)
    message(STATUS "Debug build: full symbols, no optimization")
  endif()

  # AddressSanitizer: detects heap/stack overflows, use-after-free,
  # use-after-return, and gives a precise crash report with source locations.
  # Enable with: cmake -DENABLE_ASAN=ON -DCMAKE_BUILD_TYPE=Debug ..
  if(ENABLE_ASAN)
    target_compile_options(fzf-native-module PRIVATE -fsanitize=address
                                                     -fno-omit-frame-pointer -g)
    target_compile_options(utf8proc PRIVATE -fsanitize=address
                                            -fno-omit-frame-pointer -g)
    target_link_options(fzf-native-module PRIVATE -fsanitize=address)
    message(STATUS "AddressSanitizer enabled")
  endif()

  # UndefinedBehaviorSanitizer: catches signed overflow, null dereference,
  # misaligned access, out-of-bounds shifts, etc. Enable with: cmake
  # -DENABLE_UBSAN=ON -DCMAKE_BUILD_TYPE=Debug ..
  if(ENABLE_UBSAN)
    target_compile_options(fzf-native-module PRIVATE -fsanitize=undefined
                                                     -fno-omit-frame-pointer -g)
    target_compile_options(utf8proc PRIVATE -fsanitize=undefined
                                            -fno-omit-frame-pointer -g)
    target_link_options(fzf-native-module PRIVATE -fsanitize=undefined)
    message(STATUS "UndefinedBehaviorSanitizer enabled")
  endif()

elseif(ENABLE_ASAN OR ENABLE_UBSAN)
  message(
    FATAL_ERROR "ENABLE_ASAN/ENABLE_UBSAN requires GCC, Clang, or AppleClang")
endif()
# --- End debug / sanitizer flags ---
