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)

# 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.
  set(CURL_DISABLE_INSTALL ON 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()
  FetchContent_Declare(
    CURL
    GIT_REPOSITORY https://github.com/curl/curl.git
    GIT_TAG        curl-8_11_1
  )
  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 below: 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
  )
  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 "")
  # v1.3.1 has no ZLIB_BUILD_EXAMPLES option, so its example/minigzip test
  # binaries are part of the default target. EXCLUDE_FROM_ALL drops them;
  # zlibstatic is still built because _gwseq_io links against it. The option
  # was added in CMake 3.28, so only pass it where it is understood.
  if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.28)
    set(ZLIB_EXCLUDE_ARG EXCLUDE_FROM_ALL)
  else()
    set(ZLIB_EXCLUDE_ARG "")
  endif()
  FetchContent_Declare(
    ZLIB
    GIT_REPOSITORY https://github.com/madler/zlib.git
    GIT_TAG        v1.3.1
    ${ZLIB_EXCLUDE_ARG}
  )
  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.
nanobind_add_module(
  _gwseq_io
  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)
