# calendar.py —— 常用小程序①：**日历**（第 4 层：`ui_*` 图形界面）
# calendar.py -- Everyday utility #1: a **calendar** (layer 4: the `ui_*` graphical interface)
#
# 一扇窗口里的月历：**今天高亮**，`←`/`→` 翻月、`↑`/`↓` 翻年，**点某一天**也能选中它，`Esc` 退出。
# A month calendar in a window: **today is highlighted**; Left/Right change the month, Up/Down the
# year, and **tapping a day** selects it; Esc quits.
# 这是 `Examples/c/app/calendar.c` 的 Python 移植：版式、交互、配色、"空闲 8 秒自动退出"一字不改。
# This is the Python port of `Examples/c/app/calendar.c`: the layout, the interaction, the colours
# and the "quit after 8 idle seconds" rule are unchanged.
#
# ## 第 4 层的三条规矩（不守就会"手机上行、桌面上歪"）
# ## Three rules of layer 4 (break them and it works on the phone but drifts on the desktop)
#
# · **尺寸从宿主问**：`ui_scr_get_w()/_h()`，别写死。
# · **Ask the host for the size**: `ui_scr_get_w()/_h()`, never hard-code it.
# · **颜色是真彩 `0xAARRGGBB`**（不是索引色）。
# · **Colours are true colour `0xAARRGGBB`** (not palette indexes).
# · **画完必须 `ui_2d_present()`**，漏了就是黑屏（宿主不猜你画完了没）。
# · **Always finish with `ui_2d_present()`**; skip it and you get a black screen.
#
# ## 为什么"空闲 8 秒就自己退出"
# ## Why it exits by itself after 8 seconds of idling
#
# 例程要能被**自动验收**（`scripts/vmlcli` 加 `--frame` 出一张图就够了），所以循环必须有界。
# Samples have to be **automatically verifiable** (one `scripts/vmlcli` run with `--frame` is
# enough), so the loop must be bounded.
# 写真正的程序时把 `IDLE_EXIT_MS` 去掉，其余（画格子 / 读键 / 命中判定）就是完整写法。
# Drop `IDLE_EXIT_MS` when you write a real program; the rest of the file is the complete pattern.
#
# ## 移植时绕开的四条 Python 前端限制（都**实测**过，不是照着别处抄的）
# ## Four Python front-end limits worked around here (all **measured**, not copied from elsewhere)
#
# · **函数里给模块级标量赋值 = 新建一个局部量**：写完顶层读回来还是旧值。
# · **Assigning a module-level scalar inside a function only creates a new local**: the top level
#   still reads the old value afterwards.
#   ⇒ 布局那几个量一律由**顶层**算好，函数**只读**模块级状态。
#   => the layout values are computed **at top level** and the functions only **read** state.
# · **`s[i]` 与 `"a" + "b"` 都是坏的**：前者回一个野地址（实测 `"hello"[0]` = 812843119）、
# · **`s[i]` and `"a" + "b"` are both broken**: the first returns a wild address (measured:
#   `"hello"[0]` = 812843119) and the second an empty string (`strlen("abc" + "def")` = 0).
#   后者回空串（实测 `strlen("abc" + "def")` = 0）⇒ 取字符走 `str_charat`，拼串走
#   => take characters with `str_charat` and assemble text with `strcpy`/`strcat` into buffers
#   `strcpy`/`strcat` 写进 `vml_alloc` 出来的缓冲。
#   obtained from `vml_alloc`.
# · **`str()` 是恒等、`chr()` 返回的是栈上地址** ⇒ `ui_2d_text` 收字符串，
# · **`str()` is the identity and `chr()` returns a stack address** => `ui_2d_text` takes a string,
#   所以整数转文字只能走共享库的 `itoa`（写进我们自己的缓冲，不碰它的静态缓冲）。
#   so integers become text only through the shared library's `itoa`, into our own buffer.
# · **没有 `&` 也没有 `tm_t`**：C 版的 `localtime()->tm_year` 在本语言里拿不到。
# · **There is no `&` and no `tm_t`**: the C version's `localtime()->tm_year` is out of reach here.
#   ⇒ 今天的年月日从宿主给的 `vml_get_date()`（实测回 `2026-10-07`）切三段再 `atoi`。
#   => today's date is cut out of the host's `vml_get_date()` (measured: `2026-10-07`) with
#   `str_substr`, then run through `atoi`.

# ── 显式引用：`ui_*` 来自共享调用库 `vmlui.vml`；`str_*`/`itoa`/`atoi`/`vml_alloc` 来自共享库 ──
# ── explicit references: `ui_*` from the shared call library `vmlui.vml`; `str_*`/`itoa`/`atoi`/
#    `vml_alloc` from the shared library ──
import vml_ui
import shared

IDLE_EXIT_MS = 8000

# 配色（真彩 AA RRGGBB）
# Colours (true colour AA RRGGBB)
C_BG = 0xFF101018
C_CARD = 0xFF1A1A24
C_TITLE = 0xFFE8E8F0
C_DIM = 0xFF9AA0B0
C_ACCENT = 0xFF4A90D9
C_TODAY = 0xFFFFC64A
C_WEEKEND = 0xFFE06C50
C_DAY = 0xFFD8DCE8
C_ON_FILL = 0xFF101018

# ── 暂存字符串：地址由 `vml_alloc` 给，`str_*`/`itoa` 往里写，交给 `ui_2d_text` 的就是地址 ──
# ── scratch strings: the address comes from `vml_alloc`, `str_*`/`itoa` write into it, and the
#    address is what `ui_2d_text` receives ──
S_TITLE = vml_alloc(48)
S_NUM = vml_alloc(16)
S_DAY = vml_alloc(8)

# 星期表头：C 版是一串三元表达式，本语言有列表 ⇒ 直接摆两张表，函数里按下标取。
# Weekday header: the C version is a chain of ternaries; this language has lists, so the two rows
# are plain tables and the functions index them.
WK_CN = ["日", "一", "二", "三", "四", "五", "六"]
WK_EN = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]

# 界面语言（0 = 中文，1 = English）；开局问一次，之后只读。
# UI language (0 = Chinese, 1 = English); asked once at startup, read-only afterwards.
LANG = ui_scr_get_language()

# 当前显示的月份 / 选中（高亮）的日号，0 = 没有 / 今天。
# The month on screen / the selected (highlighted) day, 0 = none / today's date.
cur_y = 0
cur_m = 0
sel_d = 0
today_y = 0
today_m = 0
today_d = 0

# 棋盘几何（画与命中判定**必须用同一份**，见 `hit_day`）—— 由顶层算好，函数只读。
# Grid geometry (drawing and hit testing **must share this one copy**, see `hit_day`) -- computed
# at top level, only read inside functions.
SW = 0
SH = 0
G_PAD = 0
G_CELL = 0
G_TOP = 0


# ── 小工具 ────────────────────────────────────────────
# ── small helpers ─────────────────────────────────────

# 是不是闰年
# Is it a leap year
def is_leap(y):
    if y % 400 == 0:
        return 1
    if y % 100 == 0:
        return 0
    if y % 4 == 0:
        return 1
    return 0


# 这个月有几天
# How many days this month has
def days_in_month(y, m):
    if m == 2:
        if is_leap(y) == 1:
            return 29
        return 28
    if m == 4 or m == 6 or m == 9 or m == 11:
        return 30
    return 31


# 某月 1 号是星期几（0=周日）。蔡勒公式，编号与 C 的 `tm_wday` 一致。
# Weekday of the 1st of a month (0 = Sunday). Zeller's congruence, numbered like C's `tm_wday`.
def weekday_of_first(y, m):
    yy = y
    mm = m
    if mm < 3:
        mm = mm + 12
        yy = yy - 1
    k = yy % 100
    j = yy // 100
    h = (1 + (13 * (mm + 1)) // 5 + k + k // 4 + j // 4 + 5 * j) % 7
    return (h + 6) % 7


MONTHS = ["January", "February", "March", "April", "May", "June",
          "July", "August", "September", "October", "November", "December"]


# 月份的英文名（列表按下标取，等价于 C 版那一串 `if`）
# The English month name (a list indexed by month, equivalent to the C version's chain of `if`s)
def month_en(m):
    return MONTHS[m - 1]


# 两位日号（补零，7×7 的格子里对齐才好看）。`itoa` 不补零，所以再 `str_padstart` 一次。
# A two-digit day number (zero padded, which keeps the 7x7 grid aligned); `itoa` does not pad, so
# `str_padstart` finishes the job.
def daystr(d):
    itoa(d, S_NUM)
    str_padstart(S_DAY, S_NUM, 2, 48)
    return S_DAY


# 第 idx 格那一格的左上角
# Top-left corner of cell `idx`
def cell_x(idx):
    return G_PAD + (idx % 7) * G_CELL


def cell_y(idx):
    return G_TOP + (idx // 7) * G_CELL


# ── 版面：四条"从宿主换算"的规则各收一处，顶层按 `scr_w/scr_h → pad → cell → top` 调一遍 ──
# ── layout: each of the four "derive from the host" rules lives in one place; the top level calls
#    them in the order `scr_w/scr_h -> pad -> cell -> top` ──

def scr_w():
    w = ui_scr_get_w()
    if w <= 0:
        w = 360
    return w


def scr_h():
    h = ui_scr_get_h()
    if h <= 0:
        h = 620
    return h


def pad_for(sw):
    p = sw // 18
    if p < 6:
        p = 6
    return p


def cell_for(sw, pad):
    c = (sw - pad * 2) // 7          # 7 列等宽；余数留在右边不动
                                     # 7 equal columns; any remainder stays on the right
    if c < 18:
        c = 18
    return c


def top_for(sh, cell):
    # ⚠ **行数固定 6**：月历最长 6 行（31 天且 1 号在周六时）。按"这个月实际几行"算的话，
    # ⚠ **Always six rows**: a month spans at most six rows. Sizing by the actual row count makes
    #   翻月时整块会上下跳（实测过：5 行与 6 行来回切，日历像在抖）。
    #   the whole block jump when you change month (measured: five rows and six rows alternating
    #   makes the calendar visibly shake).
    grid_h = 6 * cell
    t = (sh - (64 + grid_h)) // 2 + 64   # 整块垂直居中
    # centre the whole block vertically
    if t < 80:
        t = 80                            # 矮屏兜底：别把标题顶出画布
        # short screens
    return t


# ── 绘制 ──────────────────────────────────────────────
# ── drawing ───────────────────────────────────────────
def draw():
    first = weekday_of_first(cur_y, cur_m)
    n = days_in_month(cur_y, cur_m)
    cx = SW // 2
    i = 0
    d = 0
    idx = 0
    x = 0
    y = 0
    col = 0
    is_today = 0
    is_sel = 0
    by = 0

    ui_2d_clear(C_BG)

    # 标题：**中文一律靠字符串字面量拼**，别逐字节写 —— 一个汉字在 UTF-8 里是 3 个字节。
    # Title: **append Chinese text as a string literal**, never byte by byte -- one CJK glyph is
    # three bytes in UTF-8, so a byte-wise write truncates it (measured on the C side).
    if LANG == 0:
        itoa(cur_y, S_NUM)
        strcpy(S_TITLE, S_NUM)
        strcat(S_TITLE, " 年 ")
        itoa(cur_m, S_NUM)
        strcat(S_TITLE, S_NUM)
        strcat(S_TITLE, " 月")
    else:
        strcpy(S_TITLE, month_en(cur_m))
        strcat(S_TITLE, " ")
        itoa(cur_y, S_NUM)
        strcat(S_TITLE, S_NUM)
    ui_2d_text_styled(cx, G_TOP - 64, S_TITLE, C_TITLE, 18, VML_ANCHOR_CENTER, VML_FONT_BOLD)

    # 星期表头：一格一个词，位置与下面的格子**同一套算式**
    # Weekday header: one word per cell, positioned by the **same formula** the cells below use
    i = 0
    while i < 7:
        col = C_DIM
        if i == 0 or i == 6:
            col = C_WEEKEND
        if LANG == 0:
            ui_2d_text(G_PAD + i * G_CELL + G_CELL // 2, G_TOP - 28, WK_CN[i], col, 13,
                       VML_ANCHOR_CENTER)
        else:
            ui_2d_text(G_PAD + i * G_CELL + G_CELL // 2, G_TOP - 28, WK_EN[i], col, 13,
                       VML_ANCHOR_CENTER)
        i = i + 1

    # 日期格：先铺底，再写日号
    # Day cells: the background first, then the number
    d = 1
    while d <= n:
        idx = first + d - 1
        x = cell_x(idx)
        y = cell_y(idx)
        is_today = 0
        if cur_y == today_y:
            if cur_m == today_m:
                if d == today_d:
                    is_today = 1
        is_sel = 0
        if d == sel_d:
            is_sel = 1
        if is_sel == 1:
            ui_2d_rect(x + 3, y + 3, G_CELL - 6, G_CELL - 6, C_ACCENT, 1, 0, G_CELL // 4)
        elif is_today == 1:
            ui_2d_rect(x + 3, y + 3, G_CELL - 6, G_CELL - 6, C_TODAY, 1, 0, G_CELL // 4)
        else:
            ui_2d_rect(x + 3, y + 3, G_CELL - 6, G_CELL - 6, C_CARD, 1, 0, G_CELL // 4)
        col = C_DAY
        if idx % 7 == 0 or idx % 7 == 6:
            col = C_WEEKEND
        if is_sel == 1 or is_today == 1:
            col = C_ON_FILL
        ds = daystr(d)
        ui_2d_text(x + G_CELL // 2, y + G_CELL // 2 - 7, ds, col, 15, VML_ANCHOR_CENTER)
        d = d + 1

    # 底部：高亮说明 + 操作提示
    # Bottom: what is highlighted, and the controls
    by = G_TOP + ((first + n - 1) // 7 + 1) * G_CELL + 12
    if by > SH - 40:
        by = SH - 40
    if sel_d > 0:
        ui_2d_text(cx, by, "已选中（点一天 / 用方向键翻月）" if LANG == 0 else "selected (tap a day / arrows change month)",
                   C_ACCENT, 12, VML_ANCHOR_CENTER)
    else:
        ui_2d_text(cx, by, "黄底 = 今天" if LANG == 0 else "yellow = today",
                   C_TODAY, 12, VML_ANCHOR_CENTER)
    ui_2d_text(cx, by + 22, "← → 翻月   ↑ ↓ 翻年   点一天选中   Esc 退出" if LANG == 0
               else "Left/Right month   Up/Down year   tap a day   Esc quits",
               C_DIM, 11, VML_ANCHOR_CENTER)

    ui_2d_present()          # ⚠ 不调这句 = 黑屏
                             # ⚠ without this call you get a black screen


# 点在某一天的格子里？返回日号，没点中返回 0。
# Did the tap land in a day cell? Returns the day number, or 0 when it missed.
# ⚠ **必须与 `draw` 用同一套几何**（`cell_x`/`cell_y`）：各算一套就会出现"看着点在 7 号、
# ⚠ **It must use the same geometry as `draw`** (`cell_x`/`cell_y`): two sets of arithmetic produce
#   实际选中 8 号"这种"意图在变、位置不动"的老毛病。
#   the classic "it looks like you tapped the 7th but the 8th got selected" bug.
def hit_day(px, py):
    first = weekday_of_first(cur_y, cur_m)
    n = days_in_month(cur_y, cur_m)
    d = 1
    idx = 0
    x = 0
    y = 0
    while d <= n:
        idx = first + d - 1
        x = cell_x(idx)
        y = cell_y(idx)
        if px >= x and px < x + G_CELL and py >= y and py < y + G_CELL:
            return d
        d = d + 1
    return 0


# ── 主程序 ────────────────────────────────────────────
# ── main program ──────────────────────────────────────

# 今天：C 版走 `time`/`localtime`，本语言没有取地址 ⇒ 切宿主的 `YYYY-MM-DD` 串。
# Today: the C version goes through `time`/`localtime`; this language has no address-of, so the
# host's `YYYY-MM-DD` string is cut into pieces instead.
DS = vml_get_date()
str_substr(S_NUM, DS, 0, 4)
today_y = atoi(S_NUM)
str_substr(S_NUM, DS, 5, 2)
today_m = atoi(S_NUM)
str_substr(S_NUM, DS, 8, 2)
today_d = atoi(S_NUM)
cur_y = today_y
cur_m = today_m
sel_d = 0

# 开窗时传 0：尺寸交给宿主（用真实可用区）；开窗后再问一次真实画布（见下）。
# Open with 0 so the host decides the size (the real usable area), then ask the real canvas again
# below. Portrait plus no gamepad area (a calendar is all tapping).
ui_win_open_ex("calendar.py", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD)

# 开窗后再问一次 —— 画布尺寸与开窗前的估算**可能不同**（桌面 480×640、手机竖屏 ~395×744）。
# Ask again after opening: the canvas may differ from the pre-open estimate (desktop 480x640,
# phone portrait about 395x744).
# ⚠ 这一段是**赋值给模块级标量**，只能写在顶层（函数里写就是新建局部量，实测）。
# ⚠ These are assignments to module-level scalars, so they can only live at the top level (inside a
#   function they would just create locals, measured).
SW = scr_w()
SH = scr_h()
G_PAD = pad_for(SW)
G_CELL = cell_for(SW, G_PAD)
G_TOP = top_for(SH, G_CELL)
draw()

T0 = ui_time_get_ms()
DONE = 0
while DONE == 0 and ui_win_is_closed() == 0:
    T = ui_msg_fetch_wait(200)
    if T == VML_MSG_NONE:
        if ui_time_get_ms() - T0 > IDLE_EXIT_MS:
            DONE = 1              # 空闲超时（验收用）
            # idle timeout (for automated checks)
    elif T == VML_MSG_KEYDOWN:
        K = ui_msg_get_a()
        T0 = ui_time_get_ms()
        if K == VML_KEY_ESCAPE or K == 113 or K == 81:
            DONE = 1
        elif K == VML_KEY_LEFT:
            cur_m = cur_m - 1
            if cur_m < 1:
                cur_m = 12
                cur_y = cur_y - 1
            sel_d = 0
            draw()
        elif K == VML_KEY_RIGHT:
            cur_m = cur_m + 1
            if cur_m > 12:
                cur_m = 1
                cur_y = cur_y + 1
            sel_d = 0
            draw()
        elif K == VML_KEY_UP:
            cur_y = cur_y - 1
            sel_d = 0
            draw()
        elif K == VML_KEY_DOWN:
            cur_y = cur_y + 1
            sel_d = 0
            draw()
    elif T == VML_MSG_TOUCHDOWN or T == VML_MSG_MOUSEDOWN:
        TX = ui_msg_get_a()
        TY = ui_msg_get_b()
        T0 = ui_time_get_ms()
        HD = hit_day(TX, TY)
        if HD > 0:
            sel_d = HD
            draw()
    elif T == VML_MSG_WINDOWORIENT or T == VML_MSG_WINDOWRESIZE:
        # 画布变了几何就全变 —— 与开窗后那段**同一串赋值**（规则本身在 scr_w/pad_for/... 里）。
        # The canvas changed, so every derived value changes -- the same sequence of assignments as
        # after the window opened (the rules themselves live in scr_w/pad_for/...).
        SW = scr_w()
        SH = scr_h()
        G_PAD = pad_for(SW)
        G_CELL = cell_for(SW, G_PAD)
        G_TOP = top_for(SH, G_CELL)
        draw()
    elif T == VML_MSG_WINDOWCLOSE:
        DONE = 1
