cmake_minimum_required(VERSION 3.14)

project(everest-framework
    VERSION 0.25.0
    DESCRIPTION "The open operating system for e-mobility charging stations"
    LANGUAGES CXX C
)

if(DEFINED EVEREST_IO_WITH_MQTT AND NOT EVEREST_IO_WITH_MQTT)
    message(FATAL_ERROR "everest-framework requires MQTT support in everest::io. "
            "Set EVEREST_IO_WITH_MQTT=ON or exclude framework from the build "
            "(e.g. via EVEREST_EXCLUDE_LIBS=framework).")
endif()

find_package(everest-cmake 0.5 REQUIRED
    PATHS ../everest-cmake
)

# options
option(${PROJECT_NAME}_BUILD_TESTING "Build unit tests, used if included as dependency" OFF)
option(BUILD_TESTING "Build unit tests, used if standalone project" OFF)
option(FRAMEWORK_INSTALL "Install the library (shared data might be installed anyway)" ${EVC_MAIN_PROJECT})
option(CMAKE_RUN_CLANG_TIDY "Run clang-tidy" OFF)
option(EVEREST_ENABLE_JS_SUPPORT "Enable everestjs for JavaScript modules" OFF)
option(EVEREST_ENABLE_PY_SUPPORT "Enable everestpy for Python modules" ON)
option(EVEREST_ENABLE_RS_SUPPORT "Enable everestrs for Rust modules" OFF)
option(EVEREST_ENABLE_ADMIN_PANEL_BACKEND "Enable everest admin panel backend" OFF)
option(EVEREST_INSTALL_ADMIN_PANEL "Download and install everest admin panel" OFF)
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY "latency" CACHE STRING
    "Thread pool scaling policy for framework message handling: latency, greedy, conservative, fixed_size, custom")
set_property(CACHE EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY PROPERTY STRINGS
    latency greedy conservative fixed_size custom)
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS "50" CACHE STRING
    "Maximum queued task wait time in milliseconds before latency scaling grows the framework message handler pool")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_TICK_MS "5" CACHE STRING
    "Supervisor tick in milliseconds for framework message handler latency scaling")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD "3" CACHE STRING
    "Queue size threshold for framework message handler fixed-size scaling")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER "" CACHE STRING
    "Header to include when EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE "" CACHE STRING
    "Fully-qualified scaling policy type when EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_INCLUDE_DIR "" CACHE PATH
    "Include directory for the custom thread pool scaling policy header")

foreach(_scaling_option IN ITEMS
        EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS
        EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_TICK_MS
        EVEREST_FRAMEWORK_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD)
    if(NOT ${_scaling_option} MATCHES "^[1-9][0-9]*$")
        message(FATAL_ERROR
            "${_scaling_option} must be a positive integer, got '${${_scaling_option}}'")
    endif()
endforeach()

if(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "latency")
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 1)
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "greedy")
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 2)
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "conservative")
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 3)
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "fixed_size")
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 4)
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "custom")
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 5)

    if(NOT EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER)
        message(FATAL_ERROR
            "EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER must be set when "
            "EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
    endif()
    if(NOT EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE)
        message(FATAL_ERROR
            "EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE must be set when "
            "EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
    endif()

    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG
        "${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER}")
    set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG
        "${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE}")
else()
    message(FATAL_ERROR
        "Invalid EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY: "
        "${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY}. "
        "Allowed values: latency, greedy, conservative, fixed_size, custom")
endif()
ev_setup_cmake_variables_python_wheel()
option(${PROJECT_NAME}_USE_PYTHON_VENV "Use python venv for pip install targets" OFF)
set(${PROJECT_NAME}_PYTHON_VENV_PATH "${CMAKE_BINARY_DIR}/venv" CACHE PATH "Path to python venv")
ev_setup_python_executable(
    USE_PYTHON_VENV ${${PROJECT_NAME}_USE_PYTHON_VENV}
    PYTHON_VENV_PATH ${${PROJECT_NAME}_PYTHON_VENV_PATH}
)

if((${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME} OR ${PROJECT_NAME}_BUILD_TESTING) AND BUILD_TESTING)
    set(EVEREST_FRAMEWORK_BUILD_TESTING ON)
    # this policy allows us to link gcov to targets defined in other directories
    if(POLICY CMP0079)
        set(CMAKE_POLICY_DEFAULT_CMP0079 NEW)
    endif()
    if (NOT CMAKE_BUILD_TYPE)
        if(EVEREST_ENABLE_DEBUG_BUILD)
            set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
        else()
            set(CMAKE_BUILD_TYPE MinSizeRel CACHE STRING "Build type" FORCE)
        endif()
    endif()
endif()

# make own cmake modules available
list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

# this policy allows us to continue using the removed FindBoost module for now
if(POLICY CMP0167)
    cmake_policy(SET CMP0167 OLD)
endif()

# dependencies
find_package(Boost
    COMPONENTS
        program_options
        thread
    REQUIRED
)

if(Boost_VERSION_STRING VERSION_LESS "1.69.0")
    find_package(Boost
        COMPONENTS
            system
        REQUIRED
    )
endif()

find_package(PkgConfig REQUIRED)
pkg_check_modules(libcap
    REQUIRED
    IMPORTED_TARGET
    libcap
)

# Make sure stdc++fs is linked for GCC < 9
if (CMAKE_COMPILER_IS_GNUCC AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9)
    set(STD_FILESYSTEM_COMPAT_LIB "stdc++fs")
else()
    set(STD_FILESYSTEM_COMPAT_LIB "")
endif()

if(NOT DISABLE_EDM)
    evc_setup_edm()

    # In EDM mode, we can't install exports (because the dependencies usually do not install their exports)
    set(FRAMEWORK_INSTALL OFF)

    if (EVEREST_ENABLE_ADMIN_PANEL_BACKEND)
        # FIXME (aw): libwebsockets/ninja clean doesn't delete recursivly ..
        set_property(
            TARGET websockets_shared
            APPEND
            PROPERTY ADDITIONAL_CLEAN_FILES "${libwebsockets_BINARY_DIR}/include/libwebsockets"
        )

        # FIXME (aw): libwebsockets enum-int-mismatch FIX
        # see https://github.com/warmcat/libwebsockets/pull/2824
        if (CMAKE_COMPILER_IS_GNUCC AND CMAKE_C_COMPILER_VERSION VERSION_GREATER 13.0)
            target_compile_options(websockets_shared PRIVATE -Wno-error=enum-int-mismatch)
        endif()
    endif()
    set_property(TARGET nlohmann_json_schema_validator PROPERTY POSITION_INDEPENDENT_CODE ON)

    # FIXME (aw): add catch2's cmake folder
    if (BUILD_TESTING)
        list(APPEND CMAKE_MODULE_PATH "${Catch2_SOURCE_DIR}/contrib")
    endif()
else()
    find_package(date REQUIRED)
    find_package(nlohmann_json REQUIRED)
    find_package(nlohmann_json_schema_validator REQUIRED)
    find_package(fmt REQUIRED)

    if (EVEREST_ENABLE_ADMIN_PANEL_BACKEND)
        find_package(libwebsockets REQUIRED)
    endif()

    if (BUILD_TESTING)
        find_package(Catch2 REQUIRED)
    endif()

    include(find-mqttc)
endif()

include(${everest-sqlite_SOURCE_DIR}/cmake/CollectMigrationFiles.cmake)

set(EVEREST_FRAMEWORK_GENERATED_INC_DIR ${PROJECT_BINARY_DIR}/generated)
configure_file(
    include/compile_time_settings.hpp.in
    ${EVEREST_FRAMEWORK_GENERATED_INC_DIR}/everest/compile_time_settings.hpp
)

# library code
add_subdirectory(lib)

# executable code
add_subdirectory(src)

# auxillary files
add_subdirectory(schemas)

# everest javascript wrapper
if (EVEREST_ENABLE_JS_SUPPORT)
    add_subdirectory(everestjs)
endif()

# everest python wrapper
if (EVEREST_ENABLE_PY_SUPPORT)
    set(PYTHON_MODULE_EXTENSION ".so")
    add_subdirectory(everestpy)
endif()

if (EVEREST_ENABLE_RS_SUPPORT)
  add_subdirectory(everestrs)
endif()

# FIXME (aw): should this be installed or not?  Right now it is needed for the
#             current packaging approach
install(TARGETS framework
    EXPORT framework-targets
    LIBRARY
)

# packaging
if (FRAMEWORK_INSTALL)
    install(
        DIRECTORY include/
        DESTINATION include/everest
    )

    install(
        FILES ${EVEREST_FRAMEWORK_GENERATED_INC_DIR}/everest/compile_time_settings.hpp
        DESTINATION include/everest
    )

    evc_setup_package(
        NAME everest-framework
        EXPORT framework-targets
        NAMESPACE everest
        ADDITIONAL_CONTENT
            "find_dependency(everest-helpers)"
            "find_dependency(everest-util)"
            "find_dependency(nlohmann_json)"
            "find_dependency(nlohmann_json_schema_validator)"
            "find_dependency(fmt)"
            "find_dependency(date)"
            "set(EVEREST_SCHEMA_DIR \"@PACKAGE_EVEREST_SCHEMA_DIR@\")"
        PATH_VARS
            EVEREST_SCHEMA_DIR "${CMAKE_INSTALL_DATADIR}/everest/schemas"
    )
endif ()

# testing
if(EVEREST_FRAMEWORK_BUILD_TESTING)
    include(CTest)
    add_subdirectory(tests)
else()
    message(STATUS "Not running unit tests")
endif()

# configure clang-tidy if requested
if(CMAKE_RUN_CLANG_TIDY)
    message("Enabling clang-tidy")
    set(CMAKE_CXX_CLANG_TIDY clang-tidy)
endif()

# build doxygen documentation if doxygen is available
find_package(Doxygen)
if(DOXYGEN_FOUND)
    set( DOXYGEN_OUTPUT_DIRECTORY dist/docs )
    doxygen_add_docs(doxygen-${PROJECT_NAME} everest.js include lib src)
else()
    message("Doxygen is needed to generate documentation")
endif()
