cmake_minimum_required(VERSION 3.20)

if(APPLE)
  execute_process(
    COMMAND xcrun --show-sdk-path
    OUTPUT_VARIABLE _sdk_path
    OUTPUT_STRIP_TRAILING_WHITESPACE
  )
  set(CMAKE_OSX_SYSROOT "${_sdk_path}" CACHE PATH "macOS SDK path")
  set(CMAKE_C_COMPILER "/usr/bin/clang" CACHE PATH "C compiler" FORCE)
  set(CMAKE_CXX_COMPILER "/usr/bin/clang++" CACHE PATH "CXX compiler" FORCE)
endif()

project(relml VERSION 0.1.0 LANGUAGES CXX)

# Static relml_* libs are linked into the Python extension (.so); require PIC.
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Building just the Python extension (e.g. via pip / install.sh) only needs the
# core libs + bindings. The C++ tests, example tasks, and the standalone agent
# library (which pulls in libcurl) are developer extras — gate them so a Python
# install stays lightweight and dependency-light.
option(RELML_BUILD_EXTRAS "Build C++ tests, example tasks, and the agent library" ON)

if(APPLE AND CMAKE_OSX_SYSROOT)
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${CMAKE_OSX_SYSROOT}/usr/include/c++/v1" CACHE STRING "" FORCE)
endif()

include_directories(include)

# ---------------------------------------------------------------------------
# stdc++fs is only needed on Linux/GCC — macOS/clang has it built-in
# ---------------------------------------------------------------------------
set(STDCXXFS $<$<NOT:$<PLATFORM_ID:Darwin>>:stdc++fs>)

# ---------------------------------------------------------------------------
# OpenMP
# ---------------------------------------------------------------------------
find_package(OpenMP)
if(OpenMP_CXX_FOUND)
  set(RELML_OPENMP_FOUND 1)
endif()

# ---------------------------------------------------------------------------
# BLAS: prefer Apple Accelerate on macOS (vecLib is highly tuned for
# Apple Silicon NEON/AMX and beats OpenBLAS substantially on the small
# matrices that show up in heterogeneous GNNs — many edge types × small
# per-type GEMMs). On Linux fall back to whatever find_package(BLAS) sees.
# ---------------------------------------------------------------------------
set(RELML_BLAS_VARIANT "none")
if(APPLE)
  find_library(ACCELERATE_FRAMEWORK Accelerate)
  if(ACCELERATE_FRAMEWORK)
    set(BLAS_LIBRARIES "${ACCELERATE_FRAMEWORK}")
    set(BLAS_INCLUDE_DIRS "")
    set(BLAS_FOUND TRUE)
    set(RELML_BLAS_VARIANT "Accelerate")
    add_compile_definitions(RELML_USE_BLAS RELML_USE_ACCELERATE)
    message(STATUS "BLAS: using Apple Accelerate (${ACCELERATE_FRAMEWORK})")
  endif()
endif()
if(NOT BLAS_FOUND)
  find_package(BLAS QUIET)
  if(BLAS_FOUND)
    set(RELML_BLAS_VARIANT "${BLAS_LIBRARIES}")
    add_compile_definitions(RELML_USE_BLAS)
    message(STATUS "BLAS: ${BLAS_LIBRARIES}")
  else()
    message(STATUS "BLAS not found — using scalar fallback")
  endif()
endif()

# ---------------------------------------------------------------------------
# CUDA (optional)
# ---------------------------------------------------------------------------
option(RELML_USE_CUDA "Build CUDA backend for GPU training" OFF)
  if(NOT CMAKE_CUDA_COMPILER)
    get_filename_component(_cuda_root "${CUDAToolkit_INCLUDE_DIR}" DIRECTORY)
    if(EXISTS "${_cuda_root}/bin/nvcc")
      set(CMAKE_CUDA_COMPILER "${_cuda_root}/bin/nvcc" CACHE FILEPATH "CUDA compiler")
    endif()
    unset(_cuda_root)
  endif()
if(RELML_USE_CUDA)
  set(CMAKE_CUDA_ARCHITECTURES "52" CACHE STRING "CUDA architectures (52 = Maxwell GTX 980)")
  find_package(CUDAToolkit REQUIRED)
  enable_language(CUDA)
  message(STATUS "CUDA found: building relml_cuda")
  add_compile_definitions(RELML_CUDA)
  add_library(relml_cuda STATIC
    src/cuda/cuda_backend.cc
    src/cuda/cuda_backend_impl.cu
  )
  set_target_properties(relml_cuda PROPERTIES
    CUDA_SEPARABLE_COMPILATION ON
    POSITION_INDEPENDENT_CODE ON
  )
  target_include_directories(relml_cuda PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
  target_link_libraries(relml_cuda PUBLIC CUDA::cudart CUDA::cublas)
  set_property(TARGET relml_cuda PROPERTY CUDA_ARCHITECTURES "52")
else()
  message(STATUS "CUDA not found — GPU backend disabled")
endif()

# ---------------------------------------------------------------------------
# libcurl — only the standalone agent library needs it (a developer extra)
# ---------------------------------------------------------------------------
if(RELML_BUILD_EXTRAS)
  find_package(CURL REQUIRED)
endif()

# ---------------------------------------------------------------------------
# nlohmann/json
# ---------------------------------------------------------------------------
include(FetchContent)
FetchContent_Declare(
    nlohmann_json
    URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
)
FetchContent_MakeAvailable(nlohmann_json)

# ---------------------------------------------------------------------------
# Libraries
# ---------------------------------------------------------------------------
add_library(relml_database
    src/database/Column.cpp
    src/database/Table.cpp
    src/database/Database.cpp
    src/database/Typeinferrer.cpp
    src/database/FKDetector.cpp
    src/database/CSVLoader.cpp
)

add_library(relml_graph
    src/graph/HeteroGraph.cpp
    src/graph/Graphbuilder.cpp
)

add_library(relml_encoding
    src/encoding/CategoricalEncoder.cpp
    src/encoding/NumericalEncoder.cpp
    src/encoding/TimestampEncoder.cpp
    src/encoding/HeteroEncoder.cpp
)
if(OpenMP_CXX_FOUND)
  target_link_libraries(relml_encoding PUBLIC OpenMP::OpenMP_CXX)
endif()
if(BLAS_FOUND)
  target_link_libraries(relml_encoding PUBLIC ${BLAS_LIBRARIES})
  target_include_directories(relml_encoding PRIVATE ${BLAS_INCLUDE_DIRS})
endif()

# Apple Clang 16 ICEs while compiling HeteroGraphSAGE.cpp with -fopenmp,
# even at -O0 and with no actual #pragma omp directives in the source —
# the mere combination of the OMP frontend with templated unordered_map
# machinery crashes the compiler. Sidestep by putting the hot kernels in
# their own template-free TU (compiled WITH OpenMP) and linking it from
# relml_gnn (which is NOT linked directly to OpenMP::OpenMP_CXX).
add_library(relml_sage_kernels src/gnn/SAGEKernels.cpp)
if(OpenMP_CXX_FOUND)
  target_link_libraries(relml_sage_kernels PUBLIC OpenMP::OpenMP_CXX)
endif()

add_library(relml_gnn
    src/gnn/HeteroGraphSAGE.cpp
    src/gnn/MLPHead.cpp
)
target_link_libraries(relml_gnn PUBLIC relml_sage_kernels)

add_library(relml_training
    src/training/Adam.cpp
    src/training/Metrics.cpp
    src/training/TaskBuilder.cpp
    src/training/TaskParser.cpp
    src/training/TaskSpec.cpp
    src/training/Trainer.cpp
)
if(OpenMP_CXX_FOUND)
  target_link_libraries(relml_training PUBLIC OpenMP::OpenMP_CXX)
endif()
target_link_libraries(relml_training PUBLIC nlohmann_json::nlohmann_json)

# ---------------------------------------------------------------------------
# Developer extras: the standalone agent library, C++ tests, and example tasks.
# Skipped for a lightweight Python-extension build (-DRELML_BUILD_EXTRAS=OFF).
# ---------------------------------------------------------------------------
if(RELML_BUILD_EXTRAS)
  add_library(relml_agent
      src/agent/agent.cpp
      src/agent/ModelRegistry.cpp
      src/agent/RelMLSystem.cpp
  )
  target_include_directories(relml_agent PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
  target_link_libraries(relml_agent PUBLIC
      CURL::libcurl
      nlohmann_json::nlohmann_json
      relml_training
      relml_gnn
      relml_encoding
      relml_graph
      relml_database
  )

  # Convenience function for example tasks
  function(add_relml_task target source)
      add_executable(${target} ${source})
      target_link_libraries(${target} PRIVATE
          relml_training
          relml_gnn
          relml_encoding
          relml_graph
          relml_database
          ${STDCXXFS}
      )
  endfunction()

  # Tests
  add_executable(test_database tests/test_database.cpp)
  target_link_libraries(test_database relml_database ${STDCXXFS})

  add_executable(test_graph tests/test_graph.cpp)
  target_link_libraries(test_graph relml_database relml_graph ${STDCXXFS})

  add_executable(test_encoding tests/test_encoding.cpp)
  target_link_libraries(test_encoding relml_database relml_encoding ${STDCXXFS})

  add_executable(test_gnn tests/test_gnn.cpp)
  target_link_libraries(test_gnn relml_database relml_graph relml_encoding relml_gnn ${STDCXXFS})

  add_executable(test_training tests/test_training.cpp)
  target_link_libraries(test_training relml_training relml_gnn relml_encoding relml_graph relml_database ${STDCXXFS})

  add_executable(test_grad_check tests/test_grad_check.cpp)
  target_link_libraries(test_grad_check relml_training relml_gnn relml_encoding relml_graph relml_database ${STDCXXFS})

  add_executable(bench_gnn tests/bench_gnn.cpp)
  target_link_libraries(bench_gnn relml_training relml_gnn relml_encoding relml_graph relml_database ${STDCXXFS})

  add_executable(test_agent tests/test_agent.cpp)
  target_link_libraries(test_agent relml_agent)

  add_executable(test_system tests/test_system.cpp)
  target_link_libraries(test_system relml_agent ${STDCXXFS})

  # Example tasks
  add_relml_task(hm_churn                   src/example_tasks/hm_churn.cpp)
  add_relml_task(ml1m_rating_classification src/example_tasks/ml1m_rating_classification.cpp)
  add_relml_task(pl_outcome                 src/example_tasks/pl_outcome.cpp)
  add_relml_task(avito_user_ad_visit        src/example_tasks/avito_user_ad_visit.cpp)
  add_relml_task(sunnyside_demand           src/example_tasks/sunnyside_demand.cpp)
  add_relml_task(sunnyside_slots            src/example_tasks/sunnyside_slots.cpp)
endif()

# ---------------------------------------------------------------------------
# Python bindings (optional — only built when Python and pybind11 are available)
#
# DuckDB lives entirely on the Python side. The C++ binding receives plain
# Python dicts — no DuckDB headers or library needed here.
# ---------------------------------------------------------------------------
find_package(Python COMPONENTS Interpreter Development)
if(Python_FOUND)
  FetchContent_Declare(
      pybind11
      URL https://github.com/pybind/pybind11/archive/refs/tags/v2.12.0.tar.gz
      DOWNLOAD_EXTRACT_TIMESTAMP true
  )
  FetchContent_MakeAvailable(pybind11)

  pybind11_add_module(_relml_core bindings/relml_python.cpp)
  target_link_libraries(_relml_core PRIVATE
      relml_training
      relml_gnn
      relml_encoding
      relml_graph
      relml_database
  )
  if(OpenMP_CXX_FOUND)
    target_link_libraries(_relml_core PRIVATE OpenMP::OpenMP_CXX)
  endif()

  if(DEFINED SKBUILD)
    # Wheel build: install the freshly compiled extension into the package so
    # scikit-build-core packages it (one correct .so per platform wheel).
    install(TARGETS _relml_core LIBRARY DESTINATION guepard/qwery/relml)
  else()
    # Dev build: copy the .so into the source tree so it is importable without
    # an install step. (Skipped under scikit-build to avoid dirtying the tree.)
    add_custom_command(TARGET _relml_core POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy
            $<TARGET_FILE:_relml_core>
            ${CMAKE_SOURCE_DIR}/python/guepard/qwery/relml/$<TARGET_FILE_NAME:_relml_core>
    )
  endif()
  message(STATUS "Python bindings: enabled")
else()
  message(STATUS "Python bindings: disabled (Python not found)")
endif()