# Copyright (c) 2024-2025 Jon Sharp
# SPDX-License-Identifier: MIT
#-------------------------------------------------------------------------------
# FRST Computer — Cyberdeck Firmware
#
# Zephyr application for the FRST Model 1 and Model 4 cyberdecks.
# 9p4z, aephyr, and zephyr-pforth are pulled in as west modules.

cmake_minimum_required(VERSION 3.13.1)

# Custom board root (must be before find_package for board discovery)
set(BOARD_ROOT "${CMAKE_CURRENT_SOURCE_DIR}")

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})

#-------------------------------------------------------------------------------
# Git version info - captured at configure time
#-------------------------------------------------------------------------------
execute_process(
    COMMAND git rev-parse --short=8 HEAD
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    OUTPUT_VARIABLE GIT_COMMIT_HASH
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
)

execute_process(
    COMMAND git diff --quiet HEAD
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    RESULT_VARIABLE GIT_DIRTY
)

if(GIT_DIRTY)
    set(GIT_COMMIT_HASH "${GIT_COMMIT_HASH}-dirty")
endif()

if(NOT GIT_COMMIT_HASH)
    set(GIT_COMMIT_HASH "unknown")
endif()

# Make git hash available as a compile definition
add_compile_definitions(GIT_COMMIT_HASH="${GIT_COMMIT_HASH}")

# HAL Espressif only needed for ESP32 builds
if(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
    set(EXTRA_ZEPHYR_MODULES hal espressif)
endif()

project(app LANGUAGES C)

#-------------------------------------------------------------------------------
# Splash banner — bake FRST_VERSION_BOOT text into splash_vtiled at build time.
# Produces build/generated/splash_vtiled_with_banner.h, which terminal.c
# includes in place of the original splash header.
#-------------------------------------------------------------------------------
set(GEN_SPLASH_SRC
    ${CMAKE_CURRENT_SOURCE_DIR}/scripts/gen_splash_banner.c)
set(GEN_SPLASH_FONT
    ${CMAKE_CURRENT_SOURCE_DIR}/scripts/gen_splash_banner_font.inc)
set(GEN_SPLASH_IN
    ${CMAKE_CURRENT_SOURCE_DIR}/src/splash_vtiled_800x480.h)
set(GEN_SPLASH_VERSION
    ${CMAKE_CURRENT_SOURCE_DIR}/src/version.h)
set(GEN_SPLASH_DIR ${CMAKE_BINARY_DIR}/generated)
set(GEN_SPLASH_TOOL ${GEN_SPLASH_DIR}/gen_splash_banner)
set(GEN_SPLASH_OUT ${GEN_SPLASH_DIR}/splash_vtiled_with_banner.h)

# Pull the release name out of version.h with a simple regex.  The
# source-of-truth lives in version.h so bumping a release only touches
# one file.
file(STRINGS ${GEN_SPLASH_VERSION} _VERSION_NAME_LINE
     REGEX "^[ \t]*#define[ \t]+FRST_VERSION_NAME[ \t]+\"")
string(REGEX REPLACE
       "^[ \t]*#define[ \t]+FRST_VERSION_NAME[ \t]+\"([^\"]*)\".*"
       "\\1" FRST_VERSION_NAME "${_VERSION_NAME_LINE}")
if(NOT FRST_VERSION_NAME)
    set(FRST_VERSION_NAME "Unknown")
endif()
set(FRST_VERSION_BOOT "Booting ${FRST_VERSION_NAME}")

file(MAKE_DIRECTORY ${GEN_SPLASH_DIR})

find_program(HOST_C_COMPILER NAMES cc gcc clang)
if(NOT HOST_C_COMPILER)
    message(FATAL_ERROR "gen_splash_banner: no host C compiler found")
endif()

add_custom_command(
    OUTPUT ${GEN_SPLASH_TOOL}
    COMMAND ${HOST_C_COMPILER} -O2 -Wall -o ${GEN_SPLASH_TOOL} ${GEN_SPLASH_SRC}
    DEPENDS ${GEN_SPLASH_SRC} ${GEN_SPLASH_FONT} ${GEN_SPLASH_IN}
    COMMENT "Compiling gen_splash_banner host tool"
    VERBATIM
)

add_custom_command(
    OUTPUT ${GEN_SPLASH_OUT}
    COMMAND ${GEN_SPLASH_TOOL} "${FRST_VERSION_BOOT}" ${GEN_SPLASH_OUT}
    DEPENDS ${GEN_SPLASH_TOOL} ${GEN_SPLASH_VERSION}
    COMMENT "Baking boot banner \"${FRST_VERSION_BOOT}\" into splash"
    VERBATIM
)

add_custom_target(gen_splash_banner DEPENDS ${GEN_SPLASH_OUT})
zephyr_include_directories(${GEN_SPLASH_DIR})

#-------------------------------------------------------------------------------
# Core application sources (platform-independent)
#-------------------------------------------------------------------------------
target_sources(app PRIVATE
    src/terminal.c
    src/widget.c
    src/dashboard.c
    src/shell_extras.c
    src/shell_9p.c
    src/srv.c
    src/srv_9p_adapter.c
    src/dev_cons.c
    src/dev_env.c
    src/dev_proc.c
    src/dev_stats.c
    src/dev_srv.c
    src/ns.c
    src/fortunes.c
    src/zrc.c
    src/zrc_pim.c
    src/bbs.c
    src/gopher.c
    src/ftp.c
    src/zrc_games.c
    src/zrc_sam.c
    src/zrc_sed.c
    src/zrc_se.c
    src/zrc_vi.c
    src/con.c
    src/zmodem.c
    src/img.c
    src/stb_image_impl.c
    src/ed.c
    src/dev_fs.c
    src/cpu6502.c
    src/woz.c
    src/fsupgrade.c
    src/dev_net.c
    src/vt.c
    src/shells.c
    src/builtin_ping.c
    src/doom.c
    src/net_tcp.c
    src/intro26/intro26.c
    src/intro26/scenes.c
)

#-------------------------------------------------------------------------------
# Platform-specific sources — guarded by Kconfig
#-------------------------------------------------------------------------------

# main.c targets the device (ESP32); main_sim.c is for native_sim / WASM.
# Previously gated on CONFIG_BT, but the deck also builds in BT-off WiFi
# variants — gate on the actual hardware family instead.
if(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
    target_sources(app PRIVATE
        src/main.c
        src/kbd_9p_client.c
        src/dev_frontlight.c
        src/power.c
        src/shell_terminal.c
    )
    if(CONFIG_KBD_9P_TRANSPORT_UART)
        target_sources(app PRIVATE src/kbd_uart_probe.c)
    endif()
else()
    target_sources(app PRIVATE src/main_sim.c)

    # Synthetic /net/wifi server: no WiFi driver here, so the namespace gets a
    # mock /net/wifi by default and the bottom-bar WiFi glyph lights up.
    target_sources(app PRIVATE src/dev_wifi_sim.c)

    # Virtual e-paper display emulator (native_sim / WASM)
    target_sources(app PRIVATE src/display_epaper_emu.c)

    # Bundled LFS image via assembly .incbin (native_sim / POSIX only)
    target_sources(app PRIVATE src/lfs_image_data.S)
endif()

# Connection manager — picks a flavor based on which transport is
# actually available in this build.  BT-on builds use conn.c (BLE
# L2CAP + aether/LoRa).  BT-off + NET_TCP builds use conn_tcp.c
# (TCP only).  The two files implement the same conn.h API and are
# mutually exclusive.  Previously this was gated on platform, which
# broke the ESP32+WiFi variant — it's ESP32 (so got conn.c) but had
# BT off, leaving `srv tcp!host!port` failing with "unknown transport
# 'tcp'" because conn.c has no tcp! parse case.
if(CONFIG_BT)
    target_sources(app PRIVATE
        src/conn.c
        # conn.c now also handles tcp! -- 9P-over-TCP where the TCP is a
        # /net/tcp BORROWED via the L2CAP gateway (no native sockets), so the
        # ble build can dial out (e.g. coredump push to bbs).
        src/transport_tcp_ns.c
    )
elseif(CONFIG_NINEP_CLIENT)
    target_sources(app PRIVATE
        src/transport_tcp_client.c
        src/conn_tcp.c
    )
endif()

if(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
    # Re-open the ESP32-only block for any remaining ESP32 sources
    # not covered by the conn-manager selection above.
    # (currently empty)
else()

    # SDL keyboard input — bottom layer (host context, links against SDL2)
    if(CONFIG_SDL_DISPLAY)
        target_sources(native_simulator INTERFACE src/sdl_kbd_bottom.c)
    endif()
endif()

# OTA firmware update (MCUboot)
if(CONFIG_BOOTLOADER_MCUBOOT)
    target_sources(app PRIVATE src/dev_firmware.c)
endif()

# Crash dump server (/crash) - stored coredump over 9P
if(CONFIG_DEBUG_COREDUMP)
    target_sources(app PRIVATE src/dev_crash.c)
endif()

# All-threads coredump capture (selective additive via a CALLBACK pseudo-device)
if(CONFIG_COREDUMP_DEVICE)
    target_sources(app PRIVATE src/coredump_threads.c)
endif()

# PSA crypto / CGA
if(CONFIG_MBEDTLS_PSA_CRYPTO_C)
    target_sources(app PRIVATE src/cga.c src/dev_factotum.c)
endif()

# ESP32 PSRAM memory test and tracking
if(CONFIG_ESP_SPIRAM)
    target_sources(app PRIVATE src/memtest.c src/mem.c)
endif()

# Aether 9P transport and device server (LoRa mesh).  The /net/aether
# fs itself now lives in aephyr (aether_net_fs.c, CONFIG_AETHER_NET_FS)
# as the single canonical layer shared with the DECT/BLE gateways.
if(CONFIG_AETHER_MESH)
    target_sources(app PRIVATE src/transport_aether.c)
    target_sources(app PRIVATE src/dev_aether.c)
endif()

# pForth .fth files are now included in the skel/ LFS image
# (no longer embedded as C arrays in pforth_system_fth.c)

# FRST-Class Forth: platform vocabulary glue + PSRAM VM allocator
if(CONFIG_PFORTH)
    target_sources(app PRIVATE src/forth_frst.c)
endif()

# Frotz Z-machine interpreter sources
zephyr_include_directories(src/frotz)

target_sources(app PRIVATE
    # Core Z-machine interpreter
    src/frotz/buffer.c
    src/frotz/err.c
    src/frotz/fastmem.c
    # src/frotz/files.c  # Disabled - uses stdio FILE I/O, stubs in os_zephyr.c
    src/frotz/hotkey.c
    src/frotz/input.c
    src/frotz/main.c
    src/frotz/math.c
    src/frotz/object.c
    src/frotz/process.c
    # src/frotz/quetzal.c  # Disabled - save/restore uses FILE I/O
    src/frotz/random.c
    src/frotz/redirect.c
    src/frotz/screen.c
    src/frotz/sound.c
    src/frotz/stream.c
    src/frotz/table.c
    src/frotz/text.c
    src/frotz/variable.c
    # Zephyr port (replaces dumb_* files and provides stubs)
    src/frotz/os_zephyr.c
    src/frotz/frotz_shell.c
    src/frotz/story_data.c
    # lostpig_data.c removed - 128KB savings, use 'frotz /lfs/lostpig.z8' instead
)

#-------------------------------------------------------------------------------
# LittleFS image generation (from skel/ skeleton directory)
#-------------------------------------------------------------------------------
set(LFS_SKEL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/skel)
set(LFS_BUILD_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/scripts/build_lfs_image.py)
set(LFS_IMAGE ${CMAKE_BINARY_DIR}/littlefs.bin)
set(LFS_MANIFEST_C ${CMAKE_BINARY_DIR}/lfs_image_manifest.c)
if(EXISTS ${LFS_SKEL_DIR} AND EXISTS ${LFS_BUILD_SCRIPT})
    # The image is a real build output with real inputs, so ninja can
    # order it ahead of the .incbin that swallows it (see below).  It
    # used to be an always-run custom target, which left the build one
    # cycle behind: the image regenerated during the run, after ninja
    # had already decided the assembled object was up to date, so a
    # skel/ change only reached the binary on the NEXT build.
    #
    # CONFIGURE_DEPENDS re-globs at build time, so files added to skel/
    # are still picked up without a manual cmake reconfigure.
    file(GLOB_RECURSE LFS_SKEL_FILES CONFIGURE_DEPENDS ${LFS_SKEL_DIR}/*)
    add_custom_command(
        OUTPUT ${LFS_IMAGE} ${LFS_MANIFEST_C}
        COMMAND ${PYTHON_EXECUTABLE} ${LFS_BUILD_SCRIPT}
            --source ${LFS_SKEL_DIR}
            --size 0x800000
            --output ${LFS_IMAGE}
            --to-c-manifest ${LFS_MANIFEST_C}
        DEPENDS ${LFS_BUILD_SCRIPT} ${LFS_SKEL_FILES}
        COMMENT "Building LittleFS image from skel/"
        VERBATIM
    )
    add_custom_target(lfs_image ALL DEPENDS ${LFS_IMAGE})

    # Tell the assembler where to find littlefs.bin for .incbin
    zephyr_compile_options($<$<COMPILE_LANGUAGE:ASM>:-I${CMAKE_BINARY_DIR}>)

    # .incbin is invisible to the build graph: without this, a changed
    # skel/ regenerates littlefs.bin but the assembled object keeps the
    # OLD image, and the new files simply aren't there at runtime.
    set_source_files_properties(src/lfs_image_data.S
        PROPERTIES OBJECT_DEPENDS ${LFS_IMAGE})

    # Compile the generated manifest on native_sim/POSIX so fsupgrade works
    # there too (ESP32 skips it — no localStorage-style persistence to sync).
    if(CONFIG_BOARD_NATIVE_SIM OR CONFIG_ARCH_POSIX)
        target_sources(app PRIVATE ${LFS_MANIFEST_C})
        set_source_files_properties(${LFS_MANIFEST_C} PROPERTIES GENERATED 1)
    endif()

    add_dependencies(app lfs_image)
endif()

add_dependencies(app gen_splash_banner)

# mbedTLS heap now uses DRAM (disabled PSRAM placement due to linker conflicts)
# zephyr_linker_sources(DATA_SECTIONS linker_psram.ld)

# Wrap _exit to trace where it's called from (debug, ESP32 only)
if(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
    zephyr_ld_options(-Wl,--wrap=_exit)
endif()
