# 3.19 for COMMAND_ERROR_IS_FATAL; 3.31 opts in to CMP0177, which quiets
# install() about the prefix-root destinations below.
cmake_minimum_required(VERSION 3.19...3.31)

# Override the compiler on a fresh build directory:
#   CXX=clang++ cmake -B build -DQTEA_CORE_DIR=...
project(qtea LANGUAGES CXX)

message(STATUS "qtea: compiler = ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} (${CMAKE_CXX_COMPILER})")

# OBJCXX is detected independently of CXX; a mixed pair gives objects LTO
# cannot combine, so pin it unless asked otherwise.
if(APPLE)
    if(NOT DEFINED CMAKE_OBJCXX_COMPILER AND NOT DEFINED ENV{OBJCXX})
        set(CMAKE_OBJCXX_COMPILER "${CMAKE_CXX_COMPILER}")
    endif()
    enable_language(OBJCXX)
endif()

# The core's static build output directory: jg-static.mk and lib<name>-jg.a.
set(QTEA_CORE_DIR "" CACHE PATH "Path to the core's static build directory (contains jg-static.mk and lib<name>-jg.a)")

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE "Release" CACHE STRING
        "Build type: Debug, Release, RelWithDebInfo, MinSizeRel" FORCE)
    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
        "Debug" "Release" "RelWithDebInfo" "MinSizeRel")
endif()
message(STATUS "qtea: build type = ${CMAKE_BUILD_TYPE}")

# The core must be built with the same sanitizer to be covered.
set(QTEA_SANITIZE "" CACHE STRING
    "Sanitizers to enable, comma-separated (e.g. address, address,undefined, thread)")

# QTea's own objects only; the core archive links in as built, bitcode or
# not, so a bitcode core needs a linker that reads it (lld, Apple ld).
option(QTEA_LTO "Enable link-time optimization" OFF)

# Packaging identity, all optional; defaults come from the core.
set(QTEA_APP_NAME "" CACHE STRING
    "Display name for the desktop entry and macOS bundle; empty keeps the one the core's .desktop declares")
set(QTEA_APP_ID "" CACHE STRING
    "Reverse-DNS application identifier; empty means org.jgemu.<core>")
set(QTEA_CORE_VERSION "" CACHE STRING
    "Version of the linked core; empty means read it from the core's version.h")
set(QTEA_PROJECT_LICENSE "BSD-3-Clause" CACHE STRING
    "SPDX expression covering the built binary. QTea is BSD-3-Clause, but the core is linked into it and not every core is")
set(QTEA_RELEASE_DATE "" CACHE STRING
    "Release date for the AppStream metadata, YYYY-MM-DD. Empty omits the releases block, since reading the clock here would make builds unreproducible")

if(NOT QTEA_CORE_DIR)
    message(FATAL_ERROR
        "QTEA_CORE_DIR is not set. Pass -DQTEA_CORE_DIR=/path/to/<core>/<core> "
        "pointing at the directory that contains jg-static.mk and lib<name>-jg.a.")
endif()

get_filename_component(QTEA_CORE_DIR "${QTEA_CORE_DIR}" ABSOLUTE)

set(QTEA_CORE_MK "${QTEA_CORE_DIR}/jg-static.mk")
if(NOT EXISTS "${QTEA_CORE_MK}")
    message(FATAL_ERROR "jg-static.mk not found at ${QTEA_CORE_MK}")
endif()

# jg-static.mk is "KEY := value" lines; five keys matter.
file(STRINGS "${QTEA_CORE_MK}" QTEA_CORE_MK_LINES)

set(QTEA_CORE_NAME         "")
set(QTEA_CORE_ICONS        "")
set(QTEA_CORE_ASSETS       "")
set(QTEA_CORE_FLAGS_STATIC "")
set(QTEA_CORE_LIBS_STATIC  "")

foreach(line IN LISTS QTEA_CORE_MK_LINES)
    if(line MATCHES "^[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*[:+]?=[ \t]*(.*)$")
        set(_key "${CMAKE_MATCH_1}")
        set(_val "${CMAKE_MATCH_2}")
        string(STRIP "${_val}" _val)
        if(_key STREQUAL "NAME")
            set(QTEA_CORE_NAME "${_val}")
        elseif(_key STREQUAL "ICONS")
            string(REGEX REPLACE "[ \t]+" ";" QTEA_CORE_ICONS "${_val}")
        elseif(_key STREQUAL "ASSETS")
            string(REGEX REPLACE "[ \t]+" ";" QTEA_CORE_ASSETS "${_val}")
        elseif(_key STREQUAL "FLAGS_STATIC")
            set(QTEA_CORE_FLAGS_STATIC "${_val}")
        elseif(_key STREQUAL "LIBS_STATIC")
            set(QTEA_CORE_LIBS_STATIC "${_val}")
        endif()
    endif()
endforeach()

if(NOT QTEA_CORE_NAME)
    message(FATAL_ERROR "NAME not defined in ${QTEA_CORE_MK}")
endif()

# The executable, desktop entry, icons and app ID use the core name minus
# the -jg suffix some cores carry; the archive always has it.
string(REGEX REPLACE "-jg$" "" QTEA_CORE_BINARY_NAME "${QTEA_CORE_NAME}")
set(QTEA_CORE_ARCHIVE "${QTEA_CORE_DIR}/lib${QTEA_CORE_BINARY_NAME}-jg.a")

if(NOT EXISTS "${QTEA_CORE_ARCHIVE}")
    message(FATAL_ERROR "Core static archive not found: ${QTEA_CORE_ARCHIVE}")
endif()

message(STATUS "qtea: linking against core '${QTEA_CORE_NAME}' (${QTEA_CORE_ARCHIVE})")

if(QTEA_CORE_FLAGS_STATIC)
    message(STATUS "qtea: core FLAGS_STATIC = ${QTEA_CORE_FLAGS_STATIC}")
endif()
if(QTEA_CORE_LIBS_STATIC)
    message(STATUS "qtea: core LIBS_STATIC  = ${QTEA_CORE_LIBS_STATIC}")
endif()

include(GNUInstallDirs)

# Reads the VERSION define out of a Jolly Good version.h, QTea's or a core's.
function(qtea_read_version path out)
    set(_version "")
    if(EXISTS "${path}")
        file(READ "${path}" _text)
        if(_text MATCHES "#define[ \t]+[A-Z_]*VERSION[ \t]+\"([^\"]+)\"")
            set(_version "${CMAKE_MATCH_1}")
        endif()
    endif()
    set(${out} "${_version}" PARENT_SCOPE)
endfunction()

qtea_read_version("${CMAKE_SOURCE_DIR}/src/version.h" QTEA_VERSION)
if(NOT QTEA_VERSION)
    message(FATAL_ERROR "Could not read QTEA_VERSION from src/version.h")
endif()
message(STATUS "qtea: frontend version = ${QTEA_VERSION}")

# The packaged version is the core's: the binary is the emulator. The
# frontend's stays in --version and About. jg-static.mk lacks it, but every
# Jolly Good project keeps a version.h above the static output directory.
set(QTEA_APP_VERSION "${QTEA_CORE_VERSION}")

if(NOT QTEA_APP_VERSION)
    get_filename_component(_qtea_core_tree "${QTEA_CORE_DIR}" DIRECTORY)
    foreach(_cand "${_qtea_core_tree}/version.h" "${QTEA_CORE_DIR}/version.h")
        if(NOT QTEA_APP_VERSION)
            qtea_read_version("${_cand}" QTEA_APP_VERSION)
        endif()
    endforeach()
endif()

if(QTEA_APP_VERSION)
    message(STATUS "qtea: core version = ${QTEA_APP_VERSION}")
else()
    # Wrong, but not empty.
    set(QTEA_APP_VERSION "${QTEA_VERSION}")
    message(WARNING "qtea: no version.h found for core '${QTEA_CORE_NAME}'; "
        "packaging metadata will report the frontend version "
        "${QTEA_VERSION} instead. Set QTEA_CORE_VERSION to override.")
endif()

# Resolved, not cached: retargeting QTEA_CORE_DIR must not leave a stale id.
if(NOT QTEA_APP_ID)
    set(QTEA_APP_ID "org.jgemu.${QTEA_CORE_BINARY_NAME}")
endif()

set(QTEA_HOMEPAGE "https://gitlab.jgemu.io")

# For a Qt outside the system library path (/opt, an installer prefix).
# Implicit linker directories are excluded, so a distro build gets no RPATH.
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

# Qt first: a package referencing Qt6Gui before Qt6Config.cmake has run
# fails with "Unknown CMake command". 6.3 for qt_standard_project_setup.
find_package(Qt6 6.3 REQUIRED COMPONENTS
    Core
    Gui
    Widgets
    OpenGL
    OpenGLWidgets
)
qt_standard_project_setup()

find_package(SDL3 REQUIRED CONFIG COMPONENTS SDL3)
find_package(PkgConfig REQUIRED)

# Homebrew's pkg-config searches only its own prefix; the JG API installs to
# /usr/local. A non-default JG prefix goes in PKG_CONFIG_PATH instead.
if(APPLE)
    set(ENV{PKG_CONFIG_PATH}
        "/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig:$ENV{PKG_CONFIG_PATH}")
endif()

pkg_check_modules(JG         REQUIRED IMPORTED_TARGET jg)
pkg_check_modules(EPOXY      REQUIRED IMPORTED_TARGET epoxy)
pkg_check_modules(SPEEXDSP   REQUIRED IMPORTED_TARGET speexdsp)

if(APPLE)
    # Homebrew is the supported macOS toolchain; its libarchive is keg-only.
    # Another package manager passes -DLibArchive_INCLUDE_DIR/_LIBRARY.
    execute_process(
        COMMAND brew --prefix libarchive
        OUTPUT_VARIABLE LIBARCHIVE_PREFIX
        OUTPUT_STRIP_TRAILING_WHITESPACE
        COMMAND_ERROR_IS_FATAL ANY
    )
    set(LibArchive_INCLUDE_DIR "${LIBARCHIVE_PREFIX}/include")
endif()
find_package(LibArchive REQUIRED)

qt_add_executable(${PROJECT_NAME}
    src/audiomanager.cpp src/audiomanager.h
    src/chtmanager.cpp src/chtmanager.h
    src/parson.cpp src/parson.h
    src/qtea_about.cpp src/qtea_about.h
    src/qtea_archive.cpp src/qtea_archive.h
    src/qtea_bios.cpp src/qtea_bios.h
    src/qtea_cheats.cpp src/qtea_cheats.h
    src/qtea_detect.cpp src/qtea_detect.h
    src/qtea_dipswitches.cpp src/qtea_dipswitches.h
    src/qtea_emulator.cpp src/qtea_emulator.h
    src/qtea_glarea.cpp
    src/qtea_main.cpp
    src/qtea_menu.cpp
    src/qtea_openmultiple.cpp src/qtea_openmultiple.h
    src/qtea_inputcodes.h
    src/qtea_parse.h
    src/qtea_paths.cpp src/qtea_paths.h
    src/qtea_settings.cpp src/qtea_settings.h
    src/qtea_theme.cpp src/qtea_theme.h
    src/qtea_window.cpp src/qtea_window.h
    src/ini.h
    src/hasher.cpp src/hasher.h
    src/crc32.cpp src/crc32.h
    src/md5.cpp src/md5.h
    src/inputmanager.cpp src/inputmanager.h
    src/jgmanager.cpp src/jgmanager.h
    src/lodepng.cpp src/lodepng.h
    src/logdriver.cpp src/logdriver.h
    src/setmanager.cpp src/setmanager.h
    src/uiadapter.cpp src/uiadapter.h
    src/version.h
    src/videomanager.cpp src/videomanager.h
    src/qtea_osd.cpp src/qtea_osd.h
)

# CADisplayLink frame pacer (macOS 14+ behind @available) and the
# application-menu name.
if(APPLE)
    target_sources(${PROJECT_NAME} PRIVATE
        src/qtea_displaylink.mm src/qtea_displaylink.h
        src/qtea_macos.mm src/qtea_macos.h)
endif()

# On macOS the output name is the .app name Finder shows.
if(APPLE AND QTEA_APP_NAME)
    set(QTEA_OUTPUT_NAME "${QTEA_APP_NAME}")
else()
    set(QTEA_OUTPUT_NAME "${QTEA_CORE_BINARY_NAME}")
endif()

set_target_properties(${PROJECT_NAME} PROPERTIES
    OUTPUT_NAME "${QTEA_OUTPUT_NAME}"
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    POSITION_INDEPENDENT_CODE TRUE
)

if(APPLE)
    set_target_properties(${PROJECT_NAME} PROPERTIES
        OBJCXX_STANDARD 17
        OBJCXX_STANDARD_REQUIRED ON
    )
endif()

# The GUI subsystem, or Windows hands the process a console window of its
# own whenever Explorer starts it.
if(WIN32)
    set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE TRUE)
endif()

# Release has always built at -O2; say so rather than rely on flag order.
string(REPLACE "-O3" "-O2"
       CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")

# Only what CMake's per-config flags lack. -g3 keeps macro definitions.
target_compile_options(${PROJECT_NAME} PRIVATE
    $<$<CONFIG:Debug>:-O0 -g3 -fno-omit-frame-pointer>
    $<$<CONFIG:RelWithDebInfo>:-fno-omit-frame-pointer>
    -Wall -Wextra -Wshadow
)

# Keeps this machine's paths out of debug info; gdb's `set substitute-path`
# or lldb's `target.source-map` finds the sources again. Binary dir first,
# so a build tree nested in the source tree matches the longer prefix.
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-ffile-prefix-map=/a=/b" QTEA_HAS_FILE_PREFIX_MAP)

if(QTEA_HAS_FILE_PREFIX_MAP)
    target_compile_options(${PROJECT_NAME} PRIVATE
        "-ffile-prefix-map=${CMAKE_BINARY_DIR}=."
        "-ffile-prefix-map=${CMAKE_SOURCE_DIR}=."
    )
else()
    message(STATUS "qtea: no -ffile-prefix-map; debug builds carry "
        "absolute paths")
endif()

if(QTEA_LTO)
    include(CheckIPOSupported)
    check_ipo_supported(RESULT _qtea_ipo_ok OUTPUT _qtea_ipo_msg LANGUAGES CXX)
    if(NOT _qtea_ipo_ok)
        message(FATAL_ERROR "qtea: QTEA_LTO is set but this toolchain cannot do "
            "it. Clang needs a linker with a bitcode plugin (lld, gold, or the "
            "Apple linker); GCC needs its matching binutils plugin.\n${_qtea_ipo_msg}")
    endif()
    set_target_properties(${PROJECT_NAME} PROPERTIES
        INTERPROCEDURAL_OPTIMIZATION TRUE)
    message(STATUS "qtea: link-time optimization enabled")
    if(CMAKE_BUILD_TYPE STREQUAL "Debug")
        message(WARNING "qtea: LTO on a Debug build, which compiles at -O0 and "
            "leaves it almost nothing to do. Set -DCMAKE_BUILD_TYPE=Release.")
    endif()
endif()

if(QTEA_SANITIZE)
    message(STATUS "qtea: enabling sanitizers: ${QTEA_SANITIZE}")
    target_compile_options(${PROJECT_NAME} PRIVATE
        -fsanitize=${QTEA_SANITIZE} -fno-omit-frame-pointer)
    target_link_options(${PROJECT_NAME} PRIVATE
        -fsanitize=${QTEA_SANITIZE})
endif()

target_include_directories(${PROJECT_NAME} PRIVATE
    src/
)

target_link_libraries(${PROJECT_NAME} PRIVATE
    Qt6::Core
    Qt6::Gui
    Qt6::Widgets
    Qt6::OpenGL
    Qt6::OpenGLWidgets
    SDL3::SDL3
    PkgConfig::JG
    PkgConfig::EPOXY
    PkgConfig::SPEEXDSP
    LibArchive::LibArchive
)

# CADisplayLink is in QuartzCore; displayLinkWithTarget:selector: is AppKit.
if(APPLE)
    target_link_libraries(${PROJECT_NAME} PRIVATE
        "-framework AppKit"
        "-framework QuartzCore"
    )
endif()

# The core, plus the flags and libs its jg-static.mk declares.
target_link_libraries(${PROJECT_NAME} PRIVATE "${QTEA_CORE_ARCHIVE}")

if(QTEA_CORE_FLAGS_STATIC)
    separate_arguments(_qtea_core_flags NATIVE_COMMAND "${QTEA_CORE_FLAGS_STATIC}")
    target_link_options(${PROJECT_NAME} PRIVATE ${_qtea_core_flags})
endif()

if(QTEA_CORE_LIBS_STATIC)
    separate_arguments(_qtea_core_libs NATIVE_COMMAND "${QTEA_CORE_LIBS_STATIC}")
    target_link_libraries(${PROJECT_NAME} PRIVATE ${_qtea_core_libs})
endif()

target_compile_definitions(${PROJECT_NAME} PRIVATE
    QTEA_CORE="${QTEA_CORE_NAME}"
    # For the usage text.
    QTEA_BINARY="${QTEA_OUTPUT_NAME}"
    # One of the roots qtea::paths searches.
    QTEA_DATAROOTDIR="${CMAKE_INSTALL_FULL_DATADIR}"
)

# Flat icons/ for find_core_icon(): QTea's fallbacks, then the core's.
set(QTEA_ICON_DST "${CMAKE_BINARY_DIR}/icons")
file(MAKE_DIRECTORY "${QTEA_ICON_DST}")

set(QTEA_FALLBACK_ICON_SRC "${CMAKE_SOURCE_DIR}/icons")

if(EXISTS "${QTEA_FALLBACK_ICON_SRC}")
    file(GLOB QTEA_FALLBACK_ICON_FILES "${QTEA_FALLBACK_ICON_SRC}/*")
    file(COPY ${QTEA_FALLBACK_ICON_FILES} DESTINATION "${QTEA_ICON_DST}")
    message(STATUS "Copied qtea fallback icons -> ${QTEA_ICON_DST}")
else()
    message(WARNING "qtea fallback icons not found at ${QTEA_FALLBACK_ICON_SRC}")
endif()

# ICONS is the list, not a glob: stray files must not leak into the install.
set(QTEA_ICON_SRC "${QTEA_CORE_DIR}/icons")

if(QTEA_CORE_ICONS)
    set(_copied 0)
    foreach(icon IN LISTS QTEA_CORE_ICONS)
        set(_icon_path "${QTEA_ICON_SRC}/${icon}")
        if(EXISTS "${_icon_path}")
            file(COPY "${_icon_path}" DESTINATION "${QTEA_ICON_DST}")
            math(EXPR _copied "${_copied} + 1")
        else()
            message(WARNING "Icon listed in jg-static.mk but not found: ${_icon_path}")
        endif()
    endforeach()
    message(STATUS "Copied ${_copied} icon(s) for core '${QTEA_CORE_NAME}' -> ${QTEA_ICON_DST}")
else()
    message(STATUS "Core '${QTEA_CORE_NAME}' declares no ICONS in jg-static.mk")
endif()

# ASSETS are relative to the core's output dir, subdirectories included.
set(QTEA_ASSET_SRC "${QTEA_CORE_DIR}")

if(QTEA_CORE_ASSETS)
    set(_copied 0)
    foreach(asset IN LISTS QTEA_CORE_ASSETS)
        set(_asset_src "${QTEA_ASSET_SRC}/${asset}")
        get_filename_component(_asset_subdir "${asset}" DIRECTORY)
        set(_asset_dst_dir "${CMAKE_BINARY_DIR}/${_asset_subdir}")
        if(EXISTS "${_asset_src}")
            file(MAKE_DIRECTORY "${_asset_dst_dir}")
            file(COPY "${_asset_src}" DESTINATION "${_asset_dst_dir}")
            math(EXPR _copied "${_copied} + 1")
        else()
            message(WARNING "Asset listed in jg-static.mk but not found: ${_asset_src}")
        endif()
    endforeach()
    message(STATUS "Copied ${_copied} asset(s) for core '${QTEA_CORE_NAME}' -> ${CMAKE_BINARY_DIR}")
endif()

# The upstream licences the About dialog shows: beside the core's output
# dir, or one level up where a repository keeps it.
foreach(_lic "${QTEA_CORE_DIR}/LICENSES" "${QTEA_CORE_DIR}/../LICENSES")
    if(EXISTS "${_lic}" AND NOT QTEA_CORE_LICENSES)
        get_filename_component(QTEA_CORE_LICENSES "${_lic}" ABSOLUTE)
    endif()
endforeach()

if(QTEA_CORE_LICENSES)
    file(COPY "${QTEA_CORE_LICENSES}" DESTINATION "${CMAKE_BINARY_DIR}")
    message(STATUS "Copied LICENSES -> ${CMAKE_BINARY_DIR}")
else()
    message(WARNING
        "LICENSES not found for core '${QTEA_CORE_NAME}'; the About dialog's "
        "Licenses tab will be empty.")
endif()

# Licences of the libraries shipped alongside the binary: beside it on
# Windows, inside the bundle on macOS. Elsewhere these are system packages
# carrying their own. Direct dependencies only; transitive ones are not
# covered.
set(QTEA_DEP_LICENSES
    "${CMAKE_SOURCE_DIR}/Qt-LGPL-3.0.txt"
    "${CMAKE_SOURCE_DIR}/Qt-GPL-3.0.txt"
    "${CMAKE_SOURCE_DIR}/SDL-Zlib.txt"
    "${CMAKE_SOURCE_DIR}/SpeexDSP-BSD-3-Clause.txt"
    "${CMAKE_SOURCE_DIR}/libarchive-BSD-2-Clause.txt")
if(WIN32)
    file(COPY ${QTEA_DEP_LICENSES} DESTINATION "${CMAKE_BINARY_DIR}")
endif()

# The application icon set: the core's, else QTea's.
if(QTEA_CORE_ICONS)
    set(QTEA_APP_ICON_DIR "${QTEA_ICON_SRC}")
    set(QTEA_APP_ICONS ${QTEA_CORE_ICONS})
else()
    set(QTEA_APP_ICON_DIR "${QTEA_FALLBACK_ICON_SRC}")
    file(GLOB QTEA_APP_ICONS RELATIVE "${QTEA_FALLBACK_ICON_SRC}"
         "${QTEA_FALLBACK_ICON_SRC}/*.png" "${QTEA_FALLBACK_ICON_SRC}/*.svg")
endif()

# Beside the binary: the first root qtea::paths searches.
set(QTEA_SHADER_SRC "${CMAKE_SOURCE_DIR}/shaders")
set(QTEA_SHADER_DST "${CMAKE_BINARY_DIR}/shaders")

if(EXISTS "${QTEA_SHADER_SRC}")
    file(COPY "${QTEA_SHADER_SRC}/" DESTINATION "${QTEA_SHADER_DST}")
    message(STATUS "Copied shaders -> ${QTEA_SHADER_DST}")
else()
    message(WARNING "Shaders directory not found at ${QTEA_SHADER_SRC}")
endif()

# ---------------------------------------------------------------------------
# Application metadata
# ---------------------------------------------------------------------------
# The core's .desktop describes the system it emulates, which nothing here
# knows, so it feeds both the desktop entry and the AppStream metadata; only
# the fields naming the executable are rewritten.

# Rewrite one "Key=value" line, or append it.
function(qtea_desktop_set var key value)
    set(_text "${${var}}")
    if(_text MATCHES "(^|\n)${key}=")
        string(REGEX REPLACE "(^|\n)${key}=[^\n]*" "\\1${key}=${value}"
               _text "${_text}")
    else()
        string(APPEND _text "${key}=${value}\n")
    endif()
    set(${var} "${_text}" PARENT_SCOPE)
endfunction()

# Read one "Key=value" line; a commented-out key does not match.
function(qtea_desktop_get var key out)
    set(_value "")
    if("${${var}}" MATCHES "(^|\n)${key}=([^\n]*)")
        set(_value "${CMAKE_MATCH_2}")
    endif()
    set(${out} "${_value}" PARENT_SCOPE)
endfunction()

# Ampersand first.
function(qtea_xml_escape var)
    set(_text "${${var}}")
    string(REPLACE "&" "&amp;" _text "${_text}")
    string(REPLACE "<" "&lt;"  _text "${_text}")
    string(REPLACE ">" "&gt;"  _text "${_text}")
    set(${var} "${_text}" PARENT_SCOPE)
endfunction()

# A semicolon-separated value as an AppStream element list; empty gives none.
function(qtea_xml_list var wrapper item out)
    set(_body "")
    foreach(_entry IN LISTS ${var})
        if(_entry)
            qtea_xml_escape(_entry)
            string(APPEND _body "    <${item}>${_entry}</${item}>\n")
        endif()
    endforeach()
    if(_body)
        set(_body "  <${wrapper}>\n${_body}  </${wrapper}>\n")
    endif()
    set(${out} "${_body}" PARENT_SCOPE)
endfunction()

set(QTEA_DESKTOP_SRC "")
foreach(_cand "${QTEA_CORE_DIR}/${QTEA_CORE_NAME}.desktop"
              "${QTEA_CORE_DIR}/${QTEA_CORE_BINARY_NAME}.desktop")
    if(EXISTS "${_cand}" AND NOT QTEA_DESKTOP_SRC)
        set(QTEA_DESKTOP_SRC "${_cand}")
    endif()
endforeach()

if(QTEA_DESKTOP_SRC)
    file(READ "${QTEA_DESKTOP_SRC}" QTEA_DESKTOP_TEXT)
    message(STATUS "qtea: metadata based on ${QTEA_DESKTOP_SRC}")
else()
    # A core without one.
    set(QTEA_DESKTOP_TEXT "[Desktop Entry]\nType=Application\nVersion=1.0\n\
Name=${QTEA_CORE_BINARY_NAME}\nTerminal=false\nCategories=Game;Emulator;\n\
Keywords=emulator;\n")
    message(STATUS "qtea: no .desktop found in ${QTEA_CORE_DIR}, "
        "generating a minimal one")
endif()

# So an appended key starts its own line.
if(NOT QTEA_DESKTOP_TEXT MATCHES "\n$")
    string(APPEND QTEA_DESKTOP_TEXT "\n")
endif()

if(QTEA_APP_NAME)
    qtea_desktop_set(QTEA_DESKTOP_TEXT "Name" "${QTEA_APP_NAME}")
endif()

# Absolute, for a prefix off PATH. Icon and StartupWMClass must match the
# WM_CLASS Qt derives from the executable name.
qtea_desktop_set(QTEA_DESKTOP_TEXT "Exec"
    "${CMAKE_INSTALL_FULL_BINDIR}/${QTEA_CORE_BINARY_NAME} %f")
qtea_desktop_set(QTEA_DESKTOP_TEXT "TryExec"
    "${CMAKE_INSTALL_FULL_BINDIR}/${QTEA_CORE_BINARY_NAME}")
qtea_desktop_set(QTEA_DESKTOP_TEXT "Icon"           "${QTEA_CORE_BINARY_NAME}")
qtea_desktop_set(QTEA_DESKTOP_TEXT "StartupWMClass" "${QTEA_CORE_BINARY_NAME}")

# The core's file says NoDisplay: in jgrf a core is a plugin. Here it is
# the application.
qtea_desktop_set(QTEA_DESKTOP_TEXT "NoDisplay" "false")

# Shared by the desktop entry, AppStream metadata and bundles.
qtea_desktop_get(QTEA_DESKTOP_TEXT "Name"        QTEA_META_NAME)
qtea_desktop_get(QTEA_DESKTOP_TEXT "GenericName" QTEA_META_GENERIC)
qtea_desktop_get(QTEA_DESKTOP_TEXT "Comment"     QTEA_META_COMMENT)
qtea_desktop_get(QTEA_DESKTOP_TEXT "Categories"  QTEA_META_CATEGORIES)
qtea_desktop_get(QTEA_DESKTOP_TEXT "Keywords"    QTEA_META_KEYWORDS)
qtea_desktop_get(QTEA_DESKTOP_TEXT "MimeType"    QTEA_META_MIMETYPES)

if(NOT QTEA_META_NAME)
    set(QTEA_META_NAME "${QTEA_CORE_BINARY_NAME}")
endif()

# AppStream summaries take no trailing full stop.
if(QTEA_META_COMMENT)
    set(QTEA_META_SUMMARY "${QTEA_META_COMMENT}")
elseif(QTEA_META_GENERIC)
    set(QTEA_META_SUMMARY "${QTEA_META_GENERIC}")
else()
    set(QTEA_META_SUMMARY "Emulator built on the QTea frontend")
endif()
string(REGEX REPLACE "\\.$" "" QTEA_META_SUMMARY "${QTEA_META_SUMMARY}")

# ---------------------------------------------------------------------------
# macOS application bundle
# ---------------------------------------------------------------------------
if(APPLE)
    # What the menu bar and Get Info show.
    if(QTEA_APP_NAME)
        set(QTEA_BUNDLE_NAME "${QTEA_APP_NAME}")
    else()
        set(QTEA_BUNDLE_NAME "${QTEA_META_NAME}")
    endif()

    if(CMAKE_OSX_DEPLOYMENT_TARGET)
        set(QTEA_MACOS_MIN "${CMAKE_OSX_DEPLOYMENT_TARGET}")
    else()
        set(QTEA_MACOS_MIN "11.0")
    endif()

    configure_file("${CMAKE_SOURCE_DIR}/cmake/Info.plist.in"
                   "${CMAKE_BINARY_DIR}/Info.plist" @ONLY)

    set_target_properties(${PROJECT_NAME} PROPERTIES
        MACOSX_BUNDLE TRUE
        MACOSX_BUNDLE_INFO_PLIST "${CMAKE_BINARY_DIR}/Info.plist"
        # Built with the install RPATH so `install` never rewrites what
        # macdeployqt fixed up. The link paths keep an undeployed build
        # running; the bundle entry takes over once frameworks are copied in.
        BUILD_WITH_INSTALL_RPATH TRUE
        INSTALL_RPATH "@executable_path/../Frameworks"
    )

    set(QTEA_ICNS_SOURCES "")
    foreach(icon IN LISTS QTEA_APP_ICONS)
        if(icon MATCHES "\\.png$" AND EXISTS "${QTEA_APP_ICON_DIR}/${icon}")
            list(APPEND QTEA_ICNS_SOURCES "${QTEA_APP_ICON_DIR}/${icon}")
        endif()
    endforeach()

    if(QTEA_ICNS_SOURCES)
        set(QTEA_ICNS "${CMAKE_BINARY_DIR}/${QTEA_OUTPUT_NAME}.icns")

        add_custom_command(
            OUTPUT "${QTEA_ICNS}"
            COMMAND "${CMAKE_COMMAND}"
                    -P "${CMAKE_SOURCE_DIR}/cmake/make_icns.cmake"
                    "${QTEA_ICNS}"
                    "${CMAKE_BINARY_DIR}/qtea.iconset"
                    ${QTEA_ICNS_SOURCES}
            DEPENDS ${QTEA_ICNS_SOURCES}
                    "${CMAKE_SOURCE_DIR}/cmake/make_icns.cmake"
            COMMENT "Building ${QTEA_OUTPUT_NAME}.icns"
            VERBATIM
        )

        # A package location makes it a resource, not a source.
        target_sources(${PROJECT_NAME} PRIVATE "${QTEA_ICNS}")
        set_source_files_properties("${QTEA_ICNS}" PROPERTIES
            GENERATED TRUE
            MACOSX_PACKAGE_LOCATION "Resources")
    else()
        message(WARNING "qtea: no icons; the bundle gets a generic one")
    endif()

    # The only root that resolves inside a bundle: Finder starts the process
    # in "/" and there is no <prefix>/share.
    set(QTEA_BUNDLE_RES "$<TARGET_BUNDLE_CONTENT_DIR:${PROJECT_NAME}>/Resources")

    # From the staged copies, so it cannot drift from the other installs.
    set(_stage
        COMMAND "${CMAKE_COMMAND}" -E copy_directory
                "${QTEA_SHADER_DST}" "${QTEA_BUNDLE_RES}/shaders"
        COMMAND "${CMAKE_COMMAND}" -E copy_directory
                "${QTEA_ICON_DST}" "${QTEA_BUNDLE_RES}/icons")

    list(APPEND _stage COMMAND "${CMAKE_COMMAND}" -E copy_if_different
         "${CMAKE_SOURCE_DIR}/LICENSE" "${QTEA_BUNDLE_RES}/LICENSE")
    foreach(_lic IN LISTS QTEA_DEP_LICENSES)
        get_filename_component(_name "${_lic}" NAME)
        list(APPEND _stage COMMAND "${CMAKE_COMMAND}" -E copy_if_different
             "${_lic}" "${QTEA_BUNDLE_RES}/${_name}")
    endforeach()

    if(QTEA_CORE_LICENSES)
        list(APPEND _stage COMMAND "${CMAKE_COMMAND}" -E copy_if_different
             "${CMAKE_BINARY_DIR}/LICENSES" "${QTEA_BUNDLE_RES}/LICENSES")
    endif()

    foreach(asset IN LISTS QTEA_CORE_ASSETS)
        if(EXISTS "${QTEA_ASSET_SRC}/${asset}")
            get_filename_component(_asset_subdir "${asset}" DIRECTORY)
            list(APPEND _stage
                 COMMAND "${CMAKE_COMMAND}" -E make_directory
                         "${QTEA_BUNDLE_RES}/${_asset_subdir}"
                 COMMAND "${CMAKE_COMMAND}" -E copy_if_different
                         "${CMAKE_BINARY_DIR}/${asset}"
                         "${QTEA_BUNDLE_RES}/${asset}")
        endif()
    endforeach()

    add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD ${_stage}
        COMMENT "Staging runtime data into the bundle" VERBATIM)

    # Copies Qt and the Homebrew dylibs in and rewrites their install names.
    get_target_property(_qtea_qmake Qt6::qmake IMPORTED_LOCATION)
    get_filename_component(_qtea_qt_bin "${_qtea_qmake}" DIRECTORY)
    find_program(QTEA_MACDEPLOYQT NAMES macdeployqt HINTS "${_qtea_qt_bin}")

    if(QTEA_MACDEPLOYQT)
        add_custom_target(bundle
            COMMAND "${QTEA_MACDEPLOYQT}"
                    "$<TARGET_BUNDLE_DIR:${PROJECT_NAME}>"
                    -always-overwrite
            COMMENT "Copying Qt and its dependencies into the bundle"
            VERBATIM
        )
        add_dependencies(bundle ${PROJECT_NAME})
    else()
        message(STATUS "qtea: macdeployqt not found beside ${_qtea_qt_bin}; "
            "no 'bundle' target, the .app runs only where the libraries are "
            "installed")
    endif()
endif()

# ---------------------------------------------------------------------------
# Windows resources
# ---------------------------------------------------------------------------
# MSYS2 UCRT64. The manifest sets the process code page to UTF-8, so the
# narrow Win32 entry points agree with libstdc++'s handling of std::string
# paths.
if(WIN32)
    enable_language(RC)

    # FILEVERSION and assemblyIdentity want exactly four numeric parts.
    string(REGEX MATCHALL "[0-9]+" _qtea_vparts "${QTEA_APP_VERSION}")
    list(APPEND _qtea_vparts 0 0 0 0)
    list(SUBLIST _qtea_vparts 0 4 _qtea_vparts)
    list(JOIN _qtea_vparts "," QTEA_VERSION_RC)
    list(JOIN _qtea_vparts "." QTEA_VERSION_DOT4)

    # RC string literal escaping; a stray quote in the core's .desktop would
    # be a windres syntax error.
    set(QTEA_RC_NAME    "${QTEA_META_NAME}")
    set(QTEA_RC_SUMMARY "${QTEA_META_SUMMARY}")
    foreach(_v QTEA_RC_NAME QTEA_RC_SUMMARY)
        string(REPLACE "\\" "\\\\" ${_v} "${${_v}}")
        string(REPLACE "\"" "\"\"" ${_v} "${${_v}}")
    endforeach()

    set(QTEA_WIN_MANIFEST
        "${CMAKE_BINARY_DIR}/${QTEA_OUTPUT_NAME}.manifest")
    set(QTEA_WIN_RC "${CMAKE_BINARY_DIR}/${QTEA_OUTPUT_NAME}.rc")
    set(QTEA_WIN_RC_DEPS "${QTEA_WIN_MANIFEST}")

    # The icon, built from the PNGs rather than an .ico.
    include("${CMAKE_SOURCE_DIR}/cmake/win32_icon.cmake")
    qtea_win_icon_rc(QTEA_RC_ICON _qtea_icon_deps
                     "${QTEA_APP_ICON_DIR}" ${QTEA_APP_ICONS})

    if(_qtea_icon_deps)
        list(APPEND QTEA_WIN_RC_DEPS ${_qtea_icon_deps})
    else()
        message(WARNING "qtea: no usable icons for "
            "'${QTEA_CORE_BINARY_NAME}'; the executable and any pinned "
            "shortcut get a generic one")
    endif()

    configure_file("${CMAKE_SOURCE_DIR}/cmake/win32.manifest.in"
                   "${QTEA_WIN_MANIFEST}" @ONLY)
    configure_file("${CMAKE_SOURCE_DIR}/cmake/win32.rc.in"
                   "${QTEA_WIN_RC}" @ONLY)

    target_sources(${PROJECT_NAME} PRIVATE "${QTEA_WIN_RC}")
    set_source_files_properties("${QTEA_WIN_RC}" PROPERTIES
        OBJECT_DEPENDS "${QTEA_WIN_RC_DEPS}")
endif()

# ---------------------------------------------------------------------------
# Install
# ---------------------------------------------------------------------------
if(APPLE)
    # The prefix is where the self-contained .app lands.
    if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
        set(CMAKE_INSTALL_PREFIX "/Applications" CACHE PATH
            "Install path prefix" FORCE)
    endif()

    install(TARGETS ${PROJECT_NAME} BUNDLE DESTINATION ".")
else()
    # Windows is a zip run in place: everything flat beside the executable,
    # the first root qtea::paths searches and the layout the Qt DLLs need;
    # saves and config join it at runtime. Elsewhere the usual bindir and a
    # per-core datadir, so white-label builds do not collide.
    if(WIN32)
        set(QTEA_BIN_DEST  ".")
        set(QTEA_DATA_DEST ".")
    else()
        set(QTEA_BIN_DEST  "${CMAKE_INSTALL_BINDIR}")
        set(QTEA_DATA_DEST
            "${CMAKE_INSTALL_DATADIR}/${QTEA_CORE_BINARY_NAME}")
    endif()

    install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION "${QTEA_BIN_DEST}")

    if(EXISTS "${QTEA_FALLBACK_ICON_SRC}")
        install(DIRECTORY "${QTEA_FALLBACK_ICON_SRC}/"
                DESTINATION "${QTEA_DATA_DEST}/icons")
    endif()

    if(QTEA_CORE_ICONS)
        foreach(icon IN LISTS QTEA_CORE_ICONS)
            set(_icon_path "${QTEA_ICON_SRC}/${icon}")
            if(EXISTS "${_icon_path}")
                install(FILES "${_icon_path}"
                        DESTINATION "${QTEA_DATA_DEST}/icons")
            endif()
        endforeach()
    endif()

    if(QTEA_CORE_ASSETS)
        foreach(asset IN LISTS QTEA_CORE_ASSETS)
            set(_asset_src "${QTEA_ASSET_SRC}/${asset}")
            get_filename_component(_asset_subdir "${asset}" DIRECTORY)
            if(EXISTS "${_asset_src}")
                install(FILES "${_asset_src}"
                        DESTINATION "${QTEA_DATA_DEST}/${_asset_subdir}")
            endif()
        endforeach()
    endif()

    if(EXISTS "${QTEA_SHADER_SRC}")
        install(DIRECTORY "${QTEA_SHADER_SRC}/"
                DESTINATION "${QTEA_DATA_DEST}/shaders")
    endif()

    # The frontend's licence beside the core's.
    install(FILES "${CMAKE_SOURCE_DIR}/LICENSE"
            DESTINATION "${QTEA_DATA_DEST}")
    if(WIN32)
        install(FILES ${QTEA_DEP_LICENSES} DESTINATION "${QTEA_DATA_DEST}")
    endif()

    if(QTEA_CORE_LICENSES)
        install(FILES "${QTEA_CORE_LICENSES}"
                DESTINATION "${QTEA_DATA_DEST}")
    endif()
endif()

# ---------------------------------------------------------------------------
# Freedesktop integration
# ---------------------------------------------------------------------------
# NOT WIN32 too: an MSYS shell can leave UNIX set on a native Windows build.
if(UNIX AND NOT APPLE AND NOT WIN32)
    set(QTEA_DESKTOP_OUT
        "${CMAKE_BINARY_DIR}/${QTEA_CORE_BINARY_NAME}.desktop")
    file(WRITE "${QTEA_DESKTOP_OUT}" "${QTEA_DESKTOP_TEXT}")

    install(FILES "${QTEA_DESKTOP_OUT}"
            DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")

    # The file is named after the reverse-DNS component ID; <launchable>
    # ties it to the desktop entry, which keeps the binary name.
    set(QTEA_META_XML_NAME "${QTEA_META_NAME}")
    set(QTEA_META_XML_SUMMARY "${QTEA_META_SUMMARY}")
    qtea_xml_escape(QTEA_META_XML_NAME)
    qtea_xml_escape(QTEA_META_XML_SUMMARY)

    qtea_xml_list(QTEA_META_CATEGORIES categories category
                  QTEA_META_CATEGORIES_XML)
    qtea_xml_list(QTEA_META_KEYWORDS   keywords   keyword
                  QTEA_META_KEYWORDS_XML)
    qtea_xml_list(QTEA_META_MIMETYPES  provides   mediatype
                  QTEA_META_PROVIDES_XML)

    if(QTEA_RELEASE_DATE)
        set(QTEA_META_RELEASES_XML "  <releases>\n    <release \
version=\"${QTEA_APP_VERSION}\" date=\"${QTEA_RELEASE_DATE}\"/>\n  </releases>\n")
    else()
        set(QTEA_META_RELEASES_XML "")
        message(STATUS "qtea: QTEA_RELEASE_DATE unset, no AppStream releases block")
    endif()

    set(QTEA_METAINFO_OUT "${CMAKE_BINARY_DIR}/${QTEA_APP_ID}.metainfo.xml")
    configure_file("${CMAKE_SOURCE_DIR}/cmake/metainfo.xml.in"
                   "${QTEA_METAINFO_OUT}" @ONLY)

    install(FILES "${QTEA_METAINFO_OUT}"
            DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo")

    # What makes Icon= resolve: renamed to the binary and sorted into hicolor
    # by size. Sizes index.theme does not declare are never searched.
    set(QTEA_HICOLOR_SIZES 16 22 24 32 36 48 64 72 96 128 192 256 512)

    set(_theme_installed "")
    foreach(icon IN LISTS QTEA_APP_ICONS)
        set(_src "${QTEA_APP_ICON_DIR}/${icon}")
        if(NOT EXISTS "${_src}")
            continue()
        endif()

        get_filename_component(_stem "${icon}" NAME_WLE)
        get_filename_component(_ext  "${icon}" LAST_EXT)

        if(_ext STREQUAL ".svg")
            install(FILES "${_src}"
                    DESTINATION
                        "${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps"
                    RENAME "${QTEA_CORE_BINARY_NAME}.svg")
            list(APPEND _theme_installed "scalable")
        elseif(_ext STREQUAL ".png" AND _stem MATCHES "^(.*[^0-9])?([0-9]+)$")
            set(_size "${CMAKE_MATCH_2}")
            if("${_size}" IN_LIST QTEA_HICOLOR_SIZES)
                install(FILES "${_src}"
                        DESTINATION
                            "${CMAKE_INSTALL_DATADIR}/icons/hicolor/${_size}x${_size}/apps"
                        RENAME "${QTEA_CORE_BINARY_NAME}.png")
                list(APPEND _theme_installed "${_size}")
            endif()
        endif()
    endforeach()

    if(_theme_installed)
        list(JOIN _theme_installed " " _theme_installed_str)
        message(STATUS "qtea: icon theme sizes for "
            "'${QTEA_CORE_BINARY_NAME}': ${_theme_installed_str}")
    else()
        message(WARNING "qtea: no usable icons for the theme; the desktop "
            "entry gets a generic one")
    endif()
endif()

# For the from-source install; a package manager has its own.
configure_file(
    "${CMAKE_SOURCE_DIR}/cmake/uninstall.cmake.in"
    "${CMAKE_BINARY_DIR}/uninstall.cmake"
    @ONLY
)

add_custom_target(uninstall
    COMMAND ${CMAKE_COMMAND} -P "${CMAKE_BINARY_DIR}/uninstall.cmake"
    COMMENT "Removing files listed in install_manifest.txt"
    VERBATIM
)
