cmake_minimum_required(VERSION 3.18)
project(nanofractal LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Development.SABIModule: the stable ABI (abi3) for Python 3.12+, one wheel
# for 3.12, 3.13, 3.14 (measured: no change in call overhead or speed)
find_package(Python 3.9 COMPONENTS Interpreter Development.Module
             OPTIONAL_COMPONENTS Development.SABIModule REQUIRED)

# Locate nanobind shipped with the build environment
execute_process(
  COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
  OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT)
list(APPEND CMAKE_PREFIX_PATH "${nanobind_ROOT}")
find_package(nanobind CONFIG REQUIRED)

# OpenCV: system by default. CI (Plan B) overrides via -DOpenCV_DIR=<minimal build>
# OpenCV 5 split calib3d (solvePnP/findHomography -> geometry) and renamed
# features2d -> features; 4.x keeps the old module names.
find_package(OpenCV REQUIRED COMPONENTS core)
if(OpenCV_VERSION VERSION_GREATER_EQUAL 5)
  find_package(OpenCV REQUIRED COMPONENTS core imgproc geometry features)
else()
  find_package(OpenCV REQUIRED COMPONENTS core imgproc calib3d features2d)
endif()

nanobind_add_module(_nanofractal NB_STATIC STABLE_ABI src/_bindings.cpp)

target_include_directories(_nanofractal PRIVATE
  "${CMAKE_SOURCE_DIR}/third_party"
  "${CMAKE_SOURCE_DIR}/src"
  ${OpenCV_INCLUDE_DIRS})
target_link_libraries(_nanofractal PRIVATE ${OpenCV_LIBS})

# Release already implies -O3 on GCC/Clang; keep it explicit but MSVC-safe.
target_compile_options(_nanofractal PRIVATE
  $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-O3>)

# ---------------------------------------------------------------------------
# Architecture tuning (GCC / Clang: -march; MSVC: /arch:AVX2 for x86-64-v3).
#
#   -DNF_NATIVE=ON          local dev: -march=native
#   -DNF_MARCH=x86-64-v3   portable:  -march=<isa>  (CI wheels)
#
# x86-64-v3 = AVX2 + FMA + BMI1/2 (Haswell 2013+).  Enables auto-vectorisation
# of inner pixel loops without breaking pre-Skylake CPUs in the wild.
# No -ffast-math: the detector reproduces OpenCV's float rounding bit for bit,
# and fast-math reciprocal division already broke one marker level once
# (see PATCHES.md). The hot paths are integer/SIMD code that gains nothing from it.
#
# aarch64 / arm64: NEON is mandatory in the ARMv8-A base ISA, so GCC/Clang
# already enable it by default.  We add -ftree-vectorize to encourage the
# auto-vectorizer and skip -march=native / NF_MARCH entirely (the x86 ISA
# string is invalid on ARM and would cause a hard build failure).
# ---------------------------------------------------------------------------
option(NF_NATIVE "Enable -march=native (local dev only; not for distributed wheels)" OFF)
set(NF_MARCH "" CACHE STRING "ISA baseline passed to -march= (e.g. x86-64-v3). Used in CI wheels.")

if(MSVC)
  # MSVC has no -march; an x86-64-v3 (AVX2) baseline maps to /arch:AVX2 so the
  # SIMD paths in fractal_fast.hpp (guarded by __AVX2__) are compiled in.
  if(NF_NATIVE OR NF_MARCH MATCHES "x86-64-v3|x86-64-v4|haswell|skylake|native")
    target_compile_options(_nanofractal PRIVATE /arch:AVX2)
  endif()
elseif(NOT MSVC)
  if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|armv8")
    # ARM / aarch64 path: rely on the compiler's default ARMv8-A target
    # (NEON always available).  Do NOT apply NF_MARCH — an x86 ISA string
    # would break the build.
    target_compile_options(_nanofractal PRIVATE -O3 -ftree-vectorize)
  else()
    # x86 / x86_64 path: honour NF_NATIVE or NF_MARCH as before.
    if(NF_NATIVE)
      target_compile_options(_nanofractal PRIVATE -march=native)
    elseif(NF_MARCH)
      target_compile_options(_nanofractal PRIVATE -march=${NF_MARCH})
    endif()
  endif()
endif()

# ---------------------------------------------------------------------------
# Memory-safety build (AddressSanitizer + UBSan) — CI only, never for wheels.
#
#   cmake -DNF_SANITIZE=ON  (or scikit-build-core: -C cmake.define.NF_SANITIZE=ON)
#
# OFF by default: normal builds and wheels are completely unaffected.
# ---------------------------------------------------------------------------
option(NF_SANITIZE "Build with AddressSanitizer + UBSan (CI memory-safety job; not for wheels)" OFF)
if(NF_SANITIZE AND NOT MSVC)
  target_compile_options(_nanofractal PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer -g)
  target_link_options(_nanofractal PRIVATE -fsanitize=address,undefined)
endif()

# Single source of truth for the version: injected by scikit-build-core from
# pyproject.toml. Falls back for non-skbuild direct cmake builds.
if(DEFINED SKBUILD_PROJECT_VERSION)
  set(_NF_VERSION "${SKBUILD_PROJECT_VERSION}")
else()
  set(_NF_VERSION "0.0.0")
endif()
target_compile_definitions(_nanofractal PRIVATE NF_VERSION="${_NF_VERSION}")

include(CheckIPOSupported)
check_ipo_supported(RESULT _ipo_ok OUTPUT _ipo_msg)
if(_ipo_ok)
  set_property(TARGET _nanofractal PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()

install(TARGETS _nanofractal LIBRARY DESTINATION nanofractal)
