# Copyright 2026 Elias Benali (@ebenali) and TheCleaners.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

cmake_minimum_required(VERSION 3.27)

# Bazel's `-c opt` compiles everything (ScaNN and its dependencies) at -O2,
# with -O3 only on the specific targets that ask for it (see
# cmake/SourceFlags.cmake). Mirror that instead of CMake's -O3 Release
# default. Must be set before project() to take effect as the default.
if(NOT DEFINED CMAKE_CXX_FLAGS_RELEASE)
  set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG" CACHE STRING "Flags used by the CXX compiler during RELEASE builds.")
endif()
if(NOT DEFINED CMAKE_C_FLAGS_RELEASE)
  set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG" CACHE STRING "Flags used by the C compiler during RELEASE builds.")
endif()

# Prefer clang when no compiler was chosen (CC/CXX, -DCMAKE_<LANG>_COMPILER
# or a toolchain file): it's upstream's compiler, and the one the
# equivalence checks against upstream use. GCC >= 13 works too (see below).
if(NOT DEFINED CMAKE_CXX_COMPILER AND NOT DEFINED ENV{CXX} AND NOT CMAKE_TOOLCHAIN_FILE)
  find_program(_scann_clangxx clang++ NO_CACHE)
  find_program(_scann_clang clang NO_CACHE)
  if(_scann_clangxx AND _scann_clang AND NOT DEFINED ENV{CC} AND NOT DEFINED CMAKE_C_COMPILER)
    set(CMAKE_C_COMPILER "${_scann_clang}")
    set(CMAKE_CXX_COMPILER "${_scann_clangxx}")
  endif()
endif()

# The version lives in VERSION (also read by pyproject.toml; Cargo.toml
# repeats it, which is checked right after project()). It is a semver
# version, optionally with a pre-release suffix: 0.2.0, 0.2.0-rc.1,
# 0.2.0-beta.2. project() takes only the numeric part; Python gets the
# PEP 440 spelling (0.2.0rc1, 0.2.0b2), as in the wheel's metadata.
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" SCANN_CORE_VERSION LIMIT_COUNT 1)
if(NOT SCANN_CORE_VERSION MATCHES "^([0-9]+\\.[0-9]+\\.[0-9]+)(-(alpha|beta|rc)\\.[0-9]+)?$")
  message(FATAL_ERROR "VERSION (${SCANN_CORE_VERSION}) must be X.Y.Z or X.Y.Z-{alpha,beta,rc}.N")
endif()
project(scann_core VERSION ${CMAKE_MATCH_1} LANGUAGES C CXX)
string(REGEX REPLACE "-alpha\\." "a" SCANN_CORE_VERSION_PEP440 "${SCANN_CORE_VERSION}")
string(REGEX REPLACE "-beta\\." "b" SCANN_CORE_VERSION_PEP440 "${SCANN_CORE_VERSION_PEP440}")
string(REGEX REPLACE "-rc\\." "rc" SCANN_CORE_VERSION_PEP440 "${SCANN_CORE_VERSION_PEP440}")
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/Cargo.toml" _scann_cargo_version
     REGEX "^version = " LIMIT_COUNT 1)
if(NOT _scann_cargo_version MATCHES "^version = \"${SCANN_CORE_VERSION}\"")
  message(FATAL_ERROR "Cargo.toml (${_scann_cargo_version}) doesn't match VERSION (${SCANN_CORE_VERSION})")
endif()

# Supported compilers: clang >= 19 (upstream's compiler, and the one the
# equivalence checks against upstream use) and GCC >= 13. Older versions are
# untested; MSVC lacks the GNU vector extensions ScaNN uses.
option(SCANN_ALLOW_UNSUPPORTED_COMPILER
  "Configure even with a compiler other than clang or GCC (expect build errors)" OFF)
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 19)
    message(WARNING "scann-core is tested with clang >= 19; this is clang ${CMAKE_CXX_COMPILER_VERSION}.")
  endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13)
    message(WARNING "scann-core is tested with GCC >= 13; this is GCC ${CMAKE_CXX_COMPILER_VERSION}.")
  endif()
elseif(NOT SCANN_ALLOW_UNSUPPORTED_COMPILER)
  message(FATAL_ERROR
    "scann-core builds with clang (>= 19) or GCC (>= 13); the C++ compiler is "
    "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}. Set CC/CXX (or "
    "-DCMAKE_C_COMPILER/-DCMAKE_CXX_COMPILER), or pass "
    "-DSCANN_ALLOW_UNSUPPORTED_COMPILER=ON to try anyway.")
endif()

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Bazel passes -std=c++17, not -std=gnu++17. Besides dialect, this matters
# numerically under GCC: gnu++17 defaults to -ffp-contract=fast, c++17 to
# -ffp-contract=off, which changes FMA contraction and therefore distances.
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Default to Release, but only as the top-level project: as a subproject,
# the build type is the parent project's to choose.
if(PROJECT_IS_TOP_LEVEL AND NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()

option(SCANN_BUILD_STATIC "Build the static (.a) scann_core library" ON)
option(SCANN_BUILD_SHARED "Build the shared (.so/.dylib) scann_core library" ${PROJECT_IS_TOP_LEVEL})
# Bindings, tests and examples default to ON only when scann-core is the
# top-level project; pulled in with FetchContent/add_subdirectory, it builds
# just the library unless asked for more.
option(SCANN_BUILD_PYTHON "Build the pybind11 Python module and package" ${PROJECT_IS_TOP_LEVEL})
option(SCANN_BUILD_RUST_BINDINGS "Build the Rust bindings (via cxx)" ${PROJECT_IS_TOP_LEVEL})
option(SCANN_BUILD_TESTS "Build the C++ tests" ${PROJECT_IS_TOP_LEVEL})
option(SCANN_BUILD_EXAMPLES "Build the C++ examples" ${PROJECT_IS_TOP_LEVEL})
# Off by default, and not part of the wheel: built against the TensorFlow
# installed in Python_EXECUTABLE (see tf_op/CMakeLists.txt, docs/tensorflow.md).
option(SCANN_BUILD_TF_OP "Build the TensorFlow custom op and the scann_tf_ops package (Linux; needs TensorFlow)" OFF)
# Off by default: the native PyTorch op and the scann_torch_ops package, the
# scann-core-torch wheel (see torch_op/CMakeLists.txt, docs/integrations.md).
# Built against the headers of the torch in SCANN_TORCH_PYTHON (default
# Python_EXECUTABLE), for any torch >= 2.10.
option(SCANN_BUILD_TORCH_OP "Build the native PyTorch op and the scann_torch_ops package (Linux; needs torch >= 2.10 headers)" OFF)
option(SCANN_USE_SYSTEM_DEPS
  "Try find_package() for each dependency before fetching it (versions are still required to match the pins exactly)" OFF)
option(SCANN_RUST_BUILD_ENV_ONLY
  "Write rust/scann_core_rust_build.env for the crate's build script without invoking cargo (set by build.rs)" OFF)
option(SCANN_ENABLE_LTO "Build scann_core with link-time optimization (see README before enabling)" OFF)

if(NOT SCANN_BUILD_STATIC AND NOT SCANN_BUILD_SHARED)
  message(FATAL_ERROR "At least one of SCANN_BUILD_STATIC / SCANN_BUILD_SHARED must be ON")
endif()
if((SCANN_BUILD_RUST_BINDINGS OR SCANN_RUST_BUILD_ENV_ONLY) AND NOT SCANN_BUILD_STATIC)
  message(FATAL_ERROR "The Rust bindings (SCANN_BUILD_RUST_BINDINGS or SCANN_RUST_BUILD_ENV_ONLY) require SCANN_BUILD_STATIC=ON (the crate links the static archive)")
endif()

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

# Global flags first: like Bazel's --copt, they must also reach the
# dependencies (highway in particular), so they are set before FetchContent.
include(Flags)
include(Dependencies)
include(Proto)
include(SourceFlags)

set(SCANN_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
set(SCANN_GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
configure_file(core/scann_core/version.h.in "${SCANN_GEN_DIR}/scann_core/version.h" @ONLY)

# -----------------------------------------------------------------------
# Generated sources
# -----------------------------------------------------------------------
file(GLOB_RECURSE SCANN_PROTO_FILES CONFIGURE_DEPENDS "${SCANN_SRC_DIR}/*.proto")
scann_generate_cpp_protos(SCANN_PROTO_SRCS SCANN_PROTO_HDRS
  PROTO_ROOT "${SCANN_SRC_DIR}"
  OUT_DIR "${SCANN_GEN_DIR}"
  PROTOS ${SCANN_PROTO_FILES})

# Equivalent of hashes/internal/template_sharding.bzl's batch_size_sharder.
scann_shard_lut16_templates(SCANN_LUT16_SRCS
  TEMPLATE_DIR "${SCANN_SRC_DIR}/scann/hashes/internal/bazel_templates"
  OUT_DIR "${SCANN_GEN_DIR}/scann/hashes/internal")

# -----------------------------------------------------------------------
# Core library
# -----------------------------------------------------------------------
file(GLOB_RECURSE SCANN_CORE_SRCS CONFIGURE_DEPENDS "${SCANN_SRC_DIR}/*.cc")
# Templates are expanded above, never compiled directly.
list(FILTER SCANN_CORE_SRCS EXCLUDE REGEX "\\.tpl\\.cc$")
# The pybind11-typed facade and module entry point are consumers of the
# core, not part of it -- built by python/CMakeLists.txt.
list(FILTER SCANN_CORE_SRCS EXCLUDE REGEX "/scann_ops/cc/python/scann_pybind\\.cc$")
list(FILTER SCANN_CORE_SRCS EXCLUDE REGEX "/scann_ops/cc/scann_npy\\.cc$")

# scann-core's own additions (the C++ config builder), kept outside src/ so
# src/ stays a faithful copy of upstream plus documented fixes.
set(SCANN_EXTRA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/core")
file(GLOB_RECURSE SCANN_EXTRA_SRCS CONFIGURE_DEPENDS "${SCANN_EXTRA_DIR}/*.cc")

scann_apply_bazel_source_flags("${SCANN_SRC_DIR}" ${SCANN_LUT16_SRCS})

# Usage requirements, shared by every target built from the core objects.
add_library(scann_core_deps INTERFACE)
# SYSTEM: code using scann-core shouldn't get warnings from ScaNN's headers.
target_include_directories(scann_core_deps SYSTEM INTERFACE
  "$<BUILD_INTERFACE:${SCANN_SRC_DIR}>"
  "$<BUILD_INTERFACE:${SCANN_EXTRA_DIR}>"
  "$<BUILD_INTERFACE:${SCANN_GEN_DIR}>"
  # gtest/gtest_prod.h, included by several ScaNN headers (FRIEND_TEST).
  "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/third_party/googletest/include>"
)
# The abseil list is exactly the set of @com_google_absl labels referenced
# by the Bazel packages this tree was extracted from, mapped to abseil's
# CMake target names.
target_link_libraries(scann_core_deps INTERFACE
  protobuf::libprotobuf
  absl::algorithm_container
  absl::any_invocable
  absl::base
  absl::bits
  absl::btree
  absl::check
  absl::core_headers
  absl::endian
  absl::flags
  absl::flat_hash_map
  absl::flat_hash_set
  absl::hash
  absl::inlined_vector
  absl::int128
  absl::log
  absl::memory
  absl::no_destructor
  absl::node_hash_map
  absl::node_hash_set
  absl::nullability
  absl::optional
  absl::prefetch
  absl::random_distributions
  absl::random_random
  absl::span
  absl::status
  absl::statusor
  absl::str_format
  absl::string_view
  absl::strings
  absl::synchronization
  absl::time
  absl::variant
  hwy::hwy
  hwy::hwy_contrib
  Eigen3::Eigen
  cnpy
)

# Compiled exactly once; the static and shared libraries both relink these
# objects, so building both is not twice the compile time.
add_library(scann_core_objects OBJECT ${SCANN_CORE_SRCS} ${SCANN_EXTRA_SRCS} ${SCANN_LUT16_SRCS} ${SCANN_PROTO_SRCS})
target_link_libraries(scann_core_objects PUBLIC scann_core_deps)

if(SCANN_ENABLE_LTO)
  include(CheckIPOSupported)
  check_ipo_supported(RESULT _scann_ipo_ok OUTPUT _scann_ipo_msg LANGUAGES CXX)
  if(NOT _scann_ipo_ok)
    message(FATAL_ERROR "SCANN_ENABLE_LTO=ON but IPO is not supported: ${_scann_ipo_msg}")
  endif()
  set_target_properties(scann_core_objects PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON)
endif()

# Several distance measures register themselves by name from static
# initializers (SCANN_REGISTER_DISTANCE_MEASURE); Bazel marks those
# libraries alwayslink = 1. Any consumer of the static archive therefore
# has to link scann_core's own objects whole-archive, or the linker drops
# the registrations and config lookups like "DotProductDistance" fail at
# runtime. scann::core_static does that for CMake consumers.
if(SCANN_BUILD_STATIC)
  add_library(scann_core_static STATIC $<TARGET_OBJECTS:scann_core_objects>)
  target_link_libraries(scann_core_static PUBLIC scann_core_deps)
  set_target_properties(scann_core_static PROPERTIES OUTPUT_NAME scann_core)

  add_library(scann_core_static_whole INTERFACE)
  target_link_libraries(scann_core_static_whole INTERFACE
    "$<LINK_LIBRARY:WHOLE_ARCHIVE,scann_core_static>")
  add_library(scann::core_static ALIAS scann_core_static_whole)

  # For consumers outside CMake (the Rust crate, hand-written Makefiles):
  # every transitive static dependency archive merged into one, so linking
  # needs just libscann_core.a (whole-archive) + libscann_core_deps.a.
  include(BundleStatic)
  scann_bundle_static_deps(scann_core_deps_bundle
    ROOT scann_core_deps
    OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libscann_core_deps.a"
    SYSTEM_LIBS_VAR SCANN_CORE_SYSTEM_LIBS)
endif()

if(SCANN_BUILD_SHARED)
  add_library(scann_core_shared SHARED $<TARGET_OBJECTS:scann_core_objects>)
  # Dependencies are linked into the .so; consumers only need their headers,
  # not a second copy of abseil/protobuf.
  target_link_libraries(scann_core_shared
    PRIVATE scann_core_deps
    INTERFACE "$<COMPILE_ONLY:scann_core_deps>")
  set_target_properties(scann_core_shared PROPERTIES
    OUTPUT_NAME scann_core
    VERSION ${PROJECT_VERSION}
    SOVERSION ${PROJECT_VERSION_MAJOR})
  if(SCANN_ENABLE_LTO)
    set_target_properties(scann_core_shared PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON)
  endif()
  add_library(scann::core_shared ALIAS scann_core_shared)
endif()

if(SCANN_BUILD_STATIC)
  add_library(scann::core ALIAS scann_core_static_whole)
else()
  add_library(scann::core ALIAS scann_core_shared)
endif()

if(SCANN_BUILD_TESTS)
  enable_testing()
  add_subdirectory(tests)
endif()

if(SCANN_BUILD_EXAMPLES)
  add_subdirectory(examples)
endif()

if(SCANN_BUILD_PYTHON)
  add_subdirectory(python)
endif()

if(SCANN_BUILD_TF_OP)
  add_subdirectory(tf_op)
endif()

if(SCANN_BUILD_TORCH_OP)
  add_subdirectory(torch_op)
endif()

if(SCANN_BUILD_RUST_BINDINGS OR SCANN_RUST_BUILD_ENV_ONLY)
  add_subdirectory(rust)
endif()
