// 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.

// notepad.c  -- Simple text editor DXE application (callback-only)
//
// A callback-only DXE app (hasMainLoop = false) that provides basic text
// editing with file I/O. Demonstrates the standard DXE app pattern:
//   1. appMain creates window + widget tree, registers callbacks, returns 0
//   2. All editing happens through the TextArea widget's built-in behavior
//   3. File operations and dirty tracking are handled by menu callbacks
//
// The TextArea widget handles keyboard input, cursor movement, selection,
// scrolling, word wrap, and undo internally. Notepad only needs to wire
// up menus and file I/O around it.

#include "dvxApp.h"
#include "dvxDlg.h"
#include "dvxWgt.h"
#include "textInput/textInpt.h"
#include "dvxWm.h"
#include "dvxPlat.h"
#include "shellApp.h"

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dvxMem.h"

// ============================================================
// Constants
// ============================================================

// 32KB text buffer limit. Keeps memory usage bounded on DOS; larger files
// are silently truncated on load. The TextArea widget allocates this buffer
// internally when constructed.
#define TEXT_BUF_SIZE 32768

#define CMD_NEW    100
#define CMD_OPEN   101
#define CMD_SAVE   102
#define CMD_SAVEAS 103
#define CMD_EXIT   104
#define CMD_CUT    200
#define CMD_COPY   201
#define CMD_PASTE  202
#define CMD_SELALL 203

#define NP_WIN_W           480
#define NP_WIN_H           360
#define NP_CASCADE_OFFSET  20

// ============================================================
// Module state
// ============================================================

static DxeAppContextT *sCtx       = NULL;
static WindowT        *sWin       = NULL;
static WidgetT        *sTextArea  = NULL;
static char            sFilePath[DVX_MAX_PATH] = "";
// Hash of text content at last save/open, used for cheap dirty detection
static uint32_t        sCleanHash = 0;

// ============================================================
// Prototypes
// ============================================================

int32_t     appMain(DxeAppContextT *ctx);
static bool askSaveChanges(void);
static void doNew(void);
static void doOpen(void);
static void doSave(void);
static void doSaveAs(void);
static bool isDirty(void);
static void markClean(void);
static void onClose(WindowT *win);
static void onMenu(WindowT *win, int32_t menuId);

// ============================================================
// App descriptor
// ============================================================

// Callback-only: the TextArea widget handles all interactive editing
// entirely within the shell's event dispatch, so no dedicated task needed.
AppDescriptorT appDescriptor = {
    .name          = "Notepad",
    .hasMainLoop   = false,
    .multiInstance = true,
    .stackSize     = SHELL_STACK_DEFAULT,
    .priority      = TS_PRIORITY_NORMAL
};

// ============================================================
// Dirty tracking
// ============================================================

// Dirty tracking uses dvxTextHash from dvxApp.h.


static bool isDirty(void) {
    const char *text = wgtGetText(sTextArea);
    return dvxTextHash(text) != sCleanHash;
}


static void markClean(void) {
    const char *text = wgtGetText(sTextArea);
    sCleanHash = dvxTextHash(text);
}


// Returns true if it's OK to proceed (saved, discarded, or not dirty).
// Returns false if the user cancelled.
static bool askSaveChanges(void) {
    if (!isDirty()) {
        return true;
    }

    int32_t result = dvxMessageBox(sCtx->shellCtx, "Notepad", "Save changes?", MB_YESNOCANCEL | MB_ICONQUESTION);

    if (result == ID_YES) {
        doSave();
        return true;
    }

    if (result == ID_NO) {
        return true;
    }

    return false;
}

// ============================================================
// File operations
// ============================================================

static void doNew(void) {
    if (!askSaveChanges()) {
        return;
    }

    wgtSetText(sTextArea, "");
    sFilePath[0] = '\0';
    markClean();
    dvxSetTitle(sCtx->shellCtx, sWin, "Untitled - Notepad");
}


static void doOpen(void) {
    if (!askSaveChanges()) {
        return;
    }

    FileFilterT filters[] = {
        { "Text Files (*.txt)" },
        { "All Files (*.*)" }
    };
    char path[DVX_MAX_PATH];

    if (!dvxFileDialog(sCtx->shellCtx, "Open", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
        return;
    }

    // Open in binary mode to avoid DJGPP's CR/LF translation; the TextArea
    // widget handles line endings internally.
    FILE *f = fopen(path, "rb");

    if (!f) {
        dvxMessageBox(sCtx->shellCtx, "Error", "Could not open file.", MB_OK | MB_ICONERROR);
        return;
    }

    // Read entire file into a temporary buffer. Files larger than the
    // TextArea's buffer are silently truncated  -- this matches the behavior
    // of Windows Notepad on large files.
    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);

    if (size >= TEXT_BUF_SIZE - 1) {
        size = TEXT_BUF_SIZE - 2;
    }

    char *buf = (char *)malloc(size + 1);

    if (buf) {
        fread(buf, 1, size, f);
        buf[size] = '\0';
        platformStripLineEndings(buf, (int32_t)size);
        wgtSetText(sTextArea, buf);
        free(buf);
    }

    fclose(f);

    snprintf(sFilePath, sizeof(sFilePath), "%s", path);
    markClean();

    char title[300];
    snprintf(title, sizeof(title), "%s - Notepad", sFilePath);
    dvxSetTitle(sCtx->shellCtx, sWin, title);
}


static void doSave(void) {
    if (sFilePath[0] == '\0') {
        doSaveAs();
        return;
    }

    const char *text = wgtGetText(sTextArea);

    if (!text) {
        return;
    }

    FILE *f = fopen(sFilePath, "wb");

    if (!f) {
        dvxMessageBox(sCtx->shellCtx, "Error", "Could not save file.", MB_OK | MB_ICONERROR);
        return;
    }

    const char *eol = platformLineEnding();

    for (const char *p = text; *p; p++) {
        if (*p == '\n') {
            fputs(eol, f);
        } else {
            fputc(*p, f);
        }
    }

    fclose(f);
    markClean();
}


static void doSaveAs(void) {
    FileFilterT filters[] = {
        { "Text Files (*.txt)" },
        { "All Files (*.*)" }
    };
    char path[DVX_MAX_PATH];

    if (!dvxFileDialog(sCtx->shellCtx, "Save As", FD_SAVE, NULL, filters, 2, path, sizeof(path))) {
        return;
    }

    snprintf(sFilePath, sizeof(sFilePath), "%s", path);
    doSave();

    char title[300];
    snprintf(title, sizeof(title), "%s - Notepad", sFilePath);
    dvxSetTitle(sCtx->shellCtx, sWin, title);
}

// ============================================================
// Callbacks
// ============================================================

// onClose is the only cleanup path for a callback-only app. We null our
// pointers after destroying the window; the shell will reap the DXE on the
// next frame when it detects no windows remain for this appId.
static void onClose(WindowT *win) {
    if (!askSaveChanges()) {
        return;
    }

    dvxDestroyWindow(sCtx->shellCtx, win);
    sWin      = NULL;
    sTextArea = NULL;
}


static void onMenu(WindowT *win, int32_t menuId) {
    (void)win;

    switch (menuId) {
        case CMD_NEW:
            doNew();
            break;

        case CMD_OPEN:
            doOpen();
            break;

        case CMD_SAVE:
            doSave();
            break;

        case CMD_SAVEAS:
            doSaveAs();
            break;

        case CMD_EXIT:
            onClose(sWin);
            break;

        case CMD_CUT:
            if (sTextArea) {
                wclsOnKey(sTextArea, 24, 0);  // Ctrl+X
            }
            break;

        case CMD_COPY:
            if (sTextArea) {
                wclsOnKey(sTextArea, 3, 0);   // Ctrl+C
            }
            break;

        case CMD_PASTE:
            if (sTextArea) {
                wclsOnKey(sTextArea, 22, 0);  // Ctrl+V
            }
            break;

        case CMD_SELALL:
            if (sTextArea) {
                wclsOnKey(sTextArea, 1, 0);   // Ctrl+A
            }
            break;
    }
}

// ============================================================
// Entry point
// ============================================================

// Entry point. Offset +20 from center so multiple notepad instances cascade
// naturally rather than stacking exactly on top of each other.
int32_t appMain(DxeAppContextT *ctx) {
    sCtx = ctx;
    AppContextT *ac = ctx->shellCtx;

    int32_t screenW = ac->display.width;
    int32_t screenH = ac->display.height;
    int32_t winW    = NP_WIN_W;
    int32_t winH    = NP_WIN_H;
    int32_t winX    = (screenW - winW) / 2 + NP_CASCADE_OFFSET;
    int32_t winY    = (screenH - winH) / 3 + NP_CASCADE_OFFSET;

    sWin = dvxCreateWindow(ac, "Untitled - Notepad", winX, winY, winW, winH, true);

    if (!sWin) {
        return -1;
    }

    sWin->onClose = onClose;
    sWin->onMenu  = onMenu;

    // Menu bar
    MenuBarT *menuBar = wmAddMenuBar(sWin);

    MenuT *fileMenu = wmAddMenu(menuBar, "&File");
    wmAddMenuItem(fileMenu, "&New", CMD_NEW);
    wmAddMenuItem(fileMenu, "&Open...", CMD_OPEN);
    wmAddMenuSeparator(fileMenu);
    wmAddMenuItem(fileMenu, "&Save", CMD_SAVE);
    wmAddMenuItem(fileMenu, "Save &As...", CMD_SAVEAS);
    wmAddMenuSeparator(fileMenu);
    wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT);

    MenuT *editMenu = wmAddMenu(menuBar, "&Edit");
    wmAddMenuItem(editMenu, "Cu&t\tCtrl+X", CMD_CUT);
    wmAddMenuItem(editMenu, "&Copy\tCtrl+C", CMD_COPY);
    wmAddMenuItem(editMenu, "&Paste\tCtrl+V", CMD_PASTE);
    wmAddMenuSeparator(editMenu);
    wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELALL);

    // The widget tree is minimal: just a TextArea filling the entire content
    // area (weight=100). The TextArea widget provides editing, scrolling,
    // selection, copy/paste, and undo  -- all driven by keyboard events that
    // the shell dispatches to the focused widget.
    WidgetT *root = wgtInitWindow(ac, sWin);

    sTextArea = wgtTextArea(root, TEXT_BUF_SIZE);
    sTextArea->weight = 100;

    sFilePath[0] = '\0';
    markClean();

    return 0;
}
