cmake_minimum_required(VERSION 3.20)

if(DEFINED OPTOSKY_VERSION_TEXT)
    set(k_optosky_driver_version "${OPTOSKY_VERSION_TEXT}")
else()
    file(READ "${CMAKE_CURRENT_SOURCE_DIR}/../VERSION" k_optosky_driver_version)
    string(STRIP "${k_optosky_driver_version}" k_optosky_driver_version)
endif()

project(optosky_driver VERSION "${k_optosky_driver_version}" LANGUAGES C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)

include(CTest)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)

option(BUILD_SHARED_LIBS "Build Optosky as a shared library" ON)
option(OPTOSKY_BUILD_TESTS "Build Optosky tests" ON)
option(OPTOSKY_BUILD_EXAMPLES "Build Optosky examples" ON)
option(OPTOSKY_ENABLE_WERROR "Treat warnings as errors for Optosky sources" ON)
option(OPTOSKY_ENABLE_LIVE_TESTS "Enable tests that require connected hardware" OFF)
option(OPTOSKY_REQUIRE_LIVE_HARDWARE "Fail live tests when no matching hardware is present" OFF)
option(OPTOSKY_ENABLE_HARDWARE_OUTPUT_TESTS "Enable live tests that drive ATP GPIO or trigger pins" OFF)
option(OPTOSKY_ENABLE_DESTRUCTIVE_TESTS "Enable live tests for persistent/destructive configuration commands" OFF)
option(OPTOSKY_ENABLE_DETERMINISTIC_TEST_BACKEND "Enable the public deterministic driver backend used by non-live integration tests" OFF)
option(OPTOSKY_ENABLE_VALGRIND_TESTS "Enable Valgrind CTest wrappers for non-live tests" OFF)
option(OPTOSKY_BUILD_PACKAGE_TESTS "Build Optosky install/package smoke tests" ON)

include("${CMAKE_CURRENT_LIST_DIR}/cmake/OptoskySanitizers.cmake")

set(k_optosky_libusb_target "")
set(k_optosky_platform_thread_target "")

if(WIN32)
    find_package(PkgConfig REQUIRED)
    pkg_check_modules(LIBUSB REQUIRED IMPORTED_TARGET libusb-1.0)
    set(k_optosky_libusb_target PkgConfig::LIBUSB)
else()
    find_package(PkgConfig REQUIRED)
    pkg_check_modules(LIBUSB REQUIRED IMPORTED_TARGET libusb-1.0)
    find_package(Threads REQUIRED)
    set(k_optosky_libusb_target PkgConfig::LIBUSB)
    set(k_optosky_platform_thread_target Threads::Threads)
endif()

configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/optosky_config.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/generated/optosky_config.h
)
configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/optosky_version.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/generated/optosky_version.h
)

set(k_optosky_library_sources
    src/optosky_alloc.c
    src/optosky.c
    src/optosky_mutex.c
    src/optosky_protocol.c
    src/optosky_transport_libusb.c
)

if(OPTOSKY_ENABLE_DETERMINISTIC_TEST_BACKEND)
    list(APPEND k_optosky_library_sources src/optosky_transport_deterministic.c)
endif()

add_library(optosky ${k_optosky_library_sources})
add_library(Optosky::optosky ALIAS optosky)

target_include_directories(optosky
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src
        ${CMAKE_CURRENT_BINARY_DIR}/generated
)
target_link_libraries(optosky PRIVATE
    ${k_optosky_libusb_target}
    ${k_optosky_platform_thread_target}
)

set_target_properties(optosky PROPERTIES
    VERSION ${PROJECT_VERSION}
    SOVERSION 0
    C_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN YES
)
if(BUILD_SHARED_LIBS)
    target_compile_definitions(optosky PRIVATE OPTOSKY_BUILDING_LIBRARY)
else()
    target_compile_definitions(optosky PUBLIC OPTOSKY_STATIC_DEFINE)
endif()
if(OPTOSKY_ENABLE_DETERMINISTIC_TEST_BACKEND)
    target_compile_definitions(optosky PUBLIC OPTOSKY_ENABLE_DETERMINISTIC_TEST_BACKEND)
endif()

function(optosky_apply_warnings target_name)
    if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
        target_compile_options(${target_name} PRIVATE
            -Wall -Wextra -Wpedantic -Wconversion -Wshadow
            -Wstrict-prototypes -Wmissing-prototypes
        )
        if(OPTOSKY_ENABLE_WERROR)
            target_compile_options(${target_name} PRIVATE -Werror)
        endif()
    elseif(MSVC)
        target_compile_options(${target_name} PRIVATE /W4)
        target_compile_options(${target_name} PRIVATE /wd4127 /wd4996)
        if(OPTOSKY_ENABLE_WERROR)
            target_compile_options(${target_name} PRIVATE /WX)
        endif()
    endif()
endfunction()

optosky_apply_warnings(optosky)
optosky_apply_sanitizers(optosky)

if(OPTOSKY_BUILD_EXAMPLES)
    add_executable(optosky_list_devices examples/list_devices.c)
    add_executable(optosky_acquire_spectrum examples/acquire_spectrum.c)
    add_executable(optosky_async_acquire_spectrum examples/async_acquire_spectrum.c)
    set_target_properties(optosky_list_devices PROPERTIES OUTPUT_NAME optosky-list-devices)
    set_target_properties(optosky_acquire_spectrum PROPERTIES OUTPUT_NAME optosky-acquire-spectrum)
    set_target_properties(optosky_async_acquire_spectrum
        PROPERTIES OUTPUT_NAME optosky-async-acquire-spectrum
    )
    set(k_optosky_example_targets
        optosky_list_devices
        optosky_acquire_spectrum
        optosky_async_acquire_spectrum
    )
    if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/examples/device_control.c")
        add_executable(optosky_device_control examples/device_control.c)
        set_target_properties(optosky_device_control
            PROPERTIES OUTPUT_NAME optosky-device-control
        )
        list(APPEND k_optosky_example_targets optosky_device_control)
    endif()
    foreach(k_example IN LISTS k_optosky_example_targets)
        target_link_libraries(${k_example} PRIVATE Optosky::optosky)
        optosky_apply_warnings(${k_example})
        optosky_apply_sanitizers(${k_example})
    endforeach()
endif()

if(OPTOSKY_BUILD_TESTS)
    enable_testing()
    enable_language(CXX)
    target_compile_definitions(optosky PRIVATE OPTOSKY_ENABLE_TEST_HOOKS)

    add_library(optosky_test_internal STATIC ${k_optosky_library_sources})
    target_include_directories(optosky_test_internal
        PUBLIC
            ${CMAKE_CURRENT_SOURCE_DIR}/include
        PRIVATE
            ${CMAKE_CURRENT_SOURCE_DIR}/src
            ${CMAKE_CURRENT_BINARY_DIR}/generated
    )
    target_link_libraries(optosky_test_internal PRIVATE
        ${k_optosky_libusb_target}
        ${k_optosky_platform_thread_target}
    )
    target_compile_definitions(optosky_test_internal PRIVATE
        OPTOSKY_ENABLE_TEST_HOOKS
        OPTOSKY_BUILDING_LIBRARY
    )
    if(OPTOSKY_ENABLE_DETERMINISTIC_TEST_BACKEND)
        target_compile_definitions(optosky_test_internal
                                   PUBLIC OPTOSKY_ENABLE_DETERMINISTIC_TEST_BACKEND)
    endif()
    target_compile_definitions(optosky_test_internal PUBLIC OPTOSKY_STATIC_DEFINE)
    optosky_apply_warnings(optosky_test_internal)
    optosky_apply_sanitizers(optosky_test_internal)

    function(optosky_add_test target_name source_file test_name)
        add_executable(${target_name} ${source_file})
        target_link_libraries(${target_name} PRIVATE
            optosky_test_internal
            ${k_optosky_libusb_target}
        )
        target_include_directories(${target_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
        if(WIN32)
            target_include_directories(${target_name}
                BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests/compat
            )
        endif()
        target_compile_definitions(${target_name} PRIVATE
            OPTOSKY_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
            OPTOSKY_ENABLE_TEST_HOOKS
            OPTOSKY_EXPECTED_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}
            OPTOSKY_EXPECTED_VERSION_MINOR=${PROJECT_VERSION_MINOR}
            OPTOSKY_EXPECTED_VERSION_PATCH=${PROJECT_VERSION_PATCH}
        )
        optosky_apply_warnings(${target_name})
        optosky_apply_sanitizers(${target_name})
        add_test(NAME ${test_name} COMMAND ${target_name})
    endfunction()

    optosky_add_test(optosky_test_protocol tests/test_protocol.c optosky.protocol)
    optosky_add_test(optosky_test_public_api tests/test_public_api.c optosky.public_api)
    optosky_add_test(optosky_test_sync tests/test_sync.c optosky.sync)
    optosky_add_test(optosky_test_memory tests/test_memory.c optosky.memory)
    optosky_add_test(optosky_test_transport tests/test_transport.c optosky.transport)
    optosky_add_test(optosky_test_thread_stress tests/test_thread_stress.c optosky.thread_stress)
    optosky_add_test(optosky_test_headers tests/test_headers.c optosky.headers)
    if(OPTOSKY_ENABLE_DETERMINISTIC_TEST_BACKEND)
        optosky_add_test(optosky_test_deterministic_backend
                         tests/test_deterministic_backend.c
                         optosky.deterministic_backend)
    endif()

    add_executable(optosky_test_header_cpp tests/test_header_cpp.cpp)
    target_link_libraries(optosky_test_header_cpp PRIVATE Optosky::optosky)
    target_compile_definitions(optosky_test_header_cpp PRIVATE
        OPTOSKY_EXPECTED_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}
        OPTOSKY_EXPECTED_VERSION_MINOR=${PROJECT_VERSION_MINOR}
        OPTOSKY_EXPECTED_VERSION_PATCH=${PROJECT_VERSION_PATCH}
    )
    if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
        target_compile_options(optosky_test_header_cpp PRIVATE
            -Wall -Wextra -Wpedantic -Wconversion -Wshadow
        )
        if(OPTOSKY_ENABLE_WERROR)
            target_compile_options(optosky_test_header_cpp PRIVATE -Werror)
        endif()
    endif()
    optosky_apply_sanitizers(optosky_test_header_cpp)
    add_test(NAME optosky.header_cpp COMMAND optosky_test_header_cpp)

    if(BUILD_SHARED_LIBS AND NOT WIN32)
        set(k_optosky_public_symbols
            optosky_acquire_dark_spectrum
            optosky_acquire_spectrum
            optosky_calculate_wavelength_nm
            optosky_close_device
            optosky_context_create
            optosky_context_destroy
            optosky_device_list_destroy
            optosky_disable_external_trigger
            optosky_enable_external_trigger
            optosky_error_code_to_string
            optosky_get_average_count
            optosky_get_integration_time_ms
            optosky_get_library_version
            optosky_get_serial_baud_rate
            optosky_list_devices
            optosky_open_device_by_serial
            optosky_read_async_spectrum
            optosky_read_device_profile
            optosky_read_wavelength_calibration
            optosky_restore_factory_settings
            optosky_set_average_count
            optosky_set_gpio_output_state
            optosky_set_integration_time_ms
            optosky_set_serial_baud_rate
            optosky_spectrum_destroy
            optosky_start_async_dark_spectrum
            optosky_start_async_spectrum
            optosky_try_read_async_spectrum
        )
        add_test(
            NAME optosky.header_no_private_symbols_exported
            COMMAND ${CMAKE_COMMAND}
                -DOPTOSKY_LIBRARY=$<TARGET_FILE:optosky>
                "-DOPTOSKY_EXPECTED_PUBLIC_SYMBOLS=${k_optosky_public_symbols}"
                -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_exported_symbols.cmake
        )
    endif()

    if(OPTOSKY_BUILD_EXAMPLES)
        if(TARGET optosky_device_control)
            add_test(
                NAME optosky.examples
                COMMAND ${CMAKE_COMMAND}
                    -DOPTOSKY_LIST_DEVICES=$<TARGET_FILE:optosky_list_devices>
                    -DOPTOSKY_ACQUIRE_SPECTRUM=$<TARGET_FILE:optosky_acquire_spectrum>
                    -DOPTOSKY_ASYNC_ACQUIRE_SPECTRUM=$<TARGET_FILE:optosky_async_acquire_spectrum>
                    -DOPTOSKY_DEVICE_CONTROL=$<TARGET_FILE:optosky_device_control>
                    -DOPTOSKY_ENABLE_LIVE_TESTS=$<BOOL:${OPTOSKY_ENABLE_LIVE_TESTS}>
                    -DOPTOSKY_REQUIRE_LIVE_HARDWARE=$<BOOL:${OPTOSKY_REQUIRE_LIVE_HARDWARE}>
                    -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_examples.cmake
            )
        endif()
    endif()

    if(OPTOSKY_ENABLE_LIVE_TESTS)
        add_executable(optosky_test_live tests/test_live_atp2000p.c)
        target_link_libraries(optosky_test_live PRIVATE
            Optosky::optosky
            ${k_optosky_platform_thread_target}
        )
        if(WIN32)
            target_include_directories(optosky_test_live
                BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests/compat
            )
        endif()
        target_compile_definitions(optosky_test_live PRIVATE
            OPTOSKY_REQUIRE_LIVE_HARDWARE=$<BOOL:${OPTOSKY_REQUIRE_LIVE_HARDWARE}>
            OPTOSKY_ENABLE_HARDWARE_OUTPUT_TESTS=$<BOOL:${OPTOSKY_ENABLE_HARDWARE_OUTPUT_TESTS}>
            OPTOSKY_ENABLE_DESTRUCTIVE_TESTS=$<BOOL:${OPTOSKY_ENABLE_DESTRUCTIVE_TESTS}>
        )
        optosky_apply_warnings(optosky_test_live)
        optosky_apply_sanitizers(optosky_test_live)
        add_test(NAME optosky.live_atp2000p COMMAND optosky_test_live)
        set_tests_properties(optosky.live_atp2000p PROPERTIES LABELS "live;hardware")
    endif()

    if(OPTOSKY_BUILD_PACKAGE_TESTS)
        add_test(
            NAME optosky.package
            COMMAND ${CMAKE_COMMAND}
                -DOPTOSKY_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}
                -DOPTOSKY_PROJECT_VERSION=${PROJECT_VERSION}
                "-DOPTOSKY_PACKAGE_TEST_GENERATOR=${CMAKE_GENERATOR}"
                "-DOPTOSKY_PACKAGE_TEST_BUILD_TYPE=${CMAKE_BUILD_TYPE}"
                "-DOPTOSKY_PACKAGE_TEST_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}"
                "-DOPTOSKY_PACKAGE_TEST_VCPKG_MANIFEST_DIR=${VCPKG_MANIFEST_DIR}"
                "-DOPTOSKY_PACKAGE_TEST_VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}"
                "-DOPTOSKY_PACKAGE_TEST_PKG_CONFIG_EXECUTABLE=${PKG_CONFIG_EXECUTABLE}"
                -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_package.cmake
        )
        set_tests_properties(optosky.package PROPERTIES LABELS package)
    endif()

    if(OPTOSKY_ENABLE_VALGRIND_TESTS)
        find_program(VALGRIND_EXECUTABLE valgrind REQUIRED)
        set(k_optosky_valgrind_options
            --leak-check=full
            --show-leak-kinds=definite,indirect
            --errors-for-leak-kinds=definite,indirect
            --error-exitcode=1
        )
        foreach(k_test protocol public_api transport memory sync thread_stress)
            add_test(
                NAME optosky.valgrind.${k_test}
                COMMAND ${VALGRIND_EXECUTABLE}
                    ${k_optosky_valgrind_options}
                    $<TARGET_FILE:optosky_test_${k_test}>
            )
            set_tests_properties(optosky.valgrind.${k_test} PROPERTIES LABELS valgrind)
        endforeach()
        if(OPTOSKY_ENABLE_LIVE_TESTS)
            add_test(
                NAME optosky.valgrind.live_atp2000p
                COMMAND ${VALGRIND_EXECUTABLE}
                    ${k_optosky_valgrind_options}
                    $<TARGET_FILE:optosky_test_live>
            )
            set_tests_properties(optosky.valgrind.live_atp2000p PROPERTIES
                LABELS "live;hardware;valgrind"
            )
        endif()
    endif()
endif()

install(TARGETS optosky
    EXPORT OptoskyTargets
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT development
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT runtime NAMELINK_COMPONENT development
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
)
install(FILES include/optosky.h
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
    COMPONENT development
)

if(OPTOSKY_BUILD_EXAMPLES)
    install(TARGETS ${k_optosky_example_targets}
        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
        COMPONENT tools
    )
endif()

write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/OptoskyConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion
)

install(EXPORT OptoskyTargets
    NAMESPACE Optosky::
    FILE OptoskyTargets.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Optosky
    COMPONENT development
)
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/OptoskyConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/OptoskyConfig.cmake
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Optosky
)
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/OptoskyConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/OptoskyConfigVersion.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Optosky
    COMPONENT development
)

if(NOT WIN32)
    configure_file(
        ${CMAKE_CURRENT_SOURCE_DIR}/cmake/optosky.pc.in
        ${CMAKE_CURRENT_BINARY_DIR}/optosky.pc
        @ONLY
    )
    install(FILES ${CMAKE_CURRENT_BINARY_DIR}/optosky.pc
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
        COMPONENT development
    )
endif()
