cmake_minimum_required(VERSION 3.24)
project(LastStand LANGUAGES C CXX)

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

# Link the C runtime into the program on Windows, so the exe runs on a PC without
# the Visual C++ redistributable installed. This must be set before any target
# is created, so that raylib and our libraries all use the same runtime.
if(MSVC)
  set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

option(LS_BUILD_TESTS "Build the native unit tests and bot run" ON)
option(LS_SIM_ONLY "Build only the simulation and its tests, with no raylib or window" OFF)

# ---------------------------------------------------------------------------
# raylib: pinned version and hash. Stored in build/downloads so that deleting a
# single build folder does not download it again.
# ---------------------------------------------------------------------------
if(NOT LS_SIM_ONLY)
include(FetchContent)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
if(EMSCRIPTEN)
  set(PLATFORM "Web" CACHE STRING "" FORCE)
  set(GRAPHICS "GRAPHICS_API_OPENGL_ES3" CACHE STRING "" FORCE)
endif()
FetchContent_Declare(raylib
  URL https://github.com/raysan5/raylib/archive/refs/tags/6.2.tar.gz
  URL_HASH SHA256=df7a13129b1bec9e73a6962efff170beba310ee6eab8f94cd000b7316b4db5ee
  DOWNLOAD_NAME raylib-6.2.tar.gz
  DOWNLOAD_DIR "${CMAKE_SOURCE_DIR}/build/downloads"
  DOWNLOAD_EXTRACT_TIMESTAMP FALSE
)
FetchContent_MakeAvailable(raylib)
endif()

# ---------------------------------------------------------------------------
# The simulation is plain C++ with no raylib dependency, so that unit tests and
# the bot run can execute it without a window.
# ---------------------------------------------------------------------------
file(GLOB_RECURSE LS_SIM_SOURCES CONFIGURE_DEPENDS
  src/core/*.cpp
  src/components/Player/*.cpp
  src/components/Enemies/*.cpp
  src/components/Weapons/*.cpp
  src/components/Pickups/*.cpp
  src/components/Upgrades/*.cpp
  src/components/Obstacles/*.cpp
  src/components/Maps/Layout/*.cpp
)
list(FILTER LS_SIM_SOURCES EXCLUDE REGEX "/core/(App|Platform|NativePaths)\\.cpp$")

add_library(ls_sim STATIC ${LS_SIM_SOURCES})
target_include_directories(ls_sim PUBLIC src)
if(MSVC)
  target_compile_options(ls_sim PRIVATE /W4 /utf-8)
endif()

# The sound synthesis is plain C++ too, so the unit tests can render and check
# every sound and the music without raylib. Only Audio/Audio.cpp uses raylib.
file(GLOB_RECURSE LS_AUDIO_SOURCES CONFIGURE_DEPENDS src/components/Audio/*/*.cpp)
add_library(ls_audio STATIC ${LS_AUDIO_SOURCES})
target_include_directories(ls_audio PUBLIC src)
if(MSVC)
  target_compile_options(ls_audio PRIVATE /W4 /utf-8)
endif()

if(NOT LS_SIM_ONLY)
file(GLOB_RECURSE LS_GAME_SOURCES CONFIGURE_DEPENDS
  src/components/Effects/*.cpp
  src/components/Audio/*.cpp
  src/components/Input/*.cpp
  src/components/Ui/*.cpp
  src/components/WorldView/*.cpp
  src/components/Maps/*.cpp
)
if(EMSCRIPTEN)
  # Character art is painted in code once, at startup, so it is built for download size, not speed.
  file(GLOB_RECURSE LS_ART_SOURCES CONFIGURE_DEPENDS src/components/WorldView/*Art.cpp)
  set_source_files_properties(${LS_ART_SOURCES} PROPERTIES COMPILE_OPTIONS "-Oz")
  # The ground is painted during play, as the player walks. O2 paints it about 40 percent faster
  # than Oz for about 30 KB more download, and as fast as O3 for less.
  file(GLOB_RECURSE LS_MAP_SOURCES CONFIGURE_DEPENDS src/components/Maps/*.cpp)
  set_source_files_properties(${LS_MAP_SOURCES} PROPERTIES COMPILE_OPTIONS "-O2")
endif()
list(FILTER LS_GAME_SOURCES EXCLUDE REGEX "/components/Audio/[^/]+/")
# The layout of a map belongs to the simulation, which is linked in already.
list(FILTER LS_GAME_SOURCES EXCLUDE REGEX "/components/Maps/Layout/")
file(GLOB LS_APP_SOURCE CONFIGURE_DEPENDS src/core/App.cpp)

# The UI font is compiled into the program as a byte array, so the page never
# fetches a font file. The array is written into the build folder, the same way
# for the native and browser builds, and only when the font file changes.
set(LS_FONT_TTF "${CMAKE_SOURCE_DIR}/src/components/Ui/Font/PixelifySans.ttf")
set(LS_FONT_CPP "${CMAKE_BINARY_DIR}/generated/FontData.cpp")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${LS_FONT_TTF}")
if(NOT EXISTS "${LS_FONT_CPP}" OR "${LS_FONT_TTF}" IS_NEWER_THAN "${LS_FONT_CPP}")
  file(READ "${LS_FONT_TTF}" LS_FONT_HEX HEX)
  # Break the hex into lines of 16 bytes, then turn each byte into "0xNN,".
  string(REPEAT "[0-9a-f]" 32 LS_HEX_LINE)
  string(REGEX REPLACE "(${LS_HEX_LINE})" "\\1\n" LS_FONT_HEX "${LS_FONT_HEX}")
  string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," LS_FONT_BYTES "${LS_FONT_HEX}")
  file(WRITE "${LS_FONT_CPP}"
    "// Generated by CMake from PixelifySans.ttf. Do not edit.\n"
    "#include <cstddef>\n"
    "namespace ls::font {\n"
    "extern const unsigned char kEmbeddedTtf[];\n"
    "extern const std::size_t kEmbeddedTtfSize;\n"
    "const unsigned char kEmbeddedTtf[] = {\n${LS_FONT_BYTES}0};\n"
    "const std::size_t kEmbeddedTtfSize = sizeof(kEmbeddedTtf) - 1;\n"
    "}\n")
endif()

add_executable(laststand src/main.cpp src/core/Platform.cpp ${LS_APP_SOURCE} ${LS_GAME_SOURCES} "${LS_FONT_CPP}")
target_link_libraries(laststand PRIVATE ls_sim ls_audio raylib)
# Release builds are the ones players get. They hide the debug overlay (native: unless run
# with --debug). The check is on the configuration, not NDEBUG, because the RelWithDebInfo
# build used for development also defines NDEBUG.
target_compile_definitions(laststand PRIVATE "$<$<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>>:LS_RELEASE_BUILD>")
# The native save folder code needs windows.h, so it is a file of its own, apart from raylib.
if(NOT EMSCRIPTEN)
  target_sources(laststand PRIVATE src/core/NativePaths.cpp)
endif()

if(MSVC)
  target_compile_options(laststand PRIVATE /W4 /utf-8)
  target_link_libraries(laststand PRIVATE winmm)
  # The Release exe is a Windows app with no console window for players. Other
  # configurations keep the console, so log output is still visible while debugging.
  # mainCRTStartup keeps the plain main() entry point.
  target_link_options(laststand PRIVATE "$<$<CONFIG:Release>:/SUBSYSTEM:WINDOWS;/ENTRY:mainCRTStartup>")
endif()
endif()

# ---------------------------------------------------------------------------
# Browser build
# ---------------------------------------------------------------------------
if(EMSCRIPTEN)
  set(CMAKE_EXECUTABLE_SUFFIX ".html")
  set(LS_WEB_DIR "${CMAKE_SOURCE_DIR}/web")
  foreach(tgt ls_sim ls_audio laststand)
    target_compile_options(${tgt} PRIVATE -O3 -flto -fno-exceptions -fno-rtti)
  endforeach()
  target_compile_options(raylib PRIVATE -O3 -flto)
  target_link_options(laststand PRIVATE
    -O3 -flto
    "--shell-file=${LS_WEB_DIR}/index.html"
    -sMIN_WEBGL_VERSION=2 -sMAX_WEBGL_VERSION=2
    -sENVIRONMENT=web
    -sINITIAL_MEMORY=64MB -sALLOW_MEMORY_GROWTH=1
    -sSTACK_SIZE=1MB
    -sEXPORTED_FUNCTIONS=_main,_ls_query_state,_ls_debug_command
    -fno-exceptions -fno-rtti
  )
  # Every file the page shell is made of. A change to any of them relinks, which is what
  # runs the finishing step below, so the output folder never keeps an old copy.
  # CONFIGURE_DEPENDS makes a new or deleted file rerun CMake too.
  file(GLOB_RECURSE LS_WEB_SHELL_FILES CONFIGURE_DEPENDS "${LS_WEB_DIR}/*")
  list(APPEND LS_WEB_SHELL_FILES "${CMAKE_SOURCE_DIR}/tools/generate-icons.mjs")
  set_target_properties(laststand PROPERTIES OUTPUT_NAME "index"
    LINK_DEPENDS "${LS_WEB_SHELL_FILES}")
  # Copies the page shell next to the build output and writes the real
  # .wasm size into the page, so the loading bar can show true progress.
  add_custom_command(TARGET laststand POST_BUILD
    COMMAND ${CMAKE_COMMAND}
      -DOUT_DIR=$<TARGET_FILE_DIR:laststand>
      -DWEB_DIR=${LS_WEB_DIR}
      -DTOOLS_DIR=${CMAKE_SOURCE_DIR}/tools
      -P "${CMAKE_SOURCE_DIR}/web/FinishWebBuild.cmake"
  )
endif()

# ---------------------------------------------------------------------------
# Balance tool: plays many bot games and prints tuning tables. Native only, and
# also built with LS_SIM_ONLY. It is a development tool, not part of the game.
# ---------------------------------------------------------------------------
if(NOT EMSCRIPTEN)
  find_package(Threads REQUIRED)
  file(GLOB LS_BALANCE_SOURCES CONFIGURE_DEPENDS tests/balance/*.cpp)
  add_executable(ls_balance ${LS_BALANCE_SOURCES})
  target_link_libraries(ls_balance PRIVATE ls_sim Threads::Threads)
  if(MSVC)
    target_compile_options(ls_balance PRIVATE /W4 /utf-8)
  endif()
endif()

# ---------------------------------------------------------------------------
# Native tests
# ---------------------------------------------------------------------------
if(LS_BUILD_TESTS AND NOT EMSCRIPTEN)
  enable_testing()
  file(GLOB_RECURSE LS_TEST_SOURCES CONFIGURE_DEPENDS tests/unit/*.cpp)
  add_executable(ls_tests ${LS_TEST_SOURCES})
  target_link_libraries(ls_tests PRIVATE ls_sim ls_audio)
  target_include_directories(ls_tests PRIVATE third_party/doctest)
  if(MSVC)
    target_compile_options(ls_tests PRIVATE /W4 /utf-8)
  endif()
  add_test(NAME unit COMMAND ls_tests)
endif()
