# Builds the unified gguf.cpp engine (LLM server + diffusion CLI + quantizer
# library) from the vendored source tree in ONE configure/build, and installs
# each artifact into the panel package that spawns/loads it:
#
#   gguf-server     -> src/gguf_cpp/server/bin      (spawned by the server panel)
#   diffusion       -> src/gguf_cpp/diffuser/bin    (spawned by the diffuser panel)
#   gguf_quantizer  -> src/gguf_cpp/editor/lib      (ctypes-loaded by the editor panel)
#
# The engine tree shares a single ggml kernels/ directory between all three,
# so the compute kernels compile once. Modeled on the wrappers the three
# standalone packages (gguf-server, gguf-diffusion, gguf-editor) used.
cmake_minimum_required(VERSION 3.15)
project(gguf_cpp C CXX)

option(GGUF_CPP_BUILD "Build the unified gguf.cpp engine and install it alongside the python package" ON)

if(NOT GGUF_CPP_BUILD)
    return()
endif()

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# ── Locate the engine source tree ────────────────────────────────────────────
set(GGUF_CPP_ENGINE_DIR "" CACHE PATH "Path to the unified gguf.cpp engine source tree")
if(NOT GGUF_CPP_ENGINE_DIR AND DEFINED ENV{GGUF_CPP_ENGINE_DIR})
    set(GGUF_CPP_ENGINE_DIR "$ENV{GGUF_CPP_ENGINE_DIR}")
endif()
if(NOT GGUF_CPP_ENGINE_DIR AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vendor/engine/CMakeLists.txt")
    set(GGUF_CPP_ENGINE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/engine")
endif()
if(NOT GGUF_CPP_ENGINE_DIR OR NOT EXISTS "${GGUF_CPP_ENGINE_DIR}/app/CMakeLists.txt")
    message(FATAL_ERROR
        "gguf.cpp engine source not found. Provide it via one of:\n"
        "  - the vendored copy at vendor/engine\n"
        "  - -DGGUF_CPP_ENGINE_DIR=/path/to/engine or the same env var")
endif()
get_filename_component(GGUF_CPP_ENGINE_DIR "${GGUF_CPP_ENGINE_DIR}" ABSOLUTE)
message(STATUS "gguf-cpp: engine source at ${GGUF_CPP_ENGINE_DIR}")

# ── Option helper ────────────────────────────────────────────────────────────
# Values are read from the environment as well as -D, because `pip install`
# makes environment variables far easier to pass than CMake defines:
#   GGUF_CPP_CUDA=1 pip install .
#   CMAKE_ARGS="-DGGUF_CPP_CUDA=ON" pip install .
function(gguf_cpp_option name docstring default)
    if(DEFINED ENV{${name}} AND NOT "$ENV{${name}}" STREQUAL "")
        set(default "$ENV{${name}}")
    endif()
    option(${name} "${docstring}" "${default}")
endfunction()

# ── Windows / MinGW toolchain fixes ──────────────────────────────────────────
if(WIN32 AND NOT MSVC)
    # MinGW-w64's headers pin _WIN32_WINNT to a Windows 7-era value before the
    # engine gets a chance to set it; cpp-httplib then hard-errors and
    # ::CreateFile2 goes undeclared. Set it up front, matching what the engine
    # expects everywhere else.
    add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00 NTDDI_VERSION=0x0A000000)

    # A MinGW-built binary otherwise needs libstdc++-6.dll, libgcc_s_seh-1.dll
    # and libwinpthread-1.dll from the toolchain at run time. The wheel ships
    # self-contained binaries and Python spawns them without MSYS2 on PATH, so
    # link the runtime in.
    gguf_cpp_option(GGUF_CPP_MINGW_STATIC
        "Statically link the MinGW runtime into the engine binaries" ON)
    if(GGUF_CPP_MINGW_STATIC)
        add_link_options(-static-libgcc -static-libstdc++ -static)

        # ggml uses OpenMP, and CMake's FindOpenMP resolves it to
        # libgomp.dll.a — an import library that -static cannot override,
        # leaving the binary needing libgomp-1.dll. Point OpenMP at the static
        # archive instead; if there isn't one, drop OpenMP rather than ship a
        # half-static binary (ggml falls back to its own thread pool).
        if(NOT OpenMP_gomp_LIBRARY)
            set(_gguf_saved_suffixes ${CMAKE_FIND_LIBRARY_SUFFIXES})
            set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
            find_library(GGUF_CPP_LIBGOMP_STATIC NAMES gomp)
            set(CMAKE_FIND_LIBRARY_SUFFIXES ${_gguf_saved_suffixes})
            if(GGUF_CPP_LIBGOMP_STATIC)
                message(STATUS "gguf-cpp: static OpenMP runtime ${GGUF_CPP_LIBGOMP_STATIC}")
                set(OpenMP_gomp_LIBRARY "${GGUF_CPP_LIBGOMP_STATIC}"
                    CACHE FILEPATH "gguf-cpp: static OpenMP runtime" FORCE)
            else()
                message(STATUS "gguf-cpp: no static libgomp found, building without OpenMP")
                set(GGML_OPENMP OFF CACHE BOOL "gguf-cpp: keep the binaries self-contained" FORCE)
            endif()
        endif()
    endif()
endif()

# ── Shape the engine build for a single-file-per-artifact wheel ──────────────
# Static libs (ggml, llama, llama-common, mtmd, diffusion, …) linked straight
# into the executables keep the wheel to standalone binaries with no
# rpath/DLL handling. The quantizer stays a shared library by design (ctypes).
set(BUILD_SHARED_LIBS  OFF CACHE BOOL "gguf-cpp: static engine for single-file installs" FORCE)

# The engine's own install() rules would put artifacts in the wheel's data
# directory; ours below place them inside the package instead.
set(GGUF_SERVER_INSTALL OFF CACHE BOOL "gguf-cpp: install rules are ours" FORCE)

# Router mode (`--models`) spawns child servers via subprocess.h — harmless to
# keep and the GUI's custom-command box can use it.
gguf_cpp_option(GGUF_CPP_SUBPROCESS
    "Support router mode (the server engine spawns child servers)" ON)
set(GGUF_SERVER_SUBPROCESS ${GGUF_CPP_SUBPROCESS} CACHE BOOL "" FORCE)

# ── HTTPS / OpenSSL ──────────────────────────────────────────────────────────
# Off by default: OpenSSL is only needed to *download* models over HTTPS; the
# panels always hand the engine local file paths, so the dependency buys
# nothing and costs portability (a foreign-ABI OpenSSL next to the toolchain
# breaks the build on Windows).
gguf_cpp_option(GGUF_CPP_OPENSSL
    "Link the server engine against OpenSSL for HTTPS model downloads" OFF)
set(GGUF_SERVER_OPENSSL ${GGUF_CPP_OPENSSL} CACHE BOOL "" FORCE)

# ── GPU / accelerator backends ───────────────────────────────────────────────
# One switch per backend drives the whole engine: the shared ggml kernels
# (server + diffusion) and, for CUDA/HIP, the quantizer's own device kernels.
# Anything else ggml supports can still be passed straight through as -DGGML_*.
gguf_cpp_option(GGUF_CPP_CUDA   "Build the engine with the CUDA backend"   OFF)
gguf_cpp_option(GGUF_CPP_HIP    "Build the engine with the HIP backend"    OFF)
gguf_cpp_option(GGUF_CPP_VULKAN "Build the engine with the Vulkan backend" OFF)
if(APPLE)  # matches the engine's own default; APPLE is "" elsewhere, not a valid default
    set(_gguf_metal_default ON)
else()
    set(_gguf_metal_default OFF)
endif()
gguf_cpp_option(GGUF_CPP_METAL  "Build the engine with the Metal backend"  ${_gguf_metal_default})

set(GGUF_SERVER_CUDA   ${GGUF_CPP_CUDA}   CACHE BOOL "" FORCE)
set(GGUF_SERVER_HIP    ${GGUF_CPP_HIP}    CACHE BOOL "" FORCE)
set(GGUF_SERVER_VULKAN ${GGUF_CPP_VULKAN} CACHE BOOL "" FORCE)
set(GGUF_SERVER_METAL  ${GGUF_CPP_METAL}  CACHE BOOL "" FORCE)

# The quantizer's GPU path is its own small kernel set, not ggml; CUDA/HIP
# follow the engine-wide switch. Its Metal path stays opt-in (QUANTIZER_METAL),
# matching the standalone gguf-editor defaults.
set(QUANTIZER_CUDA ${GGUF_CPP_CUDA} CACHE BOOL "" FORCE)
set(QUANTIZER_HIP  ${GGUF_CPP_HIP}  CACHE BOOL "" FORCE)

set(GGUF_CPP_ENABLED_BACKENDS "")
foreach(backend IN ITEMS CUDA HIP VULKAN METAL)
    if(GGUF_CPP_${backend})
        list(APPEND GGUF_CPP_ENABLED_BACKENDS ${backend})
    endif()
endforeach()
if(GGUF_CPP_ENABLED_BACKENDS)
    string(REPLACE ";" ", " _backend_list "${GGUF_CPP_ENABLED_BACKENDS}")
    message(STATUS "gguf-cpp: accelerator backends requested: ${_backend_list}")
else()
    message(STATUS "gguf-cpp: CPU-only build (enable e.g. -DGGUF_CPP_CUDA=ON for GPU)")
endif()

# ── Embedded web UI ──────────────────────────────────────────────────────────
# The server engine embeds whatever static assets live in GGUF_SERVER_UI_DIR
# (default <engine>/ui/dist). The tree ships none, so the engine is API-only —
# irrelevant here, because this package serves its own GUI.
if(NOT DEFINED CACHE{GGUF_SERVER_UI_DIR} AND DEFINED ENV{GGUF_SERVER_UI_DIR})
    set(GGUF_SERVER_UI_DIR "$ENV{GGUF_SERVER_UI_DIR}" CACHE PATH
        "directory with web UI assets to embed into the server engine")
endif()

# EXCLUDE_FROM_ALL keeps every target the engine defines out of the default
# build — the dependencies below pull in the three shipped artifacts and just
# the libraries they link against.
add_subdirectory("${GGUF_CPP_ENGINE_DIR}" "${CMAKE_BINARY_DIR}/gguf-cpp-engine" EXCLUDE_FROM_ALL)

foreach(required_target IN ITEMS gguf-server diffusion-cli gguf_quantizer)
    if(NOT TARGET ${required_target})
        message(FATAL_ERROR "engine did not define the ${required_target} target — is ${GGUF_CPP_ENGINE_DIR} the unified gguf.cpp source tree?")
    endif()
endforeach()

add_custom_target(gguf_cpp_engine ALL)
add_dependencies(gguf_cpp_engine gguf-server diffusion-cli gguf_quantizer)

# ── Install into the panel packages ──────────────────────────────────────────
# Install into the wheel (SKBUILD_PLATLIB_DIR) and also into the source tree
# so editable installs (`pip install -e .`) find the artifacts — the same dual
# install the three standalone packages used.
install(
    PROGRAMS $<TARGET_FILE:gguf-server>
    DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/src/gguf_cpp/server/bin"
)
install(
    PROGRAMS $<TARGET_FILE:diffusion-cli>
    DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/src/gguf_cpp/diffuser/bin"
)
install(
    TARGETS gguf_quantizer
    LIBRARY DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/src/gguf_cpp/editor/lib"
    RUNTIME DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/src/gguf_cpp/editor/lib"
    ARCHIVE DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/src/gguf_cpp/editor/lib"
)
if(DEFINED SKBUILD_PLATLIB_DIR)
    install(
        PROGRAMS $<TARGET_FILE:gguf-server>
        DESTINATION "${SKBUILD_PLATLIB_DIR}/gguf_cpp/server/bin"
    )
    install(
        PROGRAMS $<TARGET_FILE:diffusion-cli>
        DESTINATION "${SKBUILD_PLATLIB_DIR}/gguf_cpp/diffuser/bin"
    )
    install(
        TARGETS gguf_quantizer
        LIBRARY DESTINATION "${SKBUILD_PLATLIB_DIR}/gguf_cpp/editor/lib"
        RUNTIME DESTINATION "${SKBUILD_PLATLIB_DIR}/gguf_cpp/editor/lib"
        ARCHIVE DESTINATION "${SKBUILD_PLATLIB_DIR}/gguf_cpp/editor/lib"
    )
endif()
