cmake_minimum_required(VERSION 3.22)
project(doa3_android C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
get_filename_component(PORT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
set(RUNTIME "${PORT_ROOT}/recomp-runtime")
set(GAME_DIR "" CACHE PATH "Locally generated, authenticated private game code")
set(GAME_RECIPE "${PORT_ROOT}/tools/game-recipe/usa-android.json" CACHE FILEPATH "Authenticated game recipe")
set(GAME_RECIPE_SHA256 "5d03b70999092b93e8eeeea416e9696df9fa7a8bcdd7bb7927d7114d30054624" CACHE STRING "Pinned recipe identity")

if(GAME_DIR)
    file(SHA256 "${GAME_RECIPE}" recipe_sha)
    if(NOT recipe_sha STREQUAL GAME_RECIPE_SHA256)
        message(FATAL_ERROR "Game recipe identity mismatch")
    endif()
    file(READ "${GAME_RECIPE}" recipe)
    string(JSON game_xbe_sha GET "${recipe}" xbe_sha256)
    string(JSON file_count LENGTH "${recipe}" generated_files)
    math(EXPR last "${file_count} - 1")
    set(manifest "")
    set(ebp_count 0)
    set(game_sources)
    foreach(i RANGE ${last})
        string(JSON name MEMBER "${recipe}" generated_files ${i})
        string(JSON expected GET "${recipe}" generated_files "${name}")
        file(SHA256 "${GAME_DIR}/${name}" actual)
        if(NOT actual STREQUAL expected)
            message(FATAL_ERROR "Generated game identity mismatch: ${name}")
        endif()
        if(NOT name STREQUAL "recomp_stubs_unresolved.c")
            file(SIZE "${GAME_DIR}/${name}" size)
            string(APPEND manifest "${name}\t${size}\t${actual}\n")
        endif()
        if(name MATCHES "\\.c$" AND NOT name STREQUAL "recomp_stubs_unresolved.c")
            file(READ "${GAME_DIR}/${name}" source)
            string(LENGTH "${source}" before)
            string(REPLACE "\n    uint32_t ebp;\n" "\n    uint32_t ebp = g_seh_ebp;\n" derived "${source}")
            string(LENGTH "${derived}" after)
            math(EXPR applied "(${after} - ${before}) / 12")
            math(EXPR ebp_count "${ebp_count} + ${applied}")
            set(previous_text "")
            if(EXISTS "${CMAKE_CURRENT_BINARY_DIR}/${name}")
                file(READ "${CMAKE_CURRENT_BINARY_DIR}/${name}" previous_text)
            endif()
            if(NOT derived STREQUAL previous_text)
                file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/${name}" "${derived}")
            endif()
            list(APPEND game_sources "${CMAKE_CURRENT_BINARY_DIR}/${name}")
        endif()
    endforeach()
    string(JSON expected_manifest GET "${recipe}" program_manifest_sha256)
    string(JSON expected_ebp GET "${recipe}" ebp_overrides)
    string(SHA256 manifest_sha "${manifest}")
    if(NOT manifest_sha STREQUAL expected_manifest OR NOT ebp_count EQUAL expected_ebp)
        message(FATAL_ERROR "Generated manifest or EBP override mismatch")
    endif()
    message(STATUS "Authenticated game manifest; ${ebp_count} existing EBP build overrides")
    add_library(doa3_game_core STATIC ${game_sources})
    set(program_definitions)
    set(program_references)
    foreach(source IN LISTS game_sources)
        file(READ "${source}" source_text)
        string(REGEX MATCHALL "void[ \t\r\n]+sub_[0-9A-Fa-f]+[ \t\r\n]*\\([ \t\r\n]*void[ \t\r\n]*\\)" matches "${source_text}")
        foreach(match IN LISTS matches)
            string(REGEX MATCH "sub_[0-9A-Fa-f]+" name "${match}")
            list(APPEND program_definitions "${name}")
        endforeach()
        string(REGEX MATCHALL "sub_[0-9A-Fa-f]+[ \t\r\n]*[(),]" references "${source_text}")
        foreach(reference IN LISTS references)
            string(REGEX MATCH "sub_[0-9A-Fa-f]+" name "${reference}")
            list(APPEND program_references "${name}")
        endforeach()
    endforeach()
    configure_file("${GAME_DIR}/recomp_funcs.h" "${CMAKE_CURRENT_BINARY_DIR}/recomp_funcs.h" COPYONLY)
    file(READ "${RUNTIME}/program_adapters.c" adapter_text)
    file(READ "${RUNTIME}/program_forwards.c" forward_text)
    set(source_text "${adapter_text}\n${forward_text}")
    string(REGEX MATCHALL "void[ \t\r\n]+sub_[0-9A-Fa-f]+[ \t\r\n]*\\([ \t\r\n]*void[ \t\r\n]*\\)" matches "${source_text}")
    foreach(match IN LISTS matches)
        string(REGEX MATCH "sub_[0-9A-Fa-f]+" name "${match}")
        list(APPEND program_definitions "${name}")
    endforeach()
    file(GLOB hand_sources "${RUNTIME}/*.c" "${RUNTIME}/*.cpp")
    list(FILTER hand_sources EXCLUDE REGEX "_test\\.(c|cpp)$|runtime_public_fixture\\.c$")
    foreach(hand_source IN LISTS hand_sources)
        file(READ "${hand_source}" hand_text)
        string(REGEX MATCHALL "sub_[0-9A-Fa-f]+[ \t\r\n]*\\(" references "${hand_text}")
        foreach(reference IN LISTS references)
            string(REGEX MATCH "sub_[0-9A-Fa-f]+" name "${reference}")
            list(APPEND program_references "${name}")
        endforeach()
    endforeach()
    list(REMOVE_DUPLICATES program_definitions)
    list(REMOVE_DUPLICATES program_references)
    set(missing_source "${CMAKE_CURRENT_BINARY_DIR}/recomp_program_missing.c")
    set(missing_text "#include <stdint.h>\nvoid recomp_program_missing(uint32_t);\n")
    set(missing_count 0)
    foreach(name IN LISTS program_references)
        if(NOT name IN_LIST program_definitions)
            string(SUBSTRING "${name}" 4 8 address)
            string(APPEND missing_text "void ${name}(void) { recomp_program_missing(0x${address}u); }\n")
            math(EXPR missing_count "${missing_count} + 1")
        endif()
    endforeach()
    set(previous_text "")
    if(EXISTS "${missing_source}")
        file(READ "${missing_source}" previous_text)
    endif()
    if(NOT missing_text STREQUAL previous_text)
        file(WRITE "${missing_source}" "${missing_text}")
    endif()
    message(STATUS "Generated ${missing_count} fail-loud absent-body stubs")
    target_sources(doa3_game_core PRIVATE "${missing_source}")
    target_include_directories(doa3_game_core PRIVATE "${RUNTIME}" "${CMAKE_CURRENT_BINARY_DIR}")
    target_compile_options(doa3_game_core PRIVATE -fno-strict-aliasing -ffp-contract=off -frounding-math -Wno-parentheses-equality)
    set_target_properties(doa3_game_core PROPERTIES POSITION_INDEPENDENT_CODE ON)
endif()

set(SDL_ANDROID_ROOT "" CACHE PATH "Extracted official SDL3 Android AAR")
if(ANDROID AND SDL_ANDROID_ROOT)
    add_library(SDL3 SHARED IMPORTED)
    set_target_properties(SDL3 PROPERTIES IMPORTED_LOCATION "${SDL_ANDROID_ROOT}/prefab/modules/SDL3-shared/libs/android.arm64-v8a/libSDL3.so" INTERFACE_INCLUDE_DIRECTORIES "${SDL_ANDROID_ROOT}/prefab/modules/SDL3-Headers/include")
    add_library(doa3_android_backends STATIC native/audio_output_sdl.cpp native/input_host_sdl.cpp native/d3d_vblank_posix.cpp native/texture_rgba.cpp native/d3d_presenter_gles.cpp native/host_diagnostics_android.cpp native/custom_music_android.cpp)
    target_include_directories(doa3_android_backends PRIVATE "${RUNTIME}" "${CMAKE_CURRENT_SOURCE_DIR}/native")
    target_link_libraries(doa3_android_backends PUBLIC SDL3 doa3_gl_shaders GLESv3 log android)
    set_target_properties(doa3_android_backends PROPERTIES POSITION_INDEPENDENT_CODE ON)
endif()

add_executable(doa3_portable_simd_test native/portable_simd_test.c)
target_include_directories(doa3_portable_simd_test PRIVATE "${RUNTIME}")
if(NOT MSVC)
    target_compile_options(doa3_portable_simd_test PRIVATE -frounding-math -ffp-contract=off)
    target_link_libraries(doa3_portable_simd_test PRIVATE m)
endif()

set(PORTABLE_RUNTIME_SOURCES
    runtime.c stop_report.c program_adapters.c program_forwards.c guest_heap.c doa3_collision_adapter.c native_fiber.c xapi_clock.cpp xapi_time_adapter.cpp program_manual.c save_adapter.c
    d3d_creation_model.c d3d_creation_adapter.c d3d_draw_model.c d3d_draw_adapter.c
    d3d_frame_model.c d3d_frame_adapter.c d3d_miniport_adapter.c d3d_presenter_capture.cpp d3d_vertex_program.cpp
    d3d_render_state_model.c d3d_render_state_adapter.c d3d_texture_model.c d3d_texture_adapter.c
    d3d_tile_model.c d3d_tile_adapter.c d3d_vertex_shader_model.c d3d_vertex_shader_adapter.c
    xbox_adpcm.c dsound_service_model.c dsound_service_adapter.c dsound_api_adapter.c dsound_hrtf.c cri_adxm_adapter.c soundtrack_adapter.c
    movie_color_model.c cri_service_model.c cri_service_adapter.c crt_format_model.c
    crt_format_adapter.c crt_string_adapter.c fiber_model.c directory_model.c input_model.c
    input_adapter.c input_pulse_source.c symbolic_link_model.c kernel_ordinals.c kernel_dispatch.c
    kernel_config.c kernel_debug.c device_model.c ohci_model.c apu_model.c kernel_device.c
    kernel_interrupt.c kernel_memory.c kernel_rtl.c kernel_crypto.c kernel_thread.c kernel_video.c
    save_transaction.cpp controller_settings.cpp)
list(TRANSFORM PORTABLE_RUNTIME_SOURCES PREPEND "${RUNTIME}/")
add_library(doa3_runtime_models STATIC ${PORTABLE_RUNTIME_SOURCES} native/sha_portable.cpp "${PORT_ROOT}/xbe/xbe_parser.cpp")
target_include_directories(doa3_runtime_models PRIVATE "${RUNTIME}" "${PORT_ROOT}/xbe" "${CMAKE_CURRENT_SOURCE_DIR}/native")
target_compile_definitions(doa3_runtime_models PRIVATE RECOMP_FULL_PROGRAM RECOMP_D3D_FRAME_ENABLED)
target_compile_options(doa3_runtime_models PRIVATE -fno-strict-aliasing -ffp-contract=off)
set_target_properties(doa3_runtime_models PROPERTIES POSITION_INDEPENDENT_CODE ON)
if(ANDROID)
    target_sources(doa3_runtime_models PRIVATE
        "${RUNTIME}/fiber_adapter.c" "${RUNTIME}/kernel_file.c"
        native/fiber_context.c native/fiber_context_arm64.S native/host_file_compat.c)
    target_include_directories(doa3_runtime_models PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/native")
    add_executable(doa3_fiber_context_test native/fiber_context_test.c native/fiber_context.c native/fiber_context_arm64.S)
    target_link_libraries(doa3_fiber_context_test PRIVATE m)
    add_executable(doa3_save_test "${RUNTIME}/save_transaction.cpp" "${RUNTIME}/save_transaction_test.cpp")
    add_executable(doa3_button_bindings_test native/button_bindings_test.cpp)
    add_executable(doa3_iso_disc_test native/iso_disc_test.c native/host_file_compat.c)
    if(SDL_ANDROID_ROOT)
        add_executable(doa3_live_display_test native/live_display_test.cpp)
        target_include_directories(doa3_live_display_test PRIVATE "${RUNTIME}" "${CMAKE_CURRENT_SOURCE_DIR}/native" "${SDL_ANDROID_ROOT}/prefab/modules/SDL3-Headers/include")
        target_compile_options(doa3_live_display_test PRIVATE -ffunction-sections -fdata-sections)
        target_link_options(doa3_live_display_test PRIVATE -Wl,--gc-sections)
        target_link_libraries(doa3_live_display_test PRIVATE EGL GLESv3)
    endif()
    target_include_directories(doa3_save_test PRIVATE "${RUNTIME}")
endif()
add_executable(doa3_sha_test native/sha_portable.cpp native/sha_portable_test.cpp)
target_include_directories(doa3_sha_test PRIVATE "${PORT_ROOT}/xbe")

set(GLSLANG_DIR "" CACHE PATH "Official glslang source, pinned version")
set(SPIRV_CROSS_DIR "" CACHE PATH "Official SPIRV-Cross source, pinned commit")
if(GLSLANG_DIR AND SPIRV_CROSS_DIR)
    set(CMAKE_POSITION_INDEPENDENT_CODE ON)
    set(BUILD_EXTERNAL OFF CACHE BOOL "" FORCE)
    set(ENABLE_OPT OFF CACHE BOOL "" FORCE)
    set(ENABLE_HLSL ON CACHE BOOL "" FORCE)
    set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "" FORCE)
    set(GLSLANG_TESTS OFF CACHE BOOL "" FORCE)
    set(SPIRV_CROSS_CLI OFF CACHE BOOL "" FORCE)
    set(SPIRV_CROSS_ENABLE_TESTS OFF CACHE BOOL "" FORCE)
    set(SPIRV_CROSS_ENABLE_HLSL OFF CACHE BOOL "" FORCE)
    set(SPIRV_CROSS_ENABLE_MSL OFF CACHE BOOL "" FORCE)
    set(SPIRV_CROSS_ENABLE_CPP OFF CACHE BOOL "" FORCE)
    set(SPIRV_CROSS_ENABLE_C_API OFF CACHE BOOL "" FORCE)
    set(SPIRV_CROSS_ENABLE_REFLECT OFF CACHE BOOL "" FORCE)
    set(SPIRV_CROSS_ENABLE_UTIL OFF CACHE BOOL "" FORCE)
    add_subdirectory("${GLSLANG_DIR}" glslang)
    add_subdirectory("${SPIRV_CROSS_DIR}" spirv-cross)
    add_library(doa3_gl_shaders STATIC native/gl_shader_source.cpp native/draw_shader_semantics.cpp "${RUNTIME}/d3d_vertex_program.cpp" "${RUNTIME}/d3d_draw_model.c")
    target_include_directories(doa3_gl_shaders PUBLIC "${RUNTIME}" "${CMAKE_CURRENT_SOURCE_DIR}/native")
    target_link_libraries(doa3_gl_shaders PUBLIC glslang glslang-default-resource-limits SPIRV spirv-cross-glsl spirv-cross-core)
    add_executable(doa3_shader_test native/shader_test.cpp)
    target_link_libraries(doa3_shader_test PRIVATE doa3_gl_shaders)
    if(ANDROID)
        add_executable(doa3_gles_test native/gles_test.cpp)
        target_link_libraries(doa3_gles_test PRIVATE doa3_gl_shaders EGL GLESv3)
    endif()
endif()

if(ANDROID AND SDL_ANDROID_ROOT AND GAME_DIR)
    add_library(main SHARED native/android_main.cpp "${RUNTIME}/runner.cpp")
    set_source_files_properties("${RUNTIME}/runner.cpp" PROPERTIES COMPILE_DEFINITIONS "main=recomp_game_main")
    target_compile_definitions(main PRIVATE RECOMP_FULL_PROGRAM RECOMP_D3D_FRAME_ENABLED RECOMP_GAME_XBE_SHA="${game_xbe_sha}")
    target_include_directories(main PRIVATE "${RUNTIME}" "${PORT_ROOT}/xbe" "${CMAKE_CURRENT_SOURCE_DIR}/native" "${PORT_ROOT}/private/localization")
    target_link_libraries(main PRIVATE "-Wl,--start-group" doa3_game_core doa3_runtime_models doa3_android_backends "-Wl,--end-group" SDL3 doa3_gl_shaders GLESv3 log android mediandk m)
    target_link_options(main PRIVATE "-Wl,-z,max-page-size=16384" "-Wl,--no-undefined")
endif()
