-- 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.
-- ============================================================================
-- judou.lua - 句读 (Judou) 处理模块 (Refactored to use Decorate Mechanism)
-- ============================================================================

local constants = package.loaded['core.luatex-cn-constants'] or
    require('core.luatex-cn-constants')
local D = node.direct
local debug = package.loaded['debug.luatex-cn-debug'] or
    require('debug.luatex-cn-debug')

local dbg = debug.get_debugger('judou')

local judou = {}

-- =============================================================================
-- Global State (全局状态)
-- =============================================================================
-- Initialize global judou table
_G.judou = _G.judou or {}
_G.judou.punct_mode = _G.judou.punct_mode or "normal"
_G.judou.pos = _G.judou.pos or "right-bottom"
_G.judou.size = _G.judou.size or "1em"
_G.judou.color = _G.judou.color or "red"

--- Setup global judou parameters from TeX
-- @param params (table) Parameters from TeX keyvals
local function setup(params)
    params = params or {}
    if params.punct_mode and params.punct_mode ~= "" then
        _G.judou.punct_mode = params.punct_mode
    end
    if params.pos and params.pos ~= "" then
        _G.judou.pos = params.pos
    end
    if params.size and params.size ~= "" then
        _G.judou.size = params.size
    end
    if params.color and params.color ~= "" then
        _G.judou.color = params.color
    end
end

judou.setup = setup

-- =============================================================================
-- Parameter Reading (from _G.judou global)
-- =============================================================================

--- Read judou-related parameters from global state
-- @return table A table containing judou configuration values
local function read_judou_params()
    return {
        punct_mode = _G.judou.punct_mode or "normal",
        judou_pos = _G.judou.pos or "right-bottom",
        judou_size = _G.judou.size or "1em",
        judou_color = _G.judou.color or "red",
    }
end

-- ============================================================================
-- Plugin Standard API
-- ============================================================================

--- Initialize Judou Plugin
-- @param params (table) Parameters from TeX (no longer used for judou settings)
-- @param engine_ctx (table) Shared engine context
-- @return (table|nil) Plugin context or nil if disabled
function judou.initialize(params, engine_ctx)
    -- Read judou parameters directly from TeX variables
    local jp = read_judou_params()

    local mode = jp.punct_mode

    if mode == "normal" then
        return nil -- Plugin disabled for this run
    end

    return {
        mode = mode,
        punct_mode = mode,
        pos = jp.judou_pos,
        size = jp.judou_size,
        color = jp.judou_color,
    }
end

--- Process node list for Punctuation modes (Plugin Interface)
-- @param head (direct node) The node list head
-- @param params (table) Parameters containing punct_mode, etc.
-- @param ctx (table) Plugin context
-- @return (direct node) The modified head
function judou.flatten(head, params, ctx)
    if not ctx or not ctx.mode or ctx.mode == "normal" then
        return head -- Return the node as is
    end

    local mode = ctx.mode
    local d_head = D.todirect(head)
    local t = d_head
    local last_visible = nil

    while t do
        local id = D.getid(t)
        local next_node = D.getnext(t)

        if id == constants.GLYPH then
            local char = D.getfield(t, "char")
            local ptype = judou.get_punctuation_type(char)

            if mode == "none" then
                d_head, next_node, last_visible = judou.handle_none_mode(d_head, t, ptype, last_visible)
            elseif mode == "judou" then
                d_head, next_node, last_visible = judou.handle_judou_mode(d_head, t, ptype, last_visible)
            else
                last_visible = t
            end
        elseif id == constants.HLIST or id == constants.VLIST then
            last_visible = t
        end
        t = next_node
    end

    return D.tonode(d_head)
end

-- Character sets for punctuation processing
local SET_JU = {
    [0x3002] = true, -- 。
    [0xFF01] = true, -- ！
    [0xFF1F] = true, -- ？
    [0x2014] = true, -- — (em dash, usually used as ——)
    [0x2026] = true, -- … (ellipsis, usually used as ……)
}

local SET_DOU = {
    [0xFF0C] = true, -- ，
    [0xFF1A] = true, -- ：
    [0x3001] = true, -- 、
    [0xFF1B] = true, -- ；
    [0x00B7] = true, -- · (middle dot)
    [0x30FB] = true, -- ・ (katakana middle dot)
    [0xFF5E] = true, -- ～ (fullwidth tilde)
}

-- Characters that should merge consecutive duplicates (e.g., —— or ……)
local SET_MERGE_CONSECUTIVE = {
    [0x2014] = true, -- —
    [0x2026] = true, -- …
}

local SET_CLOSE_QUOTE = {
    [0x201D] = true, -- ”
    [0x2019] = true, -- ’
    [0x300D] = true, -- 」
    [0x300F] = true, -- 』
    [0xFF09] = true, -- ）
    [0x3009] = true, -- 〉
    [0x300B] = true, -- 》
    [0x3011] = true, -- 】
    [0x3015] = true, -- 〕
}

local SET_OPEN_QUOTE = {
    [0x201C] = true, -- “
    [0x2018] = true, -- ‘
    [0x300C] = true, -- 「
    [0x300E] = true, -- 『
    [0xFF08] = true, -- （
    [0x3008] = true, -- 〈
    [0x300A] = true, -- 《
    [0x3010] = true, -- 【
    [0x3014] = true, -- 〔
}

local REPLACEMENT_JU = 0x3002  -- 。
local REPLACEMENT_DOU = 0x3001 -- 、

local ju_id = nil
local dou_id = nil

--- Get the type of punctuation for a given character
-- @param char (number) Unicode character code
-- @return (string|nil) 'ju', 'dou', 'close', 'open', or nil
function judou.get_punctuation_type(char)
    if SET_JU[char] then return "ju" end
    if SET_DOU[char] then return "dou" end
    if SET_CLOSE_QUOTE[char] then return "close" end
    if SET_OPEN_QUOTE[char] then return "open" end
    return nil
end

--- Initialize Judou styles in Decorate Registry
local function ensure_judou_styles()
    if ju_id and dou_id then return end

    -- Register default styles for Judou
    -- Position is calculated from the character's bottom edge (not grid center)
    -- X offset: positive = move left, negative = move right
    -- Y offset: positive = move down (from character bottom)
    -- Small Y offset since we're starting from character bottom, not grid center
    ju_id = constants.register_decorate("。", "-0.6em", "0.5em", nil, "red", nil, 1.2)
    dou_id = constants.register_decorate("、", "-0.6em", "0.5em", nil, "red", nil, 1.2)
end

--- Create a JUDOU Decorate Marker node (GLYPH with ATTR_DECORATE_ID)
-- replaces the old create_judou_whatsit
-- @param char_code (number) REPLACEMENT_JU or REPLACEMENT_DOU
-- @param font_id (number) Font ID to use for the marker
-- @return (direct node) The created glyph node
function judou.create_judou_decorate_marker(char_code, font_id)
    ensure_judou_styles()

    local dec_id
    if char_code == REPLACEMENT_JU then
        dec_id = ju_id
    elseif char_code == REPLACEMENT_DOU then
        dec_id = dou_id
    else
        return nil
    end

    local g = D.new(constants.GLYPH)
    D.setfield(g, "char", 63)                        -- Dummy char, render_page uses registry char
    D.setfield(g, "font", font_id or font.current()) -- Set valid font
    D.setfield(g, "width", 0)
    D.setfield(g, "height", 0)
    D.setfield(g, "depth", 0)

    if constants.ATTR_DECORATE_ID then
        D.set_attribute(g, constants.ATTR_DECORATE_ID, dec_id)
    end
    if constants.ATTR_DECORATE_FONT and font_id then
        D.set_attribute(g, constants.ATTR_DECORATE_FONT, font_id)
    end

    return g
end

--- Handle punctuation removal in 'none' mode
-- @param head (direct node) Node list head
-- @param t (direct node) Current glyph node
-- @param ptype (string) Punctuation type ('ju', 'dou', 'close', 'open')
-- @return (direct node, direct node|nil) New head, and the next node to process
function judou.handle_none_mode(head, t, ptype, last_visible)
    if ptype then
        local next_node = D.getnext(t)
        head = D.remove(head, t)
        node.flush_node(D.tonode(t))
        return head, next_node, last_visible
    end
    return head, D.getnext(t), t -- Return t as last_visible if not ptype
end

--- Handle punctuation replacement in 'judou' mode
-- @param head (direct node) Node list head
-- @param t (direct node) Current glyph node
-- @param ptype (string) Punctuation type ('ju', 'dou', 'close', 'open')
-- @param last_visible (direct node|nil) Last visible non-punctuation node
-- @return (direct node, direct node|nil, direct node|nil) New head, next node, and updated last_visible
function judou.handle_judou_mode(head, t, ptype, last_visible)
    if not ptype then
        -- Regular character: Update last_visible as the anchor for future marks
        return head, D.getnext(t), t
    end

    -- CRITICAL: If this glyph already has an ATTR_DECORATE_ID,
    -- it's a custom decoration (e.g. from \改 command).
    -- We MUST NOT replace it or override its settings.
    if constants.ATTR_DECORATE_ID and D.get_attribute(t, constants.ATTR_DECORATE_ID) then
        return head, D.getnext(t), t
    end

    local char = D.getfield(t, "char")
    local next_node = D.getnext(t)
    local nodes_to_remove = { t }
    local replacement_code = nil

    if ptype == "ju" then
        replacement_code = REPLACEMENT_JU
        -- Merge consecutive duplicates (e.g., —— or ……)
        if SET_MERGE_CONSECUTIVE[char] and next_node and D.getid(next_node) == constants.GLYPH then
            local next_char = D.getfield(next_node, "char")
            if next_char == char then
                table.insert(nodes_to_remove, next_node)
                next_node = D.getnext(next_node)
            end
        end
        -- Peek next for close quote
        if next_node and D.getid(next_node) == constants.GLYPH then
            local next_char = D.getfield(next_node, "char")
            if SET_CLOSE_QUOTE[next_char] then
                table.insert(nodes_to_remove, next_node)
                next_node = D.getnext(next_node)
            end
        end
    elseif ptype == "dou" then
        replacement_code = REPLACEMENT_DOU
        -- Peek next for open quote if char is ':'
        if char == 0xFF1A and next_node and D.getid(next_node) == constants.GLYPH then
            local next_char = D.getfield(next_node, "char")
            if SET_OPEN_QUOTE[next_char] then
                table.insert(nodes_to_remove, next_node)
                next_node = D.getnext(next_node)
            end
        end
    elseif ptype == "close" or ptype == "open" then
        head = D.remove(head, t)
        node.flush_node(D.tonode(t))
        return head, next_node, last_visible
    end

    if replacement_code then
        if last_visible then
            -- Insert Decorate Marker instead of Judou Whatsit
            local font_id = D.getfield(t, "font")
            local marker = judou.create_judou_decorate_marker(replacement_code, font_id)

            if marker then
                D.insert_after(head, last_visible, marker)
                dbg.log(string.format("Added mark %s after anchor node %s",
                    (replacement_code == REPLACEMENT_JU and "JU" or "DOU"), tostring(last_visible)))
            end

            -- Remove the processed glyphs
            for _, n in ipairs(nodes_to_remove) do
                head = D.remove(head, n)
                node.flush_node(D.tonode(n))
            end
            return head, next_node, last_visible
        else
            -- No visible character before punctuation - keep punctuation to avoid data loss
            dbg.log(string.format("SKIP replacement for char %d: no last_visible anchor", char))
            return head, next_node, t -- Keep t as last_visible
        end
    end

    return head, next_node, t -- Not a replacement candidate, treat as last_visible
end

package.loaded['guji.luatex-cn-guji-judou'] = judou
return judou
