cmake_minimum_required(VERSION 3.21)

project(townengine LANGUAGES C)

set(CMAKE_MESSAGE_LOG_LEVEL "WARNING")
set(CMAKE_INSTALL_MESSAGE NEVER)

# SDL dependencies
# for whatever reason Emscripten has SDL2 config, but not actual SDL2 port by default
if(NOT EMSCRIPTEN)
        find_package(SDL2 REQUIRED GLOBAL)
endif()

# CMake actually has no default configuration and it's toolchain file dependent, set debug if not specified
if(NOT DEFINED CMAKE_BUILD_TYPE)
        set(CMAKE_BUILD_TYPE Debug)
endif()

if(NOT DEFINED TWN_SANITIZE AND CMAKE_BUILD_TYPE MATCHES Debug)
        set(TWN_SANITIZE ON)
endif()

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

set(TWN_TARGET townengine CACHE INTERNAL "")
set(TWN_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "")

# feature configuration, set them with -DFEATURE=ON/OFF in cli
option(TWN_FEATURE_DYNLIB_GAME  "Enable dynamic library loading support" ON)
option(TWN_FEATURE_PUSH_AUDIO  "Enable frame based audio push for easy realtime audio" ON)
option(TWN_USE_AMALGAM  "Enable use of twn_amalgam.c as a single compilation unit" ON)

# todo: figure out how to compile for dynamic linking instead
if(EMSCRIPTEN)
        if(TWN_FEATURE_DYNLIB_GAME)
                message(WARNING "TWN_FEATURE_DYNLIB_GAME is set, but not supported - it is turned off")
                set(TWN_FEATURE_DYNLIB_GAME OFF CACHE INTERNAL "")
        endif()
endif()

if(HAIKU)
        if(TWN_SANITIZE)
                message(WARNING "TWN_SANITIZE is set, but not supported - it is turned off")
                set(TWN_SANITIZE OFF CACHE INTERNAL "")
        endif()
endif()

# add -fPIC globally so that it's linked well
add_compile_options($<$<BOOL:${TWN_FEATURE_DYNLIB_GAME}>:-fPIC>
                    -fvisibility=hidden)

set(PHYSFS_BUILD_SHARED FALSE CACHE INTERNAL "")
set(PHYSFS_DISABLE_INSTALL TRUE CACHE INTERNAL "")
set(PHYSFS_TARGETNAME_UNINSTALL "physfs_uninstall" CACHE INTERNAL "")
set(PHYSFS_ARCHIVE_GRP OFF CACHE BOOL "")
set(PHYSFS_ARCHIVE_WAD OFF CACHE INTERNAL "")
set(PHYSFS_ARCHIVE_HOG OFF CACHE INTERNAL "")
set(PHYSFS_ARCHIVE_MVL OFF CACHE INTERNAL "")
set(PHYSFS_ARCHIVE_QPAK OFF CACHE INTERNAL "")
set(PHYSFS_ARCHIVE_SLB OFF CACHE INTERNAL "")
set(PHYSFS_ARCHIVE_ISO9660 OFF CACHE INTERNAL "")
set(PHYSFS_ARCHIVE_VDF OFF CACHE INTERNAL "")
set(PHYSFS_ARCHIVE_7Z OFF CACHE INTERNAL "")
add_subdirectory(third-party/physfs ${CMAKE_CURRENT_BINARY_DIR}/third-party/physfs SYSTEM)
add_subdirectory(third-party/libxm ${CMAKE_CURRENT_BINARY_DIR}/third-party/libxm SYSTEM)


if(LINUX)
        set(SYSTEM_SOURCE_FILES
                src/system/linux/twn_elf.c
                $<$<BOOL:${TWN_FEATURE_DYNLIB_GAME}>:src/game_object/twn_linux_game_object.c>)
elseif(WIN32)
        set(SYSTEM_SOURCE_FILES
                $<$<BOOL:${TWN_FEATURE_DYNLIB_GAME}>:src/game_object/twn_win32_game_object.c>)
else()
        set(SYSTEM_SOURCE_FILES)
endif()

if(EMSCRIPTEN)
        set(TWN_RENDERING_API WEBGL1)
else()
        set(TWN_RENDERING_API OPENGL_15)
endif()

if(TWN_RENDERING_API MATCHES OPENGL_15)
        set(SYSTEM_SOURCE_FILES ${SYSTEM_SOURCE_FILES}
                src/rendering/twn_gl_any_rendering.c
                src/rendering/twn_gl_15_rendering.c
                src/rendering/twn_gl_15_gpu_texture.c)
endif()

set(TWN_THIRD_PARTY_SOURCE_FILES
        third-party/physfs/extras/physfsrwops.c
        third-party/stb/stb_vorbis.c
        third-party/tomlc99/toml.c
        $<$<NOT:$<BOOL:${EMSCRIPTEN}>>:third-party/glad/src/glad.c>)

set(TWN_NONOPT_SOURCE_FILES
        src/twn_stb.c
        src/twn_loop.c
        src/twn_main.c
        src/twn_context.c include/twn_context.h
        src/twn_audio.c include/twn_audio.h
        src/twn_util.c include/twn_util.h
        src/twn_input.c include/twn_input.h
        src/twn_camera.c src/twn_camera_c.h
        src/twn_textures.c src/twn_textures_c.h

        src/rendering/twn_draw.c src/rendering/twn_draw_c.h
        src/rendering/twn_quads.c
        src/rendering/twn_sprites.c
        src/rendering/twn_rects.c
        src/rendering/twn_text.c
        src/rendering/twn_triangles.c
        src/rendering/twn_billboards.c
        src/rendering/twn_circles.c
        src/rendering/twn_skybox.c)

set(TWN_SOURCE_FILES
        $<IF:$<BOOL:${TWN_USE_AMALGAM}>,src/twn_amalgam.c src/twn_stb.c,${TWN_NONOPT_SOURCE_FILES}>

        # for dynamic load based solution main is compiled in a separate target
        $<$<NOT:$<BOOL:${TWN_FEATURE_DYNLIB_GAME}>>:src/twn_main.c
                                                    src/game_object/twn_static_game_object.c>

        ${SYSTEM_SOURCE_FILES})

list(TRANSFORM TWN_SOURCE_FILES PREPEND ${TWN_ROOT_DIR}/)
source_group(TREE ${TWN_ROOT_DIR} FILES ${TWN_NONOPT_SOURCE_FILES})

add_library(twn_third_parties STATIC ${TWN_THIRD_PARTY_SOURCE_FILES})

# base engine code, reused for games and tools
if(TWN_FEATURE_DYNLIB_GAME)
        add_library(${TWN_TARGET} SHARED ${TWN_SOURCE_FILES} ${twn_third_parties})
else()
        add_library(${TWN_TARGET} STATIC ${TWN_SOURCE_FILES} ${twn_third_parties})
endif()

set_target_properties(${TWN_TARGET} PROPERTIES
                                    C_STANDARD 11
                                    C_STANDARD_REQUIRED ON
                                    C_EXTENSIONS ON) # extensions are required by stb_ds.h

add_compile_definitions(${TWN_TARGET} $<$<BOOL:${TWN_FEATURE_PUSH_AUDIO}>:TWN_FEATURE_PUSH_AUDIO>)

# precompile commonly used not-so-small headers
target_precompile_headers(${TWN_TARGET} PRIVATE
        $<$<NOT:$<BOOL:${EMSCRIPTEN}>>:third-party/glad/include/glad/glad.h>
        ${SDL2_INCLUDE_DIR}/SDL.h
        third-party/physfs/src/physfs.h)


function(give_options_without_warnings target)
        set(BUILD_FLAGS
                # these SHOULDN'T break anything...
                -fno-math-errno
                -ffp-contract=fast
                -fno-signed-zeros
                -fno-trapping-math
                -freciprocal-math
                $<$<BOOL:${EMSCRIPTEN}>:-sUSE_SDL=2>)

        set(BUILD_FLAGS_RELEASE
                -O3
                -flto=$<IF:$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>,thin,auto>
                -mavx -mavx2
                -fdata-sections
                -ffunction-sections
                -funroll-loops
                -fomit-frame-pointer
                $<$<STREQUAL:${CMAKE_C_COMPILER_ID},Gnu>:-s>)

        set(BUILD_FLAGS_DEBUG
                -O0
                -g3
                -gdwarf
                -fno-omit-frame-pointer
		$<$<BOOL:${TWN_SANITIZE}>:-fstack-protector-all -fsanitize=undefined -fsanitize=address>
                $<$<BOOL:${EMSCRIPTEN}>:-gsource-map>)

        if (CMAKE_C_COMPILER_LINKER_ID MATCHES GNU OR CMAKE_C_COMPILER_LINKER_ID MATCHES GNUgold)
                set(THINLTO_USAGE "-plugin-opt,")
        endif()
        if (CMAKE_C_COMPILER_LINKER_ID MATCHES LLD)
                set(THINLTO_USAGE "--thinlto-")
        endif()

        if (THINLTO_USAGE)
                set(BUILD_SHARED_LIBRARY_FLAGS_RELEASE
                        $<$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>:-Wl,${THINLTO_USAGE}cache-dir=${CMAKE_CURRENT_BINARY_DIR}/linker-cache/>
                        $<$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>:-Wl,${THINLTO_USAGE}cache-policy=prune_after=30m>)
        endif()

        target_compile_options(${target} PUBLIC
                                         ${BUILD_FLAGS}
                                         $<$<CONFIG:Release>:${BUILD_FLAGS_RELEASE}>
                                         $<$<CONFIG:Debug>:${BUILD_FLAGS_DEBUG}>)

        target_link_options(${target} PUBLIC
                                      ${BUILD_FLAGS}
                                      # -Wl,--no-undefined # TODO: use later for implementing no-libc
                                      $<$<CONFIG:Release>:${BUILD_FLAGS_RELEASE}>
                                      $<$<CONFIG:Debug>:${BUILD_FLAGS_DEBUG}>
                                      -Bsymbolic-functions
                                      $<$<BOOL:${LINUX}>:-Wl,--hash-style=gnu>)

        get_target_property(target_type ${target} TYPE)
        if (target_type MATCHES SHARED_LIBRARY)
                target_compile_options(${target} PUBLIC
                                                 $<$<CONFIG:Release>:${BUILD_SHARED_LIBRARY_FLAGS_RELEASE}>)

                target_link_options(${target} PUBLIC
                                              $<$<CONFIG:Release>:${BUILD_SHARED_LIBRARY_FLAGS_RELEASE}>)
        endif()

        target_compile_definitions(${target} PRIVATE
                                             $<$<BOOL:${TWN_FEATURE_DYNLIB_GAME}>:TWN_FEATURE_DYNLIB_GAME>
                                             $<$<BOOL:${LINUX}>:_GNU_SOURCE>)
endfunction()


function(give_options target)
        give_options_without_warnings(${target})

        set(WARNING_FLAGS_CLANG
                -Weverything
                -Wno-padded
                -Wno-declaration-after-statement
                -Wno-unsafe-buffer-usage
                -Wno-unused-command-line-argument
                -Wno-covered-switch-default)

        set(WARNING_FLAGS
                -Wall
                -Wextra
                -Wpedantic
                -Wshadow
                -Wdouble-promotion
                -Wconversion -Wno-sign-conversion
                -Werror=vla
                -Wno-missing-field-initializers
                -Wunused-result
                $<$<STREQUAL:${CMAKE_C_COMPILER_ID},Gnu>:-Wcast-align=strict>)

        target_compile_options(${target} PRIVATE
                                         ${WARNING_FLAGS}
                                         $<$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>:${WARNING_FLAGS_CLANG}>)
endfunction()


function(include_deps target)
        # header-only libraries should be marked as "system includes"
        # to suppress compiler warnings in their code (it's not my problem after all)
        set(THIRD_PARTY_INCLUDES
                third-party/physfs/src
                third-party/physfs/extras
                third-party/libxm/include
                third-party/stb
                third-party/x-watcher
                third-party/tomlc99
                $<$<NOT:$<BOOL:${EMSCRIPTEN}>>:third-party/glad/include>)

        list(TRANSFORM THIRD_PARTY_INCLUDES PREPEND ${TWN_ROOT_DIR}/)
        target_include_directories(${target} SYSTEM PRIVATE ${THIRD_PARTY_INCLUDES})

        # allow access to headers from any point in source tree
        target_include_directories(${target} PRIVATE ${TWN_ROOT_DIR} ${TWN_ROOT_DIR}/include)
endfunction()


function(link_deps target)
        target_link_libraries(${target} PUBLIC
                                        $<$<NOT:$<BOOL:${EMSCRIPTEN}>>:SDL2::SDL2>
                                        physfs-static
                                        xms)
	target_include_directories(${target} PUBLIC ${SDL2_INCLUDE_DIRS})
endfunction()


function(use_townengine target sources output_directory)
        if(TWN_FEATURE_DYNLIB_GAME)
                # game shared library, for reloading
                add_library(${target}_game SHARED ${sources})
                give_options(${target}_game)
                include_deps(${target}_game)
                target_link_libraries(${target}_game PUBLIC $<$<NOT:$<BOOL:${EMSCRIPTEN}>>:SDL2::SDL2> ${TWN_TARGET})
                set_target_properties(${target}_game PROPERTIES
                                                     OUTPUT_NAME game
                                                     LIBRARY_OUTPUT_DIRECTORY ${output_directory}
                                                     RUNTIME_OUTPUT_DIRECTORY ${output_directory})

                # launcher binary, loads game and engine shared library
                add_executable(${target} ${TWN_ROOT_DIR}/src/twn_main.c)

                target_link_options(${target} PRIVATE $<$<BOOL:${LINUX}>:-Wl,-rpath,$ORIGIN/>)

                # todo: copy instead?
                # put libtownengine.so alongside the binary
                set_target_properties(${TWN_TARGET} PROPERTIES
                                                    LIBRARY_OUTPUT_DIRECTORY ${output_directory})
        else()
                # build and link all statically
                add_executable(${target} ${sources})
        endif()

        set_target_properties(${target} PROPERTIES
                                        LIBRARY_OUTPUT_DIRECTORY ${output_directory}
                                        RUNTIME_OUTPUT_DIRECTORY ${output_directory})

        if(EMSCRIPTEN)
                set_target_properties(${target} PROPERTIES SUFFIX .html)
        endif()

        target_compile_options(${target} PRIVATE
                                         $<$<BOOL:${EMSCRIPTEN}>:--shell-file ${TWN_ROOT_DIR}/shell_minimal.html>)

        # system libraries
        find_library(MATH_LIBRARY m)
        if (MATH_LIBRARY)
                target_link_libraries(${target} PUBLIC ${MATH_LIBRARY})
        endif()

        give_options(${target})
        include_deps(${target})
        link_deps(${target})

        target_link_libraries(${target} PUBLIC ${TWN_TARGET})
        if(WIN32)
                # copy dlls for baby windows
                add_custom_command(TARGET ${target} POST_BUILD
                                   COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:${target}>
                                                                                 $<TARGET_FILE_DIR:${target}>
                                   COMMAND_EXPAND_LISTS)
        endif()
endfunction()

give_options_without_warnings(twn_third_parties)
include_deps(twn_third_parties)
link_deps(twn_third_parties)

give_options(${TWN_TARGET})
include_deps(${TWN_TARGET})
link_deps(${TWN_TARGET})
target_link_libraries(${TWN_TARGET} PUBLIC twn_third_parties)
target_include_directories(${TWN_TARGET} PRIVATE ${TWN_ROOT_DIR}/src)