-- Copyright 2026 Open-Guji (https://github.com/open-guji)
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
-- render_page.lua - 坐标应用与视觉渲染（第三阶段主模块）
-- ============================================================================
-- 文件名: render_page.lua (原 render.lua)
-- 层级: 第三阶段 - 渲染层 (Stage 3: Render Layer)
--
-- 【模块功能 / Module Purpose】
-- 本模块负责排版流水线的第三阶段，将虚拟坐标应用到实际节点并绘制视觉元素：
--   1. 根据 layout_map 为每个节点设置 xoffset/yoffset（文字）或 kern/shift（块）
--   2. 插入负 kern 以抵消 TLT 方向盒子的水平推进
--   3. 调用子模块绘制边框、版心、背景
--   4. 文本框（Textbox）块由其内部逻辑渲染好后，在此模块仅作为整体块进行定位
--   5. 按页拆分节点流，生成多个独立的页面盒子
--   6. 可选绘制调试网格（蓝色框显示字符位置，红色框显示 textbox 块）
--
-- 【术语对照 / Terminology】
--   apply_positions   - 应用坐标位置（将虚拟坐标转为实际节点属性）
--   xoffset/yoffset   - 字形偏移（glyph 专用定位属性）
--   kern              - 字距调整（用于水平定位块级节点）
--   shift             - 盒子垂直偏移（box.shift 属性）
--   RTL               - 从右到左（Right-To-Left，竖排时列序）
--   page_nodes        - 页面节点分组（按页分组的节点列表）
--   p_head            - 页面头节点（当前页的节点链头部）
--   outer_shift       - 外边框偏移（外边框厚度+间距）
--
-- 【注意事项】
--   • Glyph 节点使用 xoffset/yoffset 定位，块级节点（HLIST/VLIST）使用 Kern+Shift
--   • RTL 列序转换：物理列号 = total_cols - 1 - 逻辑列号
--   • 绘制顺序严格控制：背景最底层 → 边框 → 版心 → 文字（通过 insert_before 实现）
--   • 所有 PDF 绘图指令使用 pdf_literal 节点（mode=0，用户坐标系）
--   • Kern 的 subtype=1 表示"显式 kern"，不会被后续清零（用于保护版心等特殊位置）
--   • 【重要】如果 xoffset/yoffset 计算错误（如 0 或 超出页面范围），文字将不可见
--   • 【重要】PDF literal 语法错误（如缺少 q/Q 对，或非法颜色值）会破坏整页渲染
--
-- 【整体架构 / Architecture】
--   输入: 节点流 + layout_map + 渲染参数（颜色、边框、页边距等）
--      ↓
--   apply_positions()
--      ├─ 按页分组节点（遍历 layout_map，根据 page 分组）
--      ├─ 对每一页：
--      │   ├─ 绘制背景色（render_background.draw_background）
--      │   ├─ 设置字体颜色（render_background.set_font_color）
--      │   ├─ 绘制外边框（render_border.draw_outer_border）
--      │   ├─ 绘制列边框（render_border.draw_column_borders，跳过版心列）
--      │   ├─ 绘制版心列（render_banxin.draw_banxin_column，含分隔线和文字）
--      │   ├─ 应用节点坐标
--      │   │   ├─ Glyph: 调用 render_position.calc_grid_position()
--      │   │   └─ Block: 使用 Kern 包裹 + Shift
--      │   └─ 可选：绘制调试网格
--      └─ 返回 result_pages[{head, cols}]
--      ↓
--   输出: 多个渲染好的页面（每页是一个 HLIST，dir=TLT）
--
-- ============================================================================

-- Load dependencies
local constants = package.loaded['core.luatex-cn-constants'] or
    require('core.luatex-cn-constants')
local D = constants.D
local utils = package.loaded['util.luatex-cn-utils'] or
    require('util.luatex-cn-utils')
local content = package.loaded['core.luatex-cn-core-content'] or
    require('core.luatex-cn-core-content')
local debug = package.loaded['debug.luatex-cn-debug'] or
    require('debug.luatex-cn-debug')

local dbg = debug.get_debugger('render')
local page_mod = package.loaded['core.luatex-cn-core-page'] or
    require('core.luatex-cn-core-page')
local textbox_mod = package.loaded['core.luatex-cn-core-textbox'] or
    require('core.luatex-cn-core-textbox')
local render_border = package.loaded['core.luatex-cn-core-render-border'] or
    require('core.luatex-cn-core-render-border')
local linemark_mod = package.loaded['decorate.luatex-cn-linemark'] or
    require('decorate.luatex-cn-linemark')
local page_process = package.loaded['core.luatex-cn-core-render-page-process'] or
    require('core.luatex-cn-core-render-page-process')


-- Internal functions for unit testing (delegated to page_process module)
local _internal = {
    handle_glyph_node = page_process.handle_glyph_node,
    handle_block_node = page_process.handle_block_node,
    handle_debug_drawing = page_process.handle_debug_drawing,
    handle_decorate_node = page_process.handle_decorate_node,
    process_page_nodes = page_process.process_page_nodes,
}


-- 辅助函数：计算渲染上下文（尺寸、偏移、列数等）
-- All values come from ctx (populated by main.lua)
local function calculate_render_context(ctx)
    -- Unpack nested contexts
    local engine = ctx.engine
    local grid = ctx.grid
    local page = ctx.page
    local visual = ctx.visual

    -- All values from ctx (main.lua populates these from style stack)
    local border_thickness = engine.border_thickness
    local half_thickness = engine.half_thickness
    local ob_thickness_val = page.ob_thickness
    local ob_sep_val = page.ob_sep
    local b_padding_top = engine.b_padding_top
    local b_padding_bottom = engine.b_padding_bottom
    local grid_width = grid.width
    local grid_height = grid.height
    local banxin_width = grid.banxin_width or 0
    local body_font_size = grid.body_font_size or grid_width

    -- Dynamic values from ctx (calculated per-invocation in main.lua)
    local outer_shift = engine.outer_shift
    local shift_x = engine.shift_x
    local shift_y = engine.shift_y

    local interval = grid.n_column
    local p_cols = grid.cols
    local line_limit = grid.line_limit

    -- Visual params from ctx.visual (populated by main.lua)
    local vertical_align = visual.vertical_align or "center"
    local background_rgb_str = utils.normalize_rgb(visual.bg_rgb)
    -- Fallback to document-level font_color if current style has no font_color
    local text_rgb_str = utils.normalize_rgb(visual.font_rgb)
    if not text_rgb_str then
        _G.document = _G.document or {}
        text_rgb_str = utils.normalize_rgb(_G.document.font_color)
    end
    -- Border shape decoration parameters
    local border_shape = visual.border_shape or "none"
    local border_color_str = utils.normalize_rgb(visual.border_color) or "0 0 0"
    local border_width = constants.to_dimen(visual.border_width) or (65536 * 0.4)
    local border_margin = constants.to_dimen(visual.border_margin) or (65536 * 1)
    -- Textbox outer border (drawn around decorative shape, not via body text mechanism)
    local textbox_outer_border = visual.textbox_outer_border or false
    local textbox_ob_thickness = constants.to_dimen(visual.textbox_ob_thickness) or (65536 * 1)
    local textbox_ob_sep = constants.to_dimen(visual.textbox_ob_sep) or (65536 * 2)

    -- Colors: border from engine (already normalized in main.lua)
    local b_rgb_str = engine.border_rgb_str

    -- Bundle column geometry triple for position functions
    local col_geom = {
        grid_width = grid_width,
        banxin_width = banxin_width,
        interval = interval,
        -- Phase 2.4: Free Mode column widths (page -> col -> width_sp)
        col_widths_sp = grid.col_widths_sp,
    }

    return {
        border_thickness = border_thickness,
        half_thickness = half_thickness,
        ob_thickness_val = ob_thickness_val,
        ob_sep_val = ob_sep_val,
        outer_shift = outer_shift,
        shift_x = shift_x,
        shift_y = shift_y,
        interval = interval,
        p_cols = p_cols,
        line_limit = line_limit,
        b_padding_top = b_padding_top,
        b_padding_bottom = b_padding_bottom,
        b_rgb_str = b_rgb_str,
        background_rgb_str = background_rgb_str,
        text_rgb_str = text_rgb_str,
        grid_height = grid_height,
        banxin_width = banxin_width,
        col_geom = col_geom,
        body_font_size = body_font_size,
        vertical_align = vertical_align,
        -- TextFlow align default (per-node align from style stack takes precedence)
        textflow_align = visual.textflow_align or "outward",
        -- Judou parameters from visual context (populated by core-main from judou plugin context)
        judou_pos = visual.judou_pos or "right-bottom",
        judou_size = visual.judou_size or "1em",
        judou_color = visual.judou_color or "red",
        -- Border shape decoration parameters (textbox only)
        border_shape = border_shape,
        border_color_str = border_color_str,
        border_width = border_width,
        border_margin = border_margin,
        -- Textbox outer border params
        textbox_outer_border = textbox_outer_border,
        textbox_ob_thickness = textbox_ob_thickness,
        textbox_ob_sep = textbox_ob_sep,
    }
end

_internal.calculate_render_context = calculate_render_context

-- 辅助函数：将节点按页分组
local function group_nodes_by_page(d_head, layout_map, total_pages)
    local page_nodes = {}
    for p = 0, total_pages - 1 do
        page_nodes[p] = { head = nil, tail = nil, max_col = 0, max_y_sp = 0 }
    end

    local t = d_head
    while t do
        local next_node = D.getnext(t)
        local pos = layout_map[t]
        D.setnext(t, nil)

        if pos then
            local p = pos.page or 0
            if page_nodes[p] then
                if not page_nodes[p].head then
                    page_nodes[p].head = t
                else
                    D.setnext(page_nodes[p].tail, t)
                end
                page_nodes[p].tail = t
                if pos.col > page_nodes[p].max_col then page_nodes[p].max_col = pos.col end
                -- Track max_y_sp (bottom of furthest cell) for sp-based height
                if pos.y_sp then
                    local y_bottom = pos.y_sp + (pos.cell_height or 0)
                    if y_bottom > page_nodes[p].max_y_sp then page_nodes[p].max_y_sp = y_bottom end
                end
            else
                node.flush_node(D.tonode(t))
            end
        else
            node.flush_node(D.tonode(t))
        end
        t = next_node
    end
    return page_nodes
end

_internal.group_nodes_by_page = group_nodes_by_page

-- 辅助函数：定位浮动文本框
_internal.position_floating_box = textbox_mod.render_floating_box

-- Local reference to process_page_nodes from the process module
local process_page_nodes = page_process.process_page_nodes

-- 辅助函数：渲染单个页面
local function render_single_page(p_head, p_max_col, p, layout_map, params, ctx, p_max_y_sp)
    if not p_head then return nil, 0 end

    local p_total_cols = p_max_col + 1
    local p_cols = ctx.p_cols
    -- Always enforce full page width to ensure correct RTL/SplitPage absolute positioning
    if p_cols and p_cols > 0 and p_total_cols < p_cols then
        p_total_cols = p_cols
    end

    -- Actual content dimensions (for special border shapes)
    local actual_cols = p_max_col + 1
    local actual_height_sp = (p_max_y_sp and p_max_y_sp > 0) and p_max_y_sp or ctx.grid_height

    local grid_width = ctx.col_geom.grid_width
    local grid_height = ctx.grid_height
    local border_thickness = ctx.border_thickness
    local line_limit = ctx.line_limit
    local b_padding_top = ctx.b_padding_top
    local b_padding_bottom = ctx.b_padding_bottom
    local grid = params.grid
    local engine = params.engine
    local page = params.page

    -- For textbox with user-specified height, use that height for border rendering.
    -- user_height_sp is only set when user explicitly specifies height (e.g., height=24cm).
    -- Subtract 2*border_margin so that user height describes the outer border extent,
    -- since render_borders adds border_margin back: final = shape_height + 2*border_m.
    if page.is_textbox and engine.user_height_sp then
        local border_m = ctx.border_margin or 0
        local target = engine.user_height_sp - 2 * border_m
        if target > actual_height_sp then
            actual_height_sp = target
        end
    end
    local plugins = params.plugins

    local draw_border = engine.draw_border
    local draw_outer_border = page.is_outer_border
    local shift_x = ctx.shift_x
    local outer_shift = ctx.outer_shift
    local b_rgb_str = ctx.b_rgb_str

    -- Reserved columns (computed via engine_ctx helper function)
    local reserved_cols = grid.get_reserved_cols and grid.get_reserved_cols(p, p_total_cols) or {}

    -- Right-align columns within content area BEFORE drawing borders
    -- Skip TitlePage (has col_widths but NOT page_col_widths_sp)
    -- Skip TextBox (has its own coordinate system, no right-align needed)
    local page_col_widths_sp = (ctx.col_geom and ctx.col_geom.col_widths_sp and ctx.col_geom.col_widths_sp[p]) or nil
    local has_legacy_col_widths = (_G.content and _G.content.col_widths and next(_G.content.col_widths))
    local has_free_mode_widths = (page_col_widths_sp and next(page_col_widths_sp))
    local is_titlepage = has_legacy_col_widths and not has_free_mode_widths

    if not is_titlepage and not page.is_textbox then
        local total_cols_width_sp
        if page_col_widths_sp and next(page_col_widths_sp) then
            -- Free Mode: sum variable column widths
            total_cols_width_sp = 0
            for _, w in pairs(page_col_widths_sp) do
                total_cols_width_sp = total_cols_width_sp + w
            end
        else
            -- Fixed-width mode: calculate total width including banxin
            local col_gwidth = ctx.col_geom and ctx.col_geom.grid_width or 0
            local col_bwidth = ctx.col_geom and ctx.col_geom.banxin_width or 0
            local col_interval = ctx.col_geom and ctx.col_geom.interval or 0

            if col_interval > 0 and col_bwidth > 0 and col_bwidth ~= col_gwidth then
                -- With banxin columns
                local n_banxin = math.floor(p_total_cols / (col_interval + 1))
                local n_content = p_total_cols - n_banxin
                total_cols_width_sp = n_content * col_gwidth + n_banxin * col_bwidth
            else
                -- Uniform width
                total_cols_width_sp = p_total_cols * col_gwidth
            end
        end

        -- Right-align: add offset if columns are narrower than content area
        local content_width_sp = grid.content_width or 0
        if content_width_sp > total_cols_width_sp then
            shift_x = shift_x + (content_width_sp - total_cols_width_sp)
        end
    end

    -- Scan layout_map for taitou columns (negative y_sp = raised border)
    local col_min_y_sp = {}
    local scan_t = p_head
    while scan_t do
        local pos = layout_map[scan_t]
        if pos then
            local col = pos.col
            local y = pos.y_sp or 0
            if y < 0 and (not col_min_y_sp[col] or y < col_min_y_sp[col]) then
                col_min_y_sp[col] = y
            end
        end
        scan_t = D.getnext(scan_t)
    end

    -- Borders, background, and decorative frames (handled by render_border module)
    p_head = render_border.render_borders(p_head, {
        -- Grid and dimensions
        p_total_cols = p_total_cols,
        actual_cols = actual_cols,
        actual_height_sp = actual_height_sp,
        grid_width = grid_width,
        grid_height = grid_height,
        col_geom = ctx.col_geom,
        banxin_width = ctx.banxin_width,
        interval = ctx.interval,
        line_limit = line_limit,
        content_height_sp = engine.content_height_sp,
        -- Border params
        border_thickness = border_thickness,
        b_padding_top = b_padding_top,
        b_padding_bottom = b_padding_bottom,
        shift_x = shift_x,
        outer_shift = outer_shift,
        b_rgb_str = b_rgb_str,
        ob_thickness_val = ctx.ob_thickness_val,
        ob_sep_val = ctx.ob_sep_val,
        -- Flags
        draw_border = draw_border,
        draw_outer_border_flag = draw_outer_border,
        is_textbox = page.is_textbox,
        reserved_cols = reserved_cols,
        -- Visual params
        border_shape = ctx.border_shape,
        border_color_str = ctx.border_color_str,
        border_width = ctx.border_width,
        border_margin = ctx.border_margin,
        background_rgb_str = ctx.background_rgb_str,
        -- Taitou raised border
        col_min_y_sp = col_min_y_sp,
        -- Textbox outer border (drawn around decorative shape)
        textbox_outer_border = ctx.textbox_outer_border,
        textbox_ob_thickness = ctx.textbox_ob_thickness,
        textbox_ob_sep = ctx.textbox_ob_sep,
    })

    -- Font color
    p_head = content.set_font_color(p_head, ctx.text_rgb_str)

    -- Node positions
    -- Update context with page-specific total_cols and col_widths_sp
    local ctx_node = {}
    for k, v in pairs(ctx) do ctx_node[k] = v end
    ctx_node.p_total_cols = p_total_cols
    -- Phase 2.4: Pass page-specific column widths for Free Mode
    ctx_node.page_num = p
    ctx_node.page_col_widths_sp = page_col_widths_sp
    -- Apply the computed right-alignment shift_x
    ctx_node.shift_x = shift_x

    p_head = process_page_nodes(p_head, layout_map, params, ctx_node)

    -- Render Line Marks (专名号/书名号) - batch PDF drawing after all glyphs are positioned
    if ctx_node.line_mark_entries and #ctx_node.line_mark_entries > 0 then
        p_head = linemark_mod.render_line_marks(p_head, ctx_node.line_mark_entries, ctx_node)
    end

    -- Render Floating TextBoxes
    if plugins.floating_map then
        for _, item in ipairs(plugins.floating_map) do
            if item.page == p then
                p_head = textbox_mod.render_floating_box(p_head, item, params)
            end
        end
    end

    -- For TextBox: return actual content dimensions, not expanded page dimensions
    -- This ensures TextBox output box has correct dimensions in main document flow
    local return_cols = page.is_textbox and actual_cols or p_total_cols
    local return_height_sp = page.is_textbox and actual_height_sp or engine.content_height_sp
    return D.tonode(p_head), return_cols, return_height_sp
end

_internal.render_single_page = render_single_page

-- @param head (node) 节点列表头部
-- @param layout_map (table) 从节点指针到 {col, row} 的映射
-- @param params (table) 渲染参数
-- @return (table) 页面信息数组 {head, cols}
local function apply_positions(head, layout_map, params)
    local d_head = D.todirect(head)

    local ctx = calculate_render_context(params)

    dbg.log(string.format("[render] apply_positions: border=%s, font=%s",
        tostring(params.border_rgb), tostring(params.font_rgb)))

    -- Group nodes by page
    local page_nodes = group_nodes_by_page(d_head, layout_map, params.total_pages)

    local result_pages = {}

    -- Process each page
    for p = 0, params.total_pages - 1 do
        local p_head = page_nodes[p].head
        local p_max_col = page_nodes[p].max_col
        local p_max_y_sp = page_nodes[p].max_y_sp
        local rendered_head, cols, height_sp = render_single_page(p_head, p_max_col, p, layout_map, params, ctx, p_max_y_sp)
        if rendered_head then
            result_pages[p + 1] = { head = rendered_head, cols = cols, height_sp = height_sp }
        end
    end

    return result_pages
end

-- Create module table
local render = {
    apply_positions = apply_positions,
    _internal = _internal
}

-- Register module
package.loaded['core.luatex-cn-core-render-page'] = render
return render
