//! Live2D Renderer: Core + Model + GL + Animation integration

use super::animation::AnimationState;
use super::assets::{self, Model3Json, TextureImage};
use super::core_ffi::{self, CubismCore};
use super::gl::{self as gl_api, RenderContext};
use gl_api::gl as gl_const;
use super::model::{CubismModel, DrawableSnapshot};
use super::pose::PoseState;
use super::text::TextRenderer;
use anyhow::Result;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};

/// 前几帧打印被遮罩 drawable 的 mask 引用关系，便于排查遮罩错误（如后臂整只伸开）。
static LIVE2D_MASK_DEBUG: AtomicU32 = AtomicU32::new(0);

/// 设备端字幕：AI 回复文字 + 自动消失时间。
pub struct Subtitle {
    pub text: String,
    pub expires_at: Instant,
}

/// 字幕共享状态（http 插件写入，live2d 渲染线程读取）。
pub type SubtitleState = Arc<Mutex<Option<Subtitle>>>;

pub struct Live2DRenderer {
    pub core: Arc<CubismCore>,
    pub model: CubismModel,
    pub model3: Model3Json,
    pub textures: Vec<TextureImage>,
    pub gl_ctx: RenderContext,
    pub texture_ids: Vec<u32>,
    pub animation: AnimationState,
    pub pose: Option<PoseState>,
    pub model_matrix: [f32; 16],
    pub running: bool,
    /// 字幕共享状态（由 live2d 插件从 http 插件接收并传入）
    pub subtitle: SubtitleState,
    /// 中文字幕渲染器（无可用字体时为 None）
    pub text: Option<TextRenderer>,
}

impl Live2DRenderer {
    pub fn new(
        core_so_path: &str,
        live2d_base_dir: &Path,
        model3_json_rel: &str,
        width: i32,
        height: i32,
        subtitle: SubtitleState,
    ) -> Result<Self> {
        let core = Arc::new(CubismCore::load(core_so_path)?);
        println!("[Live2D] Core version: {}", core.version_string());

        let paths = assets::resolve_asset_paths(live2d_base_dir, model3_json_rel)?;
        let model3 = Model3Json::from_path(&paths.model3_json)?;
        println!(
            "[Live2D] model3.json: ver={} moc={} textures={}",
            model3.version,
            model3.file_references.moc,
            model3.file_references.textures.len()
        );

        let model = CubismModel::load(Arc::clone(&core), &paths.moc3)?;
        let textures = assets::load_all_textures(&paths.model3_dir, &model3.file_references)?;

        let gl_ctx = RenderContext::new_fullscreen(width, height)?;

        let texture_ids: Vec<u32> = textures.iter().map(|t| gl_ctx.create_texture(t)).collect();
        println!("[Live2D] Uploaded {} textures", texture_ids.len());

        let canvas = model.canvas;
        // 用真实窗口尺寸（已被 gl_ctx 修正为显示器实际分辨率）计算矩阵
        let model_matrix = compute_model_matrix(canvas, gl_ctx.width, gl_ctx.height);

        model.reset_to_default(&core);
        let mut pose = if let Some(pose_rel) = model3.file_references.pose.as_deref() {
            let pose_path = paths.model3_dir.join(pose_rel);
            Some(PoseState::from_path(&pose_path, &model)?)
        } else {
            None
        };
        if let Some(pose) = pose.as_mut() {
            pose.reset(&core, &model);
        }

        // 初始化中文字幕渲染器（找不到字体则降级为无字幕）
        let text = match find_cjk_font() {
            Some(p) => match TextRenderer::new(p.as_path(), 44) {
                Ok(t) => Some(t),
                Err(e) => {
                    eprintln!("[Live2D] 字幕渲染器初始化失败，字幕功能禁用: {e}");
                    None
                }
            },
            None => {
                eprintln!("[Live2D] 未找到中文字体，字幕功能禁用");
                None
            }
        };

        Ok(Self {
            core,
            model,
            model3,
            textures,
            gl_ctx,
            texture_ids,
            animation: AnimationState::default(),
            pose,
            model_matrix,
            running: false,
            subtitle,
            text,
        })
    }

    pub fn set_talking(&mut self, talking: bool) {
        self.animation.set_talking(talking);
    }

    pub fn render_frame(&mut self) -> Result<()> {
        let now = Instant::now();
        let dt = 1.0 / 60.0;

        self.animation.update(&self.core, &self.model, now, dt);
        if let Some(pose) = self.pose.as_mut() {
            pose.update_parameters(&self.core, &self.model, dt);
        }
        self.model.update(&self.core);

        let snapshots = self.model.drawable_snapshots(&self.core);

        // 前 1 帧打印全部 drawable 的概要 + 顶点包围盒，用于排查遮罩（[Live2D][maskdbg]）
        let dbg = LIVE2D_MASK_DEBUG.fetch_add(1, Ordering::SeqCst);
        if dbg < 1 {
            eprintln!("[Live2D][maskdbg] total={}", snapshots.len());
            for s in &snapshots {
                let (mut minx, mut miny, mut maxx, mut maxy) =
                    (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
                for v in &s.vertex_positions {
                    minx = minx.min(v[0]);
                    miny = miny.min(v[1]);
                    maxx = maxx.max(v[0]);
                    maxy = maxy.max(v[1]);
                }
                let inv = (s.constant_flags & super::core_ffi::constant_flags::IS_INVERTED_MASK)
                    != 0;
                eprintln!(
                    "[Live2D][maskdbg] i={} ro={} op={:.2} fl={:#04x} masked={} masks={:?} inv={} bbox=({:.0},{:.0})-({:.0},{:.0}) tex={}",
                    s.index,
                    s.render_order,
                    s.opacity,
                    s.constant_flags,
                    !s.mask_indices.is_empty(),
                    s.mask_indices,
                    inv,
                    minx,
                    miny,
                    maxx,
                    maxy,
                    s.texture_index
                );
            }
        }

        unsafe {
            let gl = &self.gl_ctx.gl;
            // 每帧校正 viewport 为窗口实际尺寸，避免窗口被 WM 调整大小后
            // 仍用旧的小尺寸（如 800x800）绘制，导致模型只出现在左下角。
            (gl.gl_viewport)(0, 0, self.gl_ctx.width, self.gl_ctx.height);
            (gl.gl_clear_color)(0.0, 0.0, 0.0, 1.0);
            (gl.gl_clear)(gl_const::GL_COLOR_BUFFER_BIT);
            // 先画渐变背景，再画模型（draw_background 内部会恢复 BLEND）
            self.gl_ctx.draw_background(
                [0.05, 0.09, 0.17], // 顶部：深蓝（全息风）
                [0.01, 0.02, 0.04], // 底部：近黑
            );
            (gl.gl_enable)(gl_const::GL_BLEND);
            (gl.gl_use_program)(self.gl_ctx.program);
            (gl.gl_uniform_matrix4fv)(
                self.gl_ctx.uniform_matrix,
                1,
                gl_const::GL_FALSE,
                self.model_matrix.as_ptr(),
            );
        }

        // 建立 原始索引 -> 快照 的查找表，供被遮罩 drawable 引用其 mask
        let snap_by_index: std::collections::HashMap<usize, &DrawableSnapshot> =
            snapshots.iter().map(|s| (s.index, s)).collect();

        for snap in &snapshots {
            if snap.opacity <= 0.001 {
                continue;
            }
            if snap.mask_indices.is_empty() {
                self.draw_drawable(snap);
            } else {
                self.draw_masked_drawable(snap, &snap_by_index);
            }
        }

        // 设备端字幕 overlay（AI 回复文字）
        self.draw_subtitle();

        self.model.reset_dynamic_flags(&self.core);

        unsafe {
            (self.gl_ctx.gl.egl_swap_buffers)(self.gl_ctx.display, self.gl_ctx.surface);
        }

        // 每帧提升窗口到最顶层，防止桌面/其他窗口遮挡
        self.gl_ctx.raise_window();

        Ok(())
    }

    fn draw_drawable(&self, snap: &DrawableSnapshot) {
        let gl = &self.gl_ctx.gl;
        unsafe {
            let tex_idx = snap.texture_index as usize;
            if tex_idx < self.texture_ids.len() {
                (gl.gl_active_texture)(gl_const::GL_TEXTURE0);
                (gl.gl_bind_texture)(gl_const::GL_TEXTURE_2D, self.texture_ids[tex_idx]);
                (gl.gl_uniform1i)(self.gl_ctx.uniform_texture, 0);
            }

            (gl.gl_uniform4f)(self.gl_ctx.uniform_base_color, 1.0, 1.0, 1.0, snap.opacity);
            let mc = snap.multiply_color;
            (gl.gl_uniform4f)(self.gl_ctx.uniform_multiply_color, mc[0], mc[1], mc[2], mc[3]);
            let sc = snap.screen_color;
            (gl.gl_uniform4f)(self.gl_ctx.uniform_screen_color, sc[0], sc[1], sc[2], sc[3]);

            match snap.blend_mode {
                core_ffi::blend_mode::NORMAL => {
                    (gl.gl_blend_func)(gl_const::GL_SRC_ALPHA, gl_const::GL_ONE_MINUS_SRC_ALPHA);
                }
                core_ffi::blend_mode::ADDITIVE => {
                    (gl.gl_blend_func)(gl_const::GL_SRC_ALPHA, gl_const::GL_ONE);
                }
                core_ffi::blend_mode::MULTIPLICATIVE => {
                    (gl.gl_blend_func)(gl_const::GL_ZERO, gl_const::GL_SRC_COLOR);
                }
                _ => {
                    (gl.gl_blend_func)(gl_const::GL_SRC_ALPHA, gl_const::GL_ONE_MINUS_SRC_ALPHA);
                }
            }

            let vc = snap.vertex_positions.len();
            let mut vertex_data: Vec<f32> = Vec::with_capacity(vc * 4);
            for i in 0..vc {
                vertex_data.push(snap.vertex_positions[i][0]);
                vertex_data.push(snap.vertex_positions[i][1]);
                vertex_data.push(snap.vertex_uvs[i][0]);
                vertex_data.push(snap.vertex_uvs[i][1]);
            }

            let mut vbo = 0u32;
            (gl.gl_gen_buffers)(1, &mut vbo);
            (gl.gl_bind_buffer)(gl_const::GL_ARRAY_BUFFER, vbo);
            (gl.gl_buffer_data)(
                gl_const::GL_ARRAY_BUFFER,
                (vertex_data.len() * 4) as isize,
                vertex_data.as_ptr() as *const std::ffi::c_void,
                gl_const::GL_STATIC_DRAW,
            );

            let mut ibo = 0u32;
            (gl.gl_gen_buffers)(1, &mut ibo);
            (gl.gl_bind_buffer)(gl_const::GL_ELEMENT_ARRAY_BUFFER, ibo);
            (gl.gl_buffer_data)(
                gl_const::GL_ELEMENT_ARRAY_BUFFER,
                (snap.indices.len() * 2) as isize,
                snap.indices.as_ptr() as *const std::ffi::c_void,
                gl_const::GL_STATIC_DRAW,
            );

            let stride = 4 * 4;
            if self.gl_ctx.attrib_position >= 0 {
                (gl.gl_enable_vertex_attrib_array)(self.gl_ctx.attrib_position as u32);
                (gl.gl_vertex_attrib_pointer)(
                    self.gl_ctx.attrib_position as u32,
                    2,
                    gl_const::GL_FLOAT,
                    gl_const::GL_FALSE,
                    stride,
                    std::ptr::null(),
                );
            }
            if self.gl_ctx.attrib_tex_coord >= 0 {
                (gl.gl_enable_vertex_attrib_array)(self.gl_ctx.attrib_tex_coord as u32);
                (gl.gl_vertex_attrib_pointer)(
                    self.gl_ctx.attrib_tex_coord as u32,
                    2,
                    gl_const::GL_FLOAT,
                    gl_const::GL_FALSE,
                    stride,
                    (2 * 4) as *const std::ffi::c_void,
                );
            }

            (gl.gl_draw_elements)(
                gl_const::GL_TRIANGLES,
                snap.indices.len() as i32,
                gl_const::GL_UNSIGNED_SHORT,
                std::ptr::null(),
            );

            (gl.gl_delete_buffers)(1, &vbo);
            (gl.gl_delete_buffers)(1, &ibo);
        }
    }

    /// 绘制一个被遮罩（masked）的 drawable：
    /// 先用 mask 着色器把所有 mask drawable 写入 stencil 缓冲，再以 stencil 裁切
    /// 实际 drawable，从而把后臂/头发等限制在 mask 形状内、不再整块溢出。
    fn draw_masked_drawable(
        &self,
        snap: &DrawableSnapshot,
        snap_by_index: &std::collections::HashMap<usize, &DrawableSnapshot>,
    ) {
        let gl = &self.gl_ctx.gl;
        unsafe {
            // 1) 用 mask 程序把 mask drawable 形状写入 stencil（颜色关闭）
            (gl.gl_enable)(gl_const::GL_STENCIL_TEST);
            (gl.gl_stencil_mask)(0xFF);
            (gl.gl_clear)(gl_const::GL_STENCIL_BUFFER_BIT);
            (gl.gl_color_mask)(0, 0, 0, 0);
            (gl.gl_stencil_func)(gl_const::GL_ALWAYS, 1, 0xFF);
            (gl.gl_stencil_op)(gl_const::GL_KEEP, gl_const::GL_KEEP, gl_const::GL_INCR);

            (gl.gl_use_program)(self.gl_ctx.mask_program);
            (gl.gl_uniform_matrix4fv)(
                self.gl_ctx.mask_uniform_matrix,
                1,
                gl_const::GL_FALSE,
                self.model_matrix.as_ptr(),
            );
            for &mi in &snap.mask_indices {
                if let Some(m) = snap_by_index.get(&(mi as usize)) {
                    self.draw_mask_drawable(m);
                }
            }

            // 2) 用 stencil 裁切实际 drawable
            (gl.gl_color_mask)(1, 1, 1, 1);
            (gl.gl_stencil_op)(gl_const::GL_KEEP, gl_const::GL_KEEP, gl_const::GL_KEEP);
            let inverted =
                (snap.constant_flags & super::core_ffi::constant_flags::IS_INVERTED_MASK) != 0;
            if inverted {
                (gl.gl_stencil_func)(gl_const::GL_EQUAL, 0, 0xFF);
            } else {
                (gl.gl_stencil_func)(gl_const::GL_NOTEQUAL, 0, 0xFF);
            }

            (gl.gl_use_program)(self.gl_ctx.program);
            self.draw_drawable(snap);

            // 复位，避免影响后续无遮罩 drawable
            (gl.gl_disable)(gl_const::GL_STENCIL_TEST);
            (gl.gl_stencil_mask)(0xFF);
        }
    }

    /// 用 mask 着色器把一个 drawable 画进 stencil（颜色关闭，仅 alpha 形状）。
    fn draw_mask_drawable(&self, snap: &DrawableSnapshot) {
        let gl = &self.gl_ctx.gl;
        unsafe {
            let tex_idx = snap.texture_index as usize;
            if tex_idx < self.texture_ids.len() {
                (gl.gl_active_texture)(gl_const::GL_TEXTURE0);
                (gl.gl_bind_texture)(gl_const::GL_TEXTURE_2D, self.texture_ids[tex_idx]);
                (gl.gl_uniform1i)(self.gl_ctx.mask_uniform_texture, 0);
            }
            (gl.gl_uniform4f)(
                self.gl_ctx.mask_uniform_multiply_color,
                snap.multiply_color[0],
                snap.multiply_color[1],
                snap.multiply_color[2],
                snap.multiply_color[3],
            );
            (gl.gl_uniform4f)(
                self.gl_ctx.mask_uniform_screen_color,
                snap.screen_color[0],
                snap.screen_color[1],
                snap.screen_color[2],
                snap.screen_color[3],
            );

            let vc = snap.vertex_positions.len();
            let mut vertex_data: Vec<f32> = Vec::with_capacity(vc * 4);
            for i in 0..vc {
                vertex_data.push(snap.vertex_positions[i][0]);
                vertex_data.push(snap.vertex_positions[i][1]);
                vertex_data.push(snap.vertex_uvs[i][0]);
                vertex_data.push(snap.vertex_uvs[i][1]);
            }

            let mut vbo = 0u32;
            (gl.gl_gen_buffers)(1, &mut vbo);
            (gl.gl_bind_buffer)(gl_const::GL_ARRAY_BUFFER, vbo);
            (gl.gl_buffer_data)(
                gl_const::GL_ARRAY_BUFFER,
                (vertex_data.len() * 4) as isize,
                vertex_data.as_ptr() as *const std::ffi::c_void,
                gl_const::GL_STATIC_DRAW,
            );

            let mut ibo = 0u32;
            (gl.gl_gen_buffers)(1, &mut ibo);
            (gl.gl_bind_buffer)(gl_const::GL_ELEMENT_ARRAY_BUFFER, ibo);
            (gl.gl_buffer_data)(
                gl_const::GL_ELEMENT_ARRAY_BUFFER,
                (snap.indices.len() * 2) as isize,
                snap.indices.as_ptr() as *const std::ffi::c_void,
                gl_const::GL_STATIC_DRAW,
            );

            let stride = 4 * 4;
            if self.gl_ctx.mask_attrib_position >= 0 {
                (gl.gl_enable_vertex_attrib_array)(self.gl_ctx.mask_attrib_position as u32);
                (gl.gl_vertex_attrib_pointer)(
                    self.gl_ctx.mask_attrib_position as u32,
                    2,
                    gl_const::GL_FLOAT,
                    gl_const::GL_FALSE,
                    stride,
                    std::ptr::null(),
                );
            }
            if self.gl_ctx.mask_attrib_tex_coord >= 0 {
                (gl.gl_enable_vertex_attrib_array)(self.gl_ctx.mask_attrib_tex_coord as u32);
                (gl.gl_vertex_attrib_pointer)(
                    self.gl_ctx.mask_attrib_tex_coord as u32,
                    2,
                    gl_const::GL_FLOAT,
                    gl_const::GL_FALSE,
                    stride,
                    (2 * 4) as *const std::ffi::c_void,
                );
            }

            (gl.gl_draw_elements)(
                gl_const::GL_TRIANGLES,
                snap.indices.len() as i32,
                gl_const::GL_UNSIGNED_SHORT,
                std::ptr::null(),
            );

            (gl.gl_delete_buffers)(1, &vbo);
            (gl.gl_delete_buffers)(1, &ibo);
        }
    }

    /// 绘制设备端字幕：屏幕底部半透明背景框 + 白色中文（AI 回复文字）。
    fn draw_subtitle(&mut self) {
        let (text, expired) = {
            let guard = self.subtitle.lock().unwrap();
            match guard.as_ref() {
                Some(s) => (s.text.clone(), s.expires_at <= Instant::now()),
                None => return,
            }
        };
        if expired || text.trim().is_empty() {
            return;
        }
        let text_renderer = match self.text.as_mut() {
            Some(t) => t,
            None => return,
        };

        let gl = &self.gl_ctx.gl;
        // 复制 text program 的 uniform/attrib 位置到局部，避免与 &mut text_renderer 借位冲突
        let prog = self.gl_ctx.text_program;
        let u_mat = self.gl_ctx.text_uniform_matrix;
        let u_col = self.gl_ctx.text_uniform_color;
        let u_tex = self.gl_ctx.text_uniform_texture;
        let a_pos = self.gl_ctx.text_attrib_position;
        let a_tco = self.gl_ctx.text_attrib_tex_coord;

        unsafe {
            (gl.gl_enable)(gl_const::GL_BLEND);
            (gl.gl_blend_func)(gl_const::GL_SRC_ALPHA, gl_const::GL_ONE_MINUS_SRC_ALPHA);
            (gl.gl_use_program)(prog);
            let w = self.gl_ctx.width as f32;
            let h = self.gl_ctx.height as f32;
            // 像素坐标（原点左上，y 向下）-> 裁剪空间
            let ortho: [f32; 16] = [
                2.0 / w, 0.0, 0.0, 0.0,
                0.0, -2.0 / h, 0.0, 0.0,
                0.0, 0.0, 1.0, 0.0,
                -1.0, 1.0, 0.0, 1.0,
            ];
            (gl.gl_uniform_matrix4fv)(u_mat, 1, gl_const::GL_FALSE, ortho.as_ptr());
        }

        let font_px = text_renderer.font_size() as f32;
        let line_height = (font_px * 1.35) as i32;
        let margin = 40i32;
        let max_width = self.gl_ctx.width - margin * 2;
        let padding = 18i32;

        // 按宽度换行（CJK 任意位置可断，遇 \n 强制断）
        let mut lines: Vec<String> = Vec::new();
        let mut cur = String::new();
        let mut cur_w = 0i32;
        for ch in text.chars() {
            if ch == '\n' {
                lines.push(std::mem::take(&mut cur));
                cur_w = 0;
                continue;
            }
            let aw = text_renderer.measure_advance(ch);
            if cur_w + aw > max_width && !cur.is_empty() {
                lines.push(std::mem::take(&mut cur));
                cur_w = 0;
            }
            cur.push(ch);
            cur_w += aw;
        }
        if !cur.is_empty() {
            lines.push(cur);
        }
        if lines.is_empty() {
            return;
        }

        let total_text_h = lines.len() as i32 * line_height;
        let box_h = total_text_h + padding * 2;
        let box_y = self.gl_ctx.height - margin - box_h;
        let box_x = margin;
        let box_w = self.gl_ctx.width - margin * 2;

        // 背景框
        let white = text_renderer.white_texture(gl);
        draw_text_quad(
            gl, white, box_x as f32, box_y as f32, box_w as f32, box_h as f32, [0.0, 0.0, 0.0, 0.55],
            u_tex, u_col, a_pos, a_tco,
        );

        // 逐行绘制文字（居中）
        for (li, line) in lines.iter().enumerate() {
            let line_w: i32 = line.chars().map(|c| text_renderer.measure_advance(c)).sum();
            let start_x = ((self.gl_ctx.width - line_w) / 2) as f32;
            let baseline_y =
                (box_y + padding + li as i32 * line_height + (line_height as f32 * 0.78) as i32) as f32;
            let mut pen_x = start_x;
            for ch in line.chars() {
                if let Some(g) = text_renderer.glyph(gl, ch) {
                    if g.tex != 0 && g.w > 0 && g.h > 0 {
                        let gx = pen_x + g.left as f32;
                        let gy = baseline_y - g.top as f32;
                        draw_text_quad(
                            gl, g.tex, gx, gy, g.w as f32, g.h as f32, [1.0, 1.0, 1.0, 1.0],
                            u_tex, u_col, a_pos, a_tco,
                        );
                    }
                    pen_x += (g.advance >> 6) as f32;
                } else {
                    pen_x += font_px * 0.5;
                }
            }
        }
    }

    pub fn run_loop(&mut self) -> Result<()> {
        self.running = true;
        let frame_duration = Duration::from_millis(16);

        while self.running {
            let start = Instant::now();
            if let Err(e) = self.render_frame() {
                eprintln!("[Live2D] render frame failed: {e}");
                break;
            }
            let elapsed = start.elapsed();
            if elapsed < frame_duration {
                std::thread::sleep(frame_duration - elapsed);
            }
        }
        Ok(())
    }

    pub fn stop(&mut self) {
        self.running = false;
    }
}

/// Compute model -> NDC transform matrix (column-major 4x4).
///
/// `csmGetDrawableVertexPositions` 返回的顶点位于**模型单位**坐标系，范围约为
/// ±(canvas_w/2 / ppu)。`pixels_per_unit`(ppu) 指明 1 模型单位 = ppu 个画布像素。
/// 因此顶点必须先乘 `ppu` 回到画布像素，再映射到 [-1,1] 裁剪空间。
///
/// 实测 Haru 的 `origin = (canvas_w/2, canvas_h/2)`，即模型坐标原点本身就位于
/// 画布中心，所以**无需再做原点平移**（平移反而会把模型推出屏幕）。Cubism 顶点
/// 以画布中心为原点、Y 轴向上，与裁剪空间一致，故无需平移或翻转。
///
/// 适配策略：**contain（整只角色恰好放入视口）**，单一均匀缩放即可。
/// NDC 立方体由视口自动拉伸到窗口，因此**绝对不能**对 x/y 轴分别乘不同系数，
/// 否则会破坏长宽比（手臂被拉变形）。这里只按画布较长边归一，使整只角色完整
/// 可见、不裁切、不拉伸；窗口为横屏时角色会填满高度、两侧留渐变背景。
fn compute_model_matrix(
    canvas: super::model::CanvasInfo,
    _width: i32,
    _height: i32,
) -> [f32; 16] {
    let cw = canvas.size[0].max(1.0);
    let ch = canvas.size[1].max(1.0);
    let ppu = canvas.pixels_per_unit.max(1.0);

    // 均匀缩放：按画布较长边把整只角色映射到裁剪空间（margin=1.0 即恰好贴满，
    // 若想留一点边距可改小，如 0.95）。
    let margin = 1.0;
    let u = 2.0 * ppu * margin / cw.max(ch);

    [
        u, 0.0, 0.0, 0.0,
        0.0, u, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0,
    ]
}

/// 用 text program 画一个带纹理的四边形（TRIANGLE_STRIP）。
///
/// `color` 为 RGBA；着色器输出 = u_color.rgb 配合字形纹理的 alpha，因此背景框可传
/// 纯色 + alpha，字形可传白色 + alpha=1（字形纹理本身含 alpha 覆盖）。
fn draw_text_quad(
    gl: &gl_api::Gl,
    tex: gl_const::GLuint,
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    color: [f32; 4],
    uniform_texture: gl_const::GLint,
    uniform_color: gl_const::GLint,
    attrib_position: gl_const::GLint,
    attrib_tex_coord: gl_const::GLint,
) {
    unsafe {
        (gl.gl_active_texture)(gl_const::GL_TEXTURE0);
        (gl.gl_bind_texture)(gl_const::GL_TEXTURE_2D, tex);
        (gl.gl_uniform1i)(uniform_texture, 0);
        (gl.gl_uniform4f)(uniform_color, color[0], color[1], color[2], color[3]);
        let verts: [f32; 16] = [
            x, y, 0.0, 0.0,
            x + w, y, 1.0, 0.0,
            x, y + h, 0.0, 1.0,
            x + w, y + h, 1.0, 1.0,
        ];
        let mut vbo = 0u32;
        (gl.gl_gen_buffers)(1, &mut vbo);
        (gl.gl_bind_buffer)(gl_const::GL_ARRAY_BUFFER, vbo);
        (gl.gl_buffer_data)(
            gl_const::GL_ARRAY_BUFFER,
            (verts.len() * 4) as isize,
            verts.as_ptr() as *const std::ffi::c_void,
            gl_const::GL_STATIC_DRAW,
        );
        let stride = 4 * 4;
        if attrib_position >= 0 {
            (gl.gl_enable_vertex_attrib_array)(attrib_position as u32);
            (gl.gl_vertex_attrib_pointer)(
                attrib_position as u32,
                2,
                gl_const::GL_FLOAT,
                gl_const::GL_FALSE,
                stride,
                std::ptr::null(),
            );
        }
        if attrib_tex_coord >= 0 {
            (gl.gl_enable_vertex_attrib_array)(attrib_tex_coord as u32);
            (gl.gl_vertex_attrib_pointer)(
                attrib_tex_coord as u32,
                2,
                gl_const::GL_FLOAT,
                gl_const::GL_FALSE,
                stride,
                (2 * 4) as *const std::ffi::c_void,
            );
        }
        (gl.gl_draw_arrays)(gl_const::GL_TRIANGLE_STRIP, 0, 4);
        (gl.gl_delete_buffers)(1, &vbo);
    }
}

/// 在设备中查找可用的中文字体（优先 Noto Sans/Serif CJK）。
fn find_cjk_font() -> Option<PathBuf> {
    let candidates = [
        "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
        "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc",
        "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
        "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
    ];
    candidates
        .iter()
        .map(Path::new)
        .find(|p| p.exists())
        .map(|p| p.to_path_buf())
}
