cmake_minimum_required(VERSION 3.15...3.27)
project(gwseq_io LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Prefer static libraries to make the resulting Python extension self-contained.
# Not on Linux: distro static archives are commonly built without -fPIC (e.g.
# Debian's libcurl.a), and the linker rejects those inside a shared module. The
# shared libs are already installed alongside them, so use CMake's default
# shared-first search order there.
if(APPLE OR WIN32)
  set(CMAKE_FIND_LIBRARY_SUFFIXES .a .lib .so .dylib)
endif()

# Static dependencies we build ourselves (the FetchContent fallbacks below) end
# up inside a shared module, so they have to be position-independent too.
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Default to Release build when no build type is set
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()

# Development.Module was renamed across CMake versions
if(CMAKE_VERSION VERSION_LESS 3.18)
  set(DEV_MODULE Development)
else()
  set(DEV_MODULE Development.Module)
endif()

# Locate Python and nanobind
find_package(Python 3.9 COMPONENTS Interpreter ${DEV_MODULE} REQUIRED)

execute_process(
  COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
  OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT)
find_package(nanobind CONFIG REQUIRED)

include(FetchContent)

# What a fetched dependency is declared with, so that none of its install rules
# runs as part of ours: install rules defined in a directory added with
# EXCLUDE_FROM_ALL are ignored when the parent is installed. Without it, a
# dependency's own libraries and headers land in the wheel next to the gwseq_io
# package — and a static archive left in site-packages is then what the next
# configure's find_package picks up, so the build after it links that instead of
# the system library and fails on its missing transitive dependencies. Each of
# them still builds: _gwseq_io links them, and an inter-target dependency
# supersedes the exclusion.
#
# Note: the keyword reached FetchContent_Declare in CMake 3.28, so on anything
# older the dependencies' own options are all that keeps them out — see the
# curl block below, which sets both of the ones that matter.
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.28)
  set(GWSEQ_FETCH_EXCLUDE EXCLUDE_FROM_ALL)
else()
  set(GWSEQ_FETCH_EXCLUDE "")
endif()

# External dependencies (required).
find_package(CURL QUIET)

# Which zlib to build against. zlib-ng in compat mode reimplements the zlib API
# and ABI exactly -- z_stream, inflateReset, deflateBound, crc32, the 15+32
# auto-detect -- so nothing under src/cpp changes either way, and the two
# streaming codecs in util/streams.cpp keep working untouched. What differs is
# speed: zlib-ng vectorises the inner loops, which is worth having on a library
# that deflates every block it writes and inflates every block it reads.
#
# Note: not a fallback for a missing zlib. Nearly every machine has one, so
# looking first and fetching only on failure would find stock zlib everywhere
# and the option would never do anything. "zlib" is there for builds that want
# the system library regardless: offline, distribution packaging, or comparing
# output bytes against a file written by an older build.
set(GWSEQ_ZLIB_IMPL "auto" CACHE STRING "zlib implementation: auto, zlib-ng, zlib")
set_property(CACHE GWSEQ_ZLIB_IMPL PROPERTY STRINGS auto zlib-ng zlib)

# If not found locally, download and build from source on any platform.
if(NOT CURL_FOUND)
  message(STATUS "curl not found locally — fetching source")
  set(BUILD_CURL_EXE    OFF CACHE INTERNAL "")
  set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "")
  set(CURL_USE_LIBSSH2  OFF CACHE INTERNAL "")
  # Keep curl's own install rules out of our install step, or its headers and
  # libraries land in the wheel next to the gwseq_io package. Two options, not
  # one: CURL_DISABLE_INSTALL covers the headers, the pkg-config file and the
  # curl binary, all of which its top-level CMakeLists guards, but the archive
  # itself is installed by lib/CMakeLists.txt behind CURL_ENABLE_EXPORT_TARGET.
  # With only the first set, every build left a lib/libcurl.a in site-packages
  # and the next configure found it — see GWSEQ_FETCH_EXCLUDE above, which stops
  # both regardless of what curl calls its options.
  set(CURL_DISABLE_INSTALL      ON  CACHE INTERNAL "")
  set(CURL_ENABLE_EXPORT_TARGET OFF CACHE INTERNAL "")
  # Configure native TLS backend based on the OS
  if(WIN32)
    set(CURL_USE_SCHANNEL  ON  CACHE INTERNAL "")  # Windows native TLS
    set(CURL_USE_OPENSSL   OFF CACHE INTERNAL "")
  elseif(APPLE)
    set(CURL_USE_SECTRANSP ON  CACHE INTERNAL "")  # macOS native TLS (Secure Transport)
    set(CURL_USE_OPENSSL   OFF CACHE INTERNAL "")
  else()
    set(CURL_USE_OPENSSL   ON  CACHE INTERNAL "")  # Linux — requires OpenSSL installed
  endif()
  # GIT_SHALLOW: clone only the tagged commit rather than the project's whole
  # history, which is the bulk of what these fetches download. It needs a branch
  # or tag name to resolve against — every GIT_TAG here is a tag, so pinning one
  # of them to a raw commit later means dropping the flag on that dependency.
  FetchContent_Declare(
    CURL
    GIT_REPOSITORY https://github.com/curl/curl.git
    GIT_TAG        curl-8_11_1
    GIT_SHALLOW    TRUE
    ${GWSEQ_FETCH_EXCLUDE}
  )
  FetchContent_MakeAvailable(CURL)
endif()

if(NOT TARGET CURL::libcurl)
  message(FATAL_ERROR "curl is required but could not be found or built.")
endif()

if(NOT GWSEQ_ZLIB_IMPL STREQUAL "zlib")
  message(STATUS "zlib implementation: zlib-ng (compat mode) — fetching source")
  set(ZLIB_COMPAT        ON  CACHE INTERNAL "")  # build the zlib API, not zng_*
  set(ZLIB_ENABLE_TESTS  OFF CACHE INTERNAL "")
  set(ZLIBNG_ENABLE_TESTS OFF CACHE INTERNAL "")
  set(WITH_GTEST         OFF CACHE INTERNAL "")
  set(BUILD_SHARED_LIBS  OFF CACHE INTERNAL "")
  # As zlib and curl: keep its install rules out of ours, or its headers and
  # libraries land in the wheel next to the gwseq_io package.
  set(SKIP_INSTALL_ALL   ON  CACHE INTERNAL "")
  FetchContent_Declare(
    zlib-ng
    GIT_REPOSITORY https://github.com/zlib-ng/zlib-ng.git
    GIT_TAG        2.2.2
    GIT_SHALLOW    TRUE
    ${GWSEQ_FETCH_EXCLUDE}
  )
  FetchContent_MakeAvailable(zlib-ng)
  # In compat mode the targets carry zlib's own names, as the whole point of
  # that mode is to be indistinguishable from it. Which of them is the real
  # library and which an alias depends on BUILD_SHARED_LIBS and on the version,
  # and an ALIAS may not point at another ALIAS, so each candidate is resolved
  # through ALIASED_TARGET before being aliased again.
  if(NOT TARGET ZLIB::ZLIB)
    set(GWSEQ_ZLIB_TARGET "")
    foreach(candidate zlib zlibstatic zlibngstatic zlib-ng)
      if(TARGET ${candidate})
        get_target_property(GWSEQ_ZLIB_ALIASED ${candidate} ALIASED_TARGET)
        if(GWSEQ_ZLIB_ALIASED)
          set(GWSEQ_ZLIB_TARGET ${GWSEQ_ZLIB_ALIASED})
        else()
          set(GWSEQ_ZLIB_TARGET ${candidate})
        endif()
        break()
      endif()
    endforeach()
    if(GWSEQ_ZLIB_TARGET)
      add_library(ZLIB::ZLIB ALIAS ${GWSEQ_ZLIB_TARGET})
    endif()
  endif()
  if(NOT TARGET ZLIB::ZLIB)
    if(GWSEQ_ZLIB_IMPL STREQUAL "zlib-ng")
      message(FATAL_ERROR "zlib-ng was requested but could not be built.")
    endif()
    message(WARNING "zlib-ng could not be built — falling back to zlib")
    set(GWSEQ_ZLIB_IMPL "zlib")
  endif()
endif()

if(GWSEQ_ZLIB_IMPL STREQUAL "zlib")
  find_package(ZLIB QUIET)
endif()

if(GWSEQ_ZLIB_IMPL STREQUAL "zlib" AND NOT ZLIB_FOUND)
  message(STATUS "zlib not found locally — fetching source")
  # Same as curl above: zlib installs libz, zlib.h, the man page and a .pc file
  # by default, all of which would be copied into the wheel root.
  set(SKIP_INSTALL_ALL ON CACHE INTERNAL "")
  # GWSEQ_FETCH_EXCLUDE earns its keep twice here: v1.3.1 has no
  # ZLIB_BUILD_EXAMPLES option, so its example and minigzip binaries are part of
  # the default target, and excluding the directory drops them too. zlibstatic
  # is still built, _gwseq_io linking against it.
  FetchContent_Declare(
    ZLIB
    GIT_REPOSITORY https://github.com/madler/zlib.git
    GIT_TAG        v1.3.1
    GIT_SHALLOW    TRUE
    ${GWSEQ_FETCH_EXCLUDE}
  )
  FetchContent_MakeAvailable(ZLIB)
  # FetchContent exposes 'zlibstatic'; create the canonical ZLIB::ZLIB alias
  if(NOT TARGET ZLIB::ZLIB)
    add_library(ZLIB::ZLIB ALIAS zlibstatic)
  endif()
endif()

if(NOT TARGET ZLIB::ZLIB)
  message(FATAL_ERROR "zlib is required but could not be found or built.")
endif()

# Single translation unit: every module under src/cpp is a self-contained .cpp
# guarded by `#pragma once` and #included by binding/binding.cpp — see
# src/cpp/util/README.md. Only binding/binding.cpp is compiled; CMake's
# dependency scanner picks up the included modules and rebuilds when any of them
# changes.
#
# NOMINSIZE: without it nanobind appends -Os to this target in Release
# (nanobind_opt_size, applied unless the option is given), and a target option
# comes after CMAKE_CXX_FLAGS_RELEASE on the command line, so -Os beats the -O3
# that would otherwise apply. That default is sized for a module that is mostly
# binding glue; here the one translation unit is also every bbi/bam/hic parser
# and both streaming codecs, and those byte-level loops want -O3's inlining and
# vectorisation. It is all-or-nothing with a single TU: the glue gets -O3 too,
# and the module grows.
nanobind_add_module(
  _gwseq_io
  NOMINSIZE
  src/cpp/binding/binding.cpp
)

target_include_directories(_gwseq_io PRIVATE src/cpp)

# Build-time tags, exposed on the module as __version__, __build_type__ and
# __compiler__. SKBUILD_PROJECT_VERSION is injected by scikit-build-core from
# pyproject.toml — pyproject stays the single source of truth for the version.
if(NOT DEFINED SKBUILD_PROJECT_VERSION)
  set(SKBUILD_PROJECT_VERSION "0.0.0+local")  # plain-cmake build, outside pip
endif()
message(STATUS "gwseq_io version: ${SKBUILD_PROJECT_VERSION}")

target_compile_definitions(_gwseq_io PRIVATE
  GWSEQ_IO_VERSION="${SKBUILD_PROJECT_VERSION}"
  GWSEQ_IO_BUILD_TYPE="${CMAKE_BUILD_TYPE}"
  GWSEQ_IO_COMPILER="${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}")

if(CURL_FOUND)
  message(STATUS "curl found: ${CURL_VERSION_STRING}")
endif()
target_link_libraries(_gwseq_io PRIVATE CURL::libcurl)

if(GWSEQ_ZLIB_IMPL STREQUAL "zlib")
  if(ZLIB_FOUND)
    message(STATUS "zlib found: ${ZLIB_VERSION_STRING}")
  else()
    message(STATUS "zlib: built from source")
  endif()
else()
  message(STATUS "zlib: zlib-ng in compat mode")
endif()
target_link_libraries(_gwseq_io PRIVATE ZLIB::ZLIB)

install(TARGETS _gwseq_io LIBRARY DESTINATION gwseq_io)
