cmake_minimum_required(VERSION 3.28 FATAL_ERROR)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
set(CMAKE_VERBOSE_MAKEFILE OFF)
set(BUILD_SHARED_LIBS ON CACHE BOOL "Build shared libraries")

if(NOT SKBUILD_PROJECT_NAME)
  set(SKBUILD_PROJECT_NAME "scaler")
endif()

# Path to the version file
set(VERSION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/src/scaler/version.txt")

# Check if the version file exists
if(NOT EXISTS "${VERSION_FILE}")
    message(FATAL_ERROR "Version file not found: ${VERSION_FILE}")
endif()

# Read version from file
file(READ "${VERSION_FILE}" VERSION_CONTENTS)

# Strip whitespace from the version string
string(STRIP "${VERSION_CONTENTS}" PROJECT_VERSION)

project(
  ${SKBUILD_PROJECT_NAME}
  LANGUAGES C CXX
  VERSION ${PROJECT_VERSION}
)

configure_file(
	${PROJECT_SOURCE_DIR}/version.h.in
	${PROJECT_BINARY_DIR}/version.h
)

enable_testing()

if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE "Release")
  message(STATUS "${PROJECT_NAME} defaulting to Release build")
elseif(NOT CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT CMAKE_BUILD_TYPE STREQUAL "Release")
  message(FATAL_ERROR "${PROJECT_NAME} invalid CMAKE_BUILD_TYPE: '${CMAKE_BUILD_TYPE}'. Must be 'Debug' or 'Release'.")
else()
  message(STATUS "${PROJECT_NAME} ${CMAKE_BUILD_TYPE} build")
endif()

# Compiler configuration
if(WIN32)
    # Windows
    message(STATUS "Windows")

    set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS on)
    add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX)
    add_compile_options("/EHsc")

    # Python.org's python313.dll links the release CRT (msvcr / ucrt). Python C extensions and any
    # static library they consume must match, otherwise Debug builds fail to link with unresolved
    # symbols like __imp__calloc_dbg. Force every target to MultiThreadedDLL so Debug and Release
    # both link the release CRT. Source-level debugging still works (PDBs are emitted).
    set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")

elseif(APPLE)
    if(CMAKE_OSX_ARCHITECTURES MATCHES "arm64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64")
        # Mac Arm
        message(STATUS "macOS on ARM64")
    else()
        # Mac Intel
        message(STATUS "macOS on x86_64")
    endif()

    add_compile_options(-Wall -Wextra)
    add_link_options(-Wl,-rpath,@loader_path)

elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
    message(STATUS "Emscripten")

    # Emscripten controls libc++ and threading flags via emcc; do not override them.
    add_compile_options(-Wall)

    # Pyodide builds its libc++ with the wasm-eh exception ABI. Extensions must
    # match or libc++ symbol mangling diverges (e.g. ``__ne190106`` inline
    # namespace tags) and Pyodide's dynamic loader fails to resolve imports.
    add_compile_options(-fwasm-exceptions -sSUPPORT_LONGJMP)
    add_link_options(-fwasm-exceptions -sSUPPORT_LONGJMP)

    # Pyodide's pywasmcross enforces ``-sSIDE_MODULE=2`` with an explicit
    # ``EXPORTED_FUNCTIONS=PyInit_<mod>`` list. With default visibility the
    # extension exports thousands of unrelated symbols, balloons the .so size,
    # and confuses Pyodide's dynamic loader. Hide everything but ``PyInit_*``.
    add_compile_options(-fvisibility=hidden -fvisibility-inlines-hidden)

    # ``-fmerge-all-constants`` (clang default at -O2) lets the linker overlay
    # short string literals onto the tail bytes of longer ones in mergeable
    # ``.rodata.str*`` sections. Pyodide's wasm SIDE_MODULE relocator then
    # mis-resolves offsets within those merged sections so short literals end
    # up pointing into the middle of unrelated strings at load time (e.g.
    # ``"module"`` is read as ``"ile"``). Force the compiler to emit each
    # literal as its own non-mergeable symbol.
    add_compile_options(-fno-merge-all-constants)

elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        # Linux GCC
        message(STATUS "Linux GCC ${CMAKE_CXX_COMPILER_VERSION}")

        add_compile_options(-Wall -Wsubobject-linkage)
        add_link_options(-Wl,-rpath,$ORIGIN)

    elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # matches Clang and AppleClang
        #Linux Clang
        message(STATUS "Linux Clang ${CMAKE_CXX_COMPILER_VERSION}")

        add_compile_options(-Wall -stdlib=libc++)
        add_link_options(-stdlib=libc++)

        # needed for jthread in libc++
        add_compile_options(-fexperimental-library)
        add_link_options(-fexperimental-library)

    else()
        message(WARNING "Unknown compiler on Linux")
    endif()
else()
    message(WARNING "Unknown platform: ${CMAKE_SYSTEM_NAME}")
endif()

# Include Python-related utilities
include(cmake/ScalerPythonModule.cmake)

find_package(CapnProto CONFIG REQUIRED)
get_target_property(CAPNP_INCLUDE_DIRS CapnProto::capnp INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "Found Capnp includes in ${CAPNP_INCLUDE_DIRS}")

if(EMSCRIPTEN)
    # CapnProtoConfig.cmake hard-codes CAPNP_EXECUTABLE / CAPNPC_CXX_EXECUTABLE
    # to $<TARGET_FILE:CapnProto::capnp_tool> etc., but those targets aren't
    # built when cross-compiling to wasm. The wasm install layout exposes the
    # host capnp tools as <prefix>/bin/<tool>.js (created by library_tool.sh
    # capnp install --target=wasm); point the codegen variables at them.
    find_program(_SCALER_CAPNP_HOST NAMES capnp.js capnp REQUIRED)
    find_program(_SCALER_CAPNPC_CXX_HOST NAMES capnpc-c++.js capnpc-c++ REQUIRED)
    set(CAPNP_EXECUTABLE "${_SCALER_CAPNP_HOST}" CACHE FILEPATH "Location of capnp executable" FORCE)
    set(CAPNPC_CXX_EXECUTABLE "${_SCALER_CAPNPC_CXX_HOST}" CACHE FILEPATH "Location of capnpc-c++ executable" FORCE)
    unset(_SCALER_CAPNP_HOST CACHE)
    unset(_SCALER_CAPNPC_CXX_HOST CACHE)
endif()

# Make LSP happy
include_directories(${CAPNP_INCLUDE_DIRS})
include_directories(${PROJECT_SOURCE_DIR})
include_directories(${PROJECT_SOURCE_DIR}/src)
include_directories(${PROJECT_SOURCE_DIR}/src/cpp)

add_subdirectory(src/cpp/scaler)

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
  add_subdirectory(tests)
  add_subdirectory(examples)
endif()
