cmake_minimum_required(VERSION 3.20)
project(engine VERSION 0.2.0 LANGUAGES C CXX)

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

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

option(ENGINE_BUILD_RENDER   "Build the window/renderer (raylib), run() and the examples" ON)
option(ENGINE_BUILD_TESTS    "Build the headless test executable"                         ON)
option(ENGINE_BUILD_EXAMPLES "Build the examples (needs ENGINE_BUILD_RENDER)"             ON)
option(ENGINE_BUILD_GFX      "Generate eng::gfx, the engine's equivalent of every raylib/rlgl/raymath function, from raylib's headers (needs ENGINE_BUILD_RENDER)" ON)
option(ENGINE_BUILD_GPU_CHECK "Build tools/gpu_check: runs the renderer on a software OpenGL driver (Linux, Mesa EGL) and compares its pixels with a CPU model" OFF)
option(ENGINE_SANITIZE       "Build the engine's own code with ASan + UBSan (not the dependencies)" OFF)

include(cmake/deps.cmake)
include(cmake/gfx.cmake)

# ---------------------------------------------------------------------------
# engine_core: ECS, math, input state, resources, textures and materials, physics (Jolt), SVG (pugixml) and
# extrusion (Clipper2), plus everything the renderer decides without a GPU: visibility,
# lights and shadow fitting, ambient zones and the per-frame draw plan.
# No window and no OpenGL, so it links and runs headless.
# ---------------------------------------------------------------------------
add_library(engine_core STATIC
  src/ecs.cpp
  src/visibility.cpp
  src/input.cpp
  src/resources.cpp
  src/physics.cpp
  src/svg.cpp
  src/extrude.cpp
  src/spawn.cpp
  src/scene.cpp
  src/ambient.cpp
  src/texture.cpp
  src/material.cpp
  src/frame.cpp)
target_include_directories(engine_core PUBLIC include PRIVATE src)
target_link_libraries(engine_core
  PUBLIC  EnTT::EnTT
  PRIVATE Jolt Clipper2 pugixml::pugixml)

# ---------------------------------------------------------------------------
# engine_render: raylib window + GPU-instanced renderer behind eng::run().
# `engine` is the target game code links: the whole engine in one name.
# ---------------------------------------------------------------------------
if(ENGINE_BUILD_RENDER)
  add_library(engine_render STATIC src/render.cpp src/app.cpp)
  target_include_directories(engine_render PRIVATE src)
  target_link_libraries(engine_render PUBLIC engine_core PRIVATE raylib)

  # eng::gfx (include <engine/gfx.hpp>): generated from raylib's own headers, see cmake/gfx.cmake. Optional: when it
  # cannot be generated the engine still builds, without it.
  if(ENGINE_BUILD_GFX)
    engine_configure_gfx()
    if(ENGINE_GFX_OK)
      add_library(engine_gfx STATIC ${ENGINE_GFX_SOURCES})
      target_include_directories(engine_gfx PUBLIC "${ENGINE_GFX_INCLUDE_DIR}")
      target_compile_definitions(engine_gfx PRIVATE ${ENGINE_GFX_DEFINITIONS})
      target_link_libraries(engine_gfx PUBLIC engine_core PRIVATE raylib)
      add_library(engine::gfx ALIAS engine_gfx)
      target_link_libraries(engine_render PUBLIC engine_gfx)
    endif()
  endif()

  add_library(engine INTERFACE)
  target_link_libraries(engine INTERFACE engine_render)
  add_library(engine::engine ALIAS engine)

  if(ENGINE_BUILD_EXAMPLES)
    add_executable(demo examples/demo/main.cpp)
    target_link_libraries(demo PRIVATE engine)
    add_custom_command(TARGET demo POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy_if_different
              ${CMAKE_CURRENT_SOURCE_DIR}/examples/demo/level.svg $<TARGET_FILE_DIR:demo>/level.svg)

    add_executable(instancing examples/instancing/main.cpp)
    target_link_libraries(instancing PRIVATE engine)
  endif()
endif()

add_library(engine::core ALIAS engine_core)

# ---------------------------------------------------------------------------
# gpu_check (optional): src/render.cpp and src/app.cpp compiled against a software OpenGL
# implementation of the raylib calls they make (tools/gpu_check/gl_shim.cpp) instead of raylib itself,
# then run on Mesa's llvmpipe through EGL - no display, no GPU. It renders scenes and compares the pixels
# with an independent CPU model of the lighting, exact shadow ray tests and the CPU ambient grid.
# Only raylib's HEADERS are used (raylib is not linked, the shim provides those symbols).
# ---------------------------------------------------------------------------
if(ENGINE_BUILD_GPU_CHECK)
  if(NOT ENGINE_BUILD_RENDER OR NOT TARGET raylib)
    message(FATAL_ERROR "ENGINE_BUILD_GPU_CHECK needs ENGINE_BUILD_RENDER (it uses raylib's headers)")
  endif()
  if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
    message(FATAL_ERROR "ENGINE_BUILD_GPU_CHECK needs Linux (EGL + Mesa)")
  endif()
  find_package(PNG REQUIRED)
  add_executable(gpu_check
    tools/gpu_check/render_check.cpp
    tools/gpu_check/gl_shim.cpp
    src/render.cpp
    src/app.cpp)
  target_include_directories(gpu_check PRIVATE src tools/gpu_check
    $<TARGET_PROPERTY:raylib,INTERFACE_INCLUDE_DIRECTORIES>)
  target_link_libraries(gpu_check PRIVATE engine_core PNG::PNG ${CMAKE_DL_LIBS})
  if(ENGINE_BUILD_TESTS)
    add_test(NAME gpu_check COMMAND gpu_check)
    set_tests_properties(gpu_check PROPERTIES
      ENVIRONMENT "ENGINE_GPU_CHECK_OUT=${CMAKE_BINARY_DIR}/gpu_check_out"
      LABELS gpu)
  endif()
endif()

if(ENGINE_BUILD_TESTS)
  enable_testing()
  add_executable(engine_tests tests/tests.cpp)
  target_link_libraries(engine_tests PRIVATE engine_core)
  if(TARGET engine_gfx)
    # Only the generated HEADERS (the hand-written eng::gfx helpers are tested here); nothing of raylib is linked.
    target_include_directories(engine_tests PRIVATE "${ENGINE_GFX_INCLUDE_DIR}")
  endif()
  add_test(NAME engine_tests COMMAND engine_tests)

  # The generated eng::gfx wrappers against a recording stand-in for raylib: no window, no GPU, raylib is not linked
  # (only its headers are used). Checks every wrapper passes every argument, in order, unchanged.
  if(TARGET engine_gfx AND ENGINE_GFX_SELFTEST_SOURCES)
    add_executable(gfx_selftest ${ENGINE_GFX_SELFTEST_SOURCES})
    target_include_directories(gfx_selftest PRIVATE
      "${ENGINE_GFX_INCLUDE_DIR}" "${ENGINE_GFX_SELFTEST_INCLUDE_DIR}" "${ENGINE_GFX_RAYLIB_INCLUDE}"
      $<TARGET_PROPERTY:engine_core,INTERFACE_INCLUDE_DIRECTORIES>)
    target_compile_definitions(gfx_selftest PRIVATE ${ENGINE_GFX_DEFINITIONS})
    if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
      target_compile_options(gfx_selftest PRIVATE -ffp-contract=off)   # raymath results are compared bit for bit
    endif()
    add_test(NAME gfx_selftest COMMAND gfx_selftest)
  endif()
endif()

# Sanitizers for our own code only.
if(ENGINE_SANITIZE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
  foreach(t engine_core engine_render engine_gfx demo instancing engine_tests gfx_selftest gpu_check)
    if(TARGET ${t})
      target_compile_options(${t} PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
      target_link_options(${t} PRIVATE -fsanitize=address,undefined)
    endif()
  endforeach()
endif()

# Warnings for our own code only (never for the dependencies).
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
  foreach(t engine_core engine_render demo instancing engine_tests gpu_check)
    if(TARGET ${t})
      target_compile_options(${t} PRIVATE -Wall -Wextra -Wno-unused-parameter)
    endif()
  endforeach()
endif()
