// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

// shellApp.h  -- DVX Shell application lifecycle types and API
//
// The shell supports two kinds of DXE apps:
//
// 1. Callback-only apps (hasMainLoop = false):
//    appMain() is called directly in the shell's task 0. It creates
//    windows, registers event callbacks, and returns immediately. The
//    app then lives entirely through GUI callbacks driven by the shell's
//    main loop. This is simpler and cheaper (no extra stack/task) and
//    is the right choice for apps that are purely event-driven (dialogs,
//    simple tools). Lifecycle ends when the last window is closed.
//
// 2. Main-loop apps (hasMainLoop = true):
//    A dedicated cooperative task is created for the app. appMain() runs
//    in that task and can do its own polling/processing loop, calling
//    tsYield() to share CPU. This is needed for apps that have continuous
//    work (terminal emulators, games, long computations). Lifecycle ends
//    when appMain() returns or the task is killed.
//
// Both types use the same DXE interface (appDescriptor + appMain), so
// app authors choose by setting hasMainLoop in their descriptor.
#ifndef SHELL_APP_H
#define SHELL_APP_H

#include "dvxApp.h"
#include "dvxWgt.h"
#include "taskSwch.h"

#include <stdint.h>
#include <stdbool.h>

// ============================================================
// App descriptor (exported by each DXE app)
// ============================================================

#define SHELL_APP_NAME_MAX 64

// Launch argument buffer size (DxeAppContextT.args and shell-side copies)
#define SHELL_ARGS_MAX     1024

// Shared size for shell status/error message buffers
#define SHELL_MSG_MAX      512

// Every DXE app exports a global AppDescriptorT named "appDescriptor".
// The shell reads it at load time to determine how to launch the app.

// Use in AppDescriptorT.stackSize to get the default task stack size
#define SHELL_STACK_DEFAULT 0

typedef struct {
    char    name[SHELL_APP_NAME_MAX];
    bool    hasMainLoop;
    bool    multiInstance;  // true = allow multiple instances via temp copy
    int32_t stackSize;      // SHELL_STACK_DEFAULT or explicit byte count
    int32_t priority;       // TS_PRIORITY_* or custom
} AppDescriptorT;

// ============================================================
// App context (passed to appMain)
// ============================================================

// Passed as the sole argument to appMain(). Gives the app access to
// the shell's GUI context (for creating windows, drawing, etc.) and
// its own identity. appDir is derived from the .app file path at load
// time so the app can find its own resources (icons, data files) via
// relative paths  -- important because the working directory is shared
// by all apps and can't be changed per-app in DOS.
typedef struct {
    AppContextT *shellCtx;  // the shell's GUI context
    int32_t      appId;     // this app's ID
    char         appPath[DVX_MAX_PATH];   // full path to the .app file
    char         appDir[DVX_MAX_PATH];    // directory containing the .app file
    char         configDir[DVX_MAX_PATH]; // writable config directory (CONFIG/<apppath>/)
    char         args[SHELL_ARGS_MAX];    // launch arguments (empty if none)
    char         helpFile[DVX_MAX_PATH];  // help file path (for F1 context help)
    char         helpTopic[128];          // current help topic ID (updated by app)
    void       (*onHelpQuery)(void *ctx); // called on F1 to refresh helpTopic
    void        *helpQueryCtx;            // context for onHelpQuery
} DxeAppContextT;

// ============================================================
// Per-app state
// ============================================================

// State machine:  Free -> Loaded -> Running -> Terminating -> Free
// LoadedE is transient (only during shellLoadApp before entry is called).
// TerminatingE means the app's task has exited but cleanup (window
// destruction, DXE unload) hasn't happened yet  -- the shell's main loop
// reaps these each frame via shellReapApps().
typedef enum {
    AppStateFreeE,          // slot available
    AppStateLoadedE,        // DXE loaded, not yet started
    AppStateRunningE,       // entry point called, active
    AppStateTerminatingE    // shutdown in progress
} AppStateE;

typedef struct {
    int32_t     appId;          // unique ID = slot index (1-based; 0 = shell)
    char        name[SHELL_APP_NAME_MAX];
    char        path[DVX_MAX_PATH];
    char        tempPath[DVX_MAX_PATH];  // temp copy path for multi-instance (empty = not a copy)
    void       *dxeHandle;      // dlopen() handle
    AppStateE   state;
    bool        hasMainLoop;
    uint32_t    mainTaskId;     // task ID if hasMainLoop, else 0
    int32_t   (*entryFn)(DxeAppContextT *);
    void      (*shutdownFn)(void);   // may be NULL
    DxeAppContextT *dxeCtx;     // heap-allocated; address stable across sApps realloc
} ShellAppT;

// ============================================================
// Shell global state
// ============================================================


// ============================================================
// App lifecycle API
// ============================================================

// Initialize the app slot table
void shellAppInit(void);

// Canonical path of the DVX help viewer app, relative to the DVX root.
// Uses the platform path separator so displayed paths match the user's
// convention.
#define DVX_HELP_VIEWER_APP_LITERAL \
    "APPS" DVX_PATH_SEP "KPUNCH" DVX_PATH_SEP "DVXHELP" DVX_PATH_SEP "DVXHELP.APP"

// Load and start an app from a DXE file. Returns app ID (>= 1) or -1 on error.
// For callback-only apps, appMain runs synchronously and returns before
// shellLoadApp returns. For main-loop apps, a task is created and the
// app starts running on the next tsYield.
int32_t shellLoadApp(AppContextT *ctx, const char *path);

// Load and run an app with arguments. The args string is copied into
// DxeAppContextT.args before appMain is called.
int32_t shellLoadAppWithArgs(AppContextT *ctx, const char *path, const char *args);

// Reap finished apps (call each frame from main loop). Returns true if
// any apps were reaped, so the caller can notify the desktop to refresh.
// For callback-only apps, termination is detected here by scanning for
// zero remaining windows owned by the app (they have no main loop to set
// AppStateTerminatingE). For main-loop apps, termination happens when
// appMain returns (via appTaskWrapper marking AppStateTerminatingE).
bool shellReapApps(AppContextT *ctx);

// Gracefully shut down a single app  -- calls shutdownFn if present,
// destroys windows, kills task, closes DXE handle.
void shellReapApp(AppContextT *ctx, ShellAppT *app);

// Forcibly kill an app (Task Manager "End Task", crash recovery, shell
// shutdown).  Does NOT run the app's shutdownFn -- a force kill targets a
// hung or faulting app whose own code cannot be trusted to run (it would
// re-hang the cooperative shell or re-fault past cleanup).  The shell purges
// the callbacks the app registered itself, via shellPurgeAppCallbacks, so
// nothing dangles after dlclose.
void shellForceKillApp(AppContextT *ctx, ShellAppT *app);

// Terminate all running apps (shell shutdown)
void shellTerminateAllApps(AppContextT *ctx);

// Get app slot by ID (returns NULL if invalid/free)
ShellAppT *shellGetApp(int32_t appId);

// Total number of app slots (for iteration bounds)
int32_t shellAppSlotCount(void);

// Count running apps (not counting the shell itself)
int32_t shellRunningAppCount(void);

// Ensure an app's config directory exists (creates all parent dirs).
// Returns 0 on success, -1 on failure.
int32_t shellEnsureConfigDir(const DxeAppContextT *ctx);

// Build a full path to a file in the app's config directory.
// e.g. shellConfigPath(ctx, "settings.ini", buf, sizeof(buf))
// -> "CONFIG/PROGMAN/settings.ini"
void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize);


// ============================================================
// Desktop callback
// ============================================================

// Default desktop app path
#define SHELL_DESKTOP_APP "apps/kpunch/progman/progman.app"

// Register a callback for app state changes (load, reap, crash).
// Apps call this during appMain to receive notifications when app state
// changes (load, reap, crash).  Multiple callbacks are supported.
void shellRegisterDesktopUpdate(void (*updateFn)(void));

// Remove a previously registered callback (call before app shutdown).
void shellUnregisterDesktopUpdate(void (*updateFn)(void));

// Notify the desktop app that app state has changed (load, reap, crash).
void shellDesktopUpdate(void);

// Register an idle handler called every idle frame by the shell main loop,
// regardless of which app is foreground.  Serial/secLink pollers register
// here so background connections keep pumping.  Idempotent on the (fn, ctx)
// pair.  The registering module MUST unregister before it is unloaded (for
// app DXEs, from the _appShutdown hook) or the handler will dangle.
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);

// Remove a previously registered idle handler.  No-op if absent, so a
// double-unregister is harmless.
void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx);

// Drop every idle and desktop-update callback an app registered, keyed by the
// appId captured at registration time.  shellForceKillApp and shellReapApp
// call this before dlclose so a force or crash kill can reclaim an app WITHOUT
// running the app's own shutdownFn.  appId < 1 (shell-owned and
// persistent-DXE registrations) is left untouched.
void shellPurgeAppCallbacks(int32_t appId);

// ============================================================
// Ctrl+Esc handler (set by taskmgr library)
// ============================================================

// Function pointer set by the taskmgr DXE's constructor. The shell
// calls this on Ctrl+Esc. NULL if taskmgr is not loaded.
extern void (*shellCtrlEscFn)(AppContextT *ctx);

#endif // SHELL_APP_H
