# 3.20 to match lib/vinecopulib, whose cmake/findDependencies.cmake this build
# includes; the upper bound opts into policies through 4.0. Two of those are
# read here: CMP0144 lets find_package see BOOST_ROOT / Eigen3_ROOT, which the
# install-dependencies action exports, and CMP0167 drops the FindBoost module
# that include no longer falls back to. Do not lower.
cmake_minimum_required(VERSION 3.20...4.0)

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

project(
  pyvinecopulib
  VERSION ${SKBUILD_PROJECT_VERSION}
  LANGUAGES CXX)

if(NOT SKBUILD)
  message(
    WARNING
      "\
  This CMake file is meant to be executed using 'scikit-build'. Running
  it directly will almost certainly not produce the desired result. If
  you are a user trying to install this package, please use the command
  below, which will install all necessary build dependencies, compile
  the package in an isolated environment, and then install it.
  =====================================================================
   $ pip install .
  =====================================================================
  If you are a software developer, and this is your own package, then
  it is usually much more efficient to install the build dependencies
  in your environment once and use the following command that avoids
  a costly creation of a new virtual environment at every compilation:
  =====================================================================
   $ pip install nanobind scikit-build-core[pyproject]
   $ pip install --no-build-isolation -ve .
  =====================================================================
  You may optionally add -Ceditable.rebuild=true to auto-rebuild when
  the package is imported. Otherwise, you need to re-run the above
  after editing C++ files.")
endif()

# Fail early with an actionable message when the lib/ git submodules have not
# been checked out (the includes below would otherwise error cryptically).
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/lib/vinecopulib/cmake/options.cmake")
  message(
    FATAL_ERROR
      "The lib/ git submodules are not checked out (missing "
      "lib/vinecopulib/cmake/options.cmake). Run\n"
      "    git submodule update --init --recursive\n" "and re-run the build.")
endif()

# Tuning for the build machine's CPU. Off by default: the result runs only on
# CPUs at least as new as the builder's, and a developer build whose instruction
# set differs from the wheel's hides numerical drift from the torch/C++ parity
# checks. scripts/bench_march_drive.sh is the only consumer.
option(PYVINECOPULIB_NATIVE_ARCH
       "Tune for this machine's CPU (not redistributable)" OFF)
# A normal variable takes precedence over upstream's option() (CMP0077).
set(VINECOPULIB_NATIVE_ARCH ${PYVINECOPULIB_NATIVE_ARCH})

include(lib/vinecopulib/cmake/options.cmake REQUIRED)

include(lib/vinecopulib/cmake/compilerDefOpt.cmake REQUIRED)

# Distributed wheels target x86-64-v3 (AVX2 + FMA, i.e. CPUs since ~2013). macOS
# arm64 and MSVC get nothing: the arm64 baseline is already ARMv8.4, and an
# /arch: flag would move contraction behavior on one leg of five.
set(PYVINECOPULIB_X86_64_V3 OFF)
if(DEFINED ENV{CIBUILDWHEEL}
   AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang"
   AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
  string(APPEND CMAKE_CXX_FLAGS_RELEASE " -march=x86-64-v3")
  set(PYVINECOPULIB_X86_64_V3 ON)
endif()

# The import-time CPU check must know whether *this* build actually needs the
# baseline: applied only above, so an ordinary source build on a pre-AVX2 CPU is
# correct and must not be refused. Written as Python because the check has to
# run before the extension is loaded.
if(PYVINECOPULIB_X86_64_V3)
  set(_pvc_requires_v3 "True")
else()
  set(_pvc_requires_v3 "False")
endif()
file(
  WRITE "${CMAKE_SOURCE_DIR}/src/pyvinecopulib/_build_info.py"
  "\"\"\"Build-time facts the import-time CPU check needs. Generated by CMake.\"\"\"\n\nREQUIRES_X86_64_V3 = ${_pvc_requires_v3}\n"
)

message(STATUS "pyvinecopulib: release flags = ${CMAKE_CXX_FLAGS_RELEASE}")

# Tuning for the build machine would make the wheel crash on older CPUs, and the
# failure is silent at build time. Asserted here rather than in CI, because
# cibuildwheel builds the Linux wheels inside a container whose build tree the
# workflow cannot read.
if(DEFINED ENV{CIBUILDWHEEL} AND CMAKE_CXX_FLAGS_RELEASE MATCHES
                                 "-march=native|-mcpu=")
  message(FATAL_ERROR "a redistributable wheel must not be tuned for the build "
                      "machine; release flags are '${CMAKE_CXX_FLAGS_RELEASE}'")
endif()

set(vinecopulib_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/lib/vinecopulib/include)
set(wdm_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/lib/wdm/include)
set(kde1d_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/lib/kde1d/include)
set(pyvinecopulib_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/src/include)

set(BUILD_TESTING OFF)

# Quoted, and tested against the empty string rather than with DEFINED: an empty
# value must leave the variable unset, so that findDependencies.cmake runs its
# find_package instead of building an imported target with no include directory.
if(NOT "$ENV{EIGEN3_INCLUDE_DIR}" STREQUAL "")
  set(EIGEN3_INCLUDE_DIR "$ENV{EIGEN3_INCLUDE_DIR}")
endif()

if(NOT "$ENV{Boost_INCLUDE_DIR}" STREQUAL "")
  set(Boost_INCLUDE_DIRS "$ENV{Boost_INCLUDE_DIR}")
endif()

# Boost here is header-only -- Graph, Math and Random, with BOOST_ALL_NO_LIB set
# -- so a headers directory is the whole requirement, which is how every wheel
# is built. But findDependencies.cmake reaches Boost in config mode only
# (CMP0167 removed FindBoost), so a package shipping the headers without
# BoostConfig.cmake -- conda-forge's libboost-headers -- fails there naming
# Boost while the headers are in fact present. Fall back to the path, which is
# the route the environment variable above already takes. Leaving
# Boost_INCLUDE_DIRS unset where nothing suitable is found is what keeps the
# clear configure-time failure for a Boost that is missing or too old.
if(NOT DEFINED Boost_INCLUDE_DIRS)
  find_package(Boost 1.75 QUIET CONFIG)
  if(NOT Boost_FOUND)
    find_path(PYVINECOPULIB_BOOST_HEADER_DIR boost/version.hpp)
    # EXISTS rather than a truth test: the variable is a cache entry a caller
    # may set by hand, and file(STRINGS) on a missing file is a fatal error.
    if(EXISTS "${PYVINECOPULIB_BOOST_HEADER_DIR}/boost/version.hpp")
      # BOOST_VERSION is major * 100000 + minor * 100 + patch; 1.75 is 107500.
      file(STRINGS "${PYVINECOPULIB_BOOST_HEADER_DIR}/boost/version.hpp"
           _boost_version_line REGEX "^#define BOOST_VERSION ")
      string(REGEX MATCH "[0-9]+" _boost_version "${_boost_version_line}")
      if(_boost_version AND NOT _boost_version LESS 107500)
        set(Boost_INCLUDE_DIRS "${PYVINECOPULIB_BOOST_HEADER_DIR}")
        message(STATUS "Found Boost headers without a config package: "
                       "${Boost_INCLUDE_DIRS}")
      endif()
    endif()
  endif()
endif()

include(lib/vinecopulib/cmake/findDependencies.cmake REQUIRED)

find_package(
  Python 3.11 REQUIRED
  COMPONENTS Interpreter Development.Module
  OPTIONAL_COMPONENTS Development.SABIModule)
message(STATUS "Python_EXECUTABLE= ${Python_EXECUTABLE}")

find_package(nanobind CONFIG REQUIRED)

include(lib/vinecopulib/cmake/printInfo.cmake REQUIRED)

include_directories(SYSTEM ${external_includes})
include_directories(${vinecopulib_INCLUDE_DIRS} ${kde1d_INCLUDE_DIRS}
                    ${pyvinecopulib_INCLUDE_DIRS})

# Regenerate src/include/docstr.hpp from the C++ headers via libclang.
# Gitignored: the build is the single source of truth.
execute_process(
  COMMAND "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/find_libclang.py"
          --search-paths "${CMAKE_PREFIX_PATH}"
  OUTPUT_VARIABLE LIBCLANG_PATH
  OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT LIBCLANG_PATH OR NOT EXISTS "${LIBCLANG_PATH}")
  message(
    FATAL_ERROR
      "Could not locate libclang. Install via 'pip install libclang' "
      "(already in [build-system] requires) or include python-clang in "
      "your conda env.")
endif()
message(STATUS "Found libclang: ${LIBCLANG_PATH}")

# Both .hpp and .ipp are passed so the script can extract symbols defined in
# .ipp files; the glue TU only #includes .hpp to avoid Windows redefinitions
# (.ipp lacks include guards). Trailing `<key>.<ext>` (no wildcard) prevents
# matching unrelated files like `tools_stats_ghalton.hpp`.
file(
  GLOB_RECURSE
  DOCSTR_INPUT_HEADERS
  "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include/*class.hpp"
  "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include/*class.ipp"
  "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include/*controls.hpp"
  "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include/*controls.ipp"
  "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include/*family.hpp"
  "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include/*family.ipp"
  "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include/*structure.hpp"
  "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include/*structure.ipp"
  "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include/*stats.hpp"
  "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include/*stats.ipp"
  "${CMAKE_SOURCE_DIR}/lib/wdm/include/*wdm.hpp"
  "${CMAKE_SOURCE_DIR}/lib/kde1d/include/*kde1d.hpp"
  "${CMAKE_SOURCE_DIR}/lib/kde1d/include/*stats.hpp")

set(DOCSTR_OUTPUT "${CMAKE_SOURCE_DIR}/src/include/docstr.hpp")

# libclang (from the PyPI `libclang` wheel) ships no builtin headers and does
# not reliably auto-discover the host C++ standard library + compiler intrinsics
# on every platform: it works on manylinux / Windows but misses Alpine's
# libstdc++ layout (musllinux) and the macOS SDK's libc++. Without them the
# headers fail to parse, the AST is partial, and docstr.hpp is generated with
# whole symbols missing / overloads mis-named — surfacing only as cryptic "no
# member named ..." compile errors. We feed libclang the compiler's own implicit
# system include dirs (which carry the C++ stdlib AND the builtin / intrinsic
# headers like stddef.h, xmmintrin.h, arm_neon.h). This whole block runs only
# for non-MSVC: Windows builds green today and must not be fed MSVC's include
# dirs.
set(DOCSTR_SYS_INCLUDES "")
set(DOCSTR_CLANG_ARGS "")
if(NOT MSVC)
  # Pass every implicit dir (normalized; some toolchains write the stdlib dirs
  # relative to the gcc dir). These supply both the C++ stdlib and the builtin/
  # intrinsic headers, and MUST precede Eigen/Boost below: system Boost resolves
  # to `/usr/include`, and a bare `/usr/include` placed before the libstdc++ dir
  # breaks `<cmath>`'s `#include_next <math.h>` ("'math.h' file not found"). We
  # do NOT drop the gcc/clang builtin dirs: libclang emits ~100 harmless
  # vendor-intrinsic errors when it parses them (version-specific builtins), but
  # those are error-severity, not fatal — they don't truncate the AST or affect
  # the declarations docstrings are read from (see generate_docstring.py).
  foreach(_dir IN LISTS CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES)
    get_filename_component(_rp "${_dir}" REALPATH)
    list(APPEND DOCSTR_SYS_INCLUDES -isystem "${_rp}")
  endforeach()

  # Don't let clang's default 20-error limit (tripped by the intrinsic-header
  # noise above) emit a "too many errors" *fatal* that truncates the AST.
  list(APPEND DOCSTR_CLANG_ARGS "--clang-arg=-ferror-limit=0")

  if(APPLE AND CMAKE_OSX_SYSROOT)
    list(APPEND DOCSTR_CLANG_ARGS "--clang-arg=-isysroot"
         "--clang-arg=${CMAKE_OSX_SYSROOT}")
  endif()
endif()

add_custom_command(
  OUTPUT "${DOCSTR_OUTPUT}"
  COMMAND
    "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/generate_docstring.py"
    -std "c++${CMAKE_CXX_STANDARD}" ${DOCSTR_CLANG_ARGS} -I
    "${CMAKE_SOURCE_DIR}/lib/vinecopulib/include" -I
    "${CMAKE_SOURCE_DIR}/lib/wdm/include" -I
    "${CMAKE_SOURCE_DIR}/lib/kde1d/include" ${DOCSTR_SYS_INCLUDES} -isystem
    "${EIGEN3_INCLUDE_DIR}" -isystem "${Boost_INCLUDE_DIRS}" -output
    "${DOCSTR_OUTPUT}" -library_file "${LIBCLANG_PATH}" ${DOCSTR_INPUT_HEADERS}
  DEPENDS ${DOCSTR_INPUT_HEADERS}
          "${CMAKE_SOURCE_DIR}/scripts/generate_docstring.py"
          "${CMAKE_SOURCE_DIR}/scripts/find_libclang.py"
  COMMENT "Generating docstr.hpp from C++ headers via libclang"
  VERBATIM)

add_custom_target(generate_docstr DEPENDS "${DOCSTR_OUTPUT}")

nanobind_add_module(pyvinecopulib_ext STABLE_ABI LTO NOMINSIZE NB_STATIC
                    src/pyvinecopulib_ext.cpp)

add_dependencies(pyvinecopulib_ext generate_docstr)

target_compile_definitions(pyvinecopulib_ext
                           PRIVATE VERSION_INFO=\"${PROJECT_VERSION}\")

# Generate per-subpackage __init__.pyi stubs. The sklearn stub is OPTIONAL (only
# produced when scikit-learn is available at build time).
add_custom_command(
  TARGET pyvinecopulib_ext
  POST_BUILD
  COMMAND
    "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/generate_stubs.py"
    --source-pkg-dir "${CMAKE_SOURCE_DIR}/src/pyvinecopulib" --ext-so
    "$<TARGET_FILE:pyvinecopulib_ext>" --py-typed
    "${CMAKE_SOURCE_DIR}/src/pyvinecopulib/py.typed" --module-output
    "pyvinecopulib:${CMAKE_SOURCE_DIR}/src/pyvinecopulib/__init__.pyi"
    --module-output
    "pyvinecopulib.core:${CMAKE_SOURCE_DIR}/src/pyvinecopulib/core/__init__.pyi"
    --module-output
    "pyvinecopulib.families:${CMAKE_SOURCE_DIR}/src/pyvinecopulib/families/__init__.pyi"
    --module-output
    "pyvinecopulib.utils:${CMAKE_SOURCE_DIR}/src/pyvinecopulib/utils/__init__.pyi"
    --module-output
    "pyvinecopulib.sklearn:${CMAKE_SOURCE_DIR}/src/pyvinecopulib/sklearn/__init__.pyi"
  COMMENT
    "Generating per-subpackage __init__.pyi stubs from the just-built extension"
  VERBATIM)

install(TARGETS pyvinecopulib_ext LIBRARY DESTINATION pyvinecopulib)

# Install stubs + py.typed explicitly (gitignored, so git-ls-files skips them).
install(
  FILES "${CMAKE_SOURCE_DIR}/src/pyvinecopulib/__init__.pyi"
        "${CMAKE_SOURCE_DIR}/src/pyvinecopulib/py.typed"
        "${CMAKE_SOURCE_DIR}/src/pyvinecopulib/_build_info.py"
  DESTINATION pyvinecopulib)
install(FILES "${CMAKE_SOURCE_DIR}/src/pyvinecopulib/core/__init__.pyi"
        DESTINATION pyvinecopulib/core)
install(FILES "${CMAKE_SOURCE_DIR}/src/pyvinecopulib/families/__init__.pyi"
        DESTINATION pyvinecopulib/families)
install(FILES "${CMAKE_SOURCE_DIR}/src/pyvinecopulib/utils/__init__.pyi"
        DESTINATION pyvinecopulib/utils)
install(
  FILES "${CMAKE_SOURCE_DIR}/src/pyvinecopulib/sklearn/__init__.pyi"
  DESTINATION pyvinecopulib/sklearn
  OPTIONAL)
