# ---------------------------------------------------------------------------
# libpda - die Bibliothek
#
# Eigenstaendiges Projekt (P1204R0). Laeuft in zwei Betriebsarten:
#
#   1. als Teil des Superprojekts:  cmake --preset debug
#   2. allein:                      cmake -S libpda -B build/nur-lib
#
# Betriebsart 2 ist der Grund fuer den PROJECT_IS_TOP_LEVEL-Block unten: was
# sonst das Superprojekt bereitstellt, muss die Bibliothek sich dann selbst
# holen.
# ---------------------------------------------------------------------------
cmake_minimum_required(VERSION 3.28)

project(libpda
    VERSION 0.1.0
    DESCRIPTION "PDA-Kernbibliothek: Kontakte, Rechner, Editor, Explorer"
    LANGUAGES C CXX)

# PROJECT_IS_TOP_LEVEL (CMake >= 3.21) ist TRUE, wenn dieses project() das
# oberste ist -- also genau in Betriebsart 2.
if(PROJECT_IS_TOP_LEVEL)
    list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake")

    include(ProjectDefaults)
    include(Warnings)
    include(Sanitizers)

    option(BUILD_SHARED_LIBS "Bibliothek als Shared Library bauen" OFF)
    option(PDA_BUILD_TESTS   "Tests bauen"                        ON)

    if(PDA_BUILD_TESTS)
        enable_testing()

        # Die zweiargumentige Form von add_subdirectory: das Quellverzeichnis
        # liegt AUSSERHALB dieses Projekts, also muss man CMake sagen, wohin
        # die Build-Artefakte sollen. Ohne das zweite Argument bricht CMake ab.
        add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party" third_party)
    endif()
endif()

# ---------------------------------------------------------------------------
# Quellen. Kein file(GLOB): CMake merkt nicht, wenn eine Datei dazukommt --
# der Build bleibt gruen und die neue Datei ist einfach nicht dabei.
#
# *.test.c und *.test.cpp gehoeren NICHT hierher (P1204R0 Regel 7.1); die
# werden weiter unten zu eigenen Executables.
# ---------------------------------------------------------------------------
set(LIBPDA_SOURCES
    libpda/textfile.c
    libpda/calculator.cpp
    libpda/contact.cpp
    libpda/editor.cpp
    libpda/explorer.cpp
)

# Header hier aufzuzaehlen ist fuer den Compiler ueberfluessig -- er findet sie
# ueber die #includes. Es hat zwei andere Zwecke: IDEs zeigen sie im
# Projektbaum, und ein Blick in diese Liste sagt, was die Bibliothek ausmacht.
set(LIBPDA_HEADERS
    libpda/textfile.h
    libpda/calculator.hpp
    libpda/contact.hpp
    libpda/editor.hpp
    libpda/explorer.hpp
    libpda/details/version.hpp
)

# Targetname "pda", nicht "libpda": CMake stellt auf Unix selbst ein "lib"
# voran, das Ergebnis heisst also libpda.a. Ein Target namens "libpda" ergaebe
# liblibpda.a.
add_library(pda ${LIBPDA_SOURCES} ${LIBPDA_HEADERS})

# Der Alias ist das, was Benutzer schreiben: target_link_libraries(x PRIVATE
# pda::pda). Er kostet nichts und hat einen konkreten Nutzen -- ein Tippfehler
# in einem Namen MIT Doppelpunkt ist ein sofortiger CMake-Fehler, ohne
# Doppelpunkt haelt CMake ihn fuer den Namen einer Systembibliothek und
# scheitert erst beim Linken.
add_library(pda::pda ALIAS pda)

# ---------------------------------------------------------------------------
# Include-Pfade -- der wichtigste Block dieser Datei.
#
# PUBLIC heisst: gilt fuer diese Bibliothek UND fuer jeden, der sie linkt.
# (PRIVATE = nur hier, INTERFACE = nur fuer die anderen.)
#
# Die beiden Generator-Ausdruecke unterscheiden zwei Welten:
#
#   BUILD_INTERFACE   waehrend hier gebaut wird. Der Include-Root ist
#                     <repo>/libpda, damit <libpda/contact.hpp> auf
#                     <repo>/libpda/libpda/contact.hpp zeigt.
#
#   INSTALL_INTERFACE nachdem installiert wurde. Der Pfad ist relativ zum
#                     Installationspraefix, also <prefix>/include, und
#                     <libpda/contact.hpp> zeigt auf
#                     <prefix>/include/libpda/contact.hpp.
#
# Ohne diese Trennung wuerde die installierte Bibliothek in ihrer
# CMake-Konfigurationsdatei auf DEIN Build-Verzeichnis zeigen. Beim Benutzer
# existiert das nicht -- der klassische "works on my machine"-Fehler beim
# Verteilen einer Bibliothek.
# ---------------------------------------------------------------------------
target_include_directories(pda PUBLIC
    "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
    "$<INSTALL_INTERFACE:include>"
)

# Auch das generierte Export-Header-Verzeichnis muss in beiden Welten stimmen.
target_include_directories(pda PUBLIC
    "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/generated>"
    "$<INSTALL_INTERFACE:include>"
)

# Die Warnungen gelten beim Bauen DIESER Bibliothek. Wer sie linkt, soll
# unsere Warnungsliste nicht erben -- fremde Projekte haben ihre eigene.
#
# Warum $<BUILD_INTERFACE:...> und nicht einfach PRIVATE:
#
# Bei einer STATISCHEN Bibliothek reicht PRIVATE nicht. Eine .a-Datei linkt
# ihre Abhaengigkeiten nicht selbst -- das muss der tun, der sie benutzt.
# CMake traegt PRIVATE-Abhaengigkeiten deshalb trotzdem als $<LINK_ONLY:...>
# ins INTERFACE_LINK_LIBRARIES ein. install(EXPORT) sieht dort pda_warnings
# stehen, findet es in keinem Export-Set und bricht ab:
#
#     install(EXPORT "pdaTargets" ...) includes target "pda" which requires
#     target "pda_warnings" that is not in any export set.
#
# Drei Wege heraus: pda_warnings mitexportieren (verseucht das Paket mit
# unseren Flags), die Flags direkt per target_compile_options setzen (dann
# bekommt third_party/ sie auch), oder -- richtig -- den Generator-Ausdruck
# unten: er loest im Build-Baum zu "pda_warnings" auf und beim Installieren
# zu nichts.
target_link_libraries(pda PRIVATE "$<BUILD_INTERFACE:pda_warnings>")

# Der C++-Standard als PUBLIC-Anforderung: wer libpda benutzt, braucht
# zwingend C++23, weil contact.hpp <expected> im Interface hat. Ohne das
# scheitert das Benutzerprojekt mit einem unverstaendlichen Header-Fehler
# statt mit einer klaren Meldung.
target_compile_features(pda PUBLIC cxx_std_23)

# ---------------------------------------------------------------------------
# Export-Header: erzeugt <libpda/pda_export.h> mit dem Makro PDA_EXPORT.
#
# Warum: bei einer Shared Library sind Symbole dank CXX_VISIBILITY_PRESET
# hidden unsichtbar, unter Windows grundsaetzlich. Das Makro loest sich je
# nach Plattform und Bauart in __declspec(dllexport), __attribute__((visibility
# ("default"))) oder nichts auf.
#
# Wir generieren die Datei, statt sie von Hand zu schreiben: CMake kennt die
# richtige Variante fuer jeden Compiler.
# ---------------------------------------------------------------------------
include(GenerateExportHeader)
generate_export_header(pda
    BASE_NAME PDA
    EXPORT_FILE_NAME "${CMAKE_CURRENT_BINARY_DIR}/generated/libpda/pda_export.h")

# Ohne diese Definition loest PDA_EXPORT im statischen Bau unter Windows zu
# __declspec(dllimport) auf -- der Linker sucht dann eine DLL, die es nicht
# gibt. generate_export_header sieht dafuer PDA_STATIC_DEFINE vor.
#
# PUBLIC, nicht PRIVATE: die Definition muss auch beim BENUTZER gelten, denn
# er inkludiert denselben Header.
if(NOT BUILD_SHARED_LIBS)
    target_compile_definitions(pda PUBLIC PDA_STATIC_DEFINE)
endif()

set_target_properties(pda PROPERTIES
    VERSION   ${PROJECT_VERSION}      # libpda.dylib.0.1.0
    SOVERSION ${PROJECT_VERSION_MAJOR} # libpda.dylib.0  <- ABI-Stand
)

# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
if(PDA_BUILD_TESTS)
    # Unit-Test in C: <modul>.test.c -> Executable <modul>.test -> CTest
    # "unity.<modul>". Der neotest-Adapter in Neovim erwartet genau diesen Namen.
    function(libpda_add_unity_test module)
        if(NOT TARGET unity)
            return()
        endif()
        set(target "${module}.test")
        add_executable(${target} "libpda/${module}.test.c")
        target_link_libraries(${target} PRIVATE pda unity pda_warnings)
        add_test(NAME "unity.${module}" COMMAND ${target})
    endfunction()

    # Unit-Test in C++: doctest_discover_tests() ruft die Executable beim Build
    # mit --list-test-cases auf und legt EINEN CTest-Eintrag pro TEST_CASE an.
    # Das ist Pflicht, nicht Kosmetik: neotest-ctest kann Testnamen nur so auf
    # Quellpositionen abbilden.
    function(libpda_add_doctest_test module)
        if(NOT TARGET doctest)
            return()
        endif()
        set(target "${module}.test")
        add_executable(${target} "libpda/${module}.test.cpp")
        target_link_libraries(${target} PRIVATE pda doctest pda_warnings)
        doctest_discover_tests(${target} TEST_PREFIX "doctest.")
    endfunction()

    libpda_add_unity_test(textfile)
    libpda_add_doctest_test(calculator)
    libpda_add_doctest_test(contact)
    libpda_add_doctest_test(editor)
    libpda_add_doctest_test(explorer)

    add_subdirectory(tests)
endif()

# ---------------------------------------------------------------------------
# Installation und Export
#
# Ab hier wird aus einem Build-Verzeichnis eine Bibliothek, die ANDERE
# Projekte mit find_package(pda) benutzen koennen. Vier Schritte:
#
#   1. install(TARGETS ... EXPORT ...)  Dateien kopieren, Target vormerken
#   2. install(DIRECTORY ...)           Header kopieren
#   3. install(EXPORT ...)              pdaTargets.cmake erzeugen
#   4. configure_package_config_file()  pdaConfig.cmake erzeugen
#
# find_package(pda) sucht nach pdaConfig.cmake, das laedt pdaTargets.cmake,
# und darin steht das Target pda::pda mit allen Include-Pfaden und Flags.
# ---------------------------------------------------------------------------
include(GNUInstallDirs)     # liefert CMAKE_INSTALL_LIBDIR usw. -- auf Fedora
                            # ist das lib64, nicht lib. Nie selbst hinschreiben.
include(CMakePackageConfigHelpers)

install(TARGETS pda
    EXPORT pdaTargets
    RUNTIME       DESTINATION "${CMAKE_INSTALL_BINDIR}"
    LIBRARY       DESTINATION "${CMAKE_INSTALL_LIBDIR}"
    ARCHIVE       DESTINATION "${CMAKE_INSTALL_LIBDIR}"
    INCLUDES      DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")

# Header. FILES_MATCHING mit PATTERN, weil sonst auch .cpp und .test.cpp
# mitkopiert wuerden -- install(DIRECTORY) nimmt per Default ALLES.
install(DIRECTORY libpda/
    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libpda"
    FILES_MATCHING
        PATTERN "*.h"
        PATTERN "*.hpp"
        PATTERN "private" EXCLUDE)

# Der generierte Export-Header liegt im Build-, nicht im Quellbaum.
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/generated/libpda/pda_export.h"
    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libpda")

# NAMESPACE pda:: sorgt dafuer, dass das importierte Target genauso heisst wie
# der Alias oben. Benutzer schreiben pda::pda -- egal ob sie die Bibliothek
# installiert haben oder per add_subdirectory einbinden.
install(EXPORT pdaTargets
    FILE pdaTargets.cmake
    NAMESPACE pda::
    DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/pda")

# Versionsdatei. SameMajorVersion heisst: find_package(pda 0.1) akzeptiert
# 0.9, aber nicht 1.0 -- die uebliche Semver-Zusicherung.
write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/pdaConfigVersion.cmake"
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion)

# configure_package_config_file statt configure_file: es definiert zusaetzlich
# PACKAGE_INIT, das die Pfade relativ zum tatsaechlichen Installationsort
# aufloest. Damit funktioniert das Paket auch, wenn es jemand nach dem
# Installieren verschiebt.
configure_package_config_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/pdaConfig.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/pdaConfig.cmake"
    INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/pda")

install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/pdaConfig.cmake"
    "${CMAKE_CURRENT_BINARY_DIR}/pdaConfigVersion.cmake"
    DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/pda")
