cmake_minimum_required(VERSION 3.15.0)

if(WIN32)
    cmake_policy(SET CMP0091 NEW)
    set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL" CACHE STRING "" FORCE)
endif()


if(WIN32 AND CMAKE_GENERATOR MATCHES "Visual Studio")
    if(NOT DEFINED CMAKE_GENERATOR_TOOLSET)
        set(CMAKE_GENERATOR_TOOLSET "ClangCL" CACHE STRING "Use ClangCL toolset for C99/C11 support on Windows." FORCE)
    endif()
endif()

project(python-blosc2)

# Update to the latest minimum C-Blosc2 version, and to the actual bundled one
set(BLOSC2_MIN_VERSION 3.3.0)
set(BLOSC2_BUNDLED_VERSION v3.3.2)
# set(BLOSC2_BUNDLED_VERSION a7f9a5be3527e87a7cbce1c16977835361d27033)

if(WIN32 AND NOT CMAKE_C_COMPILER_ID STREQUAL "Clang")
    message(FATAL_ERROR "Windows builds require clang-cl. Set CC/CXX to clang-cl or configure CMake with -T ClangCL.")
endif()
# Specifying Python version below is tricky, but if you don't specify the minimum version here,
# it would not consider python3 when looking for the executable. This is problematic since Fedora
# does not include a python symbolic link to python3.
# find_package(Python 3.12 COMPONENTS Interpreter NumPy Development.Module REQUIRED)
# IMO, this would need to be solved in Fedora, so we can just use the following line:
find_package(Python COMPONENTS Interpreter NumPy Development.Module REQUIRED)

# Add custom command to generate the version file
add_custom_command(
  OUTPUT src/blosc2/version.py
  COMMAND ${Python_EXECUTABLE} generate_version.py
  DEPENDS generate_version.py pyproject.toml
  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  VERBATIM
)

# Compile the Cython extension manually...
add_custom_command(
  OUTPUT blosc2_ext.c
  COMMAND Python::Interpreter -m cython
          "${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/blosc2_ext.pyx" --output-file blosc2_ext.c
  DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/blosc2_ext.pyx"
  VERBATIM)

add_custom_command(
  OUTPUT indexing_ext.c
  COMMAND Python::Interpreter -m cython
          "${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/indexing_ext.pyx" --output-file indexing_ext.c
  DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/indexing_ext.pyx"
  VERBATIM)

add_custom_command(
  OUTPUT groupby_ext.c
  COMMAND Python::Interpreter -m cython
          "${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/groupby_ext.pyx" --output-file groupby_ext.c
  DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/groupby_ext.pyx"
  VERBATIM)

add_custom_command(
  OUTPUT utf8_ext.c
  COMMAND Python::Interpreter -m cython
          "${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/utf8_ext.pyx" --output-file utf8_ext.c
  DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/utf8_ext.pyx"
  VERBATIM)

# ...and add it to the target
Python_add_library(blosc2_ext MODULE blosc2_ext.c WITH_SOABI)
target_sources(blosc2_ext PRIVATE src/blosc2/matmul_kernels.c)
target_include_directories(blosc2_ext PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2)
if(UNIX)
  target_link_libraries(blosc2_ext PRIVATE ${CMAKE_DL_LIBS})
endif()
Python_add_library(indexing_ext MODULE indexing_ext.c WITH_SOABI)
Python_add_library(groupby_ext MODULE groupby_ext.c WITH_SOABI)
Python_add_library(utf8_ext MODULE utf8_ext.c WITH_SOABI)
# NpyString_pack() and friends are part of NumPy's 2.0 C API; opt in
# explicitly since numpy/*.h otherwise targets an older API version by
# default for source compatibility.
target_compile_definitions(utf8_ext PRIVATE NPY_TARGET_VERSION=NPY_2_0_API_VERSION)

# We need to link against NumPy
target_link_libraries(blosc2_ext PRIVATE Python::NumPy)
target_link_libraries(indexing_ext PRIVATE Python::NumPy)
target_link_libraries(groupby_ext PRIVATE Python::NumPy)
target_link_libraries(utf8_ext PRIVATE Python::NumPy)

# Fetch and build miniexpr library
include(FetchContent)

set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(MINIEXPR_BUILD_SHARED OFF CACHE BOOL "Build miniexpr shared library" FORCE)
set(MINIEXPR_BUILD_TESTS OFF CACHE BOOL "Build miniexpr tests" FORCE)
set(MINIEXPR_BUILD_EXAMPLES OFF CACHE BOOL "Build miniexpr examples" FORCE)
set(MINIEXPR_BUILD_BENCH OFF CACHE BOOL "Build miniexpr benchmarks" FORCE)
# Keep miniexpr's and C-Blosc2's bundled files inside the blosc2 package.
# Without this, their install rules use the default CMAKE_INSTALL_LIBDIR
# ("lib"), which scikit-build places at site-packages/lib.
#
# These must stay RELATIVE to CMAKE_INSTALL_PREFIX (scikit-build-core points it
# at the wheel's platlib).  Both blosc2.pc.in and the exported Blosc2Targets
# compose ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}, so an absolute value
# here produces a doubled path that no consumer can use.
set(CMAKE_INSTALL_INCLUDEDIR blosc2/include)
set(CMAKE_INSTALL_LIBDIR blosc2/lib)
set(CMAKE_INSTALL_DATADIR blosc2/share)

if(EMSCRIPTEN)
  set(MINIEXPR_ENABLE_TCC_JIT ON CACHE BOOL "Enable TCC JIT in Emscripten builds" FORCE)
  set(MINIEXPR_WASM32_SIDE_MODULE ON CACHE BOOL "Use host-registered wasm32 JIT helpers" FORCE)
endif()

FetchContent_Declare(miniexpr
    GIT_REPOSITORY https://github.com/Blosc/miniexpr.git
    GIT_TAG bd2c602a652c50b306def625c5fe5491cbd13f76
    # SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../miniexpr
)
FetchContent_MakeAvailable(miniexpr)

# Link against miniexpr static library
target_link_libraries(blosc2_ext PRIVATE miniexpr_static)
if(APPLE)
  target_link_libraries(blosc2_ext PRIVATE "-framework Accelerate")
endif()

target_compile_features(blosc2_ext PRIVATE c_std_11)
target_compile_features(indexing_ext PRIVATE c_std_11)
target_compile_features(groupby_ext PRIVATE c_std_11)
target_compile_features(utf8_ext PRIVATE c_std_11)
if(WIN32 AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
    execute_process(
        COMMAND "${CMAKE_C_COMPILER}" -print-resource-dir
        OUTPUT_VARIABLE _clang_resource_dir
        OUTPUT_STRIP_TRAILING_WHITESPACE
        ERROR_QUIET
    )
    if(_clang_resource_dir)
        if(CMAKE_SIZEOF_VOID_P EQUAL 8)
            set(_clang_builtins "${_clang_resource_dir}/lib/windows/clang_rt.builtins-x86_64.lib")
        else()
            set(_clang_builtins "${_clang_resource_dir}/lib/windows/clang_rt.builtins-i386.lib")
        endif()
        if(EXISTS "${_clang_builtins}")
            target_link_libraries(blosc2_ext PRIVATE "${_clang_builtins}")
        endif()
        unset(_clang_builtins)
    endif()
    unset(_clang_resource_dir)
endif()

if(DEFINED ENV{USE_SYSTEM_BLOSC2})
    set(USE_SYSTEM_BLOSC2 ON)
endif()

if(USE_SYSTEM_BLOSC2)
    set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
    find_package(PkgConfig REQUIRED)
    pkg_check_modules(Blosc2 REQUIRED IMPORTED_TARGET blosc2>=${BLOSC2_MIN_VERSION})
    target_link_libraries(blosc2_ext PRIVATE PkgConfig::Blosc2)
else()
    set(STATIC_LIB ON CACHE BOOL "Build a static version of the blosc library.")
    set(SHARED_LIB ON CACHE BOOL "Build a shared library version of the blosc library.")
    set(BUILD_TESTS OFF CACHE BOOL "Build C-Blosc2 tests")
    set(BUILD_EXAMPLES OFF CACHE BOOL "Build C-Blosc2 examples")
    set(BUILD_BENCHMARKS OFF CACHE BOOL "Build C-Blosc2 benchmarks")
    set(BUILD_FUZZERS OFF CACHE BOOL "Build C-Blosc2 fuzzers")
    set(CMAKE_POSITION_INDEPENDENT_CODE ON)
    set(Blosc2_INSTALL_CMAKEDIR ${CMAKE_INSTALL_LIBDIR}/cmake/blosc2) # directory for cmake files
    set(CMAKE_INSTALL_BINDIR blosc2/lib) # directory for libblosc2.dll on windows
    # we will put the binaries of the C-Blosc2 library into the wheels according to PEP
    set(BLOSC_INSTALL ON)
    include(FetchContent)
    FetchContent_Declare(blosc2
        GIT_REPOSITORY https://github.com/Blosc/c-blosc2
        GIT_TAG ${BLOSC2_BUNDLED_VERSION}
        # SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../c-blosc2
    )
    FetchContent_MakeAvailable(blosc2)
    include_directories("${blosc2_SOURCE_DIR}/include")
    target_link_libraries(blosc2_ext PRIVATE blosc2_static)

    # scikit-build-core's wheel writer follows symlinks, so each of the three
    # names C-Blosc2 installs for the shared library (the VERSION real file
    # plus the SOVERSION and bare symlinks) lands in the wheel as a full copy.
    # Teaching the writer to preserve them would not help either: pip
    # materialises a symlink stored in a wheel as a regular file holding the
    # link target as text.
    #
    # Nothing references the fully versioned name -- it is only a symlink
    # target -- so drop VERSION and keep the two that are used:
    # libblosc2.<SOVERSION>, the SONAME a linked application loads, and the
    # bare name that -lblosc2 resolves.  Two copies instead of three.
    if(TARGET blosc2_shared)
        set_property(TARGET blosc2_shared PROPERTY VERSION)
    endif()

    # Overwrite the blosc2.pc that C-Blosc2 just installed with a relocatable
    # one; see cmake/blosc2-relocatable.pc.in.  Two things make this work:
    #  - it is declared after FetchContent_MakeAvailable(), so it runs after
    #    C-Blosc2's own install rule;
    #  - it uses install(CODE) rather than install(FILES), because the latter
    #    goes through file(INSTALL), which skips the copy when source and
    #    destination share a timestamp -- and both .pc files are generated
    #    within the same second, so the upstream one would silently survive.
    # The version is read from the bundled header rather than from
    # BLOSC2_BUNDLED_VERSION, which may be pinned to a bare commit hash.
    file(STRINGS "${blosc2_SOURCE_DIR}/include/blosc2.h" _blosc2_version_line
         REGEX "^#define[ \t]+BLOSC2_VERSION_STRING")
    string(REGEX REPLACE ".*\"([^\"]+)\".*" "\\1" BLOSC2_PC_VERSION "${_blosc2_version_line}")
    install(CODE "
        set(BLOSC2_PC_VERSION \"${BLOSC2_PC_VERSION}\")
        configure_file(
            \"${PROJECT_SOURCE_DIR}/cmake/blosc2-relocatable.pc.in\"
            \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/pkgconfig/blosc2.pc\"
            @ONLY)
    ")
endif()

# TODO
# CHECK THIS
if(UNIX)
  set_target_properties(blosc2_ext PROPERTIES
    BUILD_WITH_INSTALL_RPATH TRUE
    INSTALL_RPATH "$<IF:$<PLATFORM_ID:Darwin>,@loader_path/lib,\$ORIGIN/lib>"
  )
endif()

if(WIN32)
  if(TARGET blosc2_shared)
    add_custom_command(TARGET blosc2_ext POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy_if_different
              $<TARGET_FILE:blosc2_shared>
              $<TARGET_FILE_DIR:blosc2_ext>
    )
  endif()
endif()

# Python extension -> site-packages/blosc2
install(
  TARGETS blosc2_ext indexing_ext groupby_ext utf8_ext
  LIBRARY DESTINATION ${SKBUILD_PLATLIB_DIR}/blosc2
)

install(
  FILES "${miniexpr_SOURCE_DIR}/src/me_jit_glue.js"
  DESTINATION ${SKBUILD_PLATLIB_DIR}/blosc2
)

# The wheel bundles miniexpr's libtcc (LGPL 2.1), so its license texts have to
# travel with it.  miniexpr's own install rules put them under blosc2/share,
# which keeps them next to the library for C consumers; mirror them into
# .dist-info/licenses as well, which is where PEP 639 tooling looks.
install(
  FILES
    "${miniexpr_SOURCE_DIR}/LICENSE"
    "${miniexpr_SOURCE_DIR}/LICENSE-LIBTCC"
    "${miniexpr_SOURCE_DIR}/LICENSE-SLEEF"
    "${miniexpr_SOURCE_DIR}/LICENSE-TINYEXPR"
    "${miniexpr_SOURCE_DIR}/THIRD_PARTY_NOTICES.md"
  DESTINATION ${SKBUILD_METADATA_DIR}/licenses/miniexpr
)
