cmake_minimum_required(VERSION 3.18 FATAL_ERROR)

project(ErlangPythonNIF C)

# CMake policies
if(POLICY CMP0028)
  cmake_policy(SET CMP0028 NEW)
endif()
if(POLICY CMP0054)
  cmake_policy(SET CMP0054 NEW)
endif()
if(POLICY CMP0074)
  cmake_policy(SET CMP0074 NEW)
endif()

list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/CMake")

# Add standard package manager paths for macOS (MacPorts and Homebrew)
if(APPLE)
    # MacPorts
    if(EXISTS "/opt/local")
        list(APPEND CMAKE_PREFIX_PATH "/opt/local")
        list(APPEND CMAKE_INCLUDE_PATH "/opt/local/include")
        list(APPEND CMAKE_LIBRARY_PATH "/opt/local/lib")
    endif()
    # Homebrew on Apple Silicon
    if(EXISTS "/opt/homebrew")
        list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew")
        list(APPEND CMAKE_INCLUDE_PATH "/opt/homebrew/include")
        list(APPEND CMAKE_LIBRARY_PATH "/opt/homebrew/lib")
    endif()
    # Homebrew on Intel (default location)
    if(EXISTS "/usr/local/include")
        list(APPEND CMAKE_PREFIX_PATH "/usr/local")
        list(APPEND CMAKE_INCLUDE_PATH "/usr/local/include")
        list(APPEND CMAKE_LIBRARY_PATH "/usr/local/lib")
    endif()
endif()

# Output directory
set(priv_dir "${PROJECT_SOURCE_DIR}/../priv")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${priv_dir})
file(MAKE_DIRECTORY ${priv_dir})

# Build type
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

# Performance build option (for maximum optimization)
option(PERF_BUILD "Enable aggressive performance optimizations (-O3, LTO, native arch)" OFF)

# Sanitizer options for debugging race conditions and memory issues
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)

# Optional thread-callback I/O tracing. Compiled out by default; turn on
# locally to debug py_thread_callback_SUITE-style flakiness. Logs one
# line per send/receive on stderr.
option(ENABLE_PY_THREAD_CB_TRACE "Trace thread-callback I/O on stderr" OFF)
if(ENABLE_PY_THREAD_CB_TRACE)
    message(STATUS "Thread-callback I/O tracing enabled")
    add_compile_definitions(PY_THREAD_CB_TRACE=1)
endif()

if(ENABLE_ASAN)
    message(STATUS "AddressSanitizer enabled")
    add_compile_options(-fsanitize=address -fno-omit-frame-pointer -g -O1)
    add_link_options(-fsanitize=address)
    # ASan is incompatible with TSan
    if(ENABLE_TSAN)
        message(FATAL_ERROR "ASan and TSan cannot be used together")
    endif()
endif()

if(ENABLE_TSAN)
    message(STATUS "ThreadSanitizer enabled")
    add_compile_options(-fsanitize=thread -fno-omit-frame-pointer -g -O1)
    add_link_options(-fsanitize=thread)
endif()

if(ENABLE_UBSAN)
    message(STATUS "UndefinedBehaviorSanitizer enabled")
    add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer -g -O1)
    add_link_options(-fsanitize=undefined)
endif()

if(PERF_BUILD)
    message(STATUS "Performance build enabled - using aggressive optimizations")
    # Override compiler flags for maximum performance
    set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")
    # Enable Link-Time Optimization
    set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()

# Find Erlang
include(FindErlang)
include_directories(${ERLANG_ERTS_INCLUDE_PATH})

# Find Python using CMake's built-in FindPython3
#
# To specify a particular Python installation, set PYTHON_CONFIG env variable:
#   PYTHON_CONFIG=/opt/local/bin/python3.14-config cmake ...
#
# CMake will use its default search order otherwise.

if(DEFINED ENV{PYTHON_CONFIG})
    # Extract prefix from python-config for hinting
    execute_process(
        COMMAND $ENV{PYTHON_CONFIG} --prefix
        OUTPUT_VARIABLE Python3_ROOT_DIR
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    set(Python3_FIND_STRATEGY LOCATION)
endif()

find_package(Python3 REQUIRED COMPONENTS Interpreter Development)

message(STATUS "Python3 executable: ${Python3_EXECUTABLE}")
message(STATUS "Python3 version: ${Python3_VERSION}")
message(STATUS "Python3 include dirs: ${Python3_INCLUDE_DIRS}")
message(STATUS "Python3 libraries: ${Python3_LIBRARIES}")
message(STATUS "Python3 library: ${Python3_LIBRARY}")

# Detect Python features for worker pool optimization
# We use both version checks and compile tests to verify actual API availability

include(CheckCSourceCompiles)

# First check Python version - subinterpreters with OWN_GIL require Python 3.12+
if(Python3_VERSION VERSION_GREATER_EQUAL "3.12")
    message(STATUS "Python ${Python3_VERSION} >= 3.12, checking subinterpreter API...")

    # Save and set required variables for compile test
    set(CMAKE_REQUIRED_INCLUDES ${Python3_INCLUDE_DIRS})
    set(CMAKE_REQUIRED_LIBRARIES Python3::Python)

    # Clear any cached result to ensure fresh detection
    unset(HAVE_SUBINTERPRETERS CACHE)

    # Check for subinterpreter API with per-interpreter GIL (PEP 684, Python 3.12+)
    # This verifies PyInterpreterConfig and PyInterpreterConfig_OWN_GIL are available
    check_c_source_compiles("
#define PY_SSIZE_T_CLEAN
#include <Python.h>
int main(void) {
    PyInterpreterConfig config = {
        .use_main_obmalloc = 0,
        .allow_fork = 0,
        .allow_exec = 0,
        .allow_threads = 1,
        .allow_daemon_threads = 0,
        .check_multi_interp_extensions = 1,
        .gil = PyInterpreterConfig_OWN_GIL,
    };
    (void)config;
    return 0;
}
" HAVE_SUBINTERPRETERS)

    if(HAVE_SUBINTERPRETERS)
        message(STATUS "Subinterpreter API detected (PyInterpreterConfig_OWN_GIL available)")
        # OWN_GIL mode requires Python 3.14+ due to C extension global state bugs
        # in 3.12/3.13 (e.g., _decimal). See https://github.com/python/cpython/issues/106078
        if(Python3_VERSION VERSION_GREATER_EQUAL "3.14")
            set(HAVE_OWNGIL TRUE)
            message(STATUS "Python ${Python3_VERSION} >= 3.14, OWN_GIL mode enabled")
        else()
            set(HAVE_OWNGIL FALSE)
            message(STATUS "Python ${Python3_VERSION} < 3.14, OWN_GIL mode disabled (C extension bugs)")
        endif()
    else()
        message(STATUS "Subinterpreter API compile test failed, using shared GIL fallback")
        set(HAVE_OWNGIL FALSE)
    endif()
else()
    message(STATUS "Python ${Python3_VERSION} < 3.12, subinterpreter API not available")
    set(HAVE_SUBINTERPRETERS FALSE)
    set(HAVE_OWNGIL FALSE)
endif()

# Check for free-threaded Python (Python 3.13+ with --disable-gil / nogil build)
# Free-threaded builds have Py_GIL_DISABLED defined in sysconfig
execute_process(
    COMMAND ${Python3_EXECUTABLE} -c "import sysconfig; print('yes' if sysconfig.get_config_var('Py_GIL_DISABLED') else 'no')"
    OUTPUT_VARIABLE Python3_FREE_THREADED
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
)
if(Python3_FREE_THREADED STREQUAL "yes")
    set(HAVE_FREE_THREADED TRUE)
    message(STATUS "Free-threaded Python detected: GIL disabled at runtime")
else()
    set(HAVE_FREE_THREADED FALSE)
endif()

# Create the NIF shared library
add_library(py_nif MODULE py_nif.c)

# Pass Python library path to NIF for dlopen
# Python3_LIBRARY is the full path to the Python shared library
if(Python3_LIBRARY)
    target_compile_definitions(py_nif PRIVATE PYTHON_LIBRARY_PATH="${Python3_LIBRARY}")
    message(STATUS "Using Python library path for dlopen: ${Python3_LIBRARY}")
elseif(Python3_LIBRARIES)
    # Fallback to first library in the list
    list(GET Python3_LIBRARIES 0 Python3_FIRST_LIB)
    target_compile_definitions(py_nif PRIVATE PYTHON_LIBRARY_PATH="${Python3_FIRST_LIB}")
    message(STATUS "Using Python library path for dlopen: ${Python3_FIRST_LIB}")
endif()

# Add Python feature compile definitions for worker pool optimization
if(HAVE_SUBINTERPRETERS)
    target_compile_definitions(py_nif PRIVATE HAVE_SUBINTERPRETERS=1)
endif()
if(HAVE_OWNGIL)
    target_compile_definitions(py_nif PRIVATE HAVE_OWNGIL=1)
endif()
if(HAVE_FREE_THREADED)
    target_compile_definitions(py_nif PRIVATE HAVE_FREE_THREADED=1)
endif()

# Set output name
set_target_properties(py_nif PROPERTIES
    PREFIX ""
    OUTPUT_NAME "py_nif"
)

# Include directories
target_include_directories(py_nif PRIVATE
    ${ERLANG_ERTS_INCLUDE_PATH}
    ${Python3_INCLUDE_DIRS}
)

# Compiler flags
if(PERF_BUILD)
    # Performance build: aggressive optimizations
    target_compile_options(py_nif PRIVATE
        -O3
        -Wall
        -fPIC
        -march=native
        -ffast-math
        -funroll-loops
    )
else()
    # Standard build
    target_compile_options(py_nif PRIVATE
        -O2
        -Wall
        -fPIC
    )
endif()

# Platform-specific settings
if(APPLE)
    target_link_options(py_nif PRIVATE
        -undefined dynamic_lookup
        -flat_namespace
    )
    # Add CoreFoundation framework
    target_link_libraries(py_nif PRIVATE "-framework CoreFoundation")
elseif(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR
       CMAKE_SYSTEM_NAME STREQUAL "NetBSD" OR
       CMAKE_SYSTEM_NAME STREQUAL "OpenBSD")
    # BSD systems
    target_link_options(py_nif PRIVATE
        -Wl,--export-dynamic
    )
    # No need to link libdl on BSD - dlopen is in libc
elseif(UNIX)
    # Linux
    target_link_options(py_nif PRIVATE
        -Wl,--export-dynamic
    )
    # dlopen for loading libpython with RTLD_GLOBAL
    target_link_libraries(py_nif PRIVATE dl)
endif()

# Link Python
target_link_libraries(py_nif PRIVATE Python3::Python)

# Threads
find_package(Threads REQUIRED)
target_link_libraries(py_nif PRIVATE Threads::Threads)

message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS "Output directory: ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
