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

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

    ## Helper variables
    set(NON_INTERFACE_TARGETS)
    set(PROJECT_IS_INTERFACE_LIBRARY)
    set(PROJECT_IS_TOP_LEVEL)
    get_target_property(PROJECT_TYPE ${PROJECT_NAME} TYPE)
    if("${PROJECT_TYPE}" STREQUAL "INTERFACE_LIBRARY")
        set(PROJECT_IS_INTERFACE_LIBRARY TRUE)
    endif()
    if("${CMAKE_PROJECT_NAME}" STREQUAL "${PROJECT_NAME}")
        set(PROJECT_IS_TOP_LEVEL TRUE)
    endif()

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

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

    ## Fetch content
    foreach(FETCHCONTENT ${ARG_FETCHCONTENT})
        get_filename_component(FETCHCONTENT_NAME ${FETCHCONTENT} NAME)
        FetchContent_Declare(${FETCHCONTENT_NAME}
            GIT_REPOSITORY ${FETCHCONTENT}
        )
        FetchContent_MakeAvailable(${FETCHCONTENT_NAME})
    endforeach()

    ## Configure main target
    file(GLOB_RECURSE SRC CONFIGURE_DEPENDS
        src/*
    )
    file(GLOB_RECURSE INCLUDE CONFIGURE_DEPENDS
        include/*
    )
    if(NOT ("${SRC}" STREQUAL "" AND "${INCLUDE}" STREQUAL ""))
        target_sources(${PROJECT_NAME}
            PRIVATE
                ${SRC}
                ${INCLUDE}
        )
        set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY
            PUBLIC_HEADER ${INCLUDE}
        )
        if(PROJECT_IS_INTERFACE_LIBRARY)
            target_include_directories(${PROJECT_NAME}
                INTERFACE
                    "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
                    "$<INSTALL_INTERFACE:include>"
            )
        else()
            target_include_directories(${PROJECT_NAME}
                PUBLIC
                    "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
                    "$<INSTALL_INTERFACE:include>"
                PRIVATE
                    src
            )
            list(APPEND NON_INTERFACE_TARGETS ${PROJECT_NAME})
        endif()
        target_link_libraries(${PROJECT_NAME}
            PUBLIC
                ${ARG_DEPENDENCIES_PUBLIC}
            PRIVATE
                "$<BUILD_INTERFACE:${ARG_DEPENDENCIES_PRIVATE}>"
        )
        if(PROJECT_IS_TOP_LEVEL)
            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()

    ## Declare and configure test targets
    if(PROJECT_IS_TOP_LEVEL AND BUILD_TESTING)
        file(GLOB_RECURSE TESTS_COMMON CONFIGURE_DEPENDS
            tests/common/*
        )
        file(GLOB TESTS LIST_DIRECTORIES FALSE CONFIGURE_DEPENDS
            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 NON_INTERFACE_TARGETS ${TEST_TARGET})
        endforeach()
    endif()

    ## Declare and configure assets
    file(REMOVE_RECURSE
        ${CMAKE_BINARY_DIR}/assets
    )
    file(GLOB_RECURSE ASSETS RELATIVE "${PROJECT_SOURCE_DIR}" CONFIGURE_DEPENDS
        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)
        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}" MATCHES "/tests/")
            install(
                FILES
                    ${PROJECT_SOURCE_DIR}/${ASSET}
                DESTINATION
                    ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}/${ASSET_DIR}
            )
        endif()
    endforeach()

    ## Preprocessor defines
    set_property(TARGET ${NON_INTERFACE_TARGETS} APPEND PROPERTY
        COMPILE_DEFINITIONS ${ARG_DEFINES}
    )

    ## Language version
    if(NOT "${CXX_STANDARD}" STREQUAL "")
        set_target_properties(${NON_INTERFACE_TARGETS} PROPERTIES
            CXX_STANDARD          ${ARG_CXX_STANDARD}
            CXX_STANDARD_REQUIRED ON
            CXX_EXTENSIONS        OFF
        )
    endif()

    ## Build options
    if(MSVC)
        set(COMPILE_OPTIONS
            /permissive-
            /WX
            /W4
        )
    elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|.*Clang")
        if(NOT ARG_DISABLE_SANITIZERS)
            set(SANITIZE_OPTIONS
                $<$<CONFIG:Debug>:
                    -g -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(COMPILE_OPTIONS
            -pedantic
            -Werror # -Wfatal-errors
            -Wall -Wextra
            -Wconversion -Wsign-conversion
            -Wdouble-promotion
            -Wimplicit-fallthrough
            -Wvla
            # -Wzero-as-null-pointer-constant
            # -Wshadow
            -Weffc++
            ${SANITIZE_OPTIONS}
        )
        set(LINK_OPTIONS
            ${SANITIZE_OPTIONS}
        )
        check_ipo_supported(RESULT INTERPROCEDURAL_OPTIMIZATION)
    endif()
    set_property(TARGET ${NON_INTERFACE_TARGETS} APPEND PROPERTY
        COMPILE_OPTIONS "${COMPILE_OPTIONS}"
    )
    set_property(TARGET ${NON_INTERFACE_TARGETS} APPEND PROPERTY
        LINK_OPTIONS "${LINK_OPTIONS}"
    )
    set_property(TARGET ${NON_INTERFACE_TARGETS} APPEND PROPERTY
        INTERPROCEDURAL_OPTIMIZATION "${INTERPROCEDURAL_OPTIMIZATION}"
    )

    ## 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 ${NON_INTERFACE_TARGETS} APPEND PROPERTY
        ENVIRONMENT
            ASAN_OPTIONS=${ASAN_OPTIONS}
            LSAN_OPTIONS=${LSAN_OPTIONS}
            UBSAN_OPTIONS=${UBSAN_OPTIONS}
    )

    ## Tools
    if(PROJECT_IS_TOP_LEVEL)
        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}
                --enable=warning,style
                --error-exitcode=1
            )
        endif()
        if(CLANG_TIDY AND NOT ARG_DISABLE_CLANG_TIDY)
            set(CXX_CLANG_TIDY ${CLANG_TIDY})
        endif()
        if(INCLUDE_WHAT_YOU_USE AND NOT ARG_DISABLE_INCLUDE_WHAT_YOU_USE)
            set(CXX_INCLUDE_WHAT_YOU_USE ${INCLUDE_WHAT_YOU_USE})
        endif()
        set_target_properties(${NON_INTERFACE_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()