cmake_minimum_required(VERSION 3.20)
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" FZERO_VERSION LIMIT_COUNT 1)
project(FZeroSNESRecomp VERSION ${FZERO_VERSION} LANGUAGES C)
set(CMAKE_C_STANDARD 11)

include(CTest)
option(FZERO_BUILD_GAME "Build game hosts (requires privately generated ROM sources)" ON)
add_library(fzero_video STATIC src/fzero_video.c src/fzero_mode7.c src/fzero_replay.c src/fzero_hdma.c)
target_include_directories(fzero_video PUBLIC "${CMAKE_SOURCE_DIR}/src")
if(NOT MSVC)
    target_compile_options(fzero_video PRIVATE -Wall -Wextra -Werror)
    target_link_libraries(fzero_video PUBLIC m)
endif()
if(BUILD_TESTING)
    add_executable(fzero_hdma_tests tests/test_hdma.c)
    target_link_libraries(fzero_hdma_tests PRIVATE fzero_video)
    add_test(NAME fzero_hdma COMMAND fzero_hdma_tests)
    add_executable(fzero_video_tests tests/test_video.c)
    target_link_libraries(fzero_video_tests PRIVATE fzero_video)
    add_test(NAME fzero_video COMMAND fzero_video_tests)
    add_executable(fzero_mode7_tests tests/test_mode7.c)
    target_link_libraries(fzero_mode7_tests PRIVATE fzero_video)
    add_test(NAME fzero_mode7 COMMAND fzero_mode7_tests)
    # Neither of these needs SDL, a ROM, or the generated sources: they are
    # the host-side logic behind the save-state menu and rewind bindings, so
    # they build and run in the ROM-free configuration too.
    add_executable(fzero_hotkeys_tests tests/test_hotkeys.c src/fzero_hotkeys.c)
    target_include_directories(fzero_hotkeys_tests PRIVATE "${CMAKE_SOURCE_DIR}/src")
    add_test(NAME fzero_hotkeys COMMAND fzero_hotkeys_tests)
    add_executable(fzero_state_mode_tests tests/test_state_mode.c src/fzero_state_mode.c)
    target_include_directories(fzero_state_mode_tests PRIVATE "${CMAKE_SOURCE_DIR}/src")
    add_test(NAME fzero_state_mode COMMAND fzero_state_mode_tests)
    if(NOT MSVC)
        target_compile_options(fzero_hotkeys_tests PRIVATE -Wall -Wextra -Werror)
        target_compile_options(fzero_state_mode_tests PRIVATE -Wall -Wextra -Werror)
    endif()
endif()
if(NOT FZERO_BUILD_GAME)
    return()
endif()

# Both dependencies are pinned submodules of this repository (see
# .gitmodules); `bash tools/bootstrap.sh` initializes them. Point
# SNESRECOMP_ROOT at a worktree instead when developing framework changes
# alongside the game.
set(SNESRECOMP_ROOT "${CMAKE_SOURCE_DIR}/snesrecomp" CACHE PATH
    "Path to the snesrecomp checkout used by this game")
set(RECOMP_UI_ROOT "${CMAKE_SOURCE_DIR}/recomp-ui" CACHE PATH
    "Path to the shared recomp-ui launcher checkout")
set(SNESRECOMP_SDL_BACKEND "SDL3" CACHE STRING
    "Desktop SDL backend (SDL3 or SDL2)")
set(RECOMP_UI_ENABLE_MODS ON CACHE BOOL "F-Zero built-in native presentation mod" FORCE)
include(${SNESRECOMP_ROOT}/runner/runner.cmake)
include(${RECOMP_UI_ROOT}/recomp_ui.cmake)
find_package(OpenGL REQUIRED)

add_executable(FZeroRenderCapture tools/render_capture.c src/fzero_renderer.c)
target_include_directories(FZeroRenderCapture PRIVATE ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
target_link_libraries(FZeroRenderCapture PRIVATE fzero_video)
add_executable(FZeroRendererBenchmark tools/benchmark_renderer.c src/fzero_renderer.c)
target_include_directories(FZeroRendererBenchmark PRIVATE ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
target_link_libraries(FZeroRendererBenchmark PRIVATE fzero_video)
if(BUILD_TESTING)
    add_executable(fzero_renderer_tests tests/test_renderer.c src/fzero_renderer.c)
    add_executable(fzero_mods_tests tests/test_mods.c src/fzero_mods.c)
    target_include_directories(fzero_mods_tests PRIVATE "${RECOMP_UI_ROOT}/src")
    target_link_libraries(fzero_mods_tests PRIVATE fzero_video)
    if(NOT MSVC)
        target_compile_options(fzero_mods_tests PRIVATE -Wall -Wextra -Werror)
    endif()
    add_test(NAME fzero_mods COMMAND fzero_mods_tests)
    target_include_directories(fzero_renderer_tests PRIVATE ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    target_link_libraries(fzero_renderer_tests PRIVATE fzero_video)
    if(NOT MSVC)
        target_compile_options(fzero_renderer_tests PRIVATE -Wall -Wextra -Werror)
    endif()
    add_test(NAME fzero_renderer COMMAND fzero_renderer_tests)
endif()

# Release version stamp defaults to VERSION; override explicitly for local builds.
# NOTE: quote it when configuring - PowerShell rewrites an unquoted
# -DSNESRECOMP_BUILD_VERSION=0.1.0 into "0".
set(SNESRECOMP_BUILD_VERSION "${PROJECT_VERSION}" CACHE STRING
    "Version stamp baked into crash reports")

set(FZERO_GEN_DIR "${CMAKE_SOURCE_DIR}/src/gen" CACHE PATH
    "Directory containing the private generated F-Zero C sources")
file(GLOB FZERO_GEN_SOURCES CONFIGURE_DEPENDS "${FZERO_GEN_DIR}/*.c")
if(NOT FZERO_GEN_SOURCES)
    message(FATAL_ERROR
        "${FZERO_GEN_DIR} is empty - stage a verified fzero.sfc and run "
        "`bash tools/regen.sh` before building.")
endif()

# The headless host links no SDL at all, so anything in the runner that
# includes desktop/sdl_compat.h has to be held back from the shared source
# list and handed to the desktop host explicitly. keybinds.c and
# host_report.c were always in that set; snes_savestate_menu.c and
# snes_osd.c joined the runner later and pull SDL in for their keycodes and
# their tick clock.
list(REMOVE_ITEM SNESRECOMP_RUNNER_SOURCES
    "${SNESRECOMP_ROOT}/runner/src/keybinds.c"
    "${SNESRECOMP_ROOT}/runner/src/host_report.c"
    "${SNESRECOMP_ROOT}/runner/src/snes_savestate_menu.c"
    "${SNESRECOMP_ROOT}/runner/src/snes_osd.c")

set(FZERO_DELUXE_GEN_DIR "" CACHE PATH "Private generated BS Deluxe native sources")
set(FZERO_DELUXE_DATA_FILE "" CACHE FILEPATH "Imported BS Deluxe payload to embed")
if(FZERO_DELUXE_GEN_DIR)
    # The payload ships inside the executable: a download can never be missing
    # it and the game can never fail to launch over it. Default to the file the
    # importer writes beside the generated sources.
    if(NOT FZERO_DELUXE_DATA_FILE)
        get_filename_component(FZERO_DELUXE_WORK "${FZERO_DELUXE_GEN_DIR}" DIRECTORY)
        set(FZERO_DELUXE_DATA_FILE "${FZERO_DELUXE_WORK}/mods/bs-deluxe.dat")
    endif()
    if(NOT EXISTS "${FZERO_DELUXE_DATA_FILE}")
        message(FATAL_ERROR "BS Deluxe payload not found at ${FZERO_DELUXE_DATA_FILE}. "
                            "Run tools/regen_bs_deluxe.py, or set FZERO_DELUXE_DATA_FILE.")
    endif()
    find_package(Python3 COMPONENTS Interpreter REQUIRED)
    set(FZERO_DELUXE_PAYLOAD_SOURCE "${CMAKE_BINARY_DIR}/generated/bs_deluxe_payload.c")
    add_custom_command(OUTPUT "${FZERO_DELUXE_PAYLOAD_SOURCE}"
        COMMAND ${Python3_EXECUTABLE} "${CMAKE_SOURCE_DIR}/tools/embed_payload.py"
                "${FZERO_DELUXE_DATA_FILE}" "${FZERO_DELUXE_PAYLOAD_SOURCE}"
        DEPENDS "${FZERO_DELUXE_DATA_FILE}" "${CMAKE_SOURCE_DIR}/tools/embed_payload.py"
        COMMENT "Embedding BS Deluxe payload" VERBATIM)
    add_custom_target(fzero_deluxe_payload_source DEPENDS "${FZERO_DELUXE_PAYLOAD_SOURCE}")
    file(GLOB DELUXE_SOURCES CONFIGURE_DEPENDS "${FZERO_DELUXE_GEN_DIR}/*.c")
    if(NOT DELUXE_SOURCES OR NOT EXISTS "${FZERO_DELUXE_GEN_DIR}/deluxe_namespace.h")
        message(FATAL_ERROR "Generate and import BS Deluxe before enabling its native module")
    endif()
    add_library(fzero_deluxe_native OBJECT ${DELUXE_SOURCES})
    target_include_directories(fzero_deluxe_native PRIVATE
        "${CMAKE_SOURCE_DIR}/recomp" ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    if(MSVC)
        target_compile_options(fzero_deluxe_native PRIVATE "/FI${FZERO_DELUXE_GEN_DIR}/deluxe_namespace.h")
    else()
        target_compile_options(fzero_deluxe_native PRIVATE -w -include "${FZERO_DELUXE_GEN_DIR}/deluxe_namespace.h")
    endif()
endif()

# The headless host keeps its own host_report stubs: it links no SDL, and
# host_report.c includes desktop/sdl_compat.h.
add_executable(FZeroSNESRecompHeadless
    ${SNESRECOMP_RUNNER_SOURCES}
    ${FZERO_GEN_SOURCES}
    src/headless_host.c
    src/headless_main.c
    src/fzero_runtime.c)

# The desktop host takes the REAL host_report.c (always-on breadcrumb ring,
# crash minidump writer, build-version stamp) and the REAL keybinds.c, so the
# launcher's Controls page and the game read the same keybinds.ini.
add_executable(FZeroSNESRecomp
    ${SNESRECOMP_RUNNER_SOURCES}
    "${SNESRECOMP_ROOT}/runner/src/host_report.c"
    "${SNESRECOMP_ROOT}/runner/src/keybinds.c"
    "${SNESRECOMP_ROOT}/runner/src/snes_savestate_menu.c"
    "${SNESRECOMP_ROOT}/runner/src/snes_osd.c"
    "${RECOMP_UI_ROOT}/src/third_party/gl_core_3_1.c"
    ${FZERO_GEN_SOURCES}
    src/sdl_main.c
    src/fzero_diagnostics.c
    src/fzero_mods.c
    src/fzero_hotkeys.c
    src/fzero_gamepad.c
    src/fzero_runtime.c)

function(configure_fzero_target target)
    target_sources(${target} PRIVATE src/fzero_deluxe.c src/fzero_state_mode.c src/fzero_msu.c)
    if(FZERO_DELUXE_GEN_DIR)
        target_sources(${target} PRIVATE $<TARGET_OBJECTS:fzero_deluxe_native>
                                         "${FZERO_DELUXE_PAYLOAD_SOURCE}")
        add_dependencies(${target} fzero_deluxe_payload_source)
        target_compile_definitions(${target} PRIVATE FZERO_HAS_DELUXE=1)
    endif()
    target_sources(${target} PRIVATE src/fzero_renderer.c)
    target_link_libraries(${target} PRIVATE fzero_video)
    target_include_directories(${target} PRIVATE
        "${CMAKE_SOURCE_DIR}"
        "${CMAKE_SOURCE_DIR}/src"
        "${CMAKE_SOURCE_DIR}/recomp"
        ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    target_compile_definitions(${target} PRIVATE
        SYSTEM_VOLUME_MIXER_AVAILABLE=0
        SNESRECOMP_TRACE=$<BOOL:${SNESRECOMP_ENABLE_TRACE}>
        SNESRECOMP_REVERSE_DEBUG=0)
    if(NOT MSVC)
        # The recompiled C is machine-generated; quiet its warnings and
        # tolerate the K&R-style implicit declarations clang rejects.
        target_compile_options(${target} PRIVATE
            $<$<COMPILE_LANGUAGE:C>:-w;-Wno-implicit-function-declaration>)
        target_link_libraries(${target} PRIVATE m)
    endif()
    target_link_libraries(${target} PRIVATE ${SNESRECOMP_RUNNER_LIBRARIES})
endfunction()

configure_fzero_target(FZeroSNESRecompHeadless)
configure_fzero_target(FZeroSNESRecomp)
# Hosts include "desktop/sdl_compat.h" rather than <SDL.h> so both majors build.
snesrecomp_target_glsl_shader(FZeroSNESRecomp)
snesrecomp_target_sdl(FZeroSNESRecomp)

if(BUILD_TESTING AND SNESRECOMP_SDL_BACKEND STREQUAL "SDL3")
    add_executable(fzero_gamepad_tests tests/test_gamepad.c src/fzero_gamepad.c src/fzero_hotkeys.c)
    target_include_directories(fzero_gamepad_tests PRIVATE "${CMAKE_SOURCE_DIR}/src" ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    snesrecomp_target_sdl(fzero_gamepad_tests)
    add_test(NAME fzero_gamepad COMMAND fzero_gamepad_tests)
endif()
target_link_libraries(FZeroSNESRecomp PRIVATE OpenGL::GL)
recomp_target_launcher_ui(FZeroSNESRecomp CONSOLE snes)
# Identify dependencies as well as the game in player performance reports.
find_package(Git QUIET)
function(fzero_revision directory output)
    set(revision "unknown")
    if(GIT_FOUND)
        execute_process(COMMAND "${GIT_EXECUTABLE}" describe --always --dirty --abbrev=12
            WORKING_DIRECTORY "${directory}" OUTPUT_VARIABLE revision
            RESULT_VARIABLE revision_result OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
        if(NOT revision_result EQUAL 0)
            set(revision "unknown")
        endif()
    endif()
    set(${output} "${revision}" PARENT_SCOPE)
endfunction()
fzero_revision("${CMAKE_CURRENT_SOURCE_DIR}" FZERO_SOURCE_REVISION)
fzero_revision("${SNESRECOMP_ROOT}" FZERO_FRAMEWORK_REVISION)
fzero_revision("${RECOMP_UI_ROOT}" FZERO_UI_REVISION)
configure_file(src/fzero_build.h.in generated/fzero_build.h @ONLY)
target_include_directories(FZeroSNESRecomp PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
add_custom_command(TARGET FZeroSNESRecomp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        "${CMAKE_CURRENT_SOURCE_DIR}/assets/img/boxart.tga"
        "$<TARGET_FILE_DIR:FZeroSNESRecomp>/assets/img/boxart.tga"
    VERBATIM)
add_custom_command(TARGET FZeroSNESRecomp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${CMAKE_CURRENT_SOURCE_DIR}/assets/shaders"
        "$<TARGET_FILE_DIR:FZeroSNESRecomp>/assets/shaders"
    COMMENT "Staging F-Zero shader presets"
    VERBATIM)

if(SNESRECOMP_BUILD_VERSION)
    target_compile_definitions(FZeroSNESRecomp PRIVATE
        SNESRECOMP_BUILD_VERSION="${SNESRECOMP_BUILD_VERSION}")
endif()

if(BUILD_TESTING AND FZERO_DELUXE_GEN_DIR)
    # Locks the contract that matters for a download: the payload is in the
    # binary, a file beside it is only an override, and nothing here can
    # stop the game starting.
    add_executable(fzero_deluxe_tests tests/test_deluxe.c tests/deluxe_stubs.c
        src/fzero_deluxe.c "${SNESRECOMP_ROOT}/runner/src/sha256.c"
        "${FZERO_DELUXE_PAYLOAD_SOURCE}")
    add_dependencies(fzero_deluxe_tests fzero_deluxe_payload_source)
    target_compile_definitions(fzero_deluxe_tests PRIVATE FZERO_HAS_DELUXE=1)
    target_include_directories(fzero_deluxe_tests PRIVATE
        "${CMAKE_SOURCE_DIR}/src" ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
    if(NOT MSVC)
        target_compile_options(fzero_deluxe_tests PRIVATE -Wall -Wextra -Werror)
    endif()
    add_test(NAME fzero_deluxe COMMAND fzero_deluxe_tests
             WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
endif()
