#-----------------------------------------------------------------------------
#  Copyright (C) 2025 Thomas S. Ullrich
#
#  This file is part of "xyscan".
#
#  This file may be used under the terms of the GNU General Public License.
#  This project is free software; you can redistribute it and/or modify it
#  under the terms of the GNU General Public License.
#
#  Author: Thomas S. Ullrich
#  Last update: Nov 9, 2025
#-----------------------------------------------------------------------------
#
#  xyscan — CMake top-level
#
#  Purpose: Cross-platform build for xyscan using Qt 6 GUI with translations,
#           macOS bundle support, Linux desktop integration, and optional
#           packaging.
#
#-----------------------------------------------------------------------------

cmake_minimum_required(VERSION 3.16)

#=============================================================================
# Project metadata and global language settings
#=============================================================================
project(xyscan VERSION 4.7.0 LANGUAGES CXX)

if(APPLE)
  # Build a universal binary for Intel (x86_64) and Apple Silicon (arm64)
  set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "Build architectures for macOS" FORCE)
endif()

# Use a modern C++ baseline across all targets. Qt 6 is fully compatible with C++17.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Let CMake/Qt auto-run the Qt code generators:
#  - AUTOMOC: invokes moc on headers with Q_OBJECT etc.
#  - AUTOUIC: compiles .ui files from Qt Designer (if any).
#  - AUTORCC: compiles .qrc resource collections.
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

# Standard installation directory variables (bin, lib, share, …).
include(GNUInstallDirs)

#=============================================================================
# Locate Qt 6
#  - REQUIRED components must exist at configure time.
#  - PdfWidgets is optional: link it when the imported target exists.
#=============================================================================
find_package(Qt6 REQUIRED COMPONENTS
  Core
  Gui
  Widgets
  Charts
  Network
  Multimedia
  PrintSupport
  Pdf
  LinguistTools
)
find_package(Qt6 QUIET COMPONENTS PdfWidgets) # optional

#=============================================================================
# Source discovery
#  - We pick up C/C++ and UI sources & headers recursively.
#  - CONFIGURE_DEPENDS: CMake re-scans when files are added/removed.
#=============================================================================
file(GLOB_RECURSE XYSCAN_SOURCES
  "${CMAKE_SOURCE_DIR}/src/*.cpp"
  "${CMAKE_SOURCE_DIR}/src/*.cxx"
  "${CMAKE_SOURCE_DIR}/src/*.cc"
  "${CMAKE_SOURCE_DIR}/src/*.ui"
  "${CMAKE_SOURCE_DIR}/src/*.h"
  "${CMAKE_SOURCE_DIR}/src/*.hpp"
)

#=============================================================================
# Create the application target
#  - MANUAL_FINALIZATION: we call qt_finalize_executable() manually later.
#  - On macOS we’ll make it a bundle (.app).
#=============================================================================
qt_add_executable(xyscan
  MANUAL_FINALIZATION
  ${XYSCAN_SOURCES}
)

if(APPLE)
  set_target_properties(xyscan PROPERTIES MACOSX_BUNDLE TRUE)
endif()

# Compiler “note” cleanup on some compilers for cleaner logs.
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
  target_compile_options(xyscan PRIVATE -Wno-psabi)
endif()

#=============================================================================
# Translations
#  - Input .ts → output .qm via lrelease.
#  - Ensure .qm exist before RCC compiles any .qrc that embeds them.
#=============================================================================
set(XYSCAN_TRANSLATIONS_DIR "${CMAKE_SOURCE_DIR}/resources/translations")

file(GLOB XYSCAN_TS_FILES CONFIGURE_DEPENDS
  "${XYSCAN_TRANSLATIONS_DIR}/*.ts"
)

if(XYSCAN_TS_FILES)
  set(XYSCAN_QM_FILES)
  foreach(_ts IN LISTS XYSCAN_TS_FILES)
    get_filename_component(_name "${_ts}" NAME_WE)
    set(_qm "${XYSCAN_TRANSLATIONS_DIR}/${_name}.qm")
    list(APPEND XYSCAN_QM_FILES "${_qm}")

    add_custom_command(
      OUTPUT   "${_qm}"
      COMMAND  Qt6::lrelease "${_ts}" -qm "${_qm}"
      DEPENDS  "${_ts}"
      COMMENT  "lrelease ${_name}.ts → ${_name}.qm"
      VERBATIM
    )
  endforeach()

  add_custom_target(xyscan_translations DEPENDS ${XYSCAN_QM_FILES})
  add_dependencies(xyscan xyscan_translations)

  file(GLOB XYSCAN_QRC_FILES CONFIGURE_DEPENDS
    "${CMAKE_SOURCE_DIR}/resources/*.qrc"
  )
  if(XYSCAN_QRC_FILES)
    set_source_files_properties(
      ${XYSCAN_QRC_FILES}
      PROPERTIES OBJECT_DEPENDS "${XYSCAN_QM_FILES}"
    )
  endif()
endif()

#=============================================================================
# Link libraries
#=============================================================================
target_link_libraries(xyscan PRIVATE
  Qt6::Core
  Qt6::Gui
  Qt6::Widgets
  Qt6::Charts
  Qt6::Network
  Qt6::Multimedia
  Qt6::PrintSupport
  Qt6::Pdf
)
if(TARGET Qt6::PdfWidgets)
  target_link_libraries(xyscan PRIVATE Qt6::PdfWidgets)
endif()
target_compile_definitions(xyscan PRIVATE QT_CHARTS_USE_NAMESPACE)

#=============================================================================
# Resources (.qrc)
#  - Pick the first existing candidate and let AUTORCC compile it.
#=============================================================================
set(_qrc_candidates
  "${CMAKE_SOURCE_DIR}/resources/xyscan.qrc"
  "${CMAKE_SOURCE_DIR}/src/resources/xyscan.qrc"
  "${CMAKE_SOURCE_DIR}/xyscan.qrc"
)
foreach(_cand IN LISTS _qrc_candidates)
  if(EXISTS "${_cand}")
    message(STATUS "Using Qt resource file: ${_cand}")
    target_sources(xyscan PRIVATE "${_cand}")
    break()
  endif()
endforeach()

#=============================================================================
# Finalize the Qt target (non-Apple)
#  - Qt recommends calling qt_finalize_executable() on all platforms.
#  - On macOS we call it later, right before running macdeployqt.
#=============================================================================
if(NOT APPLE)
  qt_finalize_executable(xyscan)
endif()

#=============================================================================
# macOS bundle details (APPLE)
#  - Info.plist and .icns icon
#  - Dev-time copies of docs and localizations into the built bundle
#  - License files copied into bundle root (Contents/)
#=============================================================================
if(APPLE)
  # Info.plist (fall back to default if not present).
  set(XYSCAN_PLIST "${CMAKE_SOURCE_DIR}/platforms/macOS/BundleItems/Info.plist")
  if(EXISTS "${XYSCAN_PLIST}")
    set_target_properties(xyscan PROPERTIES
      MACOSX_BUNDLE_INFO_PLIST "${XYSCAN_PLIST}"
    )
  else()
    message(WARNING "Info.plist not found at ${XYSCAN_PLIST}. Using default generated plist.")
  endif()

  # App icon (.icns).
  set(XYSCAN_ICNS "${CMAKE_SOURCE_DIR}/platforms/macOS/BundleItems/xyscan.icns")
  if(EXISTS "${XYSCAN_ICNS}")
    set_source_files_properties("${XYSCAN_ICNS}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
    target_sources(xyscan PRIVATE "${XYSCAN_ICNS}")
    set_target_properties(xyscan PROPERTIES MACOSX_BUNDLE_ICON_FILE "xyscan.icns")
  else()
    message(WARNING "Icon not found at ${XYSCAN_ICNS}.")
  endif()

  # Dev-time docs for running from the build tree.
  add_custom_command(TARGET xyscan POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E make_directory
            "$<TARGET_BUNDLE_CONTENT_DIR:xyscan>/Resources/docs"
  )
  if(EXISTS "${CMAKE_SOURCE_DIR}/docs")
    add_custom_command(TARGET xyscan POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy_directory
              "${CMAKE_SOURCE_DIR}/docs"
              "$<TARGET_BUNDLE_CONTENT_DIR:xyscan>/Resources/docs"
    )
  endif()

  # Dev-time localizations: copy any *.lproj we ship.
  foreach(LPROJ en.lproj fr.lproj de.lproj es.lproj)
    if(EXISTS "${CMAKE_SOURCE_DIR}/platforms/macOS/BundleItems/${LPROJ}")
      add_custom_command(TARGET xyscan POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory
                "${CMAKE_SOURCE_DIR}/platforms/macOS/BundleItems/${LPROJ}"
                "$<TARGET_BUNDLE_CONTENT_DIR:xyscan>/Resources/${LPROJ}"
      )
    endif()
  endforeach()
endif()

#=============================================================================
# Install rules (common)
#  - On macOS, install the .app bundle at CMAKE_INSTALL_PREFIX (DESTINATION .)
#  - On Unix/Linux, install the runtime to standard bin dir.
#=============================================================================
install(TARGETS xyscan
  BUNDLE  DESTINATION .
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

#=============================================================================
# macOS install-time extras (APPLE)
#  - Install docs, licenses, icon, and localizations inside the bundle.
#=============================================================================
if(APPLE)
  if(EXISTS "${CMAKE_SOURCE_DIR}/docs")
    install(DIRECTORY "${CMAKE_SOURCE_DIR}/docs/"
            DESTINATION "xyscan.app/Contents/Resources/docs")
  endif()

  foreach(LIC_FILE gpl.txt license.txt)
    if(EXISTS "${CMAKE_SOURCE_DIR}/platforms/${LIC_FILE}")
      install(FILES "${CMAKE_SOURCE_DIR}/platforms/${LIC_FILE}"
              DESTINATION "xyscan.app/Contents")
    endif()
  endforeach()

  if(EXISTS "${XYSCAN_ICNS}")
    install(FILES "${XYSCAN_ICNS}"
            DESTINATION "xyscan.app/Contents/Resources")
  endif()

  # Include es.lproj at install-time as well (matches dev-time).
  foreach(LPROJ en.lproj fr.lproj de.lproj es.lproj)
    if(EXISTS "${CMAKE_SOURCE_DIR}/platforms/macOS/BundleItems/${LPROJ}")
      install(DIRECTORY "${CMAKE_SOURCE_DIR}/platforms/macOS/BundleItems/${LPROJ}/"
              DESTINATION "xyscan.app/Contents/Resources/${LPROJ}")
    endif()
  endforeach()
endif()

#=============================================================================
# macOS deployment of Qt frameworks/plugins (APPLE)
#  - Run macdeployqt at install time so the installed .app is self-contained.
#  - We call qt_finalize_executable() here on macOS before macdeployqt.
#=============================================================================
if(APPLE)
  qt_finalize_executable(xyscan)

  set(_macdeployqt "")
  if(TARGET Qt6::qmake)
    get_target_property(_qmake_exe Qt6::qmake IMPORTED_LOCATION)
    if(_qmake_exe)
      get_filename_component(_qt_bin_dir "${_qmake_exe}" DIRECTORY)
      set(_macdeployqt "${_qt_bin_dir}/macdeployqt")
    endif()
  endif()
  if(NOT EXISTS "${_macdeployqt}")
    set(_macdeployqt "$ENV{MACDEPLOYQT}")
  endif()

  if(EXISTS "${_macdeployqt}")
    install(CODE "
      message(STATUS \"Running macdeployqt: ${_macdeployqt}\")
      execute_process(
        COMMAND \"${_macdeployqt}\" \"\${CMAKE_INSTALL_PREFIX}/xyscan.app\" -verbose=1
        RESULT_VARIABLE _rv
        OUTPUT_VARIABLE _out
        ERROR_VARIABLE  _err
      )
      if (NOT _rv EQUAL 0)
        message(FATAL_ERROR \"macdeployqt failed (\${_rv})\\n\${_out}\\n\${_err}\")
      endif()
    ")
  else()
    message(WARNING
      "macdeployqt not found. Installed app may miss Qt frameworks/plugins. "
      "Put macdeployqt on PATH or set MACDEPLOYQT=/path/to/macdeployqt."
    )
  endif()
endif()

#=============================================================================
# macOS packaging helper (APPLE)
#  - Convenience target to run your packaging script.
#=============================================================================
if(APPLE)
  set(PACKAGING_SCRIPT "${CMAKE_SOURCE_DIR}/platforms/macOS/mac_makePackage.sh")
  add_custom_target(mac_package
    COMMAND bash "${PACKAGING_SCRIPT}" "$<TARGET_FILE:xyscan>"
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
    USES_TERMINAL
    VERBATIM
  )
endif()

#=============================================================================
# Linux/Unix installs & desktop integration (UNIX AND NOT APPLE)
#  - Installs docs, licenses, translations, man page, .desktop entry,
#    hicolor icons (PNG + optional SVG), and runs cache refreshers when found.
#  - Configures CPack to build DEB/RPM/TGZ for distribution.
#=============================================================================
if(UNIX AND NOT APPLE)
  # Keep RPATH clean under /usr/local on most distros.
  set_target_properties(xyscan PROPERTIES BUILD_WITH_INSTALL_RPATH OFF)

  # Base dir for our Unix assets.
  set(XYSCAN_UNIX_DIR "${CMAKE_SOURCE_DIR}/platforms/unix")

  # Documentation.
  if(EXISTS "${CMAKE_SOURCE_DIR}/docs")
    install(DIRECTORY "${CMAKE_SOURCE_DIR}/docs/"
            DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/docs/xyscan")
  endif()

  # Licenses.
  foreach(LIC_FILE gpl.txt license.txt)
    if(EXISTS "${CMAKE_SOURCE_DIR}/platforms/${LIC_FILE}")
      install(FILES "${CMAKE_SOURCE_DIR}/platforms/${LIC_FILE}"
              DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/licenses/xyscan")
    endif()
  endforeach()

  # Optional install of generated .qm files (in addition to embedding via .qrc).
  if(DEFINED XYSCAN_QM_FILES AND XYSCAN_QM_FILES)
    install(FILES ${XYSCAN_QM_FILES}
            DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/xyscan/translations")
  endif()

  # Man page (section 1).
  set(XYSCAN_MANPAGE "${XYSCAN_UNIX_DIR}/xyscan.1")
  if(EXISTS "${XYSCAN_MANPAGE}")
    install(FILES "${XYSCAN_MANPAGE}"
            DESTINATION "${CMAKE_INSTALL_MANDIR}/man1")
  endif()

  # Desktop entry for menus/launchers.
  set(XYSCAN_DESKTOP "${XYSCAN_UNIX_DIR}/xyscan.desktop")
  if(EXISTS "${XYSCAN_DESKTOP}")
    install(FILES "${XYSCAN_DESKTOP}"
            DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications")
  endif()

  # hicolor icon theme — PNG sizes we ship.
  foreach(_sz 16 24 32 48 64 96 128 256 512)
    set(_icon_png "${XYSCAN_UNIX_DIR}/icons/hicolor/${_sz}x${_sz}/apps/xyscan.png")
    if(EXISTS "${_icon_png}")
      install(FILES "${_icon_png}"
              DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${_sz}x${_sz}/apps"
              RENAME xyscan.png)
    endif()
  endforeach()

  # hicolor scalable/symbolic SVG icons (if present).
  foreach(_subdir "scalable/apps" "symbolic/apps")
    set(_icon_svg "${XYSCAN_UNIX_DIR}/icons/hicolor/${_subdir}/xyscan.svg")
    if(EXISTS "${_icon_svg}")
      install(FILES "${_icon_svg}"
              DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${_subdir}")
    endif()
  endforeach()

  # Optional /usr/share/pixmaps fallback.
  set(_pixmap_png "${XYSCAN_UNIX_DIR}/icons/pixmaps/xyscan.png")
  if(EXISTS "${_pixmap_png}")
    install(FILES "${_pixmap_png}"
            DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/pixmaps")
  endif()

  # Safe desktop database refresh (no-op under DESTDIR).
  find_program(UPDATE_DESKTOP_DATABASE NAMES update-desktop-database)
  if(UPDATE_DESKTOP_DATABASE)
    install(CODE [===[
      if(NOT DEFINED ENV{DESTDIR})
        set(_apps_dir "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/applications")
        if(EXISTS "${_apps_dir}")
          message(STATUS "Updating desktop database in: ${_apps_dir}")
          execute_process(
            COMMAND "${UPDATE_DESKTOP_DATABASE}" "${_apps_dir}"
            RESULT_VARIABLE _rv
          )
          if(NOT _rv EQUAL 0)
            message(STATUS "update-desktop-database returned ${_rv} (non-fatal)")
          endif()
        else()
          message(STATUS "Skipping desktop database update (no ${_apps_dir})")
        endif()
      else()
        message(STATUS "Skipping desktop database update under DESTDIR")
      endif()
    ]===])
  endif()

  # Safe hicolor icon cache refresh (no-op if dir missing or under DESTDIR).
  find_program(GTK_UPDATE_ICON_CACHE NAMES gtk-update-icon-cache)
  if(GTK_UPDATE_ICON_CACHE)
    install(CODE [===[
      if(NOT DEFINED ENV{DESTDIR})
        set(_hicolor_dir "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor")
        # Run only if the dir exists AND has either index.theme or our app icons.
        if(EXISTS "${_hicolor_dir}" AND
           (EXISTS "${_hicolor_dir}/index.theme" OR EXISTS "${_hicolor_dir}/apps"))
          message(STATUS "Updating hicolor icon cache in: ${_hicolor_dir}")
          execute_process(
            COMMAND "${GTK_UPDATE_ICON_CACHE}" -q -t -f "${_hicolor_dir}"
            RESULT_VARIABLE _rv
          )
          if(NOT _rv EQUAL 0)
            message(STATUS "gtk-update-icon-cache returned ${_rv} (non-fatal)")
          endif()
        else()
          message(STATUS "Skipping icon cache update (no ${_hicolor_dir} or no theme/apps)")
        endif()
      else()
        message(STATUS "Skipping icon cache update under DESTDIR")
      endif()
    ]===])
  endif()

  # CPack configuration to generate binary packages:
  #  - DEB (Debian/Ubuntu), RPM (Fedora/openSUSE), TGZ (generic).
  set(CPACK_PACKAGE_NAME               "xyscan")
  set(CPACK_PACKAGE_VENDOR             "Thomas S. Ullrich")
  set(CPACK_PACKAGE_CONTACT            "thomas.s.ullrich@gmail.com")  # TODO: set a real contact
  set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "xyscan: Data thief for scientists")
  set(CPACK_PACKAGE_VERSION            ${PROJECT_VERSION})
  set(CPACK_GENERATOR                  "DEB;RPM;TGZ")
  set(CPACK_DEBIAN_PACKAGE_SECTION     "science")
  set(CPACK_DEBIAN_PACKAGE_HOMEPAGE    "https://rhig.physics.yale.edu/~ullrich/software/xyscan/index.html")  # TODO: set real homepage
  set(CPACK_RPM_PACKAGE_LICENSE        "GPL-3.0-or-later")

  include(CPack)
endif()

#=============================================================================
# Version stamping (keep visible version synchronized everywhere)
#  - Reads version from the freshly built binary:  xyscan --numeric-version
#  - Stamps text into:
#      * ${CMAKE_BINARY_DIR}/xyscan/platforms/license.txt  (build tree)
#      * (macOS only) the bundle Info.plist
#  - The stamping script expects @VERSION@ in the templates where version goes.
#=============================================================================
if(TARGET xyscan)
  # Template input and stamped output locations for the license file.
  set(XYSCAN_LICENSE_SRC "${CMAKE_SOURCE_DIR}/platforms/license.txt")
  set(XYSCAN_LICENSE_OUT "${CMAKE_BINARY_DIR}/xyscan/platforms/license.txt")

  # Post-build stamping step (macOS only: also updates Info.plist in-bundle).
  if(APPLE)
    add_custom_command(TARGET xyscan POST_BUILD
      COMMAND ${CMAKE_COMMAND}
        -DAPP_BIN="$<TARGET_FILE:xyscan>"
        -DLICENSE_SRC="${XYSCAN_LICENSE_SRC}"
        -DLICENSE_DST="${XYSCAN_LICENSE_OUT}"
        $<$<BOOL:APPLE>:-DPLIST_INBUNDLE=$<TARGET_BUNDLE_CONTENT_DIR:xyscan>/Info.plist>
        $<$<BOOL:APPLE>:-DPLIST_TEMPLATE=${CMAKE_SOURCE_DIR}/platforms/macOS/BundleItems/Info.plist>
        -P "${CMAKE_SOURCE_DIR}/cmake/update_version.cmake"
      COMMENT "Stamping version using xyscan --numeric-version"
    )

    # After stamping, copy the generated license into the bundle (build tree).
    add_custom_command(TARGET xyscan POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy
              "${XYSCAN_LICENSE_OUT}"
              "$<TARGET_BUNDLE_CONTENT_DIR:xyscan>/license.txt"
    )

    # Install the stamped license inside the .app payload at install time.
    install(FILES ${XYSCAN_LICENSE_OUT} DESTINATION "xyscan.app/Contents")
  endif()
else()
  message(WARNING "xyscan target not found; skipping version-stamping block.")
endif()

