← 都来码 / Dolaima← 例程索引c/app/stock.c原文

c/app/stock.c · 59723 字节 · 下载原文

/* stock.c —— 常用小程序③:**股票行情**(联网抓 A 股行情 → 彩色卡片;抓不到就回落读本地)
 * stock.c -- Everyday utility #3: **stock quotes** (fetch A-share quotes over the network, draw them as
 * colour cards, and fall back to a local file when the fetch fails)
 *
 * 干什么:一屏卡片,每只股票一张 —— 代码、名称、现价、涨跌幅(中国习惯:涨红跌绿),
 * What it does: one full screen of cards, one per stock -- code, name, last price, change percent
 * 外加一条按涨跌幅画的小条(中间那根竖线是 0,往右红 = 涨,往左绿 = 跌)。
 * (red for up, green for down, the Chinese convention), plus a small bar drawn from the change percent
 * (the tick in the middle is zero: bar to the right is red/up, to the left is green/down).
 *
 * 数据从哪来:`qt.gtimg.cn` 的 **明文 HTTP**(80 端口,免密钥)——
 * Where the data comes from: plain HTTP (port 80, no API key) against `qt.gtimg.cn` --
 * VM 里没有 TLS,https 走不通,所以只挑了这个能明文说话的行情源。
 * there is no TLS in the VM so https is out of reach, and this is a quote source that speaks plaintext.
 *      GET /q=sh600000,sz000001,... HTTP/1.0
 * 响应是一行 `v_<代码>="0~1~2~…";`,**记录之间用 `;` 分、字段之间用 `~` 分**。
 * The reply is a line of `v_<code>="0~1~2~...";`, **records separated by `;` and fields by `~`**.
 *
 * 离线怎么回落:DNS / 建套接字 / 连接 / 发送 / HTTP 状态 / 正文分界,**任一环节失败**就去读
 * Offline fallback: if DNS, socket, connect, send, the HTTP status or the header/body boundary fails,
 * 同目录的 `stocks.txt`(每行 `代码|名称|现价|涨跌幅`,`#` 开头是注释,UTF-8),
 * it reads `stocks.txt` from the program's directory (one `code|name|price|change%` per line, `#` comments,
 * 标题上标一行"离线:读本地 stocks.txt",界面照常出。
 * UTF-8) and marks the title "offline: reading local stocks.txt"; the screen renders just the same.
 * 候选路径依次试 `stocks.txt`、`examples/c/app/stocks.txt`、`Examples/c/app/stocks.txt` ——
 * The candidate paths are tried in order: `stocks.txt`, `examples/c/app/stocks.txt`, `Examples/c/app/stocks.txt` --
 * 桌面上 `scripts/vmlcli` 把**相对路径解到源文件所在目录**,手机上「命令行」页的当前目录是工作区根、
 * on the desktop `scripts/vmlcli` resolves **relative paths against the source file's directory**, while on the
 * 随包的例程解在 `examples/c/app/` 下,一张候选表两边都能找到。
 * phone the command-line page's cwd is the workspace root and the bundled samples unpack under `examples/c/app/`.
 *
 * 跑法(桌面脚手架,秒级,别为改一行去打 APK):
 * How to run (desktop scaffolding, seconds -- do not build an APK just to change one line):
 *     dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll third_party/vml/Examples/c/app/stock.c \
 *         --vml-home third_party/vml --timeout 60 --frame /tmp/stock.png
 * 手机上:命令行页敲 `vml run examples/c/app/stock.c`。
 * On the phone: type `vml run examples/c/app/stock.c` in the command-line page.
 *
 * ⚠ 五个**实测**出来的坑,改动本文件前先读(结论都是量出来的,不是推的):
 * ⚠ Five pitfalls **measured** rather than guessed -- read these before changing this file:
 *
 * ① **`encoding_gbk_decode` 是桩函数,不是真 GBK 表**(2026-10-07 实测)。它返回的是
 * ① **`encoding_gbk_decode` is a stub, not a real GBK table** (measured 2026-10-07). It returns
 *    `0x554A + (b0 - 0xB0) * 94 + (b1 - 0xA1)`,`Lib/shared/src/encoding.c` 第 128 行原样如此。
 *    `0x554A + (b0 - 0xB0) * 94 + (b1 - 0xA1)` -- verbatim from line 128 of `Lib/shared/src/encoding.c`.
 *    实测:`浦` 的 GBK 字节 `C6 D6` 本该是 U+6D66(28006),它给 23955(U+5D93);
 *    Measured: the GBK bytes `C6 D6` should be U+6D66 (28006) but it hands back 23955 (U+5D93),
 *    `发` 的 `B7 A2` 本该是 U+53D1(21457),它给 22493(U+57DD)。
 *    and `B7 A2` should be U+53D1 (21457) but it gives 22493 (U+57DD).
 *    ⚠ 症状**不是乱码**(那还看得懂是编码问题),而是**画出一个长得像字、但不是那个字的汉字**,
 *    ⚠ The symptom is **not mojibake** (which at least reads as an encoding problem) but a **wrong hanzi
 *    最容易被当成"接口换股票了"。
 *    that looks like a plausible character** -- easy to blame on the API instead.
 *    ⇒ 所以本文件自带一张 **GB2312 全表**(6768 项,见文件末尾,只做数据不做逻辑),
 *    ⇒ So this file carries its own **complete GB2312 table** (6768 entries, at the end of the file; data,
 *      只借编码库两半里**确实正确**的那一半:`encoding_utf8_encode` / `encoding_utf8_decode`。
 *      not logic) and borrows only the half of the encoding library that is **actually correct**:
 *      `encoding_utf8_encode` / `encoding_utf8_decode`. Measured: `encoding_utf8_encode(28006, ...)`
 *      实测 `encoding_utf8_encode(28006, …)` 回 3 字节 `E6 B5 A6`,对的。
 *      returns the three bytes `E6 B5 A6`, which is right.
 *
 * ② **HTTP/1.0 + `Connection: close` 才拿得到干净正文**。同一台服务器:
 * ② **Only HTTP/1.0 plus `Connection: close` yields a clean body.** The same server:
 *    用 HTTP/1.1 问,它回 `Transfer-Encoding: chunked` + `Connection: keep-alive` ——
 *    ask with HTTP/1.1 and it answers `Transfer-Encoding: chunked` + `Connection: keep-alive` --
 *    正文里夹着分块长度行、而且连接**不关**,读循环收不了尾;
 *    the body is peppered with chunk-size lines and the connection **never closes**, so the read loop
 *    has no end;
 *    用 HTTP/1.0 + `Connection: close` 问,它回 `Connection: close` 并**真的关掉**(实测 3414 字节后 recv 回 0)。
 *    ask with HTTP/1.0 + `Connection: close` and it answers `Connection: close` and **really does close**
 *    (measured: recv returns 0 after 3414 bytes).
 *
 * ③ **字段下标是数出来的,不是抄来的**。拿实测响应逐个数(0 起):
 * ③ **The field indices are counted, not copied.** Counting the live response from 0:
 *    1=名称、2=代码、3=现价、4=昨收、5=今开,中间 6~28 是买卖五档(价格/量交替),29 空、
 *    1=name, 2=code, 3=last price, 4=previous close, 5=open; 6-28 are the five bid/ask levels
 *    30=时间戳 `YYYYMMDDHHMMSS`,**31=涨跌额、32=涨跌幅%**。
 *    (price/volume alternating), 29 is empty, 30 is the `YYYYMMDDHHMMSS` timestamp, and
 *    **31=change amount, 32=change percent**.
 *    实测三条:`0.30` / `3.27`、`0.22` / `1.94`、`0.79` / `1.50`,与 `9.48` 对 `9.18` 的差对得上。
 *    Measured three times: `0.30` / `3.27`, `0.22` / `1.94`, `0.79` / `1.50` -- consistent with `9.48` vs `9.18`.
 *
 * ④ **名称里可以带空格**(实测 `sz000858` 回的字段 1 就是 `五 粮 液`,三个字中间各一个空格)。
 * ④ **The name may contain spaces** (measured: field 1 for `sz000858` comes back with a space between
 *    ⇒ 解析时**不能"遇到空格就断"**,字段的边界只有 `~` 和收尾的 `"`。
 *    each pair of hanzi), so the parser **must not break on a space**: the only boundaries are `~`
 *    and the closing `"`.
 *
 * ⑤ **宿主不解析域名**(同 weather.c 的坑 ①):`#334 SocketConnect(fd, ip, port)` 内部是
 * ⑤ **The host does not resolve names** (same as pitfall 1 of weather.c): `#334 SocketConnect(fd, ip, port)`
 *    `IPAddress.Parse(ip)`,喂域名直接抛异常回 -1,所以先走 `#338 DnsResolve(host)`。
 *    is implemented as `IPAddress.Parse(ip)`, so a domain name throws and returns -1; call
 *    失败时 R0 是 **-1**(不是 0),判据得写 `<= 0`。
 *    `#338 DnsResolve(host)` first. Failure puts **-1** in R0 (not 0), so the test is `<= 0`.
 *
 * 版面:所有尺寸都从宿主问(`ui_scr_get_w()/_h()`),颜色一律真彩 `0xAARRGGBB`,
 * Layout: every dimension is asked of the host (`ui_scr_get_w()/_h()`), colours are true colour
 * 画完必须 `ui_2d_present()`(漏了就黑屏 —— 宿主不猜你画完了没)。
 * `0xAARRGGBB`, and the frame must end with `ui_2d_present()` (skip it and the screen stays black --
 * the host does not guess when you are done).
 * 循环**必须有界**(例程要能被自动验收):空闲 8 秒自己退出,`Esc`/`q` 也退出;
 * The loop **must be bounded** (samples have to be automatically verifiable): it exits after 8 s of
 * 其余任意键 = 重新抓一次并刷新。写真正的程序时把 `IDLE_EXIT_MS` 去掉即可。
 * idling, `Esc`/`q` also quits, and any other key re-fetches and redraws. Drop `IDLE_EXIT_MS` in a real program.
 */

#include <vml_ui.h>
#include <stdlib.h>   /* itoa --- digits to text via the library */
#include <encoding.h>      /* 只用它的 UTF-8 两半,GBK 那半是桩,见文件头 ① */
#include <vml_sys.h>       /* vml_dns_resolve / vml_file_* —— 本文件不再自己包汇编 */
#include <http.h>          /* http_get_to —— 套接字/剥头/状态码全在库里,不再手写 */
                           /* Only its UTF-8 half is used; the GBK half is a stub, see pitfall 1 above. */

#define HOST    "qt.gtimg.cn"
#define PORT    80

#define WANT      6        /*  请求几只 */
                           /* how many stocks to ask for */
#define MAXSTK    8        /*  扁平缓冲能装几只 */
                           /* how many the flat buffers hold */
#define BUFCAP    8192     /*  响应缓冲:6 只实测 3175 字节 */
                           /* response buffer: 3175 bytes measured for 6 */
#define MAXROUNDS 64       /*  读循环上限,防「对端不关连接」时死读 */
                           /* read-loop cap */

#define IDLE_EXIT_MS 8000

/* 请求的 6 只(沪 `sh` + 代码、深 `sz` + 代码)。特意**涨跌都有**,好一眼看出红绿两种条。
 * The six requested stocks (`sh`/`sz` plus the code). Deliberately **a mix of risers and fallers**, so
 * both the red and the green bar show up at a glance. */
#define QUERY "sh600000,sz000001,sh601318,sh600519,sz000333,sh688981"

/* 配色(真彩 AA RRGGBB)
   Colours (true colour AA RRGGBB) */
#define C_BG     0xFF0E1116
#define C_CARD   0xFF1A1F28
#define C_TRACK  0xFF2A3038
#define C_TITLE  0xFFE8E8F0
#define C_DIM    0xFF9AA0B0
#define C_UP     0xFFE05A4A    /*  涨 = 红(中国习惯) */
                               /* up = red (the Chinese convention) */
#define C_DOWN   0xFF3FAE6A    /*  跌 = 绿 */
                               /* down = green */
#define C_WARN   0xFFE0A040    /*  离线提示 */
                               /* the offline notice */

/* ⚠ 这里原先有一层手写的 `sys_*` 包装(8 个 `asm("SYSCALL #nnn, …")` + 全局 `_sysret` 中转)。
 *   2026-10-07 起改调库(`#include <vml_sys.h>`):`sys_dns`→`vml_dns_resolve`、
 *   `sys_socket`→`vml_socket_create(2,1)`、`sys_connect/send/recv/sockclose`→`vml_socket_*`、
 *   `sys_fileopen/fileread/fileclose`→`vml_file_*`。这 8 个号库里早就包好了。
 * The hand-rolled `sys_*` wrappers and the `_sysret` global are gone; the library functions (via
 * `#include <vml_sys.h>`) cover the same eight syscall numbers.
 */

/* ── GB2312 表(**数据,不是逻辑**)─────────────────────────────────── */
/* -- The GB2312 table (**data, not logic**) -- */
/* 一行 = GB2312 的一个"区"(94 个字),区号从 0xB0 排到 0xF7,共 72 行 6768 项;
 * One line is one GB2312 "zone" (94 characters), zones running from 0xB0 to 0xF7 -- 72 lines, 6768 entries.
 * 下标 = (b0 - 0xB0) * 94 + (b1 - 0xA1),查出来是 **Unicode 码点**。
 * The index is (b0 - 0xB0) * 94 + (b1 - 0xA1) and the value is a **Unicode codepoint**.
 * 表里查不到的槽位填的是 `?`(U+003F),所以下标越界也不会画出空字。
 * Slots the standard leaves undefined hold `?` (U+003F), so even a bogus index draws something.
 * ⚠ 为什么不用 `encoding_gbk_decode`:它是桩,见文件头 ①(实测会给出**另一个汉字**)。
 * ⚠ Why not `encoding_gbk_decode`: it is a stub, see pitfall 1 above (measured: it yields **a different hanzi**).
 * ⚠ 相邻字符串字面量拼接(一行一个区、跨行连着写)在本前端上是成立的(实测 6768 项一个不少)。
 * ⚠ Adjacent string-literal concatenation (one zone per line, split across lines) works on this front end
 *   (measured: all 6768 entries arrive).
 */
static char g_gtab[] =
    "啊阿埃挨哎唉哀皑癌蔼矮艾碍爱隘鞍氨安俺按暗岸胺案肮昂盎凹敖熬翱袄傲奥懊澳芭捌扒叭吧笆八疤巴拔跋靶把耙坝霸罢爸白柏百摆佰败拜稗斑班搬扳般颁板版扮拌伴瓣半办绊邦帮梆榜膀绑棒磅蚌镑傍谤苞胞包褒剥"
    "薄雹保堡饱宝抱报暴豹鲍爆杯碑悲卑北辈背贝钡倍狈备惫焙被奔苯本笨崩绷甭泵蹦迸逼鼻比鄙笔彼碧蓖蔽毕毙毖币庇痹闭敝弊必辟壁臂避陛鞭边编贬扁便变卞辨辩辫遍标彪膘表鳖憋别瘪彬斌濒滨宾摈兵冰柄丙秉饼炳"
    "病并玻菠播拨钵波博勃搏铂箔伯帛舶脖膊渤泊驳捕卜哺补埠不布步簿部怖擦猜裁材才财睬踩采彩菜蔡餐参蚕残惭惨灿苍舱仓沧藏操糙槽曹草厕策侧册测层蹭插叉茬茶查碴搽察岔差诧拆柴豺搀掺蝉馋谗缠铲产阐颤昌猖"
    "场尝常长偿肠厂敞畅唱倡超抄钞朝嘲潮巢吵炒车扯撤掣彻澈郴臣辰尘晨忱沉陈趁衬撑称城橙成呈乘程惩澄诚承逞骋秤吃痴持匙池迟弛驰耻齿侈尺赤翅斥炽充冲虫崇宠抽酬畴踌稠愁筹仇绸瞅丑臭初出橱厨躇锄雏滁除楚"
    "础储矗搐触处揣川穿椽传船喘串疮窗幢床闯创吹炊捶锤垂春椿醇唇淳纯蠢戳绰疵茨磁雌辞慈瓷词此刺赐次聪葱囱匆从丛凑粗醋簇促蹿篡窜摧崔催脆瘁粹淬翠村存寸磋撮搓措挫错搭达答瘩打大呆歹傣戴带殆代贷袋待逮"
    "怠耽担丹单郸掸胆旦氮但惮淡诞弹蛋当挡党荡档刀捣蹈倒岛祷导到稻悼道盗德得的蹬灯登等瞪凳邓堤低滴迪敌笛狄涤翟嫡抵底地蒂第帝弟递缔颠掂滇碘点典靛垫电佃甸店惦奠淀殿碉叼雕凋刁掉吊钓调跌爹碟蝶迭谍叠"
    "丁盯叮钉顶鼎锭定订丢东冬董懂动栋侗恫冻洞兜抖斗陡豆逗痘都督毒犊独读堵睹赌杜镀肚度渡妒端短锻段断缎堆兑队对墩吨蹲敦顿囤钝盾遁掇哆多夺垛躲朵跺舵剁惰堕蛾峨鹅俄额讹娥恶厄扼遏鄂饿恩而儿耳尔饵洱二"
    "贰发罚筏伐乏阀法珐藩帆番翻樊矾钒繁凡烦反返范贩犯饭泛坊芳方肪房防妨仿访纺放菲非啡飞肥匪诽吠肺废沸费芬酚吩氛分纷坟焚汾粉奋份忿愤粪丰封枫蜂峰锋风疯烽逢冯缝讽奉凤佛否夫敷肤孵扶拂辐幅氟符伏俘服"
    "浮涪福袱弗甫抚辅俯釜斧脯腑府腐赴副覆赋复傅付阜父腹负富讣附妇缚咐噶嘎该改概钙盖溉干甘杆柑竿肝赶感秆敢赣冈刚钢缸肛纲岗港杠篙皋高膏羔糕搞镐稿告哥歌搁戈鸽胳疙割革葛格蛤阁隔铬个各给根跟耕更庚羹"
    "埂耿梗工攻功恭龚供躬公宫弓巩汞拱贡共钩勾沟苟狗垢构购够辜菇咕箍估沽孤姑鼓古蛊骨谷股故顾固雇刮瓜剐寡挂褂乖拐怪棺关官冠观管馆罐惯灌贯光广逛瑰规圭硅归龟闺轨鬼诡癸桂柜跪贵刽辊滚棍锅郭国果裹过哈"
    "骸孩海氦亥害骇酣憨邯韩含涵寒函喊罕翰撼捍旱憾悍焊汗汉夯杭航壕嚎豪毫郝好耗号浩呵喝荷菏核禾和何合盒貉阂河涸赫褐鹤贺嘿黑痕很狠恨哼亨横衡恒轰哄烘虹鸿洪宏弘红喉侯猴吼厚候后呼乎忽瑚壶葫胡蝴狐糊湖"
    "弧虎唬护互沪户花哗华猾滑画划化话槐徊怀淮坏欢环桓还缓换患唤痪豢焕涣宦幻荒慌黄磺蝗簧皇凰惶煌晃幌恍谎灰挥辉徽恢蛔回毁悔慧卉惠晦贿秽会烩汇讳诲绘荤昏婚魂浑混豁活伙火获或惑霍货祸击圾基机畸稽积箕"
    "肌饥迹激讥鸡姬绩缉吉极棘辑籍集及急疾汲即嫉级挤几脊己蓟技冀季伎祭剂悸济寄寂计记既忌际妓继纪嘉枷夹佳家加荚颊贾甲钾假稼价架驾嫁歼监坚尖笺间煎兼肩艰奸缄茧检柬碱硷拣捡简俭剪减荐槛鉴践贱见键箭件"
    "健舰剑饯渐溅涧建僵姜将浆江疆蒋桨奖讲匠酱降蕉椒礁焦胶交郊浇骄娇嚼搅铰矫侥脚狡角饺缴绞剿教酵轿较叫窖揭接皆秸街阶截劫节桔杰捷睫竭洁结解姐戒藉芥界借介疥诫届巾筋斤金今津襟紧锦仅谨进靳晋禁近烬浸"
    "尽劲荆兢茎睛晶鲸京惊精粳经井警景颈静境敬镜径痉靖竟竞净炯窘揪究纠玖韭久灸九酒厩救旧臼舅咎就疚鞠拘狙疽居驹菊局咀矩举沮聚拒据巨具距踞锯俱句惧炬剧捐鹃娟倦眷卷绢撅攫抉掘倔爵觉决诀绝均菌钧军君峻"
    "俊竣浚郡骏喀咖卡咯开揩楷凯慨刊堪勘坎砍看康慷糠扛抗亢炕考拷烤靠坷苛柯棵磕颗科壳咳可渴克刻客课肯啃垦恳坑吭空恐孔控抠口扣寇枯哭窟苦酷库裤夸垮挎跨胯块筷侩快宽款匡筐狂框矿眶旷况亏盔岿窥葵奎魁傀"
    "馈愧溃坤昆捆困括扩廓阔垃拉喇蜡腊辣啦莱来赖蓝婪栏拦篮阑兰澜谰揽览懒缆烂滥琅榔狼廊郎朗浪捞劳牢老佬姥酪烙涝勒乐雷镭蕾磊累儡垒擂肋类泪棱楞冷厘梨犁黎篱狸离漓理李里鲤礼莉荔吏栗丽厉励砾历利傈例俐"
    "痢立粒沥隶力璃哩俩联莲连镰廉怜涟帘敛脸链恋炼练粮凉梁粱良两辆量晾亮谅撩聊僚疗燎寥辽潦了撂镣廖料列裂烈劣猎琳林磷霖临邻鳞淋凛赁吝拎玲菱零龄铃伶羚凌灵陵岭领另令溜琉榴硫馏留刘瘤流柳六龙聋咙笼窿"
    "隆垄拢陇楼娄搂篓漏陋芦卢颅庐炉掳卤虏鲁麓碌露路赂鹿潞禄录陆戮驴吕铝侣旅履屡缕虑氯律率滤绿峦挛孪滦卵乱掠略抡轮伦仑沦纶论萝螺罗逻锣箩骡裸落洛骆络妈麻玛码蚂马骂嘛吗埋买麦卖迈脉瞒馒蛮满蔓曼慢漫"
    "谩芒茫盲氓忙莽猫茅锚毛矛铆卯茂冒帽貌贸么玫枚梅酶霉煤没眉媒镁每美昧寐妹媚门闷们萌蒙檬盟锰猛梦孟眯醚靡糜迷谜弥米秘觅泌蜜密幂棉眠绵冕免勉娩缅面苗描瞄藐秒渺庙妙蔑灭民抿皿敏悯闽明螟鸣铭名命谬摸"
    "摹蘑模膜磨摩魔抹末莫墨默沫漠寞陌谋牟某拇牡亩姆母墓暮幕募慕木目睦牧穆拿哪呐钠那娜纳氖乃奶耐奈南男难囊挠脑恼闹淖呢馁内嫩能妮霓倪泥尼拟你匿腻逆溺蔫拈年碾撵捻念娘酿鸟尿捏聂孽啮镊镍涅您柠狞凝宁"
    "拧泞牛扭钮纽脓浓农弄奴努怒女暖虐疟挪懦糯诺哦欧鸥殴藕呕偶沤啪趴爬帕怕琶拍排牌徘湃派攀潘盘磐盼畔判叛乓庞旁耪胖抛咆刨炮袍跑泡呸胚培裴赔陪配佩沛喷盆砰抨烹澎彭蓬棚硼篷膨朋鹏捧碰坯砒霹批披劈琵毗"
    "啤脾疲皮匹痞僻屁譬篇偏片骗飘漂瓢票撇瞥拼频贫品聘乒坪苹萍平凭瓶评屏坡泼颇婆破魄迫粕剖扑铺仆莆葡菩蒲埔朴圃普浦谱曝瀑期欺栖戚妻七凄漆柒沏其棋奇歧畦崎脐齐旗祈祁骑起岂乞企启契砌器气迄弃汽泣讫掐"
    "恰洽牵扦钎铅千迁签仟谦乾黔钱钳前潜遣浅谴堑嵌欠歉枪呛腔羌墙蔷强抢橇锹敲悄桥瞧乔侨巧鞘撬翘峭俏窍切茄且怯窃钦侵亲秦琴勤芹擒禽寝沁青轻氢倾卿清擎晴氰情顷请庆琼穷秋丘邱球求囚酋泅趋区蛆曲躯屈驱渠"
    "取娶龋趣去圈颧权醛泉全痊拳犬券劝缺炔瘸却鹊榷确雀裙群然燃冉染瓤壤攘嚷让饶扰绕惹热壬仁人忍韧任认刃妊纫扔仍日戎茸蓉荣融熔溶容绒冗揉柔肉茹蠕儒孺如辱乳汝入褥软阮蕊瑞锐闰润若弱撒洒萨腮鳃塞赛三叁"
    "伞散桑嗓丧搔骚扫嫂瑟色涩森僧莎砂杀刹沙纱傻啥煞筛晒珊苫杉山删煽衫闪陕擅赡膳善汕扇缮墒伤商赏晌上尚裳梢捎稍烧芍勺韶少哨邵绍奢赊蛇舌舍赦摄射慑涉社设砷申呻伸身深娠绅神沈审婶甚肾慎渗声生甥牲升绳"
    "省盛剩胜圣师失狮施湿诗尸虱十石拾时什食蚀实识史矢使屎驶始式示士世柿事拭誓逝势是嗜噬适仕侍释饰氏市恃室视试收手首守寿授售受瘦兽蔬枢梳殊抒输叔舒淑疏书赎孰熟薯暑曙署蜀黍鼠属术述树束戍竖墅庶数漱"
    "恕刷耍摔衰甩帅栓拴霜双爽谁水睡税吮瞬顺舜说硕朔烁斯撕嘶思私司丝死肆寺嗣四伺似饲巳松耸怂颂送宋讼诵搜艘擞嗽苏酥俗素速粟僳塑溯宿诉肃酸蒜算虽隋随绥髓碎岁穗遂隧祟孙损笋蓑梭唆缩琐索锁所塌他它她塔"
    "獭挞蹋踏胎苔抬台泰酞太态汰坍摊贪瘫滩坛檀痰潭谭谈坦毯袒碳探叹炭汤塘搪堂棠膛唐糖倘躺淌趟烫掏涛滔绦萄桃逃淘陶讨套特藤腾疼誊梯剔踢锑提题蹄啼体替嚏惕涕剃屉天添填田甜恬舔腆挑条迢眺跳贴铁帖厅听烃"
    "汀廷停亭庭挺艇通桐酮瞳同铜彤童桶捅筒统痛偷投头透凸秃突图徒途涂屠土吐兔湍团推颓腿蜕褪退吞屯臀拖托脱鸵陀驮驼椭妥拓唾挖哇蛙洼娃瓦袜歪外豌弯湾玩顽丸烷完碗挽晚皖惋宛婉万腕汪王亡枉网往旺望忘妄威"
    "巍微危韦违桅围唯惟为潍维苇萎委伟伪尾纬未蔚味畏胃喂魏位渭谓尉慰卫瘟温蚊文闻纹吻稳紊问嗡翁瓮挝蜗涡窝我斡卧握沃巫呜钨乌污诬屋无芜梧吾吴毋武五捂午舞伍侮坞戊雾晤物勿务悟误昔熙析西硒矽晰嘻吸锡牺"
    "稀息希悉膝夕惜熄烯溪汐犀檄袭席习媳喜铣洗系隙戏细瞎虾匣霞辖暇峡侠狭下厦夏吓掀锨先仙鲜纤咸贤衔舷闲涎弦嫌显险现献县腺馅羡宪陷限线相厢镶香箱襄湘乡翔祥详想响享项巷橡像向象萧硝霄削哮嚣销消宵淆晓"
    "小孝校肖啸笑效楔些歇蝎鞋协挟携邪斜胁谐写械卸蟹懈泄泻谢屑薪芯锌欣辛新忻心信衅星腥猩惺兴刑型形邢行醒幸杏性姓兄凶胸匈汹雄熊休修羞朽嗅锈秀袖绣墟戌需虚嘘须徐许蓄酗叙旭序畜恤絮婿绪续轩喧宣悬旋玄"
    "选癣眩绚靴薛学穴雪血勋熏循旬询寻驯巡殉汛训讯逊迅压押鸦鸭呀丫芽牙蚜崖衙涯雅哑亚讶焉咽阉烟淹盐严研蜒岩延言颜阎炎沿奄掩眼衍演艳堰燕厌砚雁唁彦焰宴谚验殃央鸯秧杨扬佯疡羊洋阳氧仰痒养样漾邀腰妖瑶"
    "摇尧遥窑谣姚咬舀药要耀椰噎耶爷野冶也页掖业叶曳腋夜液一壹医揖铱依伊衣颐夷遗移仪胰疑沂宜姨彝椅蚁倚已乙矣以艺抑易邑屹亿役臆逸肄疫亦裔意毅忆义益溢诣议谊译异翼翌绎茵荫因殷音阴姻吟银淫寅饮尹引隐"
    "印英樱婴鹰应缨莹萤营荧蝇迎赢盈影颖硬映哟拥佣臃痈庸雍踊蛹咏泳涌永恿勇用幽优悠忧尤由邮铀犹油游酉有友右佑釉诱又幼迂淤于盂榆虞愚舆余俞逾鱼愉渝渔隅予娱雨与屿禹宇语羽玉域芋郁吁遇喻峪御愈欲狱育誉"
    "浴寓裕预豫驭鸳渊冤元垣袁原援辕园员圆猿源缘远苑愿怨院曰约越跃钥岳粤月悦阅耘云郧匀陨允运蕴酝晕韵孕匝砸杂栽哉灾宰载再在咱攒暂赞赃脏葬遭糟凿藻枣早澡蚤躁噪造皂灶燥责择则泽贼怎增憎曾赠扎喳渣札轧"
    "铡闸眨栅榨咋乍炸诈摘斋宅窄债寨瞻毡詹粘沾盏斩辗崭展蘸栈占战站湛绽樟章彰漳张掌涨杖丈帐账仗胀瘴障招昭找沼赵照罩兆肇召遮折哲蛰辙者锗蔗这浙珍斟真甄砧臻贞针侦枕疹诊震振镇阵蒸挣睁征狰争怔整拯正政"
    "帧症郑证芝枝支吱蜘知肢脂汁之织职直植殖执值侄址指止趾只旨纸志挚掷至致置帜峙制智秩稚质炙痔滞治窒中盅忠钟衷终种肿重仲众舟周州洲诌粥轴肘帚咒皱宙昼骤珠株蛛朱猪诸诛逐竹烛煮拄瞩嘱主著柱助蛀贮铸筑"
    "住注祝驻抓爪拽专砖转撰赚篆桩庄装妆撞壮状椎锥追赘坠缀谆准捉拙卓桌琢茁酌啄着灼浊兹咨资姿滋淄孜紫仔籽滓子自渍字鬃棕踪宗综总纵邹走奏揍租足卒族祖诅阻组钻纂嘴醉最罪尊遵昨左佐柞做作坐座?????"
    "亍丌兀丐廿卅丕亘丞鬲孬噩丨禺丿匕乇夭爻卮氐囟胤馗毓睾鼗丶亟鼐乜乩亓芈孛啬嘏仄厍厝厣厥厮靥赝匚叵匦匮匾赜卦卣刂刈刎刭刳刿剀剌剞剡剜蒯剽劂劁劐劓冂罔亻仃仉仂仨仡仫仞伛仳伢佤仵伥伧伉伫佞佧攸佚佝"
    "佟佗伲伽佶佴侑侉侃侏佾佻侪佼侬侔俦俨俪俅俚俣俜俑俟俸倩偌俳倬倏倮倭俾倜倌倥倨偾偃偕偈偎偬偻傥傧傩傺僖儆僭僬僦僮儇儋仝氽佘佥俎龠汆籴兮巽黉馘冁夔勹匍訇匐凫夙兕亠兖亳衮袤亵脔裒禀嬴蠃羸冫冱冽冼"
    "凇冖冢冥讠讦讧讪讴讵讷诂诃诋诏诎诒诓诔诖诘诙诜诟诠诤诨诩诮诰诳诶诹诼诿谀谂谄谇谌谏谑谒谔谕谖谙谛谘谝谟谠谡谥谧谪谫谮谯谲谳谵谶卩卺阝阢阡阱阪阽阼陂陉陔陟陧陬陲陴隈隍隗隰邗邛邝邙邬邡邴邳邶邺"
    "邸邰郏郅邾郐郄郇郓郦郢郜郗郛郫郯郾鄄鄢鄞鄣鄱鄯鄹酃酆刍奂劢劬劭劾哿勐勖勰叟燮矍廴凵凼鬯厶弁畚巯坌垩垡塾墼壅壑圩圬圪圳圹圮圯坜圻坂坩垅坫垆坼坻坨坭坶坳垭垤垌垲埏垧垴垓垠埕埘埚埙埒垸埴埯埸埤埝"
    "堋堍埽埭堀堞堙塄堠塥塬墁墉墚墀馨鼙懿艹艽艿芏芊芨芄芎芑芗芙芫芸芾芰苈苊苣芘芷芮苋苌苁芩芴芡芪芟苄苎芤苡茉苷苤茏茇苜苴苒苘茌苻苓茑茚茆茔茕苠苕茜荑荛荜茈莒茼茴茱莛荞茯荏荇荃荟荀茗荠茭茺茳荦荥"
    "荨茛荩荬荪荭荮莰荸莳莴莠莪莓莜莅荼莶莩荽莸荻莘莞莨莺莼菁萁菥菘堇萘萋菝菽菖萜萸萑萆菔菟萏萃菸菹菪菅菀萦菰菡葜葑葚葙葳蒇蒈葺蒉葸萼葆葩葶蒌蒎萱葭蓁蓍蓐蓦蒽蓓蓊蒿蒺蓠蒡蒹蒴蒗蓥蓣蔌甍蔸蓰蔹蔟蔺"
    "蕖蔻蓿蓼蕙蕈蕨蕤蕞蕺瞢蕃蕲蕻薤薨薇薏蕹薮薜薅薹薷薰藓藁藜藿蘧蘅蘩蘖蘼廾弈夼奁耷奕奚奘匏尢尥尬尴扌扪抟抻拊拚拗拮挢拶挹捋捃掭揶捱捺掎掴捭掬掊捩掮掼揲揸揠揿揄揞揎摒揆掾摅摁搋搛搠搌搦搡摞撄摭撖"
    "摺撷撸撙撺擀擐擗擤擢攉攥攮弋忒甙弑卟叱叽叩叨叻吒吖吆呋呒呓呔呖呃吡呗呙吣吲咂咔呷呱呤咚咛咄呶呦咝哐咭哂咴哒咧咦哓哔呲咣哕咻咿哌哙哚哜咩咪咤哝哏哞唛哧唠哽唔哳唢唣唏唑唧唪啧喏喵啉啭啁啕唿啐唼"
    "唷啖啵啶啷唳唰啜喋嗒喃喱喹喈喁喟啾嗖喑啻嗟喽喾喔喙嗪嗷嗉嘟嗑嗫嗬嗔嗦嗝嗄嗯嗥嗲嗳嗌嗍嗨嗵嗤辔嘞嘈嘌嘁嘤嘣嗾嘀嘧嘭噘嘹噗嘬噍噢噙噜噌噔嚆噤噱噫噻噼嚅嚓嚯囔囗囝囡囵囫囹囿圄圊圉圜帏帙帔帑帱帻帼"
    "帷幄幔幛幞幡岌屺岍岐岖岈岘岙岑岚岜岵岢岽岬岫岱岣峁岷峄峒峤峋峥崂崃崧崦崮崤崞崆崛嵘崾崴崽嵬嵛嵯嵝嵫嵋嵊嵩嵴嶂嶙嶝豳嶷巅彳彷徂徇徉後徕徙徜徨徭徵徼衢彡犭犰犴犷犸狃狁狎狍狒狨狯狩狲狴狷猁狳猃狺"
    "狻猗猓猡猊猞猝猕猢猹猥猬猸猱獐獍獗獠獬獯獾舛夥飧夤夂饣饧饨饩饪饫饬饴饷饽馀馄馇馊馍馐馑馓馔馕庀庑庋庖庥庠庹庵庾庳赓廒廑廛廨廪膺忄忉忖忏怃忮怄忡忤忾怅怆忪忭忸怙怵怦怛怏怍怩怫怊怿怡恸恹恻恺恂"
    "恪恽悖悚悭悝悃悒悌悛惬悻悱惝惘惆惚悴愠愦愕愣惴愀愎愫慊慵憬憔憧憷懔懵忝隳闩闫闱闳闵闶闼闾阃阄阆阈阊阋阌阍阏阒阕阖阗阙阚丬爿戕氵汔汜汊沣沅沐沔沌汨汩汴汶沆沩泐泔沭泷泸泱泗沲泠泖泺泫泮沱泓泯泾"
    "洹洧洌浃浈洇洄洙洎洫浍洮洵洚浏浒浔洳涑浯涞涠浞涓涔浜浠浼浣渚淇淅淞渎涿淠渑淦淝淙渖涫渌涮渫湮湎湫溲湟溆湓湔渲渥湄滟溱溘滠漭滢溥溧溽溻溷滗溴滏溏滂溟潢潆潇漤漕滹漯漶潋潴漪漉漩澉澍澌潸潲潼潺濑"
    "濉澧澹澶濂濡濮濞濠濯瀚瀣瀛瀹瀵灏灞宀宄宕宓宥宸甯骞搴寤寮褰寰蹇謇辶迓迕迥迮迤迩迦迳迨逅逄逋逦逑逍逖逡逵逶逭逯遄遑遒遐遨遘遢遛暹遴遽邂邈邃邋彐彗彖彘尻咫屐屙孱屣屦羼弪弩弭艴弼鬻屮妁妃妍妩妪妣"
    "妗姊妫妞妤姒妲妯姗妾娅娆姝娈姣姘姹娌娉娲娴娑娣娓婀婧婊婕娼婢婵胬媪媛婷婺媾嫫媲嫒嫔媸嫠嫣嫱嫖嫦嫘嫜嬉嬗嬖嬲嬷孀尕尜孚孥孳孑孓孢驵驷驸驺驿驽骀骁骅骈骊骐骒骓骖骘骛骜骝骟骠骢骣骥骧纟纡纣纥纨纩"
    "纭纰纾绀绁绂绉绋绌绐绔绗绛绠绡绨绫绮绯绱绲缍绶绺绻绾缁缂缃缇缈缋缌缏缑缒缗缙缜缛缟缡缢缣缤缥缦缧缪缫缬缭缯缰缱缲缳缵幺畿巛甾邕玎玑玮玢玟珏珂珑玷玳珀珉珈珥珙顼琊珩珧珞玺珲琏琪瑛琦琥琨琰琮琬"
    "琛琚瑁瑜瑗瑕瑙瑷瑭瑾璜璎璀璁璇璋璞璨璩璐璧瓒璺韪韫韬杌杓杞杈杩枥枇杪杳枘枧杵枨枞枭枋杷杼柰栉柘栊柩枰栌柙枵柚枳柝栀柃枸柢栎柁柽栲栳桠桡桎桢桄桤梃栝桕桦桁桧桀栾桊桉栩梵梏桴桷梓桫棂楮棼椟椠棹"
    "椤棰椋椁楗棣椐楱椹楠楂楝榄楫榀榘楸椴槌榇榈槎榉楦楣楹榛榧榻榫榭槔榱槁槊槟榕槠榍槿樯槭樗樘橥槲橄樾檠橐橛樵檎橹樽樨橘橼檑檐檩檗檫猷獒殁殂殇殄殒殓殍殚殛殡殪轫轭轱轲轳轵轶轸轷轹轺轼轾辁辂辄辇辋"
    "辍辎辏辘辚軎戋戗戛戟戢戡戥戤戬臧瓯瓴瓿甏甑甓攴旮旯旰昊昙杲昃昕昀炅曷昝昴昱昶昵耆晟晔晁晏晖晡晗晷暄暌暧暝暾曛曜曦曩贲贳贶贻贽赀赅赆赈赉赇赍赕赙觇觊觋觌觎觏觐觑牮犟牝牦牯牾牿犄犋犍犏犒挈挲掰"
    "搿擘耄毪毳毽毵毹氅氇氆氍氕氘氙氚氡氩氤氪氲攵敕敫牍牒牖爰虢刖肟肜肓肼朊肽肱肫肭肴肷胧胨胩胪胛胂胄胙胍胗朐胝胫胱胴胭脍脎胲胼朕脒豚脶脞脬脘脲腈腌腓腴腙腚腱腠腩腼腽腭腧塍媵膈膂膑滕膣膪臌朦臊膻"
    "臁膦欤欷欹歃歆歙飑飒飓飕飙飚殳彀毂觳斐齑斓於旆旄旃旌旎旒旖炀炜炖炝炻烀炷炫炱烨烊焐焓焖焯焱煳煜煨煅煲煊煸煺熘熳熵熨熠燠燔燧燹爝爨灬焘煦熹戾戽扃扈扉礻祀祆祉祛祜祓祚祢祗祠祯祧祺禅禊禚禧禳忑忐"
    "怼恝恚恧恁恙恣悫愆愍慝憩憝懋懑戆肀聿沓泶淼矶矸砀砉砗砘砑斫砭砜砝砹砺砻砟砼砥砬砣砩硎硭硖硗砦硐硇硌硪碛碓碚碇碜碡碣碲碹碥磔磙磉磬磲礅磴礓礤礞礴龛黹黻黼盱眄眍盹眇眈眚眢眙眭眦眵眸睐睑睇睃睚睨"
    "睢睥睿瞍睽瞀瞌瞑瞟瞠瞰瞵瞽町畀畎畋畈畛畲畹疃罘罡罟詈罨罴罱罹羁罾盍盥蠲钅钆钇钋钊钌钍钏钐钔钗钕钚钛钜钣钤钫钪钭钬钯钰钲钴钶钷钸钹钺钼钽钿铄铈铉铊铋铌铍铎铐铑铒铕铖铗铙铘铛铞铟铠铢铤铥铧铨铪"
    "铩铫铮铯铳铴铵铷铹铼铽铿锃锂锆锇锉锊锍锎锏锒锓锔锕锖锘锛锝锞锟锢锪锫锩锬锱锲锴锶锷锸锼锾锿镂锵镄镅镆镉镌镎镏镒镓镔镖镗镘镙镛镞镟镝镡镢镤镥镦镧镨镩镪镫镬镯镱镲镳锺矧矬雉秕秭秣秫稆嵇稃稂稞稔"
    "稹稷穑黏馥穰皈皎皓皙皤瓞瓠甬鸠鸢鸨鸩鸪鸫鸬鸲鸱鸶鸸鸷鸹鸺鸾鹁鹂鹄鹆鹇鹈鹉鹋鹌鹎鹑鹕鹗鹚鹛鹜鹞鹣鹦鹧鹨鹩鹪鹫鹬鹱鹭鹳疒疔疖疠疝疬疣疳疴疸痄疱疰痃痂痖痍痣痨痦痤痫痧瘃痱痼痿瘐瘀瘅瘌瘗瘊瘥瘘瘕瘙"
    "瘛瘼瘢瘠癀瘭瘰瘿瘵癃瘾瘳癍癞癔癜癖癫癯翊竦穸穹窀窆窈窕窦窠窬窨窭窳衤衩衲衽衿袂袢裆袷袼裉裢裎裣裥裱褚裼裨裾裰褡褙褓褛褊褴褫褶襁襦襻疋胥皲皴矜耒耔耖耜耠耢耥耦耧耩耨耱耋耵聃聆聍聒聩聱覃顸颀颃"
    "颉颌颍颏颔颚颛颞颟颡颢颥颦虍虔虬虮虿虺虼虻蚨蚍蚋蚬蚝蚧蚣蚪蚓蚩蚶蛄蚵蛎蚰蚺蚱蚯蛉蛏蚴蛩蛱蛲蛭蛳蛐蜓蛞蛴蛟蛘蛑蜃蜇蛸蜈蜊蜍蜉蜣蜻蜞蜥蜮蜚蜾蝈蜴蜱蜩蜷蜿螂蜢蝽蝾蝻蝠蝰蝌蝮螋蝓蝣蝼蝤蝙蝥螓螯螨蟒"
    "蟆螈螅螭螗螃螫蟥螬螵螳蟋蟓螽蟑蟀蟊蟛蟪蟠蟮蠖蠓蟾蠊蠛蠡蠹蠼缶罂罄罅舐竺竽笈笃笄笕笊笫笏筇笸笪笙笮笱笠笥笤笳笾笞筘筚筅筵筌筝筠筮筻筢筲筱箐箦箧箸箬箝箨箅箪箜箢箫箴篑篁篌篝篚篥篦篪簌篾篼簏簖簋"
    "簟簪簦簸籁籀臾舁舂舄臬衄舡舢舣舭舯舨舫舸舻舳舴舾艄艉艋艏艚艟艨衾袅袈裘裟襞羝羟羧羯羰羲籼敉粑粝粜粞粢粲粼粽糁糇糌糍糈糅糗糨艮暨羿翎翕翥翡翦翩翮翳糸絷綦綮繇纛麸麴赳趄趔趑趱赧赭豇豉酊酐酎酏酤"
    "酢酡酰酩酯酽酾酲酴酹醌醅醐醍醑醢醣醪醭醮醯醵醴醺豕鹾趸跫踅蹙蹩趵趿趼趺跄跖跗跚跞跎跏跛跆跬跷跸跣跹跻跤踉跽踔踝踟踬踮踣踯踺蹀踹踵踽踱蹉蹁蹂蹑蹒蹊蹰蹶蹼蹯蹴躅躏躔躐躜躞豸貂貊貅貘貔斛觖觞觚觜"
    "觥觫觯訾謦靓雩雳雯霆霁霈霏霎霪霭霰霾龀龃龅龆龇龈龉龊龌黾鼋鼍隹隼隽雎雒瞿雠銎銮鋈錾鍪鏊鎏鐾鑫鱿鲂鲅鲆鲇鲈稣鲋鲎鲐鲑鲒鲔鲕鲚鲛鲞鲟鲠鲡鲢鲣鲥鲦鲧鲨鲩鲫鲭鲮鲰鲱鲲鲳鲴鲵鲶鲷鲺鲻鲼鲽鳄鳅鳆鳇鳊鳋"
    "鳌鳍鳎鳏鳐鳓鳔鳕鳗鳘鳙鳜鳝鳟鳢靼鞅鞑鞒鞔鞯鞫鞣鞲鞴骱骰骷鹘骶骺骼髁髀髅髂髋髌髑魅魃魇魉魈魍魑飨餍餮饕饔髟髡髦髯髫髻髭髹鬈鬏鬓鬟鬣麽麾縻麂麇麈麋麒鏖麝麟黛黜黝黠黟黢黩黧黥黪黯鼢鼬鼯鼹鼷鼽鼾齄"
;

static int g_cp[6768];

/* 把 UTF-8 的表解成码点数组,只解一次
   Decode the UTF-8 table into codepoints, once */
static void load_gtab(void) {
    int p;
    int n;
    p = 0;
    n = 0;
    while (g_gtab[p] != 0 && n < 6768) {
        g_cp[n] = encoding_utf8_decode((unsigned char*)(g_gtab + p), 4);
        n = n + 1;
        p = p + encoding_utf8_seqlen((unsigned char)g_gtab[p]);
    }
}

/* ── 全局状态 ───────────────────────────────────────────────────────── */
/* -- Global state -- */
static int SW;              /* 画布宽
                               canvas width */
static int SH;              /* 画布高
                               canvas height */
static int g_lang;          /* 界面语言:0 = 中文,1 = 英文 */
                            /* The UI language: 0 = Chinese, 1 = English */

static char rbuf[BUFCAP];   /* 整个 HTTP 响应(头 + 正文),离线时当正文用 */
                            /* The whole HTTP response (headers plus body); the body only when offline */
static int g_total;         /* rbuf 里读进来多少字节
                               how many bytes are in rbuf */
static int g_head;          /* 正文首字节下标
                               index of the first body byte */
static int g_blen;          /* 正文长度
                               body length */
static int g_lim;           /* 解析时不许越过的右边界
                               the right edge the parser must not cross */
static int offline;         /* 1 = 没连上,显示的是本地文件
                               1 = offline, showing the local file */
static int g_first;         /* 1 = 第一趟,才往控制台打日志
                               1 = first pass, so console logs are printed */

/* 一组扁平缓冲:每只股票定长一格,**不用二维数组、不用指针数组、不取元素地址**
 * One flat buffer set: a fixed-size cell per stock -- **no 2-D arrays, no pointer arrays, no address-of
 * (这三种形态在各语言前端之间最不稳,本仓点名的头号坑就是"同一段代码写 22 遍")。
 * on an element** (those three forms are the least portable across the front ends, and this repo's
 * number-one pitfall is having to write the same code 22 times).
 */
static int  s_n;                    /* 实际只数
                                       the number of stocks actually parsed */
static char s_code[MAXSTK * 12];    /* 代码(ASCII)
                                       code (ASCII) */
static char s_name[MAXSTK * 32];    /* 名称(UTF-8)
                                       name (UTF-8) */
static char s_price[MAXSTK * 16];   /* 现价原文
                                       last price, as text */
static char s_pct[MAXSTK * 16];     /* 涨跌幅原文(带符号)
                                       change percent, as text (signed) */
static int  s_pct100[MAXSTK];       /* 涨跌幅 × 100(画条用)
                                       change percent x 100 (for the bar) */

/* ── 小工具 ─────────────────────────────────────────────────────────── */
/* -- Small helpers -- */

/* 整数 → 十进制串。返回**同一个静态缓冲**,别在一个表达式里连用两次。
 * Integer to a decimal string. Returns **one static buffer**; never use it twice in one expression.
 * ⚠ 本文件现在没有调用点(价格与涨跌幅都**原样显示接口给的字符串**,不重新格式化)——
 * ⚠ Nothing calls it today (both the price and the change percent are **shown exactly as the feed
 *   留着是因为下一个改这个文件的人多半要拿它拼标题或状态行。
 *   spells them**, never re-formatted); it stays because the next person editing this file will
 *   probably want it for a heading or a status line. Delete it if you do not.
 */
static char numbuf[16];
static char *numstr(int v) {
    itoa(v, numbuf);
    return numbuf;
}

/* "3.27" / "-1.50" / "+0.00" → ×100 的整数(327 / -150 / 0),认不出来回 0。
 * "3.27" / "-1.50" / "+0.00" to an integer x100 (327 / -150 / 0); 0 when it cannot be read.
 * ⚠ 只收**两位小数**:行情接口给的正好是两位(实测 "3.27"、"1.94"),第三位再往下的精度对
 * ⚠ **Two decimals only**: the quote feed hands back exactly two (measured "3.27", "1.94"), and finer
 *   一条 100 像素宽的条毫无意义。
 *   detail than that is meaningless on a 100-pixel bar.
 */
static int parse_signed_100(char* s) {
    int i = 0;
    int neg = 0;
    int v = 0;
    if (s[0] == '-') { neg = 1; i = 1; }
    else if (s[0] == '+') { i = 1; }
    while (s[i] >= '0' && s[i] <= '9') { v = v * 10 + (s[i] - '0'); i = i + 1; }
    v = v * 100;
    if (s[i] == '.') {
        i = i + 1;
        if (s[i] >= '0' && s[i] <= '9') { v = v + (s[i] - '0') * 10; i = i + 1; }
        if (s[i] >= '0' && s[i] <= '9') { v = v + (s[i] - '0'); i = i + 1; }
    }
    return neg ? 0 - v : v;
}

/* 从 rbuf 的 [start, start+len) 抄一段进 dst(补 0)。不取数组元素地址,边界自己钳。
 * Copy [start, start+len) out of rbuf into dst (NUL-terminated). No address-of an element; the bound is clamped here. */
static void copy_slice(int start, int len, char* dst, int cap) {
    int i = 0;
    int end = start + len;
    if (end > g_lim) end = g_lim;
    while (start + i < end && i < cap - 1) { dst[i] = rbuf[start + i]; i = i + 1; }
    dst[i] = 0;
}

/* 从 rbuf[start] 起解一个 GBK 字符,返回码点,消耗的字节数放 g_gbkn。
 * Decode one GBK character starting at rbuf[start]; returns the codepoint and puts the byte count in g_gbkn.
 * ASCII 直通;落在 GB2312 那 72 个区里就查表;其余(GBK 扩展区)回 '?'。
 * ASCII passes straight through, the 72 GB2312 zones are looked up, and anything else (the GBK
 * extensions) becomes '?'.
 */
static int g_gbkn;
static int gbk_cp(int start) {
    int b0;
    int b1;
    g_gbkn = 1;
    b0 = rbuf[start] & 0xFF;
    if (b0 <= 0x7F) return b0;
    if (start + 1 >= g_lim) return '?';
    b1 = rbuf[start + 1] & 0xFF;
    if (b0 < 0x81) return '?';
    g_gbkn = 2;
    if (b0 >= 0xB0 && b0 <= 0xF7 && b1 >= 0xA1 && b1 <= 0xFE) {
        return g_cp[(b0 - 0xB0) * 94 + (b1 - 0xA1)];
    }
    return '?';
}

/* GBK 的 [start, start+len) → UTF-8 写进 dst。
 * GBK [start, start+len) to UTF-8, written into dst.
 * ⚠ `encoding_utf8_encode` 只收 `unsigned char*`,所以中转用 unsigned char 数组。
 * ⚠ `encoding_utf8_encode` takes an `unsigned char*`, so the staging buffer is an unsigned char array.
 */
static void gbk_to_utf8(int start, int len, char* dst, int cap) {
    int p = start;
    int end = start + len;
    int o = 0;
    int cp;
    int n;
    int k;
    unsigned char tmp[8];
    if (end > g_lim) end = g_lim;
    while (p < end && o < cap - 1) {
        cp = gbk_cp(p);
        p = p + g_gbkn;
        n = encoding_utf8_encode(cp, tmp, 4);
        if (n <= 0) n = 0;
        if (o + n > cap - 1) break;
        k = 0;
        while (k < n) { dst[o] = (char)tmp[k]; o = o + 1; k = k + 1; }
    }
    dst[o] = 0;
}

/* ⚠ 这里原先有两个手写的协议辅助:`find_body`(找 `\r\n\r\n`)与 `http_code`(解析状态码)——
 *   库 `Lib/shared/src/http.c` 里两样都有,且那份还多做了重定向与 chunked 解码
 *   ⇒ 改用 `http_get_to` 之后一并删掉(约 30 行)。
 * Two hand-written protocol helpers used to live here; `http_get_to` covers both. */

/* 已经攒了几条记录(`v_` 且在本行行首)。用来给读循环加一道"够了就收"的保险。
 * How many records have arrived so far (a `v_` at the start of a line). This lets the read loop stop early. */
static int count_records(void) {
    int i = 0;
    int c = 0;
    while (i < g_total) {
        if (rbuf[i] == 'v' && (i == 0 || (rbuf[i - 1] & 0xFF) == 10)) c = c + 1;
        i = i + 1;
    }
    return c;
}

/* ── 解析(在线的行情行)───────────────────────────────────────────── */
/* -- Parsing (the live quote line) -- */
/* 记录形状:`v_sh600000="0~名称~代码~现价~昨收~今开~…~31 涨跌额~32 涨跌幅~…";`
 * Record shape: `v_sh600000="0~name~code~price~prevClose~open~...~31 change~32 change%~...";`
 * ⚠ 字段边界只有 `~` 和收尾的 `"` —— **不要拿空格当分界**(见文件头 ④)。
 * ⚠ The only field boundaries are `~` and the closing `"` -- **never treat a space as one** (pitfall 4).
 */
static void parse_quote(void) {
    int i;
    int end;
    int fi;
    int fs;
    int ok;
    i = g_head;
    end = g_head + g_blen;
    while (i < end && s_n < MAXSTK) {
        if (rbuf[i] != 'v') { i = i + 1; continue; }
        while (i < end && !(rbuf[i] == '=' && i + 1 < end && rbuf[i + 1] == '"')) i = i + 1;
        if (i >= end) break;
        i = i + 2;                       /* 跳过 `="`,站到字段 0 上
                                            step over `="` onto field 0 */
        fi = 0;
        fs = i;
        ok = 0;
        while (i < end) {
            if (rbuf[i] == '~' || rbuf[i] == '"') {
                if (fi == 1) gbk_to_utf8(fs, i - fs, s_name + s_n * 32, 32);
                else if (fi == 2) copy_slice(fs, i - fs, s_code + s_n * 12, 12);
                else if (fi == 3) copy_slice(fs, i - fs, s_price + s_n * 16, 16);
                else if (fi == 32) copy_slice(fs, i - fs, s_pct + s_n * 16, 16);
                if (rbuf[i] == '"') break;
                fi = fi + 1;
                i = i + 1;
                fs = i;
                continue;
            }
            i = i + 1;
        }
        if (fi >= 32) ok = 1;            /* 走到字段 32 才收
                                            only if field 32 was reached */
        if (ok != 0) {
            s_pct100[s_n] = parse_signed_100(s_pct + s_n * 16);
            s_n = s_n + 1;
        }
        i = i + 1;
    }
}

/* ── 解析(离线的 stocks.txt)──────────────────────────────────────── */
/* -- Parsing (the offline stocks.txt) -- */
/* `代码|名称|现价|涨跌幅`,`#` 开头是注释。名称本来就是 UTF-8,原样抄。
 * `code|name|price|change%`; a leading `#` is a comment. The name is already UTF-8, so it is copied as is.
 * ⚠ 一行写完不代表字段够 4 段:**凑不满 4 段的整行丢掉**(宁可少一张卡,不要一张半空的卡)。
 * ⚠ A line being present does not mean it has four fields: **a line with fewer than four is dropped**
 *   (better one card short than half a blank one).
 */
static void parse_local_line(int start, int end) {
    int p = start;
    int fs = start;
    int fi = 0;
    while (p <= end) {
        if (p == end || rbuf[p] == '|') {
            if (fi == 0) copy_slice(fs, p - fs, s_code + s_n * 12, 12);
            else if (fi == 1) copy_slice(fs, p - fs, s_name + s_n * 32, 32);
            else if (fi == 2) copy_slice(fs, p - fs, s_price + s_n * 16, 16);
            else if (fi == 3) copy_slice(fs, p - fs, s_pct + s_n * 16, 16);
            fi = fi + 1;
            fs = p + 1;
        }
        p = p + 1;
    }
    if (fi >= 4 && s_n < MAXSTK) {
        s_pct100[s_n] = parse_signed_100(s_pct + s_n * 16);
        s_n = s_n + 1;
    }
}

/* 把整份本地文件切行(`\n` 或 `\r\n`),`#` 开头的行跳过。
 * Split the whole local file into lines (`\n` or `\r\n`) and skip lines that start with `#`. */
static void parse_local(void) {
    int p = 0;
    int ls = 0;
    int le;
    while (p <= g_blen && s_n < MAXSTK) {
        if (p == g_blen || (rbuf[p] & 0xFF) == 10) {
            le = p;
            if (le > ls && (rbuf[le - 1] & 0xFF) == 13) le = le - 1;
            if (le > ls && rbuf[ls] != '#') parse_local_line(ls, le);
            ls = p + 1;
        }
        p = p + 1;
    }
}

/* ── 取数据(在线优先,失败回落本地)──────────────────────────────── */
/* -- Acquire the data (network first, local fallback) -- */
static void acquire(void) {
    char req[320];
    char* ip;
    int fd;
    int port;
    int n;
    int want;
    int addr;
    int code;
    int rounds;
    int fh;
    int mode;
    int src;
    int i;

    s_n = 0;
    offline = 0;
    g_total = 0;
    g_head = 0;
    g_blen = 0;

    if (g_first != 0) {
        print_str(g_lang == 0 ? "=== VML 股票行情 ===\n" : "=== VML Stock Quotes ===\n");
        print_str(g_lang == 0 ? "主机 " : "Host "); print_str(HOST);
        print_str(g_lang == 0 ? "  请求 " : "  query "); print_str(QUERY); print_str("\n");
    }

    /* ① 域名 → IP —— ⚠ **这一步仍要自己做**:`http.c` 头部明说它不代劳 DNS
     *   (宿主 `#334` 只吃 `IPAddress.Parse` 认得的那串点分十进制;在库里做会让一次请求变成两次往返)。
     * Resolve the name yourself: the library deliberately does not do DNS.
     * ⚠ 失败时宿主把 R0 置 **-1**(不是 0)⇒ 判据必须是 `<= 0`。 */
    ip = (char*)vml_dns_resolve(HOST);
    if ((int)ip <= 0) {
        if (g_first != 0) print_str(g_lang == 0 ? "[警告] DNS 解析失败(转为离线)\n" : "[WARN] DNS lookup failed (going offline)\n");
        offline = 1;
    } else {
        if (g_first != 0) { print_str("[1/3] DNS  "); print_str(ip); print_str("\n"); }
    }

    /* ②③ 抓数据 —— 建套接字、连接、发请求、收完整响应、剥头、状态码**全在库里**
     *   (`Lib/shared/src/http.c` 的 `http_get_to`)。
     *   ⚠ 为什么是 `_to` 而不是 `http_get`:后者把「连哪儿」与「Host 写什么」交给**同一个参数** ⇒
     *     传域名连不上(`#334` 不解析域名)、传 IP 则 Host 头成了 IP ⇒ 虚拟主机/CDN 按 Host 路由
     *     返回 **404**(实测 `api.open-meteo.com`)。`http_get_to(ip, port, 域名, 路径, …)` 把两者分开。
     * The fetch lives in the library's `http_get_to` (connect to the IP, send the hostname as `Host:`). */
    if (offline == 0) {
        req[0] = 0;
        strcat(req, "/q=");
        strcat(req, QUERY);
        code = http_get_to(ip, PORT, HOST, req, rbuf, BUFCAP);
        if (code != 200) {
            if (g_first != 0) print_str(g_lang == 0 ? "[警告] 抓取失败(转为离线)\n" : "[WARN] fetch failed (going offline)\n");
            offline = 1;
        } else {
            g_blen = strlen(rbuf);       /* `http_get_to` 只往回装**正文**(响应头已经剥掉了)*/
                                         /* stores **only the body**; headers are already stripped */
            g_head = 0;
            if (g_first != 0) {
                print_str("[3/3] HTTP "); print_int(code);
                print_str(g_lang == 0 ? "  正文 " : "  body "); print_int(g_blen);
                print_str(g_lang == 0 ? " 字节\n" : " bytes\n");
            }
        }
    }

    if (offline == 0) {
        g_lim = g_head + g_blen;
        parse_quote();
        if (s_n <= 0) {
            /* 连上了、HTTP 也对,却一条都没解出来 ⇒ 接口改版了,退回本地。
             * Connected and a valid HTTP status, yet nothing parsed -- the feed changed shape, so fall back. */
            if (g_first != 0) print_str(g_lang == 0 ? "[警告] 一条也没解出来(接口改版?转离线)\n"
                                                    : "[WARN] nothing parsed (feed changed? going offline)\n");
            offline = 1;
        }
    }

    /* ④ 离线:读本地的 stocks.txt。⚠ `mode = 0` 是**只读**,写成 1 会把文件清空再读。
     * (4) Offline: read the local stocks.txt. ⚠ `mode = 0` is **read-only**; passing 1 truncates it first. */
    if (offline != 0) {
        src = -1;
        i = 0;
        while (i < 3 && src < 0) {
            if (i == 0) { mode = 0; fh = vml_file_open("stocks.txt", mode); }
            else if (i == 1) { mode = 0; fh = vml_file_open("examples/c/app/stocks.txt", mode); }
            else { mode = 0; fh = vml_file_open("Examples/c/app/stocks.txt", mode); }
            if (fh >= 0) src = fh;
            i = i + 1;
        }
        if (src < 0) {
            print_str(g_lang == 0 ? "[失败] 离线且找不到 stocks.txt —— 把它放到本程序同目录。\n"
                                  : "[FAIL] offline and stocks.txt is nowhere -- put it next to this program.\n");
            s_n = 0;
            return;
        }
        g_blen = vml_file_read(src, (int)rbuf, BUFCAP - 1);
        vml_file_close(src);
        if (g_blen <= 0) {
            print_str(g_lang == 0 ? "[失败] stocks.txt 是空的\n" : "[FAIL] stocks.txt is empty\n");
            s_n = 0;
            return;
        }
        rbuf[g_blen] = 0;
        g_lim = g_blen;
        parse_local();
        if (g_first != 0) {
            print_str(g_lang == 0 ? "[离线] stocks.txt 解析出 " : "[offline] stocks.txt yielded ");
            print_int(s_n);
            print_str(g_lang == 0 ? " 只\n" : " stocks\n");
        }
    }

    if (g_first != 0 && s_n > 0) {
        print_str(g_lang == 0 ? "解析 " : "parsed ");
        print_int(s_n);
        print_str(g_lang == 0 ? " 只:\n" : " stocks:\n");
        i = 0;
        while (i < s_n) {
            print_str("  ");
            print_str(s_code + i * 12);
            print_str(" ");
            print_str(s_name + i * 32);
            print_str(" ");
            print_str(s_price + i * 16);
            print_str(" ");
            print_str(s_pct + i * 16);
            print_str("%\n");
            i = i + 1;
        }
    }
}

/* ── 版面与绘制 ─────────────────────────────────────────────────────── */
/* -- Layout and drawing -- */
static int g_pad;
static int g_top;
static int g_cardh;

static void layout(void) {
    int avail;
    SW = ui_scr_get_w();
    SH = ui_scr_get_h();
    if (SW <= 0) SW = 360;
    if (SH <= 0) SH = 620;
    g_pad = SW / 18;
    if (g_pad < 6) g_pad = 6;
    g_top = g_pad + 62;                 /* 标题两行之下的位置
                                           where the two heading lines end */
    avail = SH - g_top - 34;            /* 底下留一行提示
                                           leave one line for the footer */
    g_cardh = avail / (s_n > 0 ? s_n : 1);
    if (g_cardh > 96) g_cardh = 96;     /* 卡少了别把它们拉得老长
                                           do not stretch a few cards */
    if (g_cardh < 52) g_cardh = 52;
    /* 卡片块在可用高度里**居中**:只有三四只时,顶着标题下面会留一大片空。
     * **Centre** the block in the available height: with only three or four cards, hugging the heading
     * 实测 4 只时上面空 0、下面空 132 像素,看着像"下面还有内容没画出来"。
     * leaves a big gap (measured with four cards: 0 px above, 132 px below, which reads as "something
     * 六只时 `s_n * g_cardh` 正好等于 `avail`,这里加减的是 0,行为不变。
     * is missing below"). With six the product equals `avail`, so this adds zero and nothing changes. */
    g_top = g_top + (avail - s_n * g_cardh) / 2;
}

static void draw(void) {
    int i;
    int cx;
    int y;
    int h;
    int by;
    int ix;
    int iw;
    int mid;
    int amt;
    int bw;
    int col;
    char pb[24];

    cx = SW / 2;
    ui_2d_clear(C_BG);

    ui_2d_text_styled(cx, g_pad, g_lang == 0 ? "股票行情" : "Stock Quotes",
                      C_TITLE, 17, VML_ANCHOR_CENTER, VML_FONT_BOLD);
    ui_2d_text(cx, g_pad + 26,
               offline != 0
                   ? (g_lang == 0 ? "离线:读本地 stocks.txt" : "offline: reading local stocks.txt")
                   : (g_lang == 0 ? "数据源 qt.gtimg.cn" : "source qt.gtimg.cn"),
               offline != 0 ? C_WARN : C_DIM, 11, VML_ANCHOR_CENTER);

    if (s_n <= 0) {
        ui_2d_text(cx, SH / 2 - 8, g_lang == 0 ? "没有数据" : "no data", C_DIM, 14, VML_ANCHOR_CENTER);
        ui_2d_present();
        return;
    }

    i = 0;
    while (i < s_n) {
        y = g_top + i * g_cardh;
        h = g_cardh - 6;
        ui_2d_rect(g_pad, y, SW - g_pad * 2, h, C_CARD, 1, 0, 10);

        /* 代码(左上,暗)
           The code (top left, dim) */
        ui_2d_text(g_pad + 12, y + 9, s_code + i * 12, C_DIM, 11, VML_ANCHOR_LEFT);

        /* 涨跌幅(右上,带符号;涨红跌绿)
           The change percent (top right, signed; red up, green down) */
        col = C_DIM;
        if (s_pct100[i] > 0) col = C_UP;
        else if (s_pct100[i] < 0) col = C_DOWN;
        pb[0] = 0;
        if (s_pct[i * 16 + 0] != '+' && s_pct[i * 16 + 0] != '-' && s_pct100[i] >= 0) strcat(pb, "+");
        strcat(pb, s_pct + i * 16);
        strcat(pb, "%");
        ui_2d_text(SW - g_pad - 12, y + 8, pb, col, 13, VML_ANCHOR_RIGHT);

        /* 名称(左,粗)与现价(右,大)
           The name (left, bold) and the price (right, large) */
        ui_2d_text_styled(g_pad + 12, y + 28, s_name + i * 32, C_TITLE, 16, VML_ANCHOR_LEFT, VML_FONT_BOLD);
        ui_2d_text(SW - g_pad - 12, y + 26, s_price + i * 16, C_TITLE, 18, VML_ANCHOR_RIGHT);

        /* 涨跌幅条:中间那根竖线是 0,满格 = ±10%。纯装饰,但一眼看得出谁在动。
         * The change bar: the tick in the middle is zero and a full half is +/-10%. Decorative, but it
         * shows at a glance who is moving. */
        ix = g_pad + 12;
        iw = SW - g_pad * 2 - 24;
        if (iw < 40) iw = 40;
        by = y + h - 18;
        ui_2d_rect(ix, by, iw, 8, C_TRACK, 1, 0, 4);
        mid = ix + iw / 2;
        ui_2d_rect(mid - 1, by - 1, 2, 10, C_DIM, 1, 0, 1);
        amt = s_pct100[i];
        if (amt < 0) amt = 0 - amt;
        if (amt > 1000) amt = 1000;
        bw = amt * (iw / 2) / 1000;
        if (bw < 2) bw = 2;
        if (s_pct100[i] > 0) ui_2d_rect(mid, by, bw, 8, C_UP, 1, 0, 2);
        else if (s_pct100[i] < 0) ui_2d_rect(mid - bw, by, bw, 8, C_DOWN, 1, 0, 2);

        i = i + 1;
    }

    ui_2d_text(cx, SH - 26, g_lang == 0 ? "任意键刷新(重新抓一次)   Esc / q 退出"
                                        : "any key refreshes   Esc / q quits",
               C_DIM, 11, VML_ANCHOR_CENTER);

    ui_2d_present();      /* ⚠ 不调这句 = 黑屏
                             without this call you get a black screen */
}

/* ── 主流程 ─────────────────────────────────────────────────────────── */
/* -- Main flow -- */
int main(void) {
    int msg[4];
    int t;
    int t0;
    int done;

    g_lang = ui_scr_get_language();     /* 开局查一次,别每帧调
                                           queried once, not every frame */
    g_first = 1;

    load_gtab();                        /* GB2312 表解成码点,只解一次
                                           decode the GB2312 table, once */
    acquire();
    g_first = 0;

    /* 开窗传 `0, 0`(= 尺寸交给宿主),**开窗之后**再取一次尺寸 —— 那一次拿到的才是**真实画布**,
     * 窗口与画布才是一套坐标系(开窗前的 `layout()` 只是先算一版)。竖屏 + 不要手柄区。
     * Open with `0, 0` (let the host size it) and take the size **again after** the open -- that pass is the
     * **real canvas**, so window and canvas share one coordinate space (the pre-open `layout()` is just a first pass).
     * Portrait, no gamepad. */
    layout();
    ui_win_open_ex("stock.c", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
    layout();
    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) {
            int k = msg[1];
            if (k == VML_KEY_ESCAPE || k == 'q' || k == 'Q') {
                done = 1;
            } else {
                acquire();      /* 任意键 = 重新抓一次再刷新
                                   any other key re-fetches and redraws */
                layout();
                draw();
            }
            continue;
        }
        if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) { layout(); draw(); continue; }
        if (t == VML_MSG_WINDOWCLOSE) done = 1;
    }
    return 0;
}