-- 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.
-- ============================================================================
-- base_constants.lua - 基础常量与工具函数库
-- ============================================================================

-- Create module table
local constants = {}

-- Node.direct interface for performance
constants.D = node.direct

-- Node type IDs
constants.GLYPH = node.id("glyph")
constants.KERN = node.id("kern")
constants.HLIST = node.id("hlist")
constants.VLIST = node.id("vlist")
constants.WHATSIT = node.id("whatsit")
constants.GLUE = node.id("glue")
constants.PENALTY = node.id("penalty")
constants.LOCAL_PAR = node.id("local_par")
constants.RULE = node.id("rule")

-- Custom attributes for indentation
constants.ATTR_INDENT = luatexbase.attributes.cnverticalindent or luatexbase.new_attribute("cnverticalindent")
constants.ATTR_RIGHT_INDENT = luatexbase.attributes.cnverticalrightindent or
    luatexbase.new_attribute("cnverticalrightindent")
constants.ATTR_TEXTBOX_WIDTH = luatexbase.attributes.cnverticaltextboxwidth or
    luatexbase.new_attribute("cnverticaltextboxwidth")
constants.ATTR_TEXTBOX_HEIGHT = luatexbase.attributes.cnverticaltextboxheight or
    luatexbase.new_attribute("cnverticaltextboxheight")
constants.ATTR_TEXTBOX_DISTRIBUTE = luatexbase.attributes.cnverticaltextboxdistribute or
    luatexbase.new_attribute("cnverticaltextboxdistribute")

-- Block Indentation Attributes
constants.ATTR_BLOCK_ID = luatexbase.attributes.cnverticalblockid or luatexbase.new_attribute("cnverticalblockid")
constants.ATTR_FIRST_INDENT = luatexbase.attributes.cnverticalfirstindent or
    luatexbase.new_attribute("cnverticalfirstindent")

-- Attributes for Jiazhu (Interlinear Note)
constants.ATTR_JIAZHU = luatexbase.attributes.cnverticaljiazhu or luatexbase.new_attribute("cnverticaljiazhu")
constants.ATTR_JIAZHU_SUB = luatexbase.attributes.cnverticaljiazhusub or luatexbase.new_attribute("cnverticaljiazhusub")
constants.ATTR_JIAZHU_MODE = luatexbase.attributes.cnverticaljiazhumode or
    luatexbase.new_attribute("cnverticaljiazhumode")
constants.ATTR_JUDOU_FONT = luatexbase.attributes.cnverticaljudoufont or luatexbase.new_attribute("cnverticaljudoufont")
constants.ATTR_DECORATE_ID = 202610
constants.ATTR_DECORATE_VISUAL_CENTER = 202611
constants.ATTR_DECORATE_FONT = 202612
constants.ATTR_CHAPTER_REG_ID = 202613
constants.ATTR_LINE_MARK_ID = luatexbase.attributes.cnverticallinemark or luatexbase.new_attribute("cnverticallinemark")

-- Style Registry Attribute (for cross-page style preservation - Phase 2)
constants.ATTR_STYLE_REG_ID = luatexbase.attributes.cnverticalstyle or luatexbase.new_attribute("cnverticalstyle")

-- Punctuation type attribute (for modern punctuation plugin)
-- Values: 0=none, 1=open, 2=close, 3=fullstop, 4=comma, 5=middle, 6=nobreak
constants.ATTR_PUNCT_TYPE = luatexbase.attributes.cnverticalpuncttype or
    luatexbase.new_attribute("cnverticalpuncttype")

-- Vertical rotation attribute (for glyphs that need 90° clockwise rotation)
-- Used when font lacks vertical glyph forms (e.g., ellipsis, em dash)
-- Value: 1 = needs rotation, 0 or unset = normal
constants.ATTR_VERT_ROTATE = luatexbase.attributes.cnverticalrotate or
    luatexbase.new_attribute("cnverticalrotate")

-- Attributes for Column (单列排版)
-- ATTR_COLUMN: 1 = 标记为 Column 内容
-- ATTR_COLUMN_ALIGN: 对齐方式 0=top, 1=bottom, 2=center, 3=stretch
--                    当 >= 4 时为 LastColumn (值 = base_align + 4)
constants.ATTR_COLUMN = luatexbase.attributes.cnverticalcolumn or luatexbase.new_attribute("cnverticalcolumn")
constants.ATTR_COLUMN_ALIGN = luatexbase.attributes.cnverticalcolumnalign or
    luatexbase.new_attribute("cnverticalcolumnalign")

-- Column break with indent: value = number of grid cells to skip after column break
constants.ATTR_COLUMN_BREAK_INDENT = luatexbase.attributes.cnverticalcolbreakindent or
    luatexbase.new_attribute("cnverticalcolbreakindent")

-- Horizontal alignment override for individual glyphs
-- Values: 0=unset, 1=left, 2=center, 3=right
constants.ATTR_HALIGN = luatexbase.attributes.cnverticalhalign or
    luatexbase.new_attribute("cnverticalhalign")

-- Constants for Side Pizhu
constants.SIDENOTE_USER_ID = 202601
constants.FLOATING_TEXTBOX_USER_ID = 202602
constants.JUDOU_USER_ID = 202603
constants.DECORATE_USER_ID = 202604
constants.CHAPTER_MARKER_USER_ID = 202605
constants.BANXIN_USER_ID = 202606
constants.FOOTNOTE_USER_ID = 202607

--- 将 TeX 尺寸字符串转换为 scaled points (sp)
local function to_dimen(dim_str)
    if not dim_str or dim_str == "" or dim_str == "nil" then return nil end
    if type(dim_str) == "number" then return dim_str end

    -- Clean string: remove braces and whitespace
    dim_str = tostring(dim_str):gsub("[{}]", ""):gsub("^%s*(.-)%s*$", "%1")
    if dim_str == "" then return nil end

    -- Handle em units (relative to font size)
    -- Normalize: remove space between number and 'em' if present
    local clean_em = dim_str:lower():gsub("%s+", "")
    local em_val = clean_em:match("^([%-%d%.]+)em$")
    if em_val then
        return { value = tonumber(em_val), unit = "em" }
    end

    -- If it's a raw number (no units), assume it's scaled points (sp)
    if tonumber(dim_str) then
        return tonumber(dim_str)
    end

    -- Absolute dimensions (pt, mm, bp, etc.)
    -- tex.sp handles spaces if they are between number and unit usually,
    -- but we clean it just in case
    local clean_abs = dim_str:gsub("%s+", "")
    local ok, res = pcall(tex.sp, clean_abs)
    if ok and res then return res end

    -- Final fallback: try raw tex.sp if cleaning failed
    ok, res = pcall(tex.sp, dim_str)
    if ok and res then return res end

    return nil
end

constants.to_dimen = to_dimen

local function resolve_dimen(val, font_size_sp)
    if not val or val == "" then return nil end
    local d = val
    if type(d) == "string" then
        d = to_dimen(d)
    end

    if type(d) == "table" and d.unit == "em" then
        return math.floor(d.value * (font_size_sp or 655360) + 0.5)
    end

    local num = tonumber(d)
    return num
end

constants.resolve_dimen = resolve_dimen

--- Register a decoration and create a marker node
-- @param char_str (string) The decoration character (e.g., "。", "●")
-- @param xoff_str (string) X offset (e.g., "-0.6em", "5pt")
-- @param yoff_str (string) Y offset
-- @param size_str (string) Font size (nil = inherit from text)
-- @param color_str (string) Color (e.g., "red", "0.8 0 0")
-- @param font_id (number) Font ID (nil = use current font)
-- @param scale (number) Scale multiplier (default 1.0)
-- @return (number) Registry ID for this decoration
local function register_decorate(char_str, xoff_str, yoff_str, size_str, color_str, font_id, scale)
    _G.decorate_registry = _G.decorate_registry or {}

    local char_code = 63 -- Default '?'
    if char_str and char_str ~= "" then
        char_code = utf8.codepoint(char_str, 1)
    end

    -- Register style attributes in style_registry (Phase 2: Style Registry)
    local style_registry = package.loaded['util.luatex-cn-style-registry'] or
        require('util.luatex-cn-style-registry')

    local style = {}
    if color_str and color_str ~= "" then
        style.font_color = color_str
    end
    if size_str and size_str ~= "" then
        style.font_size = to_dimen(size_str)
    end
    -- Note: font_id is numeric, not storing in style registry (would need font name)

    local style_reg_id = nil
    if next(style) then
        style_reg_id = style_registry.register(style)
    end

    -- Keep decoration-specific attributes in decorate_registry
    local reg = {
        char = char_code,
        xshift = to_dimen(xoff_str) or 0,
        yshift = to_dimen(yoff_str) or 0,
        scale = tonumber(scale) or 1.0, -- Multiplier for font size
        font_id = font_id,              -- Store provided ID (may be nil)
        font_size = to_dimen(size_str),
        color = color_str,
    }
    table.insert(_G.decorate_registry, reg)
    local reg_id = #_G.decorate_registry

    local D = node.direct
    local g = D.new(constants.GLYPH)
    D.setfield(g, "char", reg.char)
    D.setfield(g, "font", reg.font_id or font.current())

    -- Set glyph dimensions to zero so it doesn't take up horizontal space
    D.setfield(g, "width", 0)
    D.setfield(g, "height", 0)
    D.setfield(g, "depth", 0)

    -- Set both decorate ID and style registry ID attributes
    if constants.ATTR_DECORATE_ID then
        D.set_attribute(g, constants.ATTR_DECORATE_ID, reg_id)
    end
    if style_reg_id and constants.ATTR_STYLE_REG_ID then
        D.set_attribute(g, constants.ATTR_STYLE_REG_ID, style_reg_id)
    end

    -- Wrap in HLIST
    local h = D.new(node.id("hlist"))
    D.setfield(h, "head", g)
    D.setfield(h, "width", 0)
    D.setfield(h, "height", 0)
    D.setfield(h, "depth", 0)

    -- Use box 0 to pass node back to TeX
    tex.box[0] = D.tonode(h)
    return reg_id
end

constants.register_decorate = register_decorate

-- ============================================================================
-- Line Mark Registration (for 专名号/书名号 - PDF-drawn lines)
-- ============================================================================
_G.line_mark_registry = _G.line_mark_registry or {}
_G.line_mark_group_counter = _G.line_mark_group_counter or 0

--- Register a line mark group and return group_id
-- @param type_str (string) "straight" or "wavy"
-- @param color_str (string) Color name or RGB (e.g., "red", "0 0 0")
-- @param offset_str (string) Offset from text center (e.g., "0.6em")
-- @param amplitude_str (string) Wavy amplitude: "small", "medium", "large"
-- @param linewidth_str (string) Line width (e.g., "0.4pt")
-- @param style_str (string) Wavy style: "standard" (tight, like U+FE34) or "cursive" (wide, expressive)
-- @return (number) group_id
local function register_line_mark(type_str, color_str, offset_str, amplitude_str, linewidth_str, style_str)
    _G.line_mark_group_counter = _G.line_mark_group_counter + 1
    local gid = _G.line_mark_group_counter

    _G.line_mark_registry[gid] = {
        type = type_str or "straight",
        color = color_str or "black",
        offset = to_dimen(offset_str) or { value = 0.6, unit = "em" },
        amplitude = amplitude_str or "medium",
        linewidth = to_dimen(linewidth_str) or tex.sp("0.8pt"),
        style = style_str or "standard",
    }

    -- Pass group_id back to TeX via macro
    token.set_macro("g__luatexcn_line_mark_gid", tostring(gid))
    return gid
end

constants.register_line_mark = register_line_mark

-- ============================================================================
-- Indent Constants
-- ============================================================================
-- Special values for indent attributes to control indent behavior

--- Force indent to be exactly 0, bypassing style stack inheritance
constants.INDENT_FORCE_ZERO = -2

--- Inherit indent from style stack (default when attribute is 0 or unset)
constants.INDENT_INHERIT = 0

--- Base value for encoding forced indent values
--- Forced indent value N is encoded as: INDENT_FORCE_BASE - N
--- Example: Force indent=3 => attribute = -1000 - 3 = -1003
--- This allows forcing any positive indent value, not just 0
constants.INDENT_FORCE_BASE = -1000

--- Check if an indent attribute value represents a forced indent
--- @param attr_value number The indent attribute value
--- @return boolean is_forced Whether this is a forced indent
--- @return number|nil forced_value The forced indent value if forced, nil otherwise
function constants.is_forced_indent(attr_value)
    if not attr_value then
        return false, nil
    end

    if attr_value == constants.INDENT_FORCE_ZERO then
        return true, 0
    end

    if attr_value < constants.INDENT_FORCE_ZERO then
        local value = constants.INDENT_FORCE_BASE - attr_value
        return true, value
    end

    return false, nil
end

--- Encode a forced indent value to an attribute value
--- @param indent_value number The indent value to force
--- @return number The encoded attribute value
function constants.encode_forced_indent(indent_value)
    if indent_value == 0 then
        return constants.INDENT_FORCE_ZERO
    else
        return constants.INDENT_FORCE_BASE - indent_value
    end
end

-- ============================================================================
-- Penalty Constants for Column/Page Breaks
-- ============================================================================
-- Special penalty values to control column and page breaking behavior

--- Smart column break: Check next node type before deciding
--- If next is textflow, don't break; if next is regular text, break to new column
--- Used by: Paragraph environment end
constants.PENALTY_SMART_BREAK = -10001

--- Force column break: Unconditionally wrap to next column
--- Used by: \换行 command, some \\ commands
constants.PENALTY_FORCE_COLUMN = -10002

--- Force page break: Unconditionally wrap to new page
--- Used by: \newpage, \clearpage commands
constants.PENALTY_FORCE_PAGE = -10003

--- Taitou column break: Force column break for 抬头 commands
--- Like PENALTY_FORCE_COLUMN, but marks the next column as the taitou scope.
--- Used by: \抬头, \相对抬头 commands
constants.PENALTY_TAITOU = -10004

--- Digital newline: Column break from DigitalContent ^^M (obeylines).
--- Like PENALTY_FORCE_COLUMN, but consecutive occurrences always produce
--- empty columns (even when cur_row == 0). This is needed because every
--- newline in the .tex source must map to a column in the PDF output.
--- Used by: DigitalContent environment obeylines handler
constants.PENALTY_DIGITAL_NEWLINE = -10005

--- Page fill marker: Allow page break, used in page splitting
--- Note: This keeps standard TeX value for compatibility
constants.PENALTY_PAGE_FILL = -10000

-- ============================================================================
-- Shared color name to RGB mapping
-- ============================================================================

constants.color_map = {
    red = "1 0 0",
    blue = "0 0 1",
    green = "0 1 0",
    black = "0 0 0",
    purple = "0.5 0 0.5",
    orange = "1 0.5 0",
}

package.loaded['core.luatex-cn-constants'] = constants
return constants
