cmake_minimum_required(VERSION 3.20)

set(PROJECT_NAME "conformallab_core")
project(${PROJECT_NAME} LANGUAGES C CXX)

message(STATUS "Configuring ${PROJECT_NAME}...")

# ── Build modes ────────────────────────────────────────────────────────────────
#
#   Default (CI fast / pure-math tests):
#     cmake -S code -B build
#     Only Eigen + GTest required.  36 non-CGAL tests.
#
#   -DWITH_CGAL_TESTS=ON   CGAL test suite only — no viewer, no CLI app.
#     cmake -S code -B build -DWITH_CGAL_TESTS=ON
#     Requires: system Boost headers (apt install libboost-dev).
#     Builds:   conformallab_cgal_tests  (158 tests).
#     Does NOT require wayland-scanner, GLFW, libigl or a display.
#     Use this in headless CI.
#
#   -DWITH_CGAL=ON         Full build: CLI app + viewer + examples + CGAL tests.
#     cmake -S code -B build -DWITH_CGAL=ON
#     Requires: Boost + Wayland/X11 dev headers (wayland-scanner, libx11-dev …).
#     Automatically enables WITH_VIEWER.
#     Use this for local development with the interactive viewer.
#
#   -DWITH_VIEWER=ON        Viewer library only (libigl / GLFW / GLAD).
#
# ──────────────────────────────────────────────────────────────────────────────
option(WITH_CGAL_TESTS "Build CGAL test suite without viewer/CLI (headless CI)" OFF)
option(WITH_CGAL       "Build conformallab_core CLI app + viewer + CGAL tests"  OFF)
option(WITH_VIEWER     "Build viewer library (libigl / GLFW / GLAD)"            OFF)

# WITH_CGAL_TESTS is a strict subset of WITH_CGAL — no viewer, no CLI.
# WITH_CGAL (full build) implies WITH_VIEWER.
if(WITH_CGAL AND NOT WITH_VIEWER)
    message(STATUS "WITH_CGAL implies WITH_VIEWER – enabling automatically.")
    set(WITH_VIEWER ON CACHE BOOL "" FORCE)
endif()

# Propagate Boost requirement for both CGAL modes + headers_check (which
# also compiles CGAL headers, hence needs Boost::graph_traits).
if(WITH_CGAL OR WITH_CGAL_TESTS OR CONFORMALLAB_HEADERS_CHECK)
    find_package(Boost REQUIRED)
endif()

# ── Standard settings ──────────────────────────────────────────────────────────
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

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

# ── ccache integration (lever D) ───────────────────────────────────────────────
#
# Detect `ccache` on the host and prepend it to the compile + link launchers.
# Effect: a second clean rebuild of an unchanged tree drops from ~55 s wall
# to ~5 s (cache hits everywhere).  Costs nothing when ccache is absent.
# Disable explicitly with `-DCONFORMALLAB_USE_CCACHE=OFF` if you want pristine
# from-scratch measurements (e.g. when re-running scripts/quality/coverage.sh).
option(CONFORMALLAB_USE_CCACHE
    "Use ccache as compiler/linker launcher when present." ON)
if(CONFORMALLAB_USE_CCACHE)
    find_program(CCACHE_PROGRAM ccache)
    if(CCACHE_PROGRAM)
        set(CMAKE_C_COMPILER_LAUNCHER   "${CCACHE_PROGRAM}")
        set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
        message(STATUS "ccache: enabled (${CCACHE_PROGRAM})")
    endif()
endif()

# ── Dev-iteration build mode (lever C, opt-in) ─────────────────────────────────
#
# Turns off Unity Build target-wide.  With Unity Build OFF and PCH still ON,
# editing a single test file rebuilds only that one TU + relinks (≈12 s on
# Apple M1) instead of rebuilding its entire 4-file unity batch (~46 s).
#
# Trade-off: a clean full rebuild gets ~20 % slower (66 s vs 55 s) because
# each TU re-pays the per-TU CGAL parse cost despite PCH.  Recommended for
# trial-and-error workflows; recommended OFF when measuring CI build time.
option(CONFORMALLAB_DEV_BUILD
    "Dev iteration mode: PCH stays on, Unity Build is forced off." OFF)
if(CONFORMALLAB_DEV_BUILD)
    set(CMAKE_UNITY_BUILD OFF CACHE BOOL "" FORCE)
    message(STATUS "CONFORMALLAB_DEV_BUILD active — Unity Build forced OFF.")
endif()

if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
  # ─── Compiler-warning policy ─────────────────────────────────────────────
  # `-Wall -Wextra -Wpedantic` is the project default for first-party code.
  # Vendored deps under code/deps/ get a separate, looser policy (handled
  # via per-target SYSTEM include marking when they are pulled in).
  #
  # `CONFORMALLAB_WARNINGS_AS_ERRORS=ON` flips on `-Werror` — used in CI's
  # promotion-track and by `scripts/quality/sanitizers.sh` to make sure no
  # new warning class slips in unannounced.  Off by default so regular
  # builds on slightly older toolchains aren't broken by a new GCC's
  # added warning.
  option(CONFORMALLAB_WARNINGS_AS_ERRORS
    "Treat compiler warnings as errors (-Werror)." OFF)

  add_compile_options(-Wall -Wextra -Wpedantic)

  if(CONFORMALLAB_WARNINGS_AS_ERRORS)
    add_compile_options(-Werror)
    message(STATUS "Warnings-as-errors mode active (-Werror).")
  endif()

  # AddressSanitizer only in Debug (gtest_discover_tests runs the binary at
  # configure time and hangs with ASan enabled).
  if(CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT BUILD_TESTING)
    add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
    add_link_options(-fsanitize=address)
  endif()
endif()

# ── GTest (only when tests are enabled) ────────────────────────────────────────
#
# CMake's standard `BUILD_TESTING` option (defaults ON via include(CTest))
# gates the entire test subtree below.  Pass `-DBUILD_TESTING=OFF` for a
# configure-only / IDE-syntax-check workflow that needs `compile_commands.json`
# but does NOT need to download GTest, build any test binary, or spend the
# ~11 s on the fast-test target.
include(FetchContent)
include(CTest)               # also defines BUILD_TESTING (default ON)

if(BUILD_TESTING)
    enable_testing()

    FetchContent_Declare(
        googletest
        GIT_REPOSITORY https://github.com/google/googletest.git
        GIT_TAG        v1.14.0
    )
    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
    set(INSTALL_GTEST          OFF CACHE BOOL "" FORCE)
    set(BUILD_GMOCK            OFF CACHE BOOL "" FORCE)
    FetchContent_MakeAvailable(googletest)
endif()

# ── External deps (lazy tarball extraction) ────────────────────────────────────
add_subdirectory(deps)

# ── Viewer library (optional) ──────────────────────────────────────────────────
if(WITH_VIEWER)
    add_subdirectory(deps/glfw-3.4)

    add_library(glad STATIC
        ${CMAKE_CURRENT_SOURCE_DIR}/deps/libigl-glad/src/glad.c)
    target_include_directories(glad PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/deps/libigl-glad/include)

    add_library(viewer STATIC src/viewer/simple_viewer.cpp)
    target_include_directories(viewer PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/deps/libigl-2.6.0/include
        ${CMAKE_CURRENT_SOURCE_DIR}/include
        ${CMAKE_CURRENT_SOURCE_DIR}/deps/libigl-glad/include
        ${CMAKE_CURRENT_SOURCE_DIR}/deps/eigen-3.4.0/)
    target_link_libraries(viewer PUBLIC glad glfw)
endif()

# ── Core CLI app (optional, requires CGAL + Viewer) ───────────────────────────
if(WITH_CGAL)
    add_executable(${PROJECT_NAME} src/apps/v0/conformallab_cli.cpp)
    target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/deps/single_includes
        ${CMAKE_CURRENT_SOURCE_DIR}/deps/eigen-3.4.0/
        ${CMAKE_CURRENT_SOURCE_DIR}/deps/CGAL-6.1.1/include
        ${CMAKE_CURRENT_SOURCE_DIR}/deps/libigl-2.6.0/include
        ${Boost_INCLUDE_DIRS})
    target_include_directories(${PROJECT_NAME} PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/include)
    target_compile_definitions(${PROJECT_NAME} PRIVATE
        CGAL_DISABLE_GMP CGAL_DISABLE_MPFR)
    target_link_libraries(${PROJECT_NAME} PRIVATE viewer)
    set_target_properties(${PROJECT_NAME} PROPERTIES
        RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bin)
endif()

# ── Example programs (require WITH_CGAL; viewer example also needs WITH_VIEWER) ─
if(WITH_CGAL)
    add_subdirectory(examples)
endif()

# ── Tests (gated on BUILD_TESTING) ────────────────────────────────────────────
#
# Default ON (CTest convention).  Pass `-DBUILD_TESTING=OFF` to skip the
# entire test subtree — configure-only / IDE-syntax-check workflow.
if(BUILD_TESTING)
    add_subdirectory(tests)
endif()

# ── headers_check target (lever A, opt-in) ────────────────────────────────────
#
# Lightweight per-header smoke-compile target.  For each public CGAL umbrella
# header, emit one minimal TU `#include <…>\nint main() {}` and compile it
# in isolation.  Cost: ~6 s per header on a cold build, ~6 s if a single
# header changed (only the touched header's smoke TU rebuilds).
#
# Use case: "did my Phase-N refactor break the public API surface?" without
# waiting 55 s for the full CGAL test build.  Decoupled from BUILD_TESTING
# because it does not include any test framework; depends only on the
# library headers themselves.
#
# Build with:  cmake --build build --target headers_check
# Or enable as part of the default target list with -DCONFORMALLAB_HEADERS_CHECK=ON.
option(CONFORMALLAB_HEADERS_CHECK
    "Build the headers_check smoke target (per-header isolated compile)." OFF)

if(CONFORMALLAB_HEADERS_CHECK OR DEFINED ENV{CI})
    set(_hc_dir "${CMAKE_BINARY_DIR}/headers_check_stubs")
    file(MAKE_DIRECTORY "${_hc_dir}")
    set(_hc_headers
        "CGAL/Discrete_conformal_map.h"
        "CGAL/Discrete_circle_packing.h"
        "CGAL/Discrete_inversive_distance.h"
        "CGAL/Conformal_layout.h"
        "CGAL/Conformal_map_traits.h"
        "CGAL/Conformal_map/internal/parameters.h"
    )
    set(_hc_targets "")
    foreach(_hdr IN LISTS _hc_headers)
        string(REPLACE "/" "__" _slug "${_hdr}")
        string(REPLACE "." "_"  _slug "${_slug}")
        set(_stub "${_hc_dir}/${_slug}.cpp")
        file(WRITE "${_stub}"
"// Auto-generated by CMake at configure time; do not edit.
// Smoke-compile sentinel for ${_hdr}.
#include <${_hdr}>
int main() { return 0; }
")
        add_executable(hc_${_slug} EXCLUDE_FROM_ALL "${_stub}")
        target_include_directories(hc_${_slug} SYSTEM PRIVATE
            ${CMAKE_CURRENT_SOURCE_DIR}/deps/eigen-3.4.0
            ${CMAKE_CURRENT_SOURCE_DIR}/deps/CGAL-6.1.1/include
            ${CMAKE_CURRENT_SOURCE_DIR}/deps/single_includes
            ${Boost_INCLUDE_DIRS})
        target_include_directories(hc_${_slug} PRIVATE
            ${CMAKE_CURRENT_SOURCE_DIR}/include)
        target_compile_definitions(hc_${_slug} PRIVATE
            CGAL_DISABLE_GMP CGAL_DISABLE_MPFR)
        list(APPEND _hc_targets hc_${_slug})
    endforeach()
    add_custom_target(headers_check DEPENDS ${_hc_targets})
endif()

# ── Install target (header-only library) ──────────────────────────────────────
# Installs all public headers to <prefix>/include/conformallab/
# Usage from another CMake project:
#   cmake --install build --prefix /usr/local
#   target_include_directories(myapp PRIVATE /usr/local/include/conformallab)
include(GNUInstallDirs)
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/conformallab
        FILES_MATCHING PATTERN "*.hpp"
        PATTERN "* 2.*" EXCLUDE)   # exclude macOS Finder duplicates
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE
              ${CMAKE_CURRENT_SOURCE_DIR}/../CITATION.cff
        DESTINATION ${CMAKE_INSTALL_DATADIR}/conformallab)

# ── Doxygen documentation target (Phase 7.5) ──────────────────────────────────
# Generates HTML API documentation into doc/doxygen/html/.
# Usage:
#   cmake --build build --target doc
#   open doc/doxygen/html/index.html
#
# Optional dependency: install Doxygen via `brew install doxygen` (macOS) or
# `apt install doxygen graphviz` (Linux). The target is silently disabled
# if Doxygen is not found.
find_package(Doxygen QUIET)
if(DOXYGEN_FOUND)
    set(DOXYGEN_PROJECT_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..)
    add_custom_target(doc
        COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_PROJECT_ROOT}/Doxyfile
        WORKING_DIRECTORY ${DOXYGEN_PROJECT_ROOT}
        COMMENT "Generating API documentation with Doxygen"
        VERBATIM)
    message(STATUS "Doxygen found: target 'doc' available (cmake --build build --target doc)")
else()
    message(STATUS "Doxygen not found — 'doc' target unavailable (install: brew/apt install doxygen)")
endif()
