cmake_minimum_required(VERSION 3.25) # Debian 12 ships 3.25
project(VulkanEngine)

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/CMake")

include(BuildInfo)
include(FetchContent)
# LTO
include(CheckIPOSupported)
check_ipo_supported(RESULT LTO_SUPPORTED OUTPUT LTO_ERROR)

# ---------------------------------------------------------------------------
# Build modes
#   (default)                    Nix dev build, deps from the Nix shell
#   FETCH_DEPS + PORTABLE        Single directory, everything static except
#                                glibc (and the Vulkan loader, loaded via volk)
#   FETCH_DEPS + BUNDLE          bin/ + lib/, project libs shared in lib/,
#                                system libstdc++ / libc / loader are used
# ---------------------------------------------------------------------------
option(VULKANENGINE_PORTABLE
  "Build a Linux binary without Nix-specific runtime paths (static libs)"
  OFF
)

option(VULKANENGINE_BUNDLE
  "Build a Linux bundle using libraries from the bundle directory"
  OFF
)

option(VULKANENGINE_FETCH_DEPS
  "Fetch dependencies with FetchContent instead of using system/Nix packages"
  OFF
)

set(VULKANENGINE_PREBUILT_SHADERS "" CACHE PATH
  "Directory containing precompiled .spv files (skips glslang/slangc)"
)

if(VULKANENGINE_PORTABLE AND VULKANENGINE_BUNDLE)
  message(FATAL_ERROR "VULKANENGINE_PORTABLE and VULKANENGINE_BUNDLE are mutually exclusive")
endif()

if((VULKANENGINE_PORTABLE OR VULKANENGINE_BUNDLE) AND NOT VULKANENGINE_FETCH_DEPS)
  message(FATAL_ERROR
    "PORTABLE/BUNDLE builds need -DVULKANENGINE_FETCH_DEPS=ON "
    "(Nix libraries must not leak into the result)")
endif()

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# RPATH / runtime paths
if(VULKANENGINE_PORTABLE)
  message(STATUS "VulkanEngine: PORTABLE build")

  set(CMAKE_SKIP_RPATH TRUE)

elseif(VULKANENGINE_BUNDLE)
  message(STATUS "VulkanEngine: BUNDLE build")

  set(CMAKE_SKIP_BUILD_RPATH FALSE)
  set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)

  # Executables are installed in bin/
  # Libraries are installed in lib/
  set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")
  set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE)

  # Every shared lib we build ends up here, installed as-is into lib/
  set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")

else()
  message(STATUS "VulkanEngine: normal build")
endif()

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  find_program(GCC_AR     NAMES gcc-ar     REQUIRED)
  find_program(GCC_RANLIB NAMES gcc-ranlib REQUIRED)
  set(CMAKE_AR     "${GCC_AR}")
  set(CMAKE_RANLIB "${GCC_RANLIB}")
endif()

# ---------------------------------------------------------------------------
# Dependencies
# ---------------------------------------------------------------------------
# Same in every mode: no link-time dependency on libvulkan, all Vulkan
# functions come from volk (otherwise vk*KHR aliases are undefined at link).
set(ENGINE_EXTRA_DEFINITIONS
  GLFW_INCLUDE_NONE
  VK_NO_PROTOTYPES
  VMA_STATIC_VULKAN_FUNCTIONS=0
  VMA_DYNAMIC_VULKAN_FUNCTIONS=1
)

if(VULKANENGINE_FETCH_DEPS)
  # Portable = static libs, bundle = shared libs (in lib/)
  if(VULKANENGINE_BUNDLE)
    set(BUILD_SHARED_LIBS ON)
  else()
    set(BUILD_SHARED_LIBS OFF)
  endif()
  set(CMAKE_POSITION_INDEPENDENT_CODE ON)

  set(VULKAN_HEADERS_ENABLE_MODULE  OFF CACHE BOOL "" FORCE)
  set(VULKAN_HEADERS_ENABLE_TESTS   OFF CACHE BOOL "" FORCE)
  set(VULKAN_HEADERS_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
  set(VOLK_PULL_IN_VULKAN OFF CACHE BOOL "" FORCE)
  set(VOLK_INSTALL        OFF CACHE BOOL "" FORCE)
  set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
  set(GLFW_BUILD_TESTS    OFF CACHE BOOL "" FORCE)
  set(GLFW_BUILD_DOCS     OFF CACHE BOOL "" FORCE)
  set(GLFW_INSTALL        OFF CACHE BOOL "" FORCE)
  set(FMT_INSTALL         OFF CACHE BOOL "" FORCE)
  set(FMT_TEST            OFF CACHE BOOL "" FORCE)
  set(SIMDJSON_DEVELOPER_MODE OFF CACHE BOOL "" FORCE)
  set(CLI11_BUILD_TESTS    OFF CACHE BOOL "" FORCE)
  set(CLI11_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
  set(GLM_BUILD_TESTS   OFF CACHE BOOL "" FORCE)
  set(GLM_BUILD_LIBRARY OFF CACHE BOOL "" FORCE)

  # Keep these versions in sync with your nixpkgs (check the tags exist)
  FetchContent_Declare(VulkanHeaders
    GIT_REPOSITORY https://github.com/KhronosGroup/Vulkan-Headers.git
    GIT_TAG v1.4.309)
  # vulkan/vk_enum_string_helper.h lives here, not in Vulkan-Headers.
  # SOURCE_SUBDIR points to a nonexistent dir so MakeAvailable only downloads
  # (we use the checked-in header, no need to run its CMake).
  FetchContent_Declare(VulkanUtilityLibraries
    GIT_REPOSITORY https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git
    GIT_TAG v1.4.309
    SOURCE_SUBDIR _no_cmake)
  FetchContent_Declare(volk
    GIT_REPOSITORY https://github.com/zeux/volk.git
    GIT_TAG 1.4.304)
  FetchContent_Declare(VMA
    GIT_REPOSITORY https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
    GIT_TAG v3.2.1)
  FetchContent_Declare(glfw
    GIT_REPOSITORY https://github.com/glfw/glfw.git
    GIT_TAG 3.4)
  FetchContent_Declare(vk-bootstrap
    GIT_REPOSITORY https://github.com/charles-lunarg/vk-bootstrap.git
    GIT_TAG v1.4.365)
  FetchContent_Declare(fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 11.0.2)
  FetchContent_Declare(CLI11
    GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git
    GIT_TAG v2.4.2)
  FetchContent_Declare(glm
    GIT_REPOSITORY https://github.com/g-truc/glm.git
    GIT_TAG 1.0.1)
  FetchContent_Declare(simdjson
    GIT_REPOSITORY https://github.com/simdjson/simdjson.git
    GIT_TAG v3.10.1)

  # Vulkan headers first: vk-bootstrap and volk pick up Vulkan::Headers
  FetchContent_MakeAvailable(VulkanHeaders VulkanUtilityLibraries)
  FetchContent_MakeAvailable(volk VMA glfw vk-bootstrap fmt CLI11 glm simdjson)

  target_link_libraries(volk PUBLIC Vulkan::Headers)

  add_library(vk_utility_headers INTERFACE)
  target_include_directories(vk_utility_headers SYSTEM INTERFACE
    ${vulkanutilitylibraries_SOURCE_DIR}/include)
  target_link_libraries(vk_utility_headers INTERFACE Vulkan::Headers)

else()
  # Nix path
  find_package( volk CONFIG REQUIRED )
  find_package( glfw3 REQUIRED )
  find_package( glm REQUIRED )
  find_package( VulkanMemoryAllocator CONFIG REQUIRED )
  find_package( CLI11 REQUIRED )
  find_package( simdjson REQUIRED )
endif()

# fastgltf because it's not in nixpkgs (declared after the switch above so it
# inherits BUILD_SHARED_LIBS)
FetchContent_Declare(
  fastgltf
  GIT_REPOSITORY https://github.com/spnda/fastgltf.git
  GIT_TAG v0.8.0
)
FetchContent_MakeAvailable(fastgltf)

# ---------------------------------------------------------------------------
# Shader compilers (not needed when precompiled shaders are provided)
# ---------------------------------------------------------------------------
if(NOT VULKANENGINE_PREBUILT_SHADERS)
  add_executable (glslang::validator IMPORTED)
  find_program (
    GLSLANG_VALIDATOR
    "glslangValidator"
    HINTS $ENV{VULKAN_SDK}/bin
    REQUIRED
  )

  set_property (
    TARGET glslang::validator
    PROPERTY IMPORTED_LOCATION "${GLSLANG_VALIDATOR}"
  )

  find_program(SLANGC_EXECUTABLE slangc HINTS $ENV{VULKAN_SDK}/bin REQUIRED)
endif()


# Helper functions to build shaders
function(addGlslShader SOURCE OUTPUT_DIR)
  get_filename_component(SHADER_NAME ${SOURCE} NAME_WE)
  set(SPV_FILE "${OUTPUT_DIR}/${SHADER_NAME}.spv")
  add_custom_command(
    OUTPUT ${SPV_FILE}
    COMMAND ${CMAKE_COMMAND} -E make_directory ${OUTPUT_DIR}
    COMMAND glslang::validator
      -V
      --target-env vulkan1.4 # Change if needed
      -o ${SPV_FILE}
      ${SOURCE}
    DEPENDS ${SOURCE}
    COMMENT "Compiling GLSL shader: ${SOURCE} -> ${SPV_FILE}"
    VERBATIM
  )
  set(SPV_FILE ${SPV_FILE} PARENT_SCOPE)
endfunction()


function(addSlangShader SOURCES ENTRY_POINTS OUTPUT_DIR)
  set(SPV_FILE "${OUTPUT_DIR}/slang.spv")
  add_custom_command(
    OUTPUT ${SPV_FILE}
    COMMAND ${CMAKE_COMMAND} -E make_directory ${OUTPUT_DIR}
    COMMAND ${SLANGC_EXECUTABLE}
      ${SOURCES}
      -target spirv
      -profile spirv_1_6+spvRayQueryKHR # Change if needed
      -emit-spirv-directly
      -fvk-use-entrypoint-name
      ${ENTRY_POINTS}
      -o ${SPV_FILE}
    DEPENDS ${SOURCES}
    COMMENT "Compiling Slang shaders -> ${SPV_FILE}"
    VERBATIM
  )
  set(SPV_FILE ${SPV_FILE} PARENT_SCOPE)
endfunction()


# A function to automate executables aditions
function ( addBinary BINARY_NAME )
  cmake_parse_arguments ( BINARY "IMGUI;LICENSE" "SOURCE" "SHADERS;SLANG_SHADERS;SLANG_ENTRIES;LIBS;TEXTURES;MODELS" ${ARGN} )
  if (DEFINED BINARY_SOURCE)
    add_executable ( ${BINARY_NAME} src/${BINARY_SOURCE}.cpp )
  endif()

  target_include_directories( ${BINARY_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/src )
  # Sets the output dir to have clear separation between binaries
  set_target_properties (
    ${BINARY_NAME}
    PROPERTIES
      RUNTIME_OUTPUT_DIRECTORY
        ${CMAKE_BINARY_DIR}/${BINARY_NAME}
      CXX_STANDARD
        20 # C++ 20
      CXX_STANDARD_REQUIRED
        ON
      CXX_EXTENSIONS
        OFF
      CXX_SCAN_FOR_MODULES
        OFF
  )
  # Sets per binary LTO
  if(LTO_SUPPORTED AND CMAKE_BUILD_TYPE STREQUAL "Release")
    set_target_properties(${BINARY_NAME} PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)
    message("Enabled LTO")
  endif()

  # Portable: libstdc++/libgcc statically linked so the binary does not depend
  # on the host's version. NOT done for bundle: the shared libs in lib/ use the
  # system libstdc++, and mixing two copies in one process is asking for trouble.
  if(VULKANENGINE_PORTABLE)
    target_link_options(${BINARY_NAME} PRIVATE -static-libstdc++ -static-libgcc)
  endif()

  # Define VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS to treat VK_ERROR_OUT_OF_DATE_KHR as a success code
  target_compile_definitions (
    ${BINARY_NAME}
    PRIVATE
    "VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS"
    $<$<CONFIG:Debug>:DEBUG> # So CMAKE expose a #define DEBUG for debug builds
    # Exposes versions
    BUILD_ID="${ENGINE_VERSION}"
    ${ENGINE_EXTRA_DEFINITIONS}
  )

  if(BINARY_IMGUI)
    target_include_directories(${BINARY_NAME} PRIVATE
      ${CMAKE_SOURCE_DIR}/include/imgui
      ${CMAKE_SOURCE_DIR}/include/imgui/backends
    )

    target_sources(${BINARY_NAME} PRIVATE
      ${CMAKE_SOURCE_DIR}/include/imgui/imgui.cpp

      ${CMAKE_SOURCE_DIR}/include/imgui/imgui_draw.cpp
      ${CMAKE_SOURCE_DIR}/include/imgui/imgui_tables.cpp
      ${CMAKE_SOURCE_DIR}/include/imgui/imgui_widgets.cpp
      ${CMAKE_SOURCE_DIR}/include/imgui/imgui_demo.cpp
      #${CMAKE_SOURCE_DIR}/include/imgui/imgui_memory_editor.h

      ${CMAKE_SOURCE_DIR}/include/imgui/backends/imgui_impl_glfw.cpp
      ${CMAKE_SOURCE_DIR}/include/imgui/backends/imgui_impl_vulkan.cpp
    )

    # imgui's Vulkan backend must use volk since there is no linked loader
    target_compile_definitions(${BINARY_NAME} PRIVATE IMGUI_IMPL_VULKAN_USE_VOLK)
  endif()


  if(BINARY_LICENSE)
    file(COPY ${CMAKE_SOURCE_DIR}/LICENSE DESTINATION ${CMAKE_BINARY_DIR}/${BINARY_NAME})
  endif()


  if(DEFINED BINARY_SHADERS)
    set(SHADER_OUTPUT_DIR ${CMAKE_BINARY_DIR}/${BINARY_NAME}/shaders)

    if(VULKANENGINE_PREBUILT_SHADERS)
      # Copy the .spv produced by the Nix job instead of compiling them
      file(GLOB PREBUILT_SPV CONFIGURE_DEPENDS "${VULKANENGINE_PREBUILT_SHADERS}/*.spv")
      if(NOT PREBUILT_SPV)
        message(FATAL_ERROR "No .spv found in ${VULKANENGINE_PREBUILT_SHADERS}")
      endif()
      file(COPY ${PREBUILT_SPV} DESTINATION ${SHADER_OUTPUT_DIR})
    else()
      set(ALL_SPV_FILES "")
      set(EXPANDED_SHADERS "")

      foreach(SHADER_PATTERN ${BINARY_SHADERS})
        if(SHADER_PATTERN MATCHES "[*?]")
          file(GLOB SHADER_FILES CONFIGURE_DEPENDS ${SHADER_PATTERN})
          list(APPEND EXPANDED_SHADERS ${SHADER_FILES})
        else()
          list(APPEND EXPANDED_SHADERS ${SHADER_PATTERN})
        endif()
      endforeach()

      foreach(SHADER_SRC ${EXPANDED_SHADERS})
        if(NOT IS_ABSOLUTE ${SHADER_SRC})
          set(SHADER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/${SHADER_SRC})
        endif()
        get_filename_component(EXT ${SHADER_SRC} EXT)
        if(EXT STREQUAL ".slang")
          addSlangShader(${SHADER_SRC} "${BINARY_SLANG_ENTRIES}" ${SHADER_OUTPUT_DIR})
          list(APPEND ALL_SPV_FILES ${SPV_FILE})
        else()
          addGlslShader(${SHADER_SRC} ${SHADER_OUTPUT_DIR})
          list(APPEND ALL_SPV_FILES ${SPV_FILE})
        endif()
      endforeach()

      add_custom_target(shaders_${BINARY_NAME} DEPENDS ${ALL_SPV_FILES})
      add_dependencies(${BINARY_NAME} shaders_${BINARY_NAME})
    endif()
  endif()

  if (DEFINED BINARY_LIBS)
    target_link_libraries ( ${BINARY_NAME} PRIVATE ${BINARY_LIBS} )
  endif()

  if (DEFINED BINARY_MODELS) # Adds the 3D models in the binary folder
    set(RESOLVED_MODELS "")
    foreach(MODEL_PATTERN ${BINARY_MODELS})
      file(GLOB MATCHED_MODELS CONFIGURE_DEPENDS
        "${CMAKE_SOURCE_DIR}/assets/models/${MODEL_PATTERN}"
      )
      list(APPEND RESOLVED_MODELS ${MATCHED_MODELS})
    endforeach()

    if (RESOLVED_MODELS)
      file(COPY ${RESOLVED_MODELS} DESTINATION ${CMAKE_BINARY_DIR}/${BINARY_NAME}/models)
    endif()
  endif()

  if (DEFINED BINARY_TEXTURES) # Adds the textures in the binary folder
    set(RESOLVED_TEXTURES "")
    foreach(TEX_PATTERN ${BINARY_TEXTURES})
      file(GLOB MATCHED_TEXTURES CONFIGURE_DEPENDS
        "${CMAKE_SOURCE_DIR}/assets/textures/${TEX_PATTERN}"
      )
      list(APPEND RESOLVED_TEXTURES ${MATCHED_TEXTURES})
    endforeach()

    if (RESOLVED_TEXTURES)
      file(COPY ${RESOLVED_TEXTURES} DESTINATION ${CMAKE_BINARY_DIR}/${BINARY_NAME}/textures)
    endif()
  endif ()
endfunction()


# ---------------------------------------------------------------------------
# Binaries
# ---------------------------------------------------------------------------
list(
  APPEND
  lib_list
  # Libs used
  glfw
  fmt
  vk-bootstrap
  CLI11::CLI11
  fastgltf
)

if(VULKANENGINE_FETCH_DEPS)
  # No libvulkan at link time: volk loads libvulkan.so.1 at runtime.
  # Remove what your code does not use (simdjson, glm...).
  list(APPEND lib_list
    volk
    Vulkan::Headers
    vk_utility_headers
    GPUOpen::VulkanMemoryAllocator
    glm::glm-header-only
    simdjson::simdjson
  )
else()
  # Nix: volk comes from nixpkgs (vulkan-volk); libvulkan is NOT linked
  list(APPEND lib_list volk::volk)
endif()

addBinary (
  engine
  SOURCE Engine/main
  # Flags
  IMGUI
  LICENSE

  SHADERS shaders/*
  MODELS  *

  LIBS ${lib_list}
)

addBinary (
  server
  SOURCE Server/main
)

# ---------------------------------------------------------------------------
# Install (used for portable / bundle packaging)
#   dist/bin/{engine,server,shaders,models,textures}
#   dist/lib/*.so*   (bundle only)
#   dist/LICENSE
# Never bundled: libc, libstdc++, libvulkan, GL, X11, Wayland.
# ---------------------------------------------------------------------------
install(TARGETS engine server RUNTIME DESTINATION bin)
install(DIRECTORY ${CMAKE_BINARY_DIR}/engine/shaders  DESTINATION bin OPTIONAL)
install(DIRECTORY ${CMAKE_BINARY_DIR}/engine/models   DESTINATION bin OPTIONAL)
install(DIRECTORY ${CMAKE_BINARY_DIR}/engine/textures DESTINATION bin OPTIONAL)
install(FILES ${CMAKE_SOURCE_DIR}/LICENSE DESTINATION .)

if(VULKANENGINE_BUNDLE)
  # Every shared lib built from fetched deps (glfw, fmt, fastgltf and its own
  # simdjson, ...) with its symlinks. libstdc++ etc. are not built here.
  install(DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/
    DESTINATION lib
    USE_SOURCE_PERMISSIONS
    FILES_MATCHING PATTERN "*.so*"
  )
endif()
