# https://git.rcrnstn.net/rcrnstn/cmake-common

## CMake modules
include(FetchContent)
include(CMakePackageConfigHelpers)
include(CheckIPOSupported)
include(GNUInstallDirs)
include(CTest)
if(NOT CPack_CMake_INCLUDED)
    include(CPack)
endif()

function(common)
    ## Arguments
    set(OPTIONS
        DISABLE_WSHADOW
        DISABLE_SANITIZERS
        DISABLE_CPPCHECK
        DISABLE_CLANG_TIDY
        DISABLE_INCLUDE_WHAT_YOU_USE
    )
    set(ONE_VALUE_ARGS
        CXX_STANDARD
    )
    set(MULTI_VALUE_ARGS
        PACKAGES
        EXTERNAL
        FETCHCONTENT
        DEPENDENCIES_PUBLIC
        DEPENDENCIES_PRIVATE
        DEPENDENCIES_TESTS
        DEFINITIONS
    )
    cmake_parse_arguments(PARSE_ARGV 0 ARG
        "${OPTIONS}"
        "${ONE_VALUE_ARGS}"
        "${MULTI_VALUE_ARGS}"
    )

    ## Variables
    set(PROJECT_IS_TOP_LEVEL)
    set(PROJECT_IS_INTERFACE_LIBRARY)
    set(PROJECT_IS_DEBUG)
    get_target_property(PROJECT_TYPE ${PROJECT_NAME} TYPE)
    string(TOLOWER "${CMAKE_BUILD_TYPE}" PROJECT_BUILD_TYPE)
    if("${CMAKE_PROJECT_NAME}" STREQUAL "${PROJECT_NAME}")
        set(PROJECT_IS_TOP_LEVEL TRUE)
    endif()
    if("${PROJECT_TYPE}" STREQUAL "INTERFACE_LIBRARY")
        set(PROJECT_IS_INTERFACE_LIBRARY TRUE)
    endif()
    if("${PROJECT_BUILD_TYPE}" STREQUAL "debug")
        set(PROJECT_IS_DEBUG TRUE)
    endif()

    ## Packages
    foreach(PACKAGE ${ARG_PACKAGES})
        find_package(${PACKAGE})
    endforeach()

    ## External
    foreach(EXTERNAL ${ARG_EXTERNAL})
        add_subdirectory(external/${EXTERNAL} EXCLUDE_FROM_ALL)
    endforeach()

    ## FetchContent
    foreach(FETCHCONTENT ${ARG_FETCHCONTENT})
        get_filename_component(FETCHCONTENT_NAME ${FETCHCONTENT} NAME)
        FetchContent_Declare(${FETCHCONTENT_NAME}
            GIT_REPOSITORY ${FETCHCONTENT}
            GIT_SHALLOW TRUE
            GIT_PROGRESS TRUE
        )
        FetchContent_MakeAvailable(${FETCHCONTENT_NAME})
    endforeach()

    ## Main target
    target_compile_definitions(${PROJECT_NAME} PUBLIC ${ARG_DEFINITIONS})
    file(GLOB_RECURSE SRC CONFIGURE_DEPENDS
        RELATIVE "${PROJECT_SOURCE_DIR}"
        src/*
    )
    file(GLOB_RECURSE INCLUDE CONFIGURE_DEPENDS
        RELATIVE "${PROJECT_SOURCE_DIR}"
        include/*
    )
    if(NOT ("${SRC}" STREQUAL "" AND "${INCLUDE}" STREQUAL ""))
        if(PROJECT_IS_INTERFACE_LIBRARY)
            target_include_directories(${PROJECT_NAME}
                INTERFACE
                    "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
                    "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
            )
        else()
            target_sources(${PROJECT_NAME}
                PRIVATE
                    ${SRC}
                    ${INCLUDE}
            )
            target_include_directories(${PROJECT_NAME}
                PUBLIC
                    "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
                    "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
                PRIVATE
                    src
            )
            target_link_libraries(${PROJECT_NAME}
                PUBLIC
                    ${ARG_DEPENDENCIES_PUBLIC}
                PRIVATE
                    "$<BUILD_INTERFACE:${ARG_DEPENDENCIES_PRIVATE}>"
            )
        endif()
        if(PROJECT_IS_TOP_LEVEL)
            install(
                DIRECTORY
                    include/
                DESTINATION
                    ${CMAKE_INSTALL_INCLUDEDIR}
            )
            install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME})
            install(EXPORT ${PROJECT_NAME}
                FILE ${PROJECT_NAME}Config.cmake
                DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
            )
            write_basic_package_version_file(${PROJECT_NAME}ConfigVersion.cmake
                COMPATIBILITY SameMajorVersion
            )
            install(
                FILES ${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
                DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
            )
        endif()
    endif()

    ## Test targets
    set(TEST_TARGETS)
    if(PROJECT_IS_TOP_LEVEL AND BUILD_TESTING)
        file(GLOB_RECURSE TESTS_COMMON CONFIGURE_DEPENDS
            tests/common/*
        )
        file(GLOB TESTS CONFIGURE_DEPENDS
            LIST_DIRECTORIES FALSE
            tests/*
        )
        foreach(TEST ${TESTS})
            get_filename_component(TEST_NAME "${TEST}" NAME_WE)
            set(TEST_TARGET "${PROJECT_NAME}-test-${TEST_NAME}")
            add_executable(${TEST_TARGET}
                ${TEST}
                ${TESTS_COMMON}
            )
            target_include_directories(${TEST_TARGET}
                PRIVATE
                    tests
            )
            target_link_libraries(${TEST_TARGET}
                PRIVATE
                    ${PROJECT_NAME}
                    ${ARG_DEPENDENCIES_TESTS}
            )
            add_test(
                NAME    ${TEST_TARGET}
                COMMAND ${TEST_TARGET}
            )
            list(APPEND TEST_TARGETS ${TEST_TARGET})
        endforeach()
    endif()

    ## Build targets
    set(BUILD_TARGETS ${TEST_TARGETS})
    if(NOT PROJECT_IS_INTERFACE_LIBRARY)
        list(APPEND BUILD_TARGETS ${PROJECT_NAME})
    endif()

    ## Assets
    file(REMOVE_RECURSE
        ${CMAKE_BINARY_DIR}/assets
    )
    file(GLOB_RECURSE ASSETS CONFIGURE_DEPENDS
        RELATIVE "${PROJECT_SOURCE_DIR}"
        assets/*
    )
    if(NOT "${ASSETS}" STREQUAL "")
        add_custom_target(${PROJECT_NAME}-assets)
        add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-assets)
    endif()
    foreach(ASSET ${ASSETS})
        get_filename_component(ASSET_DIR ${ASSET} DIRECTORY)
        if("${ASSET}" MATCHES "/tests/")
            set(ASSET_TEST TRUE)
        else()
            set(ASSET_TEST FALSE)
        endif()
        if(ASSET_TEST AND NOT PROJECT_IS_TOP_LEVEL)
            continue()
        endif()
        add_custom_command(
            DEPENDS
                ${PROJECT_SOURCE_DIR}/${ASSET}
            OUTPUT
                ${CMAKE_BINARY_DIR}/${ASSET}
            COMMAND
                ${CMAKE_COMMAND} -E make_directory
                    ${CMAKE_BINARY_DIR}/${ASSET_DIR}
            COMMAND
                ${CMAKE_COMMAND} -E create_symlink
                    ${PROJECT_SOURCE_DIR}/${ASSET}
                    ${CMAKE_BINARY_DIR}/${ASSET}
            VERBATIM
        )
        set_property(TARGET ${PROJECT_NAME}-assets APPEND PROPERTY
            SOURCES ${CMAKE_BINARY_DIR}/${ASSET}
        )
        if(NOT ASSET_TEST)
            install(
                FILES
                    ${PROJECT_SOURCE_DIR}/${ASSET}
                DESTINATION
                    ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}/${ASSET_DIR}
            )
        endif()
    endforeach()

    ## Documentation
    if(PROJECT_IS_TOP_LEVEL)
        file(GLOB_RECURSE DOCS CONFIGURE_DEPENDS
            RELATIVE "${PROJECT_SOURCE_DIR}"
            doc/*
        )
        file(GLOB DOCS_ROOT CONFIGURE_DEPENDS
            RELATIVE "${PROJECT_SOURCE_DIR}"
            README*
            LICENSE*
            COPYING*
            CHANGELOG*
            CHANGES*
            HISTORY*
            NEWS*
            RELEASES*
            AUTHORS*
            ACKNOWLEDGMENTS*
            CONTRIBUTORS*
            CONTRIBUTING*
            CODE_OF_CONDUCT*
            SECURITY*
            SUPPORT*
        )
        list(APPEND DOCS ${DOCS_ROOT})
        foreach(DOC ${DOCS})
            get_filename_component(DOC_DIR ${DOC} DIRECTORY)
            install(
                FILES
                    ${PROJECT_SOURCE_DIR}/${DOC}
                DESTINATION
                    ${CMAKE_INSTALL_DOCDIR}/${DOC_DIR}
            )
        endforeach()
    endif()

    ## Man pages
    if(PROJECT_IS_TOP_LEVEL)
        file(GLOB_RECURSE MANS CONFIGURE_DEPENDS
            RELATIVE "${PROJECT_SOURCE_DIR}"
            man/*
        )
        foreach(MAN ${MANS})
            get_filename_component(MAN_DIR ${MAN} DIRECTORY)
            install(
                FILES
                    ${PROJECT_SOURCE_DIR}/${MAN}
                DESTINATION
                    ${CMAKE_INSTALL_MANDIR}/${MAN_DIR}
            )
        endforeach()
    endif()

    ## Language version
    set_target_properties(${BUILD_TARGETS} PROPERTIES
        CXX_STANDARD          "${ARG_CXX_STANDARD}"
        CXX_STANDARD_REQUIRED ON
        CXX_EXTENSIONS        OFF
    )

    ## Build options
    if(PROJECT_IS_TOP_LEVEL)
        if(MSVC)
            set(CPLUSPLUS_OPTION)
            if(MSVC_VERSION GREATER_EQUAL 1914)
                set(CPLUSPLUS_OPTION /Zc:__cplusplus)
            endif()
            set(COMPILE_OPTIONS
                ${CPLUSPLUS_OPTION}
                /permissive-
                /WX
                /W4
            )
        elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|.*Clang")
            set(SANITIZE_OPTIONS)
            if(PROJECT_IS_DEBUG AND NOT ARG_DISABLE_SANITIZERS)
                set(SANITIZE_OPTIONS
                    $<$<CONFIG:Debug>:
                        -fno-omit-frame-pointer
                        -fsanitize=address,leak,undefined
                        -fsanitize=float-divide-by-zero
                        -fsanitize=float-cast-overflow
                        # -fsanitize=pointer-compare
                        # -fsanitize=pointer-subtract
                        # -fsanitize=implicit-conversion
                        -fno-sanitize-recover=all
                    >
                )
            endif()
            set(WSHADOW_OPTION)
            if(NOT ARG_DISABLE_WSHADOW)
                set(WSHADOW_OPTION -Wshadow)
            endif()
            set(COMPILE_OPTIONS
                -pedantic
                -Werror -Wfatal-errors
                -Wall -Wextra
                -Wno-missing-braces -Wmissing-field-initializers
                -Wconversion -Wsign-conversion
                -Wdouble-promotion
                -Wimplicit-fallthrough
                -Wvla
                -Wzero-as-null-pointer-constant
                -Weffc++
                ${WSHADOW_OPTION}
                ${SANITIZE_OPTIONS}
            )
            set(LINK_OPTIONS
                ${SANITIZE_OPTIONS}
            )
            check_ipo_supported(RESULT INTERPROCEDURAL_OPTIMIZATION)
        endif()
        set_property(TARGET ${BUILD_TARGETS} APPEND PROPERTY
            COMPILE_OPTIONS "${COMPILE_OPTIONS}"
        )
        set_property(TARGET ${BUILD_TARGETS} APPEND PROPERTY
            LINK_OPTIONS "${LINK_OPTIONS}"
        )
        set_property(TARGET ${BUILD_TARGETS} APPEND PROPERTY
            INTERPROCEDURAL_OPTIMIZATION "${INTERPROCEDURAL_OPTIMIZATION}"
        )
    endif()

    ## Sanitizer environment variables
    set(SAN_STRIP_PATH_PREFIX strip_path_prefix=${PROJECT_BINARY_DIR})
    set(ASAN_OPTIONS  "${SAN_STRIP_PATH_PREFIX}")
    set(LSAN_OPTIONS  "${SAN_STRIP_PATH_PREFIX}")
    set(UBSAN_OPTIONS "${SAN_STRIP_PATH_PREFIX}")
    file(GLOB ASAN_SUPP CONFIGURE_DEPENDS
        .asan.supp
    )
    file(GLOB LSAN_SUPP CONFIGURE_DEPENDS
        .lsan.supp
    )
    file(GLOB UBSAN_SUPP CONFIGURE_DEPENDS
        .ubsan.supp
    )
    if(NOT "${ASAN_SUPP}" STREQUAL "")
        set(ASAN_OPTIONS "${ASAN_OPTIONS},suppressions=${ASAN_SUPP}")
    endif()
    if(NOT "${LSAN_SUPP}" STREQUAL "")
        set(LSAN_OPTIONS "${LSAN_OPTIONS},suppressions=${LSAN_SUPP}")
    endif()
    if(NOT "${UBSAN_SUPP}" STREQUAL "")
        set(UBSAN_OPTIONS "${UBSAN_OPTIONS},suppressions=${UBSAN_SUPP}")
    endif()
    # set(ASAN_OPTIONS "${ASAN_OPTIONS},detect_leaks=1")
    # set(UBSAN_OPTIONS "${UBSAN_OPTIONS},print_stacktrace=1")
    set_property(TEST ${TEST_TARGETS} APPEND PROPERTY
        ENVIRONMENT
            ASAN_OPTIONS=${ASAN_OPTIONS}
            LSAN_OPTIONS=${LSAN_OPTIONS}
            UBSAN_OPTIONS=${UBSAN_OPTIONS}
    )

    ## Tools
    if(PROJECT_IS_TOP_LEVEL AND PROJECT_IS_DEBUG)
        find_program(CPPCHECK             cppcheck)
        find_program(CLANG_TIDY           clang-tidy)
        find_program(INCLUDE_WHAT_YOU_USE include-what-you-use)
        if(CPPCHECK AND NOT ARG_DISABLE_CPPCHECK)
            set(CXX_CPPCHECK ${CPPCHECK}
                --error-exitcode=1
                --enable=warning,style
                --inline-suppr
                --template "{file}:{line}: ({severity} {id}) {message}"
            )
        endif()
        if(CLANG_TIDY AND NOT ARG_DISABLE_CLANG_TIDY)
            set(CXX_CLANG_TIDY ${CLANG_TIDY}
                --extra-arg=-Wno-unknown-warning-option
                --extra-arg=-Wno-ignored-optimization-argument
            )
        endif()
        if(INCLUDE_WHAT_YOU_USE AND NOT ARG_DISABLE_INCLUDE_WHAT_YOU_USE)
            set(CXX_INCLUDE_WHAT_YOU_USE ${INCLUDE_WHAT_YOU_USE}
                -Wno-unknown-warning-option
                -Wno-ignored-optimization-argument
            )
        endif()
        set_target_properties(${BUILD_TARGETS} PROPERTIES
            EXPORT_COMPILE_COMMANDS  ON
            CXX_CPPCHECK             "${CXX_CPPCHECK}"
            CXX_CLANG_TIDY           "${CXX_CLANG_TIDY}"
            CXX_INCLUDE_WHAT_YOU_USE "${CXX_INCLUDE_WHAT_YOU_USE}"
        )
    endif()
endfunction()