enable_testing()

find_program(CPPCHECK NAMES cppcheck)
if(CPPCHECK)
    message(STATUS "which cppcheck: ${CPPCHECK}")
endif()

find_program(CLANG_TIDY NAMES clang-tidy)
if(CLANG_TIDY)
    message(STATUS "which clang-tidy: ${CLANG_TIDY}")
endif()

find_program(CLANG_FORMAT NAMES clang-format)
if(CLANG_FORMAT)
    message(STATUS "which clang-format: ${CLANG_FORMAT}")
endif()

include(CheckCXXCompilerFlag)
function(target_weak_compile_options _target _visibility _flag)
    set(options)
    set(one_value_args)
    set(multi_value_args CONFIGS)
    cmake_parse_arguments(ARGS "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN})

    set(result "has${_flag}")
    string(REGEX REPLACE "[ ]" "" result "${result}")
    string(REGEX REPLACE "[+]" "x" result "${result}")
    string(REGEX REPLACE "[^a-zA-Z0-9_]" "_" result "${result}")

    set(CMAKE_REQUIRED_LIBRARIES ${_flag})
    check_cxx_compiler_flag(${_flag} ${result})
    if(${result})
        if(NOT ARGS_CONFIGS)
            target_compile_options(${_target} ${_visibility} ${_flag})
            target_link_libraries(${_target} ${_visibility} ${_flag})
        else()
            foreach(config ${ARGS_CONFIGS})
                target_compile_options(${_target} ${_visibility} "$<$<CONFIG:${config}>:${_flag}>")
                target_link_libraries(${_target} ${_visibility} "$<$<CONFIG:${config}>:${_flag}>")
            endforeach()
        endif()
    endif()
endfunction()

function(test _target)

    get_target_property(target_type ${_target} TYPE)

    if(target_type STREQUAL "EXECUTABLE"
    OR target_type STREQUAL "STATIC_LIBRARY"
    OR target_type STREQUAL "SHARED_LIBRARY")
        target_weak_compile_options(${_target} PRIVATE -W)
        target_weak_compile_options(${_target} PRIVATE -Wextra)
        target_weak_compile_options(${_target} PRIVATE -Wpedantic)

        if(NOT MSVC)
            target_weak_compile_options(${_target} PRIVATE -Wall)
        endif()

        if(CLANG_TIDY AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
            set_target_properties(${_target} PROPERTIES
                CXX_CLANG_TIDY "${CLANG_TIDY};-checks=-clang-diagnostic-unused-command-line-argument"
            )
        endif()

        if(CPPCHECK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
            set_target_properties(${_target} PROPERTIES
                CXX_CPPCHECK "${CPPCHECK};--enable=warning,performance,portability;--template=gcc"
            )
        endif()
    endif()

    set(driver ${_target})
    if(target_type STREQUAL "EXECUTABLE")
        target_weak_compile_options(${_target} PRIVATE -ftest-coverage CONFIGS DEBUG)
        target_weak_compile_options(${_target} PRIVATE -fprofile-arcs CONFIGS DEBUG)
        target_weak_compile_options(${_target} PRIVATE -fprofile-instr-generate CONFIGS DEBUG)
        target_weak_compile_options(${_target} PRIVATE -fcoverage-mapping CONFIGS DEBUG)

        add_custom_target(${_target}.run
            COMMAND ${_target}
            WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
        )
        add_dependencies(${_target}.run ${_target})
        set(driver ${_target}.run)
    endif()

    add_test(NAME ${_target}
        COMMAND ${CMAKE_COMMAND} --build . --config $<CONFIG> --target ${driver}
        WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
    )
endfunction()

function(get_tree_nodes _prefix _nodes)
    set(nodes)
    file(GLOB files ${_prefix}/*)
    list(SORT files)
    foreach(file ${files})
        get_filename_component(node ${file} NAME_WE)
        list(APPEND nodes ${node})
    endforeach()
    list(REMOVE_DUPLICATES nodes)
    set(${_nodes} ${nodes} PARENT_SCOPE)
endfunction()

function(test_units _root _lib _prefix)
    set(options)
    set(one_value_args)
    set(multi_value_args EXCLUDE)
    cmake_parse_arguments(ARGS "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN})

    if(NOT ARGS_EXCLUDE)
        set(ARGS_EXCLUDE "^$")
    else()
        string(REPLACE ";" "|" ARGS_EXCLUDE "${ARGS_EXCLUDE}")
    endif()

    if(NOT IS_ABSOLUTE ${_prefix})
        set(_prefix "${CMAKE_CURRENT_SOURCE_DIR}/${_prefix}")
    endif()

    if(NOT IS_DIRECTORY ${_prefix} OR NOT EXISTS ${_prefix})
        message(FATAL_ERROR "'${_prefix}' is not a valid directory.")
    endif()

    if(NOT TARGET ${_root})
        add_custom_target(${_root})
    endif()

    get_tree_nodes(${_prefix} nodes)
    foreach(node ${nodes})
        if(node MATCHES ${ARGS_EXCLUDE})
            continue()
        endif()

        set(target ${_root}.${node})
        set(node "${_prefix}/${node}")
        if(EXISTS "${node}.cpp")
            add_executable(${target} EXCLUDE_FROM_ALL "${node}.cpp")
            target_link_libraries(${target} PRIVATE ${_lib})
        endif()

        if(IS_DIRECTORY ${node})
            test_units(${target} ${_lib} ${node} ${ARGN})
        endif()

        if(TARGET ${target})
            add_dependencies(${target} ${_lib})
            add_dependencies(${_root} ${target})
            test(${target})
        endif()
    endforeach()
endfunction()

function(test_headers _root _lib _prefix)
    set(options)
    set(one_value_args)
    set(multi_value_args EXCLUDE)
    cmake_parse_arguments(ARGS "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN})

    if(NOT ARGS_EXCLUDE)
        set(ARGS_EXCLUDE "^$")
    else()
        string(REPLACE ";" "|" ARGS_EXCLUDE "${ARGS_EXCLUDE}")
    endif()

    if(NOT IS_ABSOLUTE ${_prefix})
        set(_prefix "${CMAKE_CURRENT_SOURCE_DIR}/${_prefix}")
    endif()

    if(NOT IS_DIRECTORY ${_prefix} OR NOT EXISTS ${_prefix})
        message(FATAL_ERROR "'${_prefix}' is not a valid directory.")
    endif()

    if(NOT TARGET ${_root})
        add_custom_target(${_root})
    endif()

    get_tree_nodes(${_prefix} nodes)
    foreach(node ${nodes})
        if(node MATCHES ${ARGS_EXCLUDE})
            continue()
        endif()

        set(target ${_root}.${node})
        set(node "${_prefix}/${node}")
        if(EXISTS "${node}.hpp")
            set(src "${CMAKE_CURRENT_BINARY_DIR}/${target}.cpp")
            file(WRITE ${src} "#include \"${node}.hpp\"")
            add_library(${target} STATIC EXCLUDE_FROM_ALL ${src})
            target_link_libraries(${target} PRIVATE ${_lib})
        endif()

        if(IS_DIRECTORY ${node})
            test_headers(${target} ${_lib} ${node} ${ARGN})
        endif()

        if(TARGET ${target})
            add_dependencies(${target} ${_lib})
            add_dependencies(${_root} ${target})
            test(${target})
        endif()
    endforeach()
endfunction()

function(test_linking _root _lib _prefix)
    set(options)
    set(one_value_args)
    set(multi_value_args EXCLUDE)
    cmake_parse_arguments(ARGS "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN})

    if(NOT ARGS_EXCLUDE)
        set(ARGS_EXCLUDE "^$")
    else()
        string(REPLACE ";" "|" ARGS_EXCLUDE "${ARGS_EXCLUDE}")
    endif()

    if(NOT IS_ABSOLUTE ${_prefix})
        set(_prefix "${CMAKE_CURRENT_SOURCE_DIR}/${_prefix}")
    endif()

    if(NOT IS_DIRECTORY ${_prefix} OR NOT EXISTS ${_prefix})
        message(FATAL_ERROR "'${_prefix}' is not a valid directory.")
    endif()

    if(NOT TARGET ${_root})
        add_custom_target(${_root})
    endif()

    get_tree_nodes(${_prefix} nodes)
    foreach(node ${nodes})
        if(node MATCHES ${ARGS_EXCLUDE})
            continue()
        endif()

        set(target ${_root}.${node})
        set(node "${_prefix}/${node}")
        if(EXISTS "${node}.hpp")
            set(src_a "${CMAKE_CURRENT_BINARY_DIR}/${target}.a.cpp")
            set(src_b "${CMAKE_CURRENT_BINARY_DIR}/${target}.b.cpp")

            file(WRITE ${src_a} "#include \"${node}.hpp\"")
            file(WRITE ${src_b} "#include \"${node}.hpp\"")

            add_library(${target} SHARED EXCLUDE_FROM_ALL ${src_a} ${src_b})
            target_link_libraries(${target} PRIVATE ${_lib})
        endif()

        if(IS_DIRECTORY ${node})
            test_linking(${target} ${_lib} ${node} ${ARGN})
        endif()

        if(TARGET ${target})
            add_dependencies(${target} ${_lib})
            add_dependencies(${_root} ${target})
            test(${target})
        endif()
    endforeach()
endfunction()

function(test_formatting _root _lib _prefix)
    set(options)
    set(one_value_args)
    set(multi_value_args EXCLUDE)
    cmake_parse_arguments(ARGS "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN})

    if(NOT ARGS_EXCLUDE)
        set(ARGS_EXCLUDE "^$")
    else()
        string(REPLACE ";" "|" ARGS_EXCLUDE "${ARGS_EXCLUDE}")
    endif()

    if(NOT IS_ABSOLUTE ${_prefix})
        set(_prefix "${CMAKE_CURRENT_SOURCE_DIR}/${_prefix}")
    endif()

    if(NOT IS_DIRECTORY ${_prefix} OR NOT EXISTS ${_prefix})
        message(FATAL_ERROR "'${_prefix}' is not a valid directory.")
    endif()

    if(NOT TARGET ${_root})
        add_custom_target(${_root})
    endif()

    get_tree_nodes(${_prefix} nodes)
    foreach(node ${nodes})
        if(node MATCHES ${ARGS_EXCLUDE})
            continue()
        endif()

        set(target ${_root}.${node})
        set(formatted "${CMAKE_CURRENT_BINARY_DIR}/${target}.hpp")
        set(node "${_prefix}/${node}")
        set(original "${node}.hpp")
        if(EXISTS ${original})
            file(RELATIVE_PATH comment ${PROJECT_SOURCE_DIR} ${original})
            set(comment "checking whether '${comment}' is well formatted...")

            add_custom_target(${target}
                COMMAND ${CLANG_FORMAT} ${original} > ${formatted}
                COMMAND ${CMAKE_COMMAND} -E compare_files ${original} ${formatted}
                WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
                COMMENT ${comment}
            )
        endif()

        if(IS_DIRECTORY ${node})
            test_formatting(${target} ${_lib} ${node} ${ARGN})
        endif()

        if(TARGET ${target})
            add_dependencies(${_root} ${target})
            test(${target})
        endif()
    endforeach()
endfunction()

include(ExternalProject)
function(test_deployment _target _lib _version _entry)
    set(prefix "${CMAKE_CURRENT_BINARY_DIR}/dependent")
    set(src_dir "${prefix}/src/${_target}")
    set(bin_dir "${src_dir}-build")

    file(WRITE "${src_dir}/CMakeLists.txt" "\
cmake_minimum_required(VERSION ${CMAKE_VERSION})
project(dependent CXX)
find_package(${_lib} ${_version} EXACT REQUIRED)
add_executable(dependent main.cpp)
target_link_libraries(dependent PRIVATE ${_lib})
"
    )

    file(WRITE "${src_dir}/main.cpp"
        "#include <${_entry}>\nint main(){return 0;}\n"
    )

    ExternalProject_Add(${_target}
        URL ""
        DEPENDS ${_lib}
        BUILD_ALWAYS 1
        EXCLUDE_FROM_ALL 1
        PREFIX ${prefix}
        CMAKE_GENERATOR ${CMAKE_GENERATOR}
        CMAKE_ARGS
            -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
            -DCMAKE_PREFIX_PATH=${CMAKE_CURRENT_BINARY_DIR}/fakeroot
        INSTALL_COMMAND ""
    )

    ExternalProject_Add_Step(${_target} deploy
        ALWAYS 1
        EXCLUDE_FROM_ALL 1
        DEPENDERS configure
        COMMAND ${CMAKE_COMMAND} -E remove_directory fakeroot
        COMMAND ${CMAKE_COMMAND}
            -DCMAKE_INSTALL_PREFIX=fakeroot
            -P ${CMAKE_BINARY_DIR}/cmake_install.cmake
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
    )

    ExternalProject_Add_Step(${_target} clean
        ALWAYS 1
        EXCLUDE_FROM_ALL 1
        DEPENDEES configure
        COMMAND ${CMAKE_COMMAND} --build ${bin_dir} --target clean
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
    )

    test(${_target})
endfunction()
