/* clipboard.c —— 常用小程序⑨：**剪贴板小工具**（第 4 层：`ui_*` 图形界面）
 * clipboard.c -- Everyday utility #9: a **clipboard toolbox** (layer 4: the `ui_*` graphical interface)
 *
 * 把剪贴板里的文字拿来处理：读进来 → 按一下按钮做一种变换（转大写 / 转小写 / 去空行 / 去每行首尾空格）
 * Take the text from the clipboard and work on it: read it in, tap a button to transform it (upper case,
 * → **结果自动写回剪贴板**，接着在别处粘贴即可。底部实时显示统计（字节 / 行 / 空行）。
 * lower case, drop blank lines, trim each line) and the result is **written back to the clipboard**
 * so you can paste it somewhere else. A live count (bytes / lines / blanks) sits at the bottom.
 *
 * ## 剪贴板这一端没有怎么办（**桌面指令行宿主实测就没有**）
 * ## What happens where there is no clipboard (**the desktop command-line host has none, measured**)
 *
 * `ui_clipboard_get` 返回 **-1**，`ui_clipboard_set` 返回 **0**。按本仓既定的口径，
 * `ui_clipboard_get` returns **-1** and `ui_clipboard_set` returns **0**. By this repo's rule,
 * **0 不是错误，是"这一端没有这个能力"** —— 所以本程序：
 * **0 is not an error, it means "this end lacks the capability"** -- so this program:
 *   · 读不到就装一段**内置样例文本**，四种变换照样能按、能看（不然在桌面上就是一片空白）；
 *   · falls back to a **built-in sample text** so all four transforms still work and are visible
 *     (otherwise the desktop end would just be blank);
 *   · 写回失败就在状态行写"本机不支持写剪贴板"，**不报错、不退出**。
 *   · says "this machine cannot write the clipboard" in the status line when the write fails, and
 *     **does not error out or quit**.
 *
 * ## 第 4 层的规矩（与 `calendar.c` / `stock.c` 同）
 * ## Layer-4 rules (same as `calendar.c` / `stock.c`)
 *
 * 尺寸问宿主、真彩 `0xAARRGGBB`、**画完必须 `ui_2d_present()`**、坐标不写死、循环必须有界
 * Ask the host for the size, use true colour `0xAARRGGBB`, **always end with `ui_2d_present()`**, never
 * （空闲 ~10 秒自动退出，例程要能被自动验收）。
 * hard-code coordinates, and keep the loop bounded (it exits after ~10 s of idling so the sample can be
 * checked automatically).
 */

#include <vml_ui.h>
#include <stdlib.h>   /* itoa --- digits to text via the library */

#define IDLE_EXIT_MS 10000
#define TEXT_CAP 512         /* 正文容量（字节）。够演示，也不是"内存里的无底洞" */
                             /* Body capacity in bytes: enough for a demo without being a bottomless pit */
#define NBUF 24
#define NB 5                 /* 按钮个数 */
/* number of buttons */

static int SW;
static int SH;
static int lang;
static char text[TEXT_CAP];
static int  tlen;
static char status[64];       /* 状态行文字 */
/* the status-line text */
static int  status_kind;
/* 0 普通 / 1 成功 / 2 提示 */
/* 0 normal, 1 ok, 2 notice */

#define C_BG     0xFF0E1116
#define C_CARD   0xFF1A1F28
#define C_TITLE  0xFFE8E8F0
#define C_DIM    0xFF9AA0B0
#define C_OK     0xFF51E86E
#define C_WARN   0xFFE0A040
#define C_BTN    0xFF2A3550
#define C_BTN2   0xFF3A4A6A

/* 整数 → 十进制串（`ui_2d_text` 只吃字符串）*/
/* Integer to a decimal string (`ui_2d_text` takes strings) */
static char numbuf[NBUF];
static char *numstr(int v) {
    itoa(v, numbuf);
    return numbuf;
}

/* UTF-8 首字节 → 这个字符占几个字节（非法首字节按 1 算，宁可少算也不越界）
 * UTF-8 lead byte -> how many bytes this character takes (an invalid lead byte counts as 1; better to
 * under-count than to run off the end) */
static int utf8_len(char c) {
    unsigned char u = (unsigned char)c;
    if (u < 0x80) return 1;
    if ((u & 0xE0) == 0xC0) return 2;
    if ((u & 0xF0) == 0xE0) return 3;
    if ((u & 0xF8) == 0xF0) return 4;
    return 1;
}

static void set_status(char *s, int kind) {
    int i = 0;
    while (s[i] != 0 && i < 62) { status[i] = s[i]; i = i + 1; }
    status[i] = 0;
    status_kind = kind;
}

/* 统计：字节数、行数、空行数 */
/* Count bytes, lines and blank lines */
static void stats(int *lines, int *blanks) {
    int i = 0;
    int l = 1;
    int b = 0;
    int line_has = 0;
    if (tlen == 0) { *lines = 0; *blanks = 0; return; }
    while (i < tlen) {
        if (text[i] == 10) {
            if (line_has == 0) b = b + 1;
            l = l + 1;
            line_has = 0;
        } else {
            line_has = 1;
        }
        i = i + 1;
    }
    if (line_has == 0) b = b + 1;      /* 结尾那行也是空的（含只以换行结尾的情形）*/
                                       /* the trailing line counts as blank too (including a trailing newline) */
    *lines = l;
    *blanks = b;
}

/* ① 转大写 */
/* 1. upper case */
static void to_upper(void) {
    int i = 0;
    while (i < tlen) {
        if (text[i] >= 'a' && text[i] <= 'z') text[i] = (char)(text[i] - 32);
        i = i + 1;
    }
}

/* ② 转小写 */
/* 2. lower case */
static void to_lower(void) {
    int i = 0;
    while (i < tlen) {
        if (text[i] >= 'A' && text[i] <= 'Z') text[i] = (char)(text[i] + 32);
        i = i + 1;
    }
}

/* ③ 去空行（只留非空行，末尾补一个换行）*/
/* 3. drop blank lines (keep non-blank ones, end with one newline) */
static void drop_blanks(void) {
    char out[TEXT_CAP];
    int i = 0;
    int o = 0;
    int line_has = 0;
    int line_start = 0;
    while (i <= tlen) {
        if (i == tlen || text[i] == 10) {
            if (line_has != 0) {
                int k = line_start;
                while (k < i && o < TEXT_CAP - 2) { out[o] = text[k]; o = o + 1; k = k + 1; }
                out[o] = 10;
                o = o + 1;
            }
            line_has = 0;
            line_start = i + 1;
        } else if (text[i] != 13) {
            line_has = 1;
        }
        i = i + 1;
    }
    i = 0;
    while (i < o) { text[i] = out[i]; i = i + 1; }
    tlen = o;
}

/* ④ 去每行首尾空格 */
/* 4. trim spaces at both ends of every line */
static void trim_lines(void) {
    char out[TEXT_CAP];
    int i = 0;
    int o = 0;
    int s = 0;
    int e = 0;
    while (i <= tlen) {
        if (i == tlen || text[i] == 10) {
            e = i;
            while (s < e && (text[s] == ' ' || text[s] == 9)) s = s + 1;
            while (e > s && (text[e - 1] == ' ' || text[e - 1] == 9 || text[e - 1] == 13)) e = e - 1;
            while (s < e && o < TEXT_CAP - 2) { out[o] = text[s]; o = o + 1; s = s + 1; }
            if (i < tlen) { out[o] = 10; o = o + 1; }
            s = i + 1;
        }
        i = i + 1;
    }
    i = 0;
    while (i < o) { text[i] = out[i]; i = i + 1; }
    tlen = o;
}

/* 结果写回剪贴板；这一端没有就只更新状态行（**0 不是错误**）
 * Write the result back to the clipboard; on an end without one, only update the status line (**0 is not an error**) */
static void push_clipboard(void) {
    text[tlen] = 0;
    if (ui_clipboard_set(text) != 0) {
        set_status(lang == 0 ? "已写回剪贴板" : "copied back to the clipboard", 1);
    } else {
        set_status(lang == 0 ? "本机不支持写剪贴板（桌面指令行宿主就是这种）"
                             : "this machine cannot write the clipboard (the CLI host is one)", 2);
    }
}

static char btn_name(int i) {
    if (i == 0) return lang == 0 ? "转大写" : "UPPER";
    if (i == 1) return lang == 0 ? "转小写" : "lower";
    if (i == 2) return lang == 0 ? "去空行" : "no blanks";
    if (i == 3) return lang == 0 ? "去首尾空格" : "trim";
    return lang == 0 ? "重新读剪贴板" : "re-read";
}

static void apply(int i) {
    if (i == 0) { to_upper(); set_status(lang == 0 ? "已转大写" : "upper case applied", 1); }
    else if (i == 1) { to_lower(); set_status(lang == 0 ? "已转小写" : "lower case applied", 1); }
    else if (i == 2) { drop_blanks(); set_status(lang == 0 ? "已去空行" : "blank lines dropped", 1); }
    else if (i == 3) { trim_lines(); set_status(lang == 0 ? "已去首尾空格" : "lines trimmed", 1); }
    else { set_status(lang == 0 ? "重新读了一次" : "re-read", 0); }
    if (i != 4) push_clipboard();
}

/* 按钮几何：**画与命中判定共用这一份**（分家就会出现"看着点这个、实际按那个"）
 * Button geometry: **one copy shared by drawing and hit testing** (split them and taps land on the wrong button) */
static int btn_x(int i, int bw) { return 10 + (i % 3) * (bw + 8); }
static int btn_y(int i) { return SH - 150 + (i / 3) * 46; }
static int btn_w(int i, int bw) { return (i == 4) ? bw * 2 + 8 : bw; }

static void draw(void) {
    int i;
    int lines;
    int blanks;
    int bw;
    int prev_y;
    int prev_h;
    char st[80];

    ui_2d_clear(C_BG);

    ui_2d_text_styled(SW / 2, 14, lang == 0 ? "剪贴板小工具" : "Clipboard toolbox",
                      C_TITLE, 17, VML_ANCHOR_CENTER, VML_FONT_BOLD);
    ui_2d_text(SW / 2, 38, lang == 0 ? "点按钮做变换，结果自动写回剪贴板"
                                     : "tap a button to transform; the result goes back to the clipboard",
               C_DIM, 11, VML_ANCHOR_CENTER);

    /* 正文预览：按行画，超过预览高度就不再往下画（`ui_2d_text` 不会替你裁剪）
     * Body preview: one line per row; stop once the preview is full (`ui_2d_text` will not clip for you) */
    prev_y = 64;
    prev_h = SH - 64 - 170;
    if (prev_h < 80) prev_h = 80;
    ui_2d_rect(8, prev_y, SW - 16, prev_h, C_CARD, 1, 0, 8);
    {
        int i2 = 0;
        int row = 0;
        int col = 0;
        char line[64];
        int maxrows = (prev_h - 16) / 18;
        int cells = 0;
        while (i2 <= tlen && row < maxrows) {
            if (i2 == tlen || text[i2] == 10) {
                line[col] = 0;
                ui_2d_text(18, prev_y + 8 + row * 18, line, C_TITLE, 12, VML_ANCHOR_LEFT);
                row = row + 1;
                col = 0;
                cells = 0;
            } else {
                /* ⚠ **按码点搬，不按字节裁**（本仓铁律：字符串截断必须按码点）——
                 * ⚠ **Copy whole code points, never cut by byte** (this repo's rule: truncate by code point) --
                 *   从中间切开一个汉字（UTF-8 三字节）会让整串变成非法 UTF-8，宿主随即退回 CP437 逐字节画，
                 *   splitting a CJK glyph (3 bytes in UTF-8) makes the whole string invalid UTF-8, the host falls
                 *   屏幕上就是一排 `û □ τ` 乱码盒子（实测：第一行 44 字节、裁在 40 就中招）。
                 *   back to drawing it byte by byte as CP437 and you get a row of `u-box-tau` garbage boxes
                 *   (measured: the first line is 44 bytes and cutting at 40 triggers exactly that).
                 */
                int n = utf8_len(text[i2]);
                int w = (n == 1) ? 1 : 2;      /* 汉字按 2 格算 */
                /* a CJK glyph takes two cells */
                int k2;
                if (cells + w <= 40 && col + n < 60) {
                    k2 = 0;
                    while (k2 < n && i2 + k2 < tlen) { line[col] = text[i2 + k2]; col = col + 1; k2 = k2 + 1; }
                    cells = cells + w;
                    i2 = i2 + n - 1;
                }
            }
            i2 = i2 + 1;
        }
    }

    /* 统计 */
    /* Counts */
    stats(&lines, &blanks);
    st[0] = 0;
    {
        int k = 0;
        char *p;
        p = lang == 0 ? "字节 " : "bytes ";
        while (p[k] != 0) { st[k] = p[k]; k = k + 1; }
        {
            char *nv = numstr(tlen);
            int j = 0;
            while (nv[j] != 0) { st[k] = nv[j]; k = k + 1; j = j + 1; }
        }
        p = lang == 0 ? "   行 " : "   lines ";
        i = 0;
        while (p[i] != 0) { st[k] = p[i]; k = k + 1; i = i + 1; }
        {
            char *nv = numstr(lines);
            int j = 0;
            while (nv[j] != 0) { st[k] = nv[j]; k = k + 1; j = j + 1; }
        }
        p = lang == 0 ? "   空行 " : "   blank ";
        i = 0;
        while (p[i] != 0) { st[k] = p[i]; k = k + 1; i = i + 1; }
        {
            char *nv = numstr(blanks);
            int j = 0;
            while (nv[j] != 0) { st[k] = nv[j]; k = k + 1; j = j + 1; }
        }
        st[k] = 0;
    }
    ui_2d_text(12, prev_y + prev_h + 6, st, C_DIM, 11, VML_ANCHOR_LEFT);

    /* 按钮 */
    /* Buttons */
    bw = (SW - 20 - 16) / 3;
    if (bw < 60) bw = 60;
    for (i = 0; i < NB; i++) {
        int x = btn_x(i, bw);
        int y = btn_y(i);
        int w = btn_w(i, bw);
        ui_2d_rect(x, y, w, 38, (i == 4) ? C_BTN2 : C_BTN, 1, 0, 8);
        ui_2d_text(x + w / 2, y + 11, btn_name(i), C_TITLE, 13, VML_ANCHOR_CENTER);
    }

    /* 状态行 */
    /* Status line */
    ui_2d_text(SW / 2, SH - 46, status,
               status_kind == 1 ? C_OK : (status_kind == 2 ? C_WARN : C_DIM), 12, VML_ANCHOR_CENTER);
    ui_2d_text(SW / 2, SH - 24, lang == 0 ? "Esc / q 退出（空闲 10 秒也会自己退出）"
                                          : "Esc / q to quit (it also exits after 10 s idle)",
               C_DIM, 11, VML_ANCHOR_CENTER);

    ui_2d_present();      /* ⚠ 不调这句 = 黑屏 */
    /* without this call you get a black screen */
}

static void layout(void) {
    SW = ui_scr_get_w();
    SH = ui_scr_get_h();
    if (SW <= 0) SW = 360;
    if (SH <= 0) SH = 620;
}

/* 从剪贴板读一次；读不到就装样例文本（**-1 = 这一端没有剪贴板，不是错误**）
 * Read the clipboard once; fall back to sample text when there is none (**-1 means "no clipboard on this
 * end", not an error**) */
static void load_clipboard(void) {
    int n;
    n = ui_clipboard_get(text, TEXT_CAP - 1);
    if (n > 0) {
        tlen = n;
        text[tlen] = 0;
        set_status(lang == 0 ? "已从剪贴板读入" : "read from the clipboard", 1);
        return;
    }
    {
        char *s = lang == 0 ? "# 内置样例（本机读不到剪贴板）\n  hello   world  \n\n  second   line  \n"
                            : "# built-in sample (no clipboard here)\n  hello   world  \n\n  second   line  \n";
        int i = 0;
        while (s[i] != 0 && i < TEXT_CAP - 1) { text[i] = s[i]; i = i + 1; }
        tlen = i;
        text[tlen] = 0;
        set_status(lang == 0 ? "剪贴板读不到，用的是内置样例" : "no clipboard; using the built-in sample", 2);
    }
}

int main(void) {
    int msg[4];
    int t;
    int t0;
    int done;

    lang = ui_scr_get_language();
    status[0] = 0;
    status_kind = 0;

    layout();
    ui_win_open_ex("clipboard.c", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
    layout();
    load_clipboard();
    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 (for automated checks) */
            continue;
        }
        t0 = ui_time_get_ms();
        if (t == VML_MSG_KEYDOWN) {
            int k = msg[1];
            if (k == VML_KEY_ESCAPE || k == 'q' || k == 'Q') done = 1;
            else if (k == '1') { apply(0); draw(); }
            else if (k == '2') { apply(1); draw(); }
            else if (k == '3') { apply(2); draw(); }
            else if (k == '4') { apply(3); draw(); }
            continue;
        }
        if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
            int bw = (SW - 20 - 16) / 3;
            int i;
            if (bw < 60) bw = 60;
            for (i = 0; i < NB; i++) {
                int x = btn_x(i, bw);
                int y = btn_y(i);
                int w = btn_w(i, bw);
                if (msg[1] >= x && msg[1] < x + w && msg[2] >= y && msg[2] < y + 38) {
                    apply(i);
                    draw();
                    break;
                }
            }
            continue;
        }
        if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) { layout(); draw(); continue; }
        if (t == VML_MSG_WINDOWCLOSE) done = 1;
    }
    return 0;
}
