cmake_minimum_required(VERSION 3.22...3.31)
project(muslimtify VERSION 0.4.4 LANGUAGES C)

# -- Default build type --------------------------------------------------------

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY
        STRINGS Debug Release RelWithDebInfo MinSizeRel)
endif()

set(FULL_PROJECT_VERSION "${PROJECT_VERSION}")

# -- C standard and output dirs ------------------------------------------------

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

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

include(GNUInstallDirs)
include(FetchContent)

# -- Windows toolchain requirement ---------------------------------------------
# Windows builds require MSVC. src/platform/windows/geolocation_win.c includes
# <windows.devices.geolocation.h>, a WinRT header that ships with the Windows SDK
# and is reachable via MSVC's INCLUDE path. MinGW-w64 generates its own WinRT
# headers from IDL but does not provide that one (it has roapi.h,
# windows.devices.enumeration.h, windows.devices.haptics.h, and others), so the
# build otherwise fails with a bare "No such file or directory" that gives no
# hint about the cause. CI (ci.yml) and the release installers (release.yml) both
# build with MSVC. Checked before FetchContent so this fails fast rather than
# after downloading curl.
if(WIN32 AND NOT MSVC)
    message(FATAL_ERROR
        "Windows builds require MSVC (cl.exe), but CMake selected "
        "'${CMAKE_C_COMPILER_ID}' at '${CMAKE_C_COMPILER}'.\n"
        "src/platform/windows/geolocation_win.c needs the Windows SDK WinRT header "
        "<windows.devices.geolocation.h>, which MinGW does not ship.\n"
        "Fix: configure from a Developer Command Prompt for VS, or pass "
        "-DCMAKE_C_COMPILER=cl.exe (delete CMakeCache.txt first if the build "
        "directory was already configured for another compiler).")
endif()

# -- MSVC runtime --------------------------------------------------------------
# Statically link the MSVC C runtime so the binaries are self-contained and run
# on a clean Windows install WITHOUT the VC++ Redistributable. With the default
# dynamic runtime the exe imports vcruntime140.dll, which is absent on a fresh
# machine/VM -- the installer's [Run] step then launches muslimtify.exe, Windows
# raises a blocking "missing DLL" dialog, and the unattended install hangs until
# it times out (winget's Validation-Unattended-Failed). Must be set before any
# target (incl. the FetchContent curl build) is created. Requires CMP0091 (NEW
# since CMake 3.15, satisfied by our 3.22 minimum).
if(MSVC)
    set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

# -- Dependencies --------------------------------------------------------------

if(WIN32)
    set(CURL_DISABLE_INSTALL ON CACHE BOOL "" FORCE)
    set(CURL_ENABLE_EXPORT_TARGET OFF CACHE BOOL "" FORCE)
    FetchContent_Declare(curl
        URL https://github.com/curl/curl/releases/download/curl-8_20_0/curl-8.20.0.tar.xz
        DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
    set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
    set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE)
    set(CURL_DISABLE_TESTS ON CACHE BOOL "" FORCE)
    set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
    set(CURL_USE_SCHANNEL ON CACHE BOOL "" FORCE)
    # Disable optional dependencies curl would otherwise hard-require when found.
    # We only issue a plain GET to ipinfo.io, so none of these are needed.
    set(CURL_USE_LIBPSL OFF CACHE BOOL "" FORCE)
    set(USE_LIBIDN2 OFF CACHE BOOL "" FORCE)
    set(CURL_USE_LIBSSH2 OFF CACHE BOOL "" FORCE)
    set(CURL_ZLIB OFF CACHE BOOL "" FORCE)
    set(CURL_BROTLI OFF CACHE BOOL "" FORCE)
    set(CURL_ZSTD OFF CACHE BOOL "" FORCE)
    set(USE_NGHTTP2 OFF CACHE BOOL "" FORCE)
    FetchContent_MakeAvailable(curl)
else()
    find_package(PkgConfig REQUIRED)
    find_package(Threads REQUIRED)
    pkg_check_modules(LIBNOTIFY REQUIRED libnotify)
    pkg_check_modules(LIBCURL REQUIRED libcurl)
endif()

# -- Generated version header -------------------------------------------------

configure_file(src/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h @ONLY)
configure_file(systemd/muslimtify.service.in ${CMAKE_BINARY_DIR}/systemd/muslimtify.service @ONLY)

# -- Helper function ----------------------------------------------------------

function(muslimtify_set_target_defaults target)
    # PROJECT_SOURCE_DIR, not CMAKE_CURRENT_SOURCE_DIR: inside a function the
    # latter is the calling directory, and src/cli calls this too.
    target_include_directories(${target} PRIVATE
        ${PROJECT_SOURCE_DIR}/include
        ${PROJECT_SOURCE_DIR}/src
        ${PROJECT_SOURCE_DIR}/vendor
        ${CMAKE_BINARY_DIR}/generated
    )
    if(MSVC)
        target_compile_options(${target} PRIVATE /W4)
        # The C runtime marks the standard string and file functions as unsafe
        # and recommends its own _s variants, which no other platform has.
        target_compile_definitions(${target} PRIVATE _CRT_SECURE_NO_WARNINGS)
    else()
        target_compile_options(${target} PRIVATE
            -Wall -Wextra -Wpedantic -Wshadow -Wformat=2
        )
        if(CMAKE_C_COMPILER_ID STREQUAL "Clang")
            target_compile_options(${target} PRIVATE -Wno-c11-extensions)
        endif()
        if(CMAKE_BUILD_TYPE STREQUAL "Debug")
            target_compile_options(${target} PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
            target_link_options(${target} PRIVATE -fsanitize=address,undefined)
        endif()
    endif()
endfunction()

# Include and link lines for a test that links the back end. Extra arguments are
# libraries that go before muslimtify_core, such as muslimtify_cli.
function(muslimtify_link_test target)
    if(WIN32)
        target_include_directories(${target} PRIVATE ${curl_SOURCE_DIR}/include)
        target_link_libraries(${target}
            ${ARGN} muslimtify_core CURL::libcurl ole32 runtimeobject)
    else()
        target_include_directories(${target} PRIVATE
            ${LIBNOTIFY_INCLUDE_DIRS} ${LIBCURL_INCLUDE_DIRS})
        target_link_libraries(${target}
            ${ARGN} muslimtify_core ${LIBNOTIFY_LIBRARIES} ${LIBCURL_LIBRARIES} m)
    endif()
endfunction()

# On Windows an unset APPDATA or LOCALAPPDATA falls back to the user's profile,
# so a test that forgot to set its own would reach the real config and cache.
# Under ctest both point into the build tree instead.
function(muslimtify_isolate_test name)
    if(WIN32)
        set_tests_properties(${name} PROPERTIES ENVIRONMENT
            "APPDATA=${CMAKE_BINARY_DIR}/test-home;LOCALAPPDATA=${CMAKE_BINARY_DIR}/test-home")
    endif()
endfunction()

# -- Object libraries ---------------------------------------------------------
# muslimtify_core: platform-agnostic + platform abstraction
# muslimtify_cli : CLI dispatcher + command handlers

add_library(muslimtify_core OBJECT
    src/core/config.c
    src/core/cache.c
    src/core/location.c
    src/core/string_util.c
    src/core/log.c
    src/core/country.c
    src/core/prayer_checker.c
    src/core/check_cycle.c
    src/core/miniaudio.c
    src/core/audio.c
    src/core/prayertimes.c
    src/lib/muslimtify.c
    src/lib/muslimtify_settings.c
    src/lib/muslimtify_location.c
    src/lib/muslimtify_cycle.c
    src/lib/muslimtify_daemon.c
    $<IF:$<BOOL:${WIN32}>,src/platform/windows/notification_win.c,src/platform/linux/notification.c>
    $<$<BOOL:${WIN32}>:src/platform/windows/toast_activator_win.c>
    $<IF:$<BOOL:${WIN32}>,src/platform/windows/platform_win.c,src/platform/posix/platform_posix.c>
    $<$<NOT:$<BOOL:${WIN32}>>:src/platform/linux/platform_linux.c>
    $<IF:$<BOOL:${WIN32}>,src/platform/windows/daemon_win.c,src/platform/linux/daemon.c>
    # Exactly one location provider is compiled per platform, so
    # platform_get_location has a single definition and core code needs no ifdefs.
    $<$<NOT:$<BOOL:${WIN32}>>:src/platform/linux/gpsd_client.c>
    $<$<BOOL:${WIN32}>:src/platform/windows/geolocation_win.c>
    $<IF:$<BOOL:${WIN32}>,src/platform/windows/timezone.c,src/platform/posix/timezone.c>
)
muslimtify_set_target_defaults(muslimtify_core)

if(WIN32)
    target_include_directories(muslimtify_core PRIVATE ${curl_SOURCE_DIR}/include)
    # Static curl: prevent its headers from emitting __declspec(dllimport).
    target_compile_definitions(muslimtify_core PRIVATE CURL_STATICLIB)
    # windows_zones_generated.inc is #included by timezone.c, never compiled on
    # its own. List it so IDEs surface it and CMake tracks it as a build input.
    target_sources(muslimtify_core PRIVATE
        src/platform/windows/windows_zones_generated.inc)
    set_source_files_properties(
        src/platform/windows/windows_zones_generated.inc
        PROPERTIES HEADER_FILE_ONLY ON)
    # miniaudio.h is vendored and not edited here, so its one narrowing
    # conversion warning is silenced for the file that compiles it.
    set_source_files_properties(src/core/miniaudio.c
        PROPERTIES COMPILE_OPTIONS /wd4244)
else()
    target_include_directories(muslimtify_core PRIVATE
        ${LIBNOTIFY_INCLUDE_DIRS} ${LIBCURL_INCLUDE_DIRS})
    target_link_libraries(muslimtify_core PUBLIC Threads::Threads ${CMAKE_DL_LIBS})
endif()

# -- Presentation layers ------------------------------------------------------

add_subdirectory(src/cli)

# -- Testing ------------------------------------------------------------------

option(BUILD_TESTING "Build test targets" ON)

if(BUILD_TESTING)
    enable_testing()

    if(NOT WIN32)
        add_executable(test_muslimtify_daemon tests/test_muslimtify_daemon.c)
        muslimtify_set_target_defaults(test_muslimtify_daemon)
        target_include_directories(test_muslimtify_daemon PRIVATE ${LIBNOTIFY_INCLUDE_DIRS} ${LIBCURL_INCLUDE_DIRS})
        target_link_libraries(test_muslimtify_daemon muslimtify_core ${LIBNOTIFY_LIBRARIES} ${LIBCURL_LIBRARIES} m)
        add_test(NAME muslimtify_daemon COMMAND test_muslimtify_daemon)

        add_executable(test_notification_interrupt tests/test_notification_interrupt.c)
        muslimtify_set_target_defaults(test_notification_interrupt)
        target_include_directories(test_notification_interrupt PRIVATE ${LIBNOTIFY_INCLUDE_DIRS} ${LIBCURL_INCLUDE_DIRS})
        target_link_libraries(test_notification_interrupt muslimtify_core ${LIBNOTIFY_LIBRARIES} ${LIBCURL_LIBRARIES} m)
        add_test(NAME notification_interrupt COMMAND test_notification_interrupt)
        set_tests_properties(notification_interrupt PROPERTIES ENVIRONMENT "DBUS_SESSION_BUS_ADDRESS=disabled:")

        add_executable(test_cmd_daemon
            tests/test_cmd_daemon.c
            src/platform/linux/daemon.c
            src/platform/posix/platform_posix.c
            src/platform/linux/platform_linux.c
            src/core/log.c
        )
        muslimtify_set_target_defaults(test_cmd_daemon)
        target_include_directories(test_cmd_daemon PRIVATE ${LIBNOTIFY_INCLUDE_DIRS} ${LIBCURL_INCLUDE_DIRS})
        add_test(NAME cmd_daemon COMMAND test_cmd_daemon)

        add_executable(test_daemon_loop
            tests/test_daemon_loop.c
            src/cli/daemon_loop.c
        )
        muslimtify_set_target_defaults(test_daemon_loop)
        target_include_directories(test_daemon_loop PRIVATE ${PROJECT_SOURCE_DIR}/src/cli)
        target_compile_definitions(test_daemon_loop PRIVATE MUSLIMTIFY_DAEMON_LOOP_TEST)
        add_test(NAME daemon_loop COMMAND test_daemon_loop)

        # A presentation layer may include only muslimtify.h, muslimtify_cycle.h,
        # the platform interface headers and its own headers. Each layer directory
        # is listed here, so adding one means adding it to this line.
        add_test(NAME layers
            COMMAND sh ${PROJECT_SOURCE_DIR}/tests/check_layers.sh ${PROJECT_SOURCE_DIR} src/cli)
    endif()

    add_executable(test_prayer_checker tests/test_prayer_checker.c)
    muslimtify_set_target_defaults(test_prayer_checker)
    muslimtify_link_test(test_prayer_checker)
    add_test(NAME prayer_checker COMMAND test_prayer_checker)
    muslimtify_isolate_test(prayer_checker)

    add_executable(test_check_cycle tests/test_check_cycle.c)
    muslimtify_set_target_defaults(test_check_cycle)
    muslimtify_link_test(test_check_cycle)
    add_test(NAME check_cycle COMMAND test_check_cycle)
    muslimtify_isolate_test(check_cycle)

    add_executable(test_display tests/test_display.c)
    muslimtify_set_target_defaults(test_display)
    muslimtify_link_test(test_display)
    add_test(NAME display COMMAND test_display)
    muslimtify_isolate_test(display)

    add_executable(test_offset_bound tests/test_offset_bound.c)
    muslimtify_set_target_defaults(test_offset_bound)
    muslimtify_link_test(test_offset_bound)
    add_test(NAME offset_bound COMMAND test_offset_bound)
    muslimtify_isolate_test(offset_bound)

    add_executable(test_cache tests/test_cache.c)
    muslimtify_set_target_defaults(test_cache)
    muslimtify_link_test(test_cache)
    add_test(NAME cache COMMAND test_cache)
    muslimtify_isolate_test(cache)

    add_executable(test_log tests/test_log.c src/core/log.c)
    muslimtify_set_target_defaults(test_log)
    add_test(NAME log COMMAND test_log)
    muslimtify_isolate_test(log)

    add_executable(test_config tests/test_config.c)
    muslimtify_set_target_defaults(test_config)
    muslimtify_link_test(test_config)
    add_test(NAME config COMMAND test_config)
    muslimtify_isolate_test(config)

    add_executable(test_muslimtify_cycle tests/test_muslimtify_cycle.c)
    muslimtify_set_target_defaults(test_muslimtify_cycle)
    muslimtify_link_test(test_muslimtify_cycle)
    add_test(NAME muslimtify_cycle COMMAND test_muslimtify_cycle)
    muslimtify_isolate_test(muslimtify_cycle)

    add_executable(test_muslimtify tests/test_muslimtify.c)
    muslimtify_set_target_defaults(test_muslimtify)
    muslimtify_link_test(test_muslimtify)
    add_test(NAME muslimtify COMMAND test_muslimtify)
    muslimtify_isolate_test(muslimtify)

    add_executable(test_cli tests/test_cli.c)
    muslimtify_set_target_defaults(test_cli)
    muslimtify_link_test(test_cli muslimtify_cli)
    add_test(NAME cli COMMAND test_cli)
    muslimtify_isolate_test(cli)

    # test_location is cross-platform; its parse_timezone_offset target lives
    # in muslimtify_core which links curl on both platforms.
    add_executable(test_location tests/test_location.c)
    muslimtify_set_target_defaults(test_location)
    if(WIN32)
        target_include_directories(test_location PRIVATE ${curl_SOURCE_DIR}/include)
        target_link_libraries(test_location muslimtify_core CURL::libcurl ole32 runtimeobject)
    else()
        target_include_directories(test_location PRIVATE ${LIBNOTIFY_INCLUDE_DIRS} ${LIBCURL_INCLUDE_DIRS})
        target_link_libraries(test_location muslimtify_core ${LIBNOTIFY_LIBRARIES} ${LIBCURL_LIBRARIES} m)
    endif()
    add_test(NAME location COMMAND test_location)

    # Cross-platform tests (no POSIX dependencies)
    add_executable(test_platform
        tests/test_platform.c
        $<$<BOOL:${WIN32}>:src/platform/windows/platform_win.c>
        $<$<NOT:$<BOOL:${WIN32}>>:src/platform/posix/platform_posix.c>
        $<$<NOT:$<BOOL:${WIN32}>>:src/platform/linux/platform_linux.c>
    )
    muslimtify_set_target_defaults(test_platform)
    add_test(NAME platform COMMAND test_platform)

    add_executable(test_prayertimes tests/test_prayertimes.c)
    muslimtify_set_target_defaults(test_prayertimes)
    if(NOT WIN32)
        target_link_libraries(test_prayertimes m)
    endif()
    add_test(NAME prayertimes COMMAND test_prayertimes)

    add_executable(test_json tests/test_json.c)
    muslimtify_set_target_defaults(test_json)
    if(NOT WIN32)
        target_link_libraries(test_json m)
    endif()
    add_test(NAME json COMMAND test_json)

    add_executable(test_string_util tests/test_string_util.c src/core/string_util.c src/core/log.c)
    muslimtify_set_target_defaults(test_string_util)
    add_test(NAME string_util COMMAND test_string_util)

    add_executable(test_country tests/test_country.c src/core/country.c)
    muslimtify_set_target_defaults(test_country)
    add_test(NAME country COMMAND test_country)

    if(WIN32)
        # The stand-in the daemon test runs in place of schtasks.exe.
        add_executable(fake_schtasks tests/fake_schtasks.c)
        muslimtify_set_target_defaults(fake_schtasks)

        # Compiles the daemon sources directly rather than linking
        # muslimtify_core, so the test can supply its own toast activator
        # functions and never write to the registry.
        add_executable(test_muslimtify_daemon_win
            tests/test_muslimtify_daemon_win.c
            src/platform/windows/daemon_win.c
            src/lib/muslimtify_daemon.c
            src/platform/windows/platform_win.c
            src/core/log.c
        )
        muslimtify_set_target_defaults(test_muslimtify_daemon_win)
        add_dependencies(test_muslimtify_daemon_win fake_schtasks)
        add_test(NAME muslimtify_daemon_win
            COMMAND test_muslimtify_daemon_win $<TARGET_FILE:fake_schtasks>)

        add_executable(test_notification_win
            tests/test_notification_win.c
            src/platform/windows/notification_win.c
            src/core/log.c
        )
        muslimtify_set_target_defaults(test_notification_win)
        target_compile_definitions(test_notification_win PRIVATE MUSLIMTIFY_NOTIFICATION_WIN_TEST)
        target_link_libraries(test_notification_win ole32 runtimeobject)
        add_test(NAME notification_win COMMAND test_notification_win)
    endif()
endif()

# -- Install ------------------------------------------------------------------

install(FILES assets/adhan.mp3
    DESTINATION ${CMAKE_INSTALL_DATADIR}/muslimtify/)

if(WIN32)
    install(FILES assets/muslimtify.ico
        DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/)
else()
    install(FILES assets/muslimtify.png
        DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps/)
    install(FILES assets/muslimtify.png
        DESTINATION ${CMAKE_INSTALL_DATADIR}/pixmaps/)
    install(FILES ${CMAKE_BINARY_DIR}/systemd/muslimtify.service
        DESTINATION lib/systemd/user/)
endif()


if(WIN32)
    add_executable(muslimtify-service WIN32 src/platform/windows/muslimtify_service_win.c)
    muslimtify_set_target_defaults(muslimtify-service)
    target_link_libraries(muslimtify-service
        muslimtify_core ole32 runtimeobject CURL::libcurl)
    install(TARGETS muslimtify-service DESTINATION ${CMAKE_INSTALL_BINDIR})

    install(CODE [[
        # CMP0207: normalize mixed-slash paths (e.g. C:\Windows\system32/foo.dll)
        # before matching exclude regexes. Set here because the generated install
        # script does not inherit the top-level policy version range.
        if(POLICY CMP0207)
            cmake_policy(SET CMP0207 NEW)
        endif()

        file(GET_RUNTIME_DEPENDENCIES
            RESOLVED_DEPENDENCIES_VAR muslimtify_resolved_deps
            UNRESOLVED_DEPENDENCIES_VAR muslimtify_unresolved_deps
            EXECUTABLES
                "$<TARGET_FILE:muslimtify>"
                "$<TARGET_FILE:muslimtify-service>"
            PRE_EXCLUDE_REGEXES
                "api-ms-win-.*"
                "AzureAttestManager\\.dll"
                "AzureAttestNormal\\.dll"
                "HvsiFileTrust\\.dll"
                "PdmUtilities\\.dll"
                "ext-ms-.*"
            POST_EXCLUDE_REGEXES
                ".*[\\\\/][Ww][Ii][Nn][Dd][Oo][Ww][Ss][\\\\/][Ss][Yy][Ss][Tt][Ee][Mm]32[\\\\/].*"
                ".*[\\\\/][Ww][Ii][Nn][Dd][Oo][Ww][Ss][\\\\/][Ss][Yy][Ss][Ww][Oo][Ww]64[\\\\/].*"
        )

        if(muslimtify_unresolved_deps)
            message(STATUS "Ignoring unresolved runtime dependencies: ${muslimtify_unresolved_deps}")
        endif()

        foreach(muslimtify_dep IN LISTS muslimtify_resolved_deps)
            file(INSTALL DESTINATION "${CMAKE_INSTALL_PREFIX}/bin"
                TYPE SHARED_LIBRARY
                FILES "${muslimtify_dep}"
                FOLLOW_SYMLINK_CHAIN)
        endforeach()
    ]])
endif()
