## Main CMake for `compress-utils` project.
##
## C-as-core. C++ binding, Python binding, and WASM binding all consume
## the C ABI declared in include/compress_utils.h.
##
## NOTE: this is a Phase 1 in-progress state. Only ZSTD is fully migrated
## to the C core. Other algorithms (zlib, brotli, bz2, lz4, xz) will be
## enabled as they get ported. Until then the per-algorithm options
## default OFF for the un-ported algorithms.

cmake_minimum_required(VERSION 3.17)

# Default version (used when git tag is not available)
set(DEFAULT_VERSION "0.8.0")

# Include git version extraction module
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/GitVersion.cmake)
get_version_from_git(PROJECT_VERSION_FROM_GIT ${DEFAULT_VERSION})

# Upstream codec versions live in codec-versions.json. The build no longer
# reads it (sources are vendored under third_party/ — see cmake/Vendor.cmake);
# it is consumed only by tools/vendor-codecs.py when re-vendoring.

project(compress-utils
    VERSION ${PROJECT_VERSION_FROM_GIT}
    LANGUAGES C CXX
)

######### PROJECT SETUP #########

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# C++ stays available for tests and (eventually) the C++ binding.
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Hidden visibility by default — only CU_API symbols are exported.
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)

# Build everything position-independent. The static archive
# (compress_utils_static) gets linked into the Python pybind11 module —
# itself a shared library — and on Linux the linker rejects TLS access
# patterns (R_X86_64_TPOFF32 against g_last_error) from non-PIC code
# embedded into a shared object. macOS Mach-O is permissive here, which
# is why this only surfaces on Linux CI.
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Generate compile_commands.json for clangd / IDEs / fuzzers.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Debug)
endif()

if(DEFINED ENV{CMAKE_BUILD_PARALLEL_LEVEL})
    message(STATUS "CMAKE_BUILD_PARALLEL_LEVEL: $ENV{CMAKE_BUILD_PARALLEL_LEVEL}")
endif()

######### COMPILER FLAGS #########

if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
    set(OPT_FLAGS "-flto -ffunction-sections -fdata-sections")
    if(APPLE)
        set(LINKER_FLAGS "-Wl,-dead_strip")
    else()
        set(LINKER_FLAGS "-Wl,--gc-sections")
    endif()
elseif(MSVC)
    set(OPT_FLAGS "/GL")
    set(LINKER_FLAGS "/OPT:REF")
endif()

set(CMAKE_EXE_LINKER_FLAGS_RELEASE    "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS}")
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS}")

if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
    set(CMAKE_C_FLAGS_RELEASE   "${CMAKE_C_FLAGS_RELEASE} ${OPT_FLAGS} -O3")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${OPT_FLAGS} -O3")
elseif(MSVC)
    set(CMAKE_C_FLAGS_RELEASE   "${CMAKE_C_FLAGS_RELEASE} ${OPT_FLAGS} /O2")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${OPT_FLAGS} /O2")
endif()

set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")

######### OPTIONS #########

option(ENABLE_TESTS "Enable building tests" ON)
option(ENABLE_FUZZ  "Enable libFuzzer harnesses (clang only)" OFF)

# Build a single self-contained static archive (compress_utils_static) bundling
# the core dispatcher + every enabled codec's objects.
option(BUILD_STATIC_LIB "Build a self-contained compress_utils_static archive" OFF)

# Per-algorithm inclusion. All six have been migrated to the C core in
# Phase 1; default to ON. Disable individually for slimmer builds.
option(INCLUDE_ZSTD   "Include Zstd compression algorithm"   ON)
option(INCLUDE_BROTLI "Include Brotli compression algorithm" ON)
option(INCLUDE_ZLIB   "Include zlib compression algorithm"   ON)
option(INCLUDE_BZ2    "Include bzip2 compression algorithm"  ON)
option(INCLUDE_LZ4    "Include LZ4 compression algorithm"    ON)
option(INCLUDE_XZ     "Include XZ/LZMA compression algorithm" ON)
option(INCLUDE_SNAPPY "Include Snappy compression algorithm" ON)
option(INCLUDE_GZIP   "Include gzip compression algorithm (DEFLATE, RFC 1952)" ON)

if(NOT INCLUDE_ZSTD AND NOT INCLUDE_BROTLI AND NOT INCLUDE_ZLIB
        AND NOT INCLUDE_BZ2 AND NOT INCLUDE_LZ4 AND NOT INCLUDE_XZ
        AND NOT INCLUDE_SNAPPY AND NOT INCLUDE_GZIP)
    message(FATAL_ERROR "No algorithms included. Enable at least one INCLUDE_<ALGO>.")
endif()

######### LIBRARY TARGETS #########

# Core sources: ABI dispatcher + algorithm registry. Per-algorithm sources
# get appended below by their respective subdir blocks.
set(CU_CORE_SOURCES
    ${CMAKE_SOURCE_DIR}/src/compress_utils.c
    ${CMAKE_SOURCE_DIR}/src/registry.c
)

set(CU_TARGET_DEFINITIONS "")
set(CU_TARGET_LIBS "")

# Vendored codec builder. Each algorithms/<algo>/CMakeLists.txt calls
# cu_add_vendored_codec() to build <algo>_library from the curated sources in
# third_party/ (no ExternalProject fetch, no per-codec configure). See
# cmake/Vendor.cmake and third_party/VENDOR.md.
include(cmake/Vendor.cmake)

# Per-algorithm vtable source + codec library. Each enabled algorithm adds its
# .c vtable file to the core sources and its <algo>_library to CU_TARGET_LIBS.
if(INCLUDE_ZSTD)
    message(STATUS "Including ZSTD")
    add_subdirectory(algorithms/zstd)
    list(APPEND CU_CORE_SOURCES ${CMAKE_SOURCE_DIR}/src/algorithms/zstd/zstd.c)
    list(APPEND CU_TARGET_LIBS zstd_library)
    list(APPEND CU_TARGET_DEFINITIONS INCLUDE_ZSTD)
endif()
if(INCLUDE_BROTLI)
    message(STATUS "Including Brotli")
    add_subdirectory(algorithms/brotli)
    list(APPEND CU_CORE_SOURCES ${CMAKE_SOURCE_DIR}/src/algorithms/brotli/brotli.c)
    list(APPEND CU_TARGET_LIBS brotli_library)
    list(APPEND CU_TARGET_DEFINITIONS INCLUDE_BROTLI)
endif()
# zlib and gzip are the same upstream library (libz) — gzip just drives it with
# a different windowBits (RFC 1952 wrapper). Fetch/build/link libz once if
# either codec is enabled; each codec then contributes its own vtable source.
if(INCLUDE_ZLIB OR INCLUDE_GZIP)
    add_subdirectory(algorithms/zlib)
    list(APPEND CU_TARGET_LIBS zlib_library)
endif()
if(INCLUDE_ZLIB)
    message(STATUS "Including zlib")
    list(APPEND CU_CORE_SOURCES ${CMAKE_SOURCE_DIR}/src/algorithms/zlib/zlib.c)
    list(APPEND CU_TARGET_DEFINITIONS INCLUDE_ZLIB)
endif()
if(INCLUDE_GZIP)
    message(STATUS "Including gzip")
    list(APPEND CU_CORE_SOURCES ${CMAKE_SOURCE_DIR}/src/algorithms/gzip/gzip.c)
    list(APPEND CU_TARGET_DEFINITIONS INCLUDE_GZIP)
endif()
if(INCLUDE_BZ2)
    message(STATUS "Including bzip2")
    add_subdirectory(algorithms/bz2)
    list(APPEND CU_CORE_SOURCES ${CMAKE_SOURCE_DIR}/src/algorithms/bz2/bz2.c)
    list(APPEND CU_TARGET_LIBS bz2_library)
    list(APPEND CU_TARGET_DEFINITIONS INCLUDE_BZ2)
endif()
if(INCLUDE_LZ4)
    message(STATUS "Including LZ4")
    add_subdirectory(algorithms/lz4)
    list(APPEND CU_CORE_SOURCES ${CMAKE_SOURCE_DIR}/src/algorithms/lz4/lz4.c)
    list(APPEND CU_TARGET_LIBS lz4_library)
    list(APPEND CU_TARGET_DEFINITIONS INCLUDE_LZ4)
endif()
if(INCLUDE_XZ)
    message(STATUS "Including XZ")
    add_subdirectory(algorithms/xz)
    list(APPEND CU_CORE_SOURCES ${CMAKE_SOURCE_DIR}/src/algorithms/xz/xz.c)
    list(APPEND CU_TARGET_LIBS xz_library)
    list(APPEND CU_TARGET_DEFINITIONS INCLUDE_XZ LZMA_API_STATIC)
endif()
if(INCLUDE_SNAPPY)
    message(STATUS "Including Snappy")
    add_subdirectory(algorithms/snappy)
    list(APPEND CU_CORE_SOURCES ${CMAKE_SOURCE_DIR}/src/algorithms/snappy/snappy.c)
    list(APPEND CU_TARGET_LIBS snappy_library)
    list(APPEND CU_TARGET_DEFINITIONS INCLUDE_SNAPPY)
    # Snappy is now the pure-C port (andikleen/snappy-c, third_party/snappy) — no
    # C++ runtime dependency. The old c++/stdc++ link for google/snappy is gone;
    # the whole release library is pure C. google/snappy (C++) lives only in the
    # differential test (see tests/).
endif()

# Platform link deps.
if(WIN32)
    find_package(Threads REQUIRED)
    list(APPEND CU_TARGET_LIBS "legacy_stdio_definitions" Threads::Threads "msvcrt")
else()
    # Linux glibc puts log2/pow/etc. in libm, separate from libc. macOS libSystem
    # bundles them so this is a no-op there. Brotli/zlib/lz4/xz can pull from libm.
    list(APPEND CU_TARGET_LIBS m)
endif()

# ---------------------------------------------------------------------------
# Library targets.
#
# Layout: one OBJECT library (compress_utils_obj) compiles the source files
# once; the public SHARED library and the internal consumers (Python
# pybind11 module, C/fuzz tests) all consume those objects. There is no
# public static library:
#
#   - End users who want a shared lib: use `compress_utils`.
#   - End users who want a self-contained executable: build with
#     `-DBUILD_SHARED_LIBS=OFF` (CMake-native) or consume the OBJECT lib
#     directly from a sibling CMake project.
#   - The previous public `compress_utils_static.a` was only ~146 KB of
#     wrapper code — useless without manually linking 6+ algo .a files.
#     Removed.
#
# The OBJECT library is compiled with CU_BUILD_SHARED so its symbols
# carry dllexport/default visibility. They get exported by whichever
# shared lib they end up in (compress_utils.dll or compress_utils_py.pyd).
# ---------------------------------------------------------------------------

add_library(compress_utils_obj OBJECT ${CU_CORE_SOURCES})
target_include_directories(compress_utils_obj
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
    PRIVATE
        ${CMAKE_SOURCE_DIR}/src
)
target_compile_definitions(compress_utils_obj PRIVATE
    ${CU_TARGET_DEFINITIONS}
    CU_BUILD_SHARED
    # cu_version() reads this; carries the full git-describe tag including
    # any prerelease suffix (0.7.0-rc.1, 0.7.0-rc.1-3-gabcd, etc.).
    CU_BUILD_VERSION="${PROJECT_VERSION_FROM_GIT}"
)
# PUBLIC so the object library's own vtable sources compile with each codec's
# include dirs + defines (the <algo>_library targets carry them as usage
# requirements), and so consumers linking the object library inherit them too.
# The vendored codec headers live in third_party/ (committed), so no build-order
# dependency is needed to compile our sources — only to link, which
# target_link_libraries orders automatically.
target_link_libraries(compress_utils_obj PUBLIC ${CU_TARGET_LIBS})

add_library(compress_utils SHARED $<TARGET_OBJECTS:compress_utils_obj>)
target_include_directories(compress_utils
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
)
target_link_libraries(compress_utils PRIVATE ${CU_TARGET_LIBS})

# No add_dependencies() for codec builds: the <algo>_library targets are
# ordinary in-tree static libraries now (built from third_party/), so CMake
# orders their compilation and linking automatically via target_link_libraries.
# The former zstd_external/brotli_external/... ExternalProject fetch steps are
# gone entirely.

# Optional self-contained static archive. Bundles the core dispatcher objects
# (compress_utils_obj, already compiled with default-visibility cu_* symbols)
# plus every enabled codec's OBJECT library, so a single `.a`/`.lib` links
# without hunting down 8 codec archives. This is the artifact the Rust binding
# downloads from GitHub Releases; see .github/workflows/build_and_test_c_cpp.yml.
if(BUILD_STATIC_LIB)
    # Collect the OBJECT libraries behind the enabled codec <algo>_library
    # targets. CU_TARGET_LIBS also holds system libs (m, c++/stdc++, msvcrt)
    # which have no objects to bundle — those stay a link-time responsibility
    # of the consumer, documented in the release README.
    set(CU_CODEC_OBJECTS "")
    foreach(_lib IN LISTS CU_TARGET_LIBS)
        if(_lib MATCHES "_library$")
            string(REGEX REPLACE "_library$" "_objs" _objtgt "${_lib}")
            list(APPEND CU_CODEC_OBJECTS "$<TARGET_OBJECTS:${_objtgt}>")
        endif()
    endforeach()

    add_library(compress_utils_static STATIC
        $<TARGET_OBJECTS:compress_utils_obj>
        ${CU_CODEC_OBJECTS}
    )
    target_include_directories(compress_utils_static
        PUBLIC
            $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
            $<INSTALL_INTERFACE:include>
    )
    # Uniform archive name on every platform (libcompress_utils_static.a /
    # compress_utils_static.lib) so it never collides with the shared lib's
    # Windows import library (compress_utils.lib) and the Rust build.rs can link
    # `static=compress_utils_static` unconditionally. Installed in the INSTALL
    # section below once CU_DIST_DIR is defined.
endif()

######### INSTALL #########

option(SCIKIT_BUILD "Build within scikit-build environment" OFF)

set(CU_DIST_DIR "${CMAKE_SOURCE_DIR}/dist/c")

if(NOT SCIKIT_BUILD)
    install(TARGETS compress_utils
        LIBRARY DESTINATION ${CU_DIST_DIR}/lib
        ARCHIVE DESTINATION ${CU_DIST_DIR}/lib
        RUNTIME DESTINATION ${CU_DIST_DIR}/bin
    )
    install(FILES ${CMAKE_SOURCE_DIR}/include/compress_utils.h
            DESTINATION ${CU_DIST_DIR}/include)
    if(BUILD_STATIC_LIB)
        install(TARGETS compress_utils_static
            ARCHIVE DESTINATION ${CU_DIST_DIR}/lib)
    endif()
endif()

######### TESTS #########

if(ENABLE_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

######### BINDINGS #########

option(BUILD_CPP_BINDINGS    "Build C++ header-only binding"        ON)
option(BUILD_PYTHON_BINDINGS "Build Python binding (via pybind11)"  ON)
option(BUILD_WASM_BINDINGS   "Build per-algorithm .wasm modules (requires zig)" OFF)

if(BUILD_CPP_BINDINGS)
    add_subdirectory(bindings/cpp)
endif()
if(BUILD_PYTHON_BINDINGS)
    add_subdirectory(bindings/python)
endif()
if(BUILD_WASM_BINDINGS)
    # Drives one child CMake configure per algorithm with the zig-wasm
    # toolchain. Cross-compile lives in its own world; native build is
    # untouched. See bindings/wasm/host.cmake.
    include(bindings/wasm/host.cmake)
endif()
