/* notepad.c —— 常用小程序④：**记事本**（第 4 层：`ui_*` 图形界面）
 * notepad.c -- Everyday utility #4: a **notepad** (layer 4: the `ui_*` graphical interface)
 *
 * 一扇窗口里的纯文本编辑器：**打字 / 退格 / 回车换行 / 光标可见**，`Esc` **保存并退出**，
 * A plain text editor in one window: **type, backspace, Enter for a new line, a visible caret**, `Esc`
 * 下次运行把上次的内容读回来。同一份源码在手机上（点屏幕 + 软键盘）与桌面上（物理键盘）都跑。
 * **saves and quits**, and the next run reads the last session back. One source file runs on the phone
 * (touch + soft keyboard) and on the desktop (a physical keyboard) alike.
 *
 * ## 怎么编辑
 * ## How to edit
 *
 * · 可打印 ASCII（32~126）插到**光标处**；`退格`(8) 删前一个字符；`回车`(13) 换行。
 * · Printable ASCII (32-126) goes in **at the caret**; Backspace (8) deletes one character; Enter (13)
 *   starts a new line.
 * · `←`/`→` 挪光标；`Esc`(27) = **保存并退出**。
 * · Left/Right move the caret; `Esc` (27) = **save and quit**.
 * · 空闲 10 秒也退出，且**走同一条收尾**（见下面第 ⑤ 条）。
 * · Ten idle seconds quits too, through **the very same exit path** (pitfall 5 below).
 *
 * ## 存到哪
 * ## Where it saves
 *
 * **相对路径 `note.txt`** —— 手机上沙箱根 = 工作区（绝对路径会被拒）。
 * **The relative path `note.txt`** -- on the phone the sandbox root is the workspace, and absolute
 * 桌面上落在**源文件所在目录**（`vmlcli` 按 `WorkDir` 解析）。
 * paths are rejected; on the desktop it lands in the **directory of the source file** (`vmlcli`
 * derives `WorkDir`).
 * 跑法（桌面脚手架，秒级）：
 * How to run (desktop scaffolding, seconds):
 *     dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll Examples/c/app/notepad.c --vml-home third_party/vml
 * 手机上在命令行页里敲：
 * On the phone, type this in the command-line page:
 *     vml run examples/c/app/notepad.c
 *
 * ## 踩过的坑（实测）
 * ## Pitfalls (measured in this repo)
 *
 * ① **打开模式写反 = 把文件清空**：`#110` 的 `mode=1` 是 `FileMode.Create`（**先截断再写**）。
 * ① **Swapping the open modes wipes the file**: `#110` with `mode=1` means `FileMode.Create`, which
 *    **truncates first**.
 *    所以"想读内容却传了 1"读到的是**空串**。
 *    So "meant to read, passed 1" hands back an **empty string**.
 *    读=0、写=1，两个常量**各写一次、别共用**。
 *    Read = 0, write = 1, each written once and never shared.
 *
 * ② **正文不能"一个字节画一格"**：UTF-8 里一个汉字是 3 个字节。
 * ② **The body must not be drawn "one byte per cell"**: one CJK glyph is 3 bytes in UTF-8.
 *    实测：逐字节画出来是 `τ □ □ Σ …` 这样一排盒子；**按码点切**才对。
 *    Measured: drawing byte by byte came out as a row of `τ □ □ Σ` boxes; cutting by **code point**
 *    (`utf8_len` + `cell_width`) is what works.
 *
 * ③ **"一格放得下吗"只有一处判据**：画正文与定位光标若各写一套换行规则，就会出现
 * ③ **"Does it fit in a cell" has exactly one judge**: two copies of the wrap rule, one for drawing
 *    "光标停在行尾、字却掉到下一行行首"。
 *    and one for the caret, produce "the caret sits at the line end while the character lands at the
 *    start of the next line".
 *    共用 `cell_fit`。
 *    Both go through `cell_fit`.
 *
 * ④ **回车就是缓冲里的字节 `10`**：`VML_KEY_ENTER` 要自己变成 `10` 再存盘。
 * ④ **Enter is nothing but the byte 10 in the buffer**: `VML_KEY_ENTER` has to become a `10` before
 *    the save.
 *    忘了这一步，屏幕上看着换行了、存到文件里的却是一行（下次读回来全挤在一起）。
 *    Skip it and the screen breaks the line while the file keeps one long line (the next run reads it
 *    back all run together).
 *
 * ⑤ **收尾只有一处**：`Esc` / 空闲 10 秒 / 关窗三条路都从循环**之后**那一段出去。
 * ⑤ **A single exit**: `Esc`, ten idle seconds and the window's close arrow all leave through the one
 *    place **after** the loop.
 *    把保存写进 `Esc` 那一支 = 另外两条路**悄悄丢内容**。
 *    Putting the save inside the `Esc` arm would silently lose the text on the other two.
 *    本仓 `E169`「谁发起的，就要能被同一处停掉」的同族。
 *    It is the same family as this repo's `E169`, "whoever starts it must be stoppable in one place".
 *
 * ⑥ **`ui_2d_text` 的 `y` 是基线，不是"字盒的顶"**：墨迹落在 `y` 的**上方**。
 * ⑥ **`ui_2d_text`'s `y` is the baseline, not the top of the glyph box**: the ink sits **above** it.
 *    实测（字号 16、`y=58`）：拉丁字母的墨迹在 48..57、汉字在 47..59。
 *    Measured (size 16, `y=58`): the Latin ink is at 48..57 and the CJK ink at 47..59.
 *    按"y 是盒顶"去画光标，光标就落在**自己那一行的下面**（看着像"光标跳到了下一行"）。
 *    Draw the caret assuming "y is the top" and it lands **below its own line** (it reads as if the
 *    caret had jumped a line -- measured).
 *    纸面也得往上多让一点，不然第一行会顶着框；见 `draw` 里的 `ink`。
 *    The sheet needs that headroom too, or the first line touches the frame; see `ink` in `draw`.
 *
 * ⑦ **读进来的 `\r` 要清掉**：本程序写的是 LF。
 * ⑦ **Strip the `\r` on the way in**: this program writes LF.
 *    但**别人的**文件（Windows 记事本、Git 的 `core.autocrlf`）是 CRLF。
 *    But files from **someone else** (Windows Notepad, Git's `core.autocrlf`) are CRLF.
 *    `\r` **看不见，却按"一个字"占掉一格**：实测 CRLF 文件被算成 "18 字"、原样存回 20 字节。
 *    A `\r` **is invisible yet takes one cell**: measured, a CRLF file counts as "18 characters" and
 *    saves back as 20 bytes.
 *    清掉后是 "16 字" / 18 字节；`ab\r\ncd` 上光标停在 `ab` 右边**一格空位**（见 `strip_cr`）。
 *    After stripping it is "16 characters" / 18 bytes; on `ab\r\ncd` the caret sits **one empty cell**
 *    right of `ab` (see `strip_cr`).
 *
 * ⑧ **`VML_KEY_LEFT/UP/RIGHT/DOWN` = 37/38/39/40，全都落在"可打印 ASCII 32..126"里**。
 * ⑧ **`VML_KEY_LEFT/UP/RIGHT/DOWN` are 37/38/39/40, all inside "printable ASCII 32..126".**
 *    所以**先判可打印**就会把方向键当成字符插进去：实测连按三下左箭头，正文里多了 3 个 `%`。
 *    So testing the printable range first types the arrow key in as a character: measured, three Left
 *    presses left three `%` in the text.
 *    而光标一步没动 —— 判 `VML_MSG_KEYDOWN` 时**特殊键一律排在可打印区间之前**。
 *    The caret did not move at all -- so when handling `VML_MSG_KEYDOWN`, **special keys always come
 *    before the printable range**.
 *
 * ## 界面文字跟着宿主语言走
 * ## The on-screen text follows the host language
 *
 * `ui_scr_get_language()`（与 `demo_ui.c`/`calendar.c` 同一套做法）。
 * `ui_scr_get_language()`, exactly as `demo_ui.c`/`calendar.c` do it.
 */

#include <vml_ui.h>
#include <stdlib.h>   /* itoa --- digits to text via the library */
#include <file.h>   /* fopen 家族（真签名 `int mode`）—— 本文件不再自己包汇编 */

#define IDLE_EXIT_MS 10000       /* 空闲这么久就自己退出（例程要能被自动验收）*/
                                 /* Idle this long and it quits (samples must be automatically verifiable). */
#define NOTE_CAP     2048        /* 正文缓冲容量（字节）*/
                                 /* Text buffer capacity (bytes) */
#define NOTE_FILE    "note.txt"  /* ⚠ 相对路径 */
                                 /* ⚠ A relative path */

/* 配色（真彩 AARRGGBB，第 4 层没有索引色）*/
/* Colours (true colour AARRGGBB; layer 4 has no palette) */
#define C_BG     0xFF101018      /* 窗口底 */
                                 /* window background */
#define C_PAPER  0xFF1A1A24      /* 正文"纸"面 */
                                 /* the sheet the text sits on */
#define C_TITLE  0xFFE8E8F0
#define C_TEXT   0xFFD8DCE8
#define C_DIM    0xFF9AA0B0
#define C_CUR    0xFFFFC64A      /* 光标（黄色竖条）*/
                                 /* the caret (a yellow bar) */
#define C_HINT   0xFF7A8090

/* ⚠ 这里原先有一层手写的 `sys_*` 包装（`asm("SYSCALL #110…")` + 全局 `_sysret` 中转）。
 *   2026-10-07 起改调库：`#include <file.h>` 带来的 `fopen`/`fread`/`fwrite`/`fclose`
 *   **返回值就是 syscall 的返回值**，不再需要 `_sysret` 这个中转站。
 * The hand-rolled `sys_*` wrappers are gone: the library functions (via `#include <file.h>`) return the
 * syscall result directly, so the `_sysret` relay is unnecessary. */

/* ── 正文模型：**一维扁平缓冲** ─────────────────────────────────────── */
/* -- The text model: **one flat buffer** --
 * ⚠ 不用二维数组、不用"指针数组"、不取数组元素的地址（`&arr[i]`）—— 这几样在本仓的多语言
 * ⚠ No 2-D arrays, no arrays of pointers, no address-of on an array element (`&arr[i]`): those three are
 *   前端之间最不稳（例程是要照着移植到 22 门语言的），一律一维 + 扁平下标。
 *   the least portable forms across this repo's front ends (the samples get ported to 22 languages), so
 *   everything is one-dimensional with flat indexes. */
char nbuf[NOTE_CAP];   /* 正文；**不保证以 0 结尾**（长度看 `blen`）*/
                       /* The text; **not guaranteed NUL-terminated** (the length is `blen`). */
int  blen = 0;         /* 已用字节数 */
                       /* bytes in use */
int  tcur = 0;         /* 光标：字节下标 0..blen */
                       /* caret: a byte index 0..blen */
int  tloaded = 0;      /* 启动时有没有读到文件（1=读到了）*/
                       /* whether the file was read at startup */

/* ── 版式（**一律从宿主给的尺寸算**，别写死坐标）───────────────────── */
/* -- Layout (**always computed from the host size**, never hard-coded) -- */
static int SW;
static int SH;
static int lang;       /* 0 = 中文，1 = English */
                       /* 0 = Chinese, 1 = English */
static int g_pad;      /* 页边距 */
                       /* page margin */
static int g_fs;       /* 正文字号 */
                       /* body font size */
static int g_cw;       /* **一格的宽**（不是"字体量的宽"）*/
                       /* the **cell width** (not a measured glyph width) */
static int g_lh;       /* 一行的高 */
                       /* line height */
static int g_x0;       /* 正文区左上角 */
                       /* body origin */
static int g_y0;
static int g_cols;     /* 每行几格 */
                       /* cells per line */
static int g_rows;     /* 一屏几行 */
                       /* lines per screen */
static int g_top;      /* 滚到第几个显示行（**光标的显示行**决定它）*/
                       /* first visible display row */

/* 整数 → 十进制串（`ui_2d_text` 只收字符串，第 4 层没有 `print_int` 那种东西）
 * Integer to a decimal string (`ui_2d_text` takes strings only; layer 4 has no `print_int`).
 * ⚠ 返回的是**同一个静态缓冲**：别在一个表达式里连着用两次（那要用两次就先 `strcat` 到自己串上）。
 * ⚠ It returns **one static buffer**: never use it twice inside a single expression. */
static char numbuf[16];
static char *numstr(int v) {
    itoa(v, numbuf);
    return numbuf;
}

/* 这个字节起头是几个字节的 UTF-8 序列（认不出按 1 算）/ How many bytes the UTF-8 sequence starting
 * here is (unrecognized counts as 1). */
static int utf8_len(int lead) {
    if (lead < 0x80) return 1;
    if ((lead & 0xE0) == 0xC0) return 2;
    if ((lead & 0xF0) == 0xE0) return 3;
    if ((lead & 0xF8) == 0xF0) return 4;
    return 1;
}

/* 这个字占几格：ASCII 与 2 字节的拉丁扩展各 1 格，CJK/emoji（3、4 字节）占 2 格 —— 全角。
 * How many cells this character takes: ASCII and the 2-byte Latin blocks take 1, CJK/emoji (3- and
 * 4-byte) take 2 -- they are full width. */
static int cell_width(int lead) {
    if (lead < 0x80) return 1;
    if ((lead & 0xE0) == 0xC0) return 1;
    return 2;
}

/* 从 `*pr` 行的 `*pc` 格放一个占 `w` 格的字：放不下就**软换行**到下一行行首。
 * Put one character that takes `w` cells at row `*pr`, column `*pc`: if it does not fit, **soft-wrap**
 * to the start of the next row.
 * ⚠ **画正文与定位光标必须共用这一处** —— 各写一套，光标就会"停在行尾而字掉到下一行"（见文件头 ③）。
 * ⚠ **Drawing and the caret must share this one place** -- two copies and the caret "sits at the end of
 *   the line while the character lands at the start of the next" (pitfall 3 in the file header). */
static void cell_fit(int *pr, int *pc, int w) {
    if (*pc + w > g_cols) { *pr = *pr + 1; *pc = 0; }
}

/* 光标所在的**显示行/列**（走一遍正文得到；`\n` 与软换行都算）
 * The **display row/column** of the caret (found by walking the text; `\n` and soft wraps both count). */
static void cursor_rc(int idx, int *orow, int *ocol) {
    int r = 0;
    int c = 0;
    int i = 0;
    int ch;
    int n;
    while (i < idx) {
        ch = nbuf[i] & 0xFF;
        if (ch == 10) { r = r + 1; c = 0; i = i + 1; continue; }
        n = utf8_len(ch);
        if (i + n > blen) n = 1;        /* 缓冲尾上只剩半个序列（不该发生）*/
                                        /* half a sequence at the tail */
        cell_fit(&r, &c, cell_width(ch));
        c = c + cell_width(ch);
        i = i + n;
    }
    /* 正好落在一行末尾那一格之外 ⇒ 按下一行行首算 —— 这与"再打一个字会被放到哪儿"是同一个判据 */
    /* Exactly past the last cell of a row means the next row's start -- the same judge as "where would
     * the next character go". */
    cell_fit(&r, &c, 1);
    *orow = r;
    *ocol = c;
}

/* 行数与字数（只给标题栏看）*/
/* Line and character counts (for the title bar only) */
static int count_lines(void) {
    int i;
    int n = 1;
    for (i = 0; i < blen; i++) if ((nbuf[i] & 0xFF) == 10) n = n + 1;
    return n;
}

static int count_chars(void) {
    int i = 0;
    int n = 0;
    int ch;
    while (i < blen) {
        ch = nbuf[i] & 0xFF;
        i = i + utf8_len(ch);
        if (ch != 10) n = n + 1;        /* 换行不算"字" */
                                        /* a line break is not a "character" */
    }
    return n;
}

static void layout(void) {
    int usable;
    int body;
    SW = ui_scr_get_w();
    SH = ui_scr_get_h();
    if (SW <= 0) SW = 480;      /* 宿主答不上来时的兜底（开窗也会用同一对数）*/
    if (SH <= 0) SH = 640;      /* fallback when the host cannot answer (the same pair opens the window) */
    g_pad = SW / 20;
    if (g_pad < 8) g_pad = 8;

    g_fs = SW / 30;             /* 字号随宽度走：手机窄屏小一点，桌面大一点 */
    if (g_fs < 11) g_fs = 11;   /* The font follows the width: smaller on a narrow phone, larger on the desktop. */
    if (g_fs > 17) g_fs = 17;
    g_cw = g_fs + 2;            /* 一格宽 = 字高 + 2px 字距 */
                                /* one cell = the font height plus 2px of tracking */
    g_lh = g_fs + 7;            /* 行高（比字高大一半行距，多行看着才不挤）*/
                                /* line height */

    g_y0 = g_pad + 34;          /* 标题栏占掉上面一条 */
                                /* the title bar takes the strip above */
    usable = SW - g_pad * 2;
    g_cols = usable / g_cw;
    if (g_cols < 8) g_cols = 8;
    if (g_cols > 40) g_cols = 40;   /* 宽窗口下别把一行拉得太长 */
                                    /* do not stretch one line forever on a wide window */
    g_x0 = (SW - g_cols * g_cw) / 2;    /* 整块水平居中 */
                                        /* centre the block horizontally */
    if (g_x0 < g_pad) g_x0 = g_pad;

    body = SH - g_pad - 46 - g_y0;      /* 底下给两行提示留位置 */
                                        /* two hint lines get the bottom strip */
    if (body < g_lh * 3) body = g_lh * 3;
    g_rows = body / g_lh;
    if (g_rows < 1) g_rows = 1;
}

static void draw(void) {
    int crow;
    int ccol;
    int r;
    int c;
    int i;
    int n;
    int w;
    int ch;
    int y;
    int k;
    int bar;
    int ink;
    int ptop;
    int pbot;
    char u[8];
    char tb[64];

    /* ① 先算光标在哪一格 —— 滚动要照着它来，所以**必须在画之前**算好 */
    /* ① Work out the caret cell **before drawing**: scrolling follows it. */
    cursor_rc(tcur, &crow, &ccol);
    g_top = 0;
    if (crow >= g_rows) g_top = crow - g_rows + 1;

    ui_2d_clear(C_BG);

    /* ② 标题栏：左边文件名，右边行数 / 字数 */
    /* ② Title bar: the file name on the left, line and character counts on the right. */
    ui_2d_text_styled(g_pad, g_pad, NOTE_FILE, C_TITLE, g_fs + 3, VML_ANCHOR_LEFT, VML_FONT_BOLD);
    tb[0] = 0;
    strcat(tb, numstr(count_lines()));
    strcat(tb, lang == 0 ? " 行 · " : " lines · ");
    strcat(tb, numstr(count_chars()));
    strcat(tb, lang == 0 ? " 字" : " chars");
    ui_2d_text(SW - g_pad, g_pad + 4, tb, C_DIM, g_fs - 2, VML_ANCHOR_RIGHT);

    /* ③ 正文：一块"纸"+ 逐字上格
     * ③ The body: a sheet of "paper" plus one character per cell. */
    /* ⚠ `ui_2d_text` 给的 y **不是"字盒的顶"而是贴着基线**（实测：字号 16、y=58 时，
     * ⚠ The y given to `ui_2d_text` is **not the top of the glyph box but the baseline** (measured:
     *   拉丁字母的墨迹在 48..57、汉字在 47..59 —— 都在 y 的**上方**）。
     *   at size 16 with y=58 the Latin ink sits at 48..57 and CJK ink at 47..59, both **above** y).
     *   所以纸面要按"墨迹高出多少"往上让，光标也要跟着往上抬 —— 不然光标会画在自己那一行的
     *   So the sheet and the caret both have to account for that overhang; otherwise the caret is drawn
     *   自己那一行的下面（看着像"光标跳到了下一行"，实测过）。
     *   below its own line (it reads as if the caret had jumped a line -- measured). */
    ink = (g_fs * 3) / 4;                   /* 墨迹高出 y 的量（字号 16 ⇒ 12）*/
                                            /* the overhang (size 16 => 12) */
    ptop = g_y0 - ink - 5;
    pbot = g_y0 + (g_rows - 1) * g_lh + g_fs + 6;
    ui_2d_rect(g_x0 - 6, ptop, g_cols * g_cw + 12, pbot - ptop, C_PAPER, 1, 0, 8);

    r = 0;
    c = 0;
    i = 0;
    while (i < blen) {
        ch = nbuf[i] & 0xFF;
        if (ch == 10) { r = r + 1; c = 0; i = i + 1; continue; }
        /* ⚠ **按码点切，不按字节切**：一个汉字是 3 个字节，逐字节画出来就是 3 个乱码格
         * ⚠ **Cut by code point, never by byte**: a CJK character is 3 bytes, so drawing byte by byte
         *    （实测：`第一行中文` 画成了 `τ □ □ Σ …` 这样一排盒子）。
         *   yields three mojibake cells (measured: a row of `τ □ □ Σ` boxes came out of it). */
        n = utf8_len(ch);
        if (i + n > blen) n = 1;            /* 末尾只剩半个序列（不该发生）*/
                                            /* half a sequence at the tail */
        w = cell_width(ch);
        cell_fit(&r, &c, w);
        if (r >= g_top && r < g_top + g_rows) {
            y = g_y0 + (r - g_top) * g_lh;
            k = 0;
            while (k < n) { u[k] = nbuf[i + k]; k = k + 1; }
            u[n] = 0;
            ui_2d_text(g_x0 + c * g_cw, y, u, C_TEXT, g_fs, VML_ANCHOR_LEFT);
        }
        c = c + w;
        i = i + n;
    }

    /* ④ 空文档给一行欢迎语（**只是占位，不进 `nbuf`** —— 否则第一次退出就会把欢迎语
     * ④ An empty document shows a welcome line (**a placeholder only, never put into `nbuf`** --
     *    当成用户的内容存进 `note.txt`）。 */
    /*    otherwise the first exit would save the welcome line into `note.txt` as the user's text.) */
    if (blen == 0) {
        ui_2d_text(g_x0, g_y0, lang == 0 ? "空白笔记 —— 直接打字" : "empty note - just type",
                   C_DIM, g_fs, VML_ANCHOR_LEFT);
        ui_2d_text(g_x0, g_y0 + g_lh, lang == 0 ? "Esc 保存并退出" : "Esc saves and quits",
                   C_DIM, g_fs, VML_ANCHOR_LEFT);
    }

    /* ⑤ 光标：一条竖条，画在**当前字符位置**上（正好在一行末尾时它会落在下一行行首）
     * ⑤ The caret: a vertical bar drawn **at the current character position** (at the end of a full row
     *    it lands at the start of the next one). */
    if (crow >= g_top && crow < g_top + g_rows) {
        bar = g_fs / 6;
        if (bar < 2) bar = 2;
        if (bar > 4) bar = 4;
        y = g_y0 + (crow - g_top) * g_lh;
        ui_2d_rect(g_x0 + ccol * g_cw, y - ink + 1, bar, g_fs + 3, C_CUR, 1, 0, 0);
    }

    /* ⑥ 底部：操作提示 + 文件状态（第二次启动一眼能看出"上次存的内容读回来了"）*/
    /* ⑥ Bottom: the controls and the file state (a second run shows at a glance that the last session
     *    was read back). */
    ui_2d_text(g_pad, SH - g_pad - 32, lang == 0 ? "回车 换行 · 退格 删 · Esc 保存并退出"
                                                 : "Enter newline · Backspace delete · Esc save & quit",
               C_HINT, g_fs - 3, VML_ANCHOR_LEFT);
    tb[0] = 0;
    strcat(tb, lang == 0 ? "文件 " : "file ");
    strcat(tb, NOTE_FILE);
    if (tloaded != 0) {
        strcat(tb, lang == 0 ? " 已读取 " : " loaded ");
        strcat(tb, numstr(blen));
        strcat(tb, lang == 0 ? " 字节" : " bytes");
    } else {
        strcat(tb, lang == 0 ? " 新建（退出时写入）" : " new (written on exit)");
    }
    ui_2d_text(g_pad, SH - g_pad - 14, tb, C_HINT, g_fs - 3, VML_ANCHOR_LEFT);

    ui_2d_present();      /* ⚠ 不调这句 = 黑屏 */
                          /* ⚠ without this call you get a black screen */
}

/* ── 文件（⚠ 读用 0、写用 1，见文件头 ①）─────────────────────────── */
/* -- Files (⚠ read = 0, write = 1, see pitfall 1 in the file header) -- */

/* `\r` 一律清掉：本程序自己写的是 LF，可**别人**（Windows 记事本、Git 的 `core.autocrlf`）留下的
 * Drop every `\r`: this program writes LF, but files left by **someone else** (Windows Notepad, or Git's
 * 文件是 CRLF。`\r` **看不见，但在这个"一个字一格"的模型里照样占掉一格** —— 数数与光标都会把它算进去
 * `core.autocrlf`) are CRLF. A `\r` **is invisible yet still takes a cell in this "one character per
 * （实测：CRLF 的文件被算成 "18 字"、原样存回 20 字节；`ab\r\ncd` 上光标停在 `ab` 右边一格空位）。
 * cell" model** -- both the count and the caret count it (measured: a CRLF file reads as "18 characters"
 * 单独的 CR（老 Mac 的换行）也算一次换行 —— 不然整份文件会挤成一行。写回去时统一成 LF。
 * and saves back as 20 bytes; on `ab\r\ncd` the caret sits one empty cell right of `ab`). A lone CR (a
 * classic-Mac line break) counts as a line break too, otherwise the whole file collapses onto one line.
 * Writes go back out as LF only. */
static void strip_cr(void) {
    int i = 0;
    int o = 0;
    int ch;
    while (i < blen) {
        ch = nbuf[i] & 0xFF;
        if (ch == 13) {
            if (i + 1 < blen && (nbuf[i + 1] & 0xFF) == 10) { i = i + 1; continue; }   /* CRLF ⇒ 只留 LF */
                                                                                      /* CRLF -> keep the LF */
            nbuf[o] = 10;                       /* 光秃秃的 CR ⇒ 当成换行 */
                                                /* a bare CR becomes a line break */
            o = o + 1;
            i = i + 1;
            continue;
        }
        nbuf[o] = nbuf[i];
        o = o + 1;
        i = i + 1;
    }
    blen = o;
}

/* 读回来。返回 1 表示读到了（`blen`/`tcur` 已就位），0 表示"没有这个文件"（保持空文档）。
 * Reads it back. Returns 1 when it was read (`blen`/`tcur` are set), 0 for "no such file" (the document
 * stays empty). */
static int load_file(void) {
    int mode;
    int fh;
    int n;
    mode = 0;                       /* 0 = 只读（FileMode.Open）—— 写成 1 会先把文件清空 */
                                    /* 0 = read-only (FileMode.Open); a 1 here would truncate the file first */
    fh = fopen(NOTE_FILE, mode);
    if (fh < 0) return 0;           /* 错误码是**负数**（-200 文件不存在 / -202 越界）*/
                                    /* error codes are **negative** */
    n = fread(fh, nbuf, NOTE_CAP - 1);
    fclose(fh);
    if (n < 0) n = 0;
    blen = n;
    strip_cr();                     /* ⚠ 见 `strip_cr`：`\r` 看不见却占一格，光标会浮在文字右边 */
                                    /* ⚠ See `strip_cr`: a `\r` is invisible yet takes a cell, so the caret
                                       floats right of the text. */
    nbuf[blen] = 0;                 /* 存盘时不用它，但调试/打印时省事 */
                                    /* not needed for saving, handy for printing */
    tcur = blen;                    /* 光标停在上次内容的末尾，接着往下打就是追加 */
                                    /* The caret sits at the end, so typing continues where the user left off. */
    return 1;
}

/* 写回去。返回写成功的字节数（失败返回 -1）*/
/* Writes it back. Returns the bytes written, or -1. */
static int save_file(void) {
    int mode;
    int fh;
    int w;
    mode = 1;                       /* 1 = 只写（FileMode.Create，**先截断**）—— 所以我们才要整份重写 */
                                    /* 1 = write-only (FileMode.Create, **truncating**) -- hence a full rewrite */
    fh = fopen(NOTE_FILE, mode);
    if (fh < 0) return -1;
    w = fwrite(fh, nbuf, blen);
    fclose(fh);
    if (w != blen) return -1;
    return w;
}

/* ── 编辑 ───────────────────────────────────────────────────────────── */
/* -- Editing -- */

/* 在光标处插一个字节（可打印 ASCII 或 `\n`）。缓冲满了就**丢掉**（记事本不该因为打满而崩）。
 * Inserts one byte at the caret (a printable ASCII character or `\n`). A full buffer **drops** the key
 * (a notepad must not fall over when it fills up). */
static void insert_ch(int ch) {
    int i;
    if (blen >= NOTE_CAP - 1) return;
    i = blen;
    while (i > tcur) { nbuf[i] = nbuf[i - 1]; i = i - 1; }
    nbuf[tcur] = (char)ch;
    blen = blen + 1;
    tcur = tcur + 1;
}

/* 退格：删掉光标**前一个字符**。⚠ 要跨过**整个 UTF-8 序列** —— 只退一个字节会把一个汉字
 * Backspace: delete the character **before** the caret. ⚠ It has to step over the **whole UTF-8
 * 劈成"前导字节 + 半个身子"，屏幕上是乱码、存盘后文件里也是坏字节（实测）。
 * sequence** -- dropping one byte splits a CJK character into "lead byte + half a body", which is
 * mojibake on screen and broken bytes in the file (measured). */
static void backspace(void) {
    int start;
    int n;
    int i;
    if (tcur <= 0) return;
    start = tcur - 1;
    while (start > 0 && ((nbuf[start] & 0xFF) & 0xC0) == 0x80) start = start - 1;
    n = tcur - start;
    i = start;
    while (i + n < blen) { nbuf[i] = nbuf[i + n]; i = i + 1; }
    blen = blen - n;
    tcur = start;
}

/* 光标左移一个**字符**（不是一字节，同理）*/
/* Move the caret one **character** left (not one byte, same reason). */
static void cursor_left(void) {
    int i;
    if (tcur <= 0) return;
    i = tcur - 1;
    while (i > 0 && ((nbuf[i] & 0xFF) & 0xC0) == 0x80) i = i - 1;
    tcur = i;
}

/* 光标右移一个字符 */
/* Move the caret one character right. */
static void cursor_right(void) {
    int n;
    if (tcur >= blen) return;
    n = utf8_len(nbuf[tcur] & 0xFF);
    tcur = tcur + n;
    if (tcur > blen) tcur = blen;
}

/* ── 主流程 ─────────────────────────────────────────────────────────── */
/* -- Main flow -- */

int main(void) {
    int msg[4];
    int t;
    int t0;
    int done;
    int k;
    int wrote;

    lang = ui_scr_get_language();

    /* 开窗：**传 `0, 0`（= 尺寸交给宿主），开窗之后**再取一次尺寸 —— 那次才是**真实画布**
     * Open the window: **pass `0, 0` (let the host size it) and take the size again after the open** -- that
     * （开窗前的估算**可能不同**：桌面 = 480×640、手机竖屏 ~395×744），窗口与画布才是一套坐标系。
     * is the **real canvas** (the pre-open estimate may differ: desktop 480x640, phone portrait about 395x744),
     * so window and canvas share one coordinate space. */
    layout();
    ui_win_open_ex("notepad.c", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
    layout();

    tloaded = load_file();
    if (tloaded == 0) { blen = 0; tcur = 0; }    /* 没有文件 = 空文档 + 一行欢迎语（见 `draw` ④）*/
                                                 /* no file = an empty document plus a welcome line (see `draw` 4) */
    print_str("[notepad] ");
    print_str(NOTE_FILE);
    if (tloaded != 0) {
        print_str(lang == 0 ? " 读到 " : " loaded ");
        print_int(blen);
        print_str(lang == 0 ? " 字节\n" : " bytes\n");
    } else {
        print_str(lang == 0 ? " 还没有，从空文档开始（内容只能是这个程序的输入）\n"
                            : " not found, starting empty (content is only what you type here)\n");
    }

    draw();

    t0 = ui_time_get_ms();
    done = 0;
    while (!done && ui_win_is_closed() == 0) {
        t = ui_msg_wait(msg, 200);
        if (t == VML_MSG_NONE) {
            if (ui_time_get_ms() - t0 > IDLE_EXIT_MS) done = 1;   /* 空闲超时（验收用）*/
                                                                  /* idle timeout */
            continue;
        }
        t0 = ui_time_get_ms();
        if (t == VML_MSG_KEYDOWN) {
            k = msg[1];
            /* ⚠ **顺序是有讲究的：方向键那一支必须在"可打印 ASCII"之前** ——
             * ⚠ **The order matters: the arrow-key arms must come before the "printable ASCII" one** --
             *   `VML_KEY_LEFT/UP/RIGHT/DOWN` = 37/38/39/40，**全都落在 32..126 里**，
             *   `VML_KEY_LEFT/UP/RIGHT/DOWN` are 37/38/39/40, **all inside 32..126**, so testing the
             *   先判可打印的话，按一下左箭头会往正文里**插一个 `%`**（实测：连按三下左箭头，
             *   printable range first turns one Left press into **a literal `%` in the text** (measured:
             *   存下来的文件里多了 3 个 `%`，光标一步没动）。
             *   three Left presses left three `%` in the saved file and moved the caret not at all). */
            if (k == VML_KEY_ESCAPE) {                       /* Esc = 保存并退出（保存统一在循环外做）*/
                done = 1;                                    /* Esc = save and quit (the save happens after the loop) */
            } else if (k == VML_KEY_BACKSPACE) {
                backspace();
                draw();
            } else if (k == VML_KEY_ENTER) {
                insert_ch(10);                               /* ⚠ 回车要变成缓冲里的 10，见文件头 ④ */
                draw();                                      /* ⚠ Enter becomes a 10 in the buffer, see pitfall 4 */
            } else if (k == VML_KEY_LEFT) {
                cursor_left();
                draw();
            } else if (k == VML_KEY_RIGHT) {
                cursor_right();
                draw();
            } else if (k >= 32 && k <= 126) {
                insert_ch(k);
                draw();
            }
            continue;
        }
        if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) {
            layout();
            draw();
            continue;
        }
        if (t == VML_MSG_WINDOWCLOSE) done = 1;
    }

    /* ⚠ **收尾只有这一处**：`Esc`、空闲 10 秒、用户点了窗口的返回箭头，三条路都从这里出去。
     * ⚠ **This is the only exit**: `Esc`, ten idle seconds and the window's back arrow all leave here.
     *   把保存写进 `Esc` 那一支 = 另外两条路悄悄丢内容（文件头 ⑤）。
     *   Writing the save inside the `Esc` arm would silently lose the text on the other two (header 5). */
    wrote = save_file();
    print_str(lang == 0 ? "[notepad] 写入 " : "[notepad] wrote ");
    print_str(NOTE_FILE);
    print_str(" ");
    print_int(wrote < 0 ? 0 : wrote);
    print_str(lang == 0 ? " 字节，退出\n" : " bytes, quitting\n");
    return 0;
}
