-- @amzxyz  https://github.com/amzxyz/rime_wanxiang
-- Ctrl+1..9,0：上屏首选前 N 字；按 preedit/script_text 的前 N 音节对齐 raw input
local wanxiang = require("wanxiang")

local M = {}

-- 数字键映射（主键盘 + 小键盘）
local DIGIT = { [0x31]=1,[0x32]=2,[0x33]=3,[0x34]=4,[0x35]=5,[0x36]=6,[0x37]=7,[0x38]=8,[0x39]=9,[0x30]=10 }
local KP    = { [0xFFB1]=1,[0xFFB2]=2,[0xFFB3]=3,[0xFFB4]=4,[0xFFB5]=5,[0xFFB6]=6,[0xFFB7]=7,[0xFFB8]=8,[0xFFB9]=9,[0xFFB0]=10 }

-- 工具：字符串缩略 / 获取分隔符 / 安全转义 / 清洗 raw
local function short(s)
    if not s then return "" end
    if #s > 120 then
        return s:sub(1, 117) .. "..."
    end
    return s
end

local function get_delimiters(ctx)
    local cfg = ctx.engine and ctx.engine.schema and ctx.engine.schema.config
    local delimiter = (cfg and cfg:get_string("speller/delimiter")) or " '"
    return delimiter:sub(1, 1), delimiter:sub(2, 2)  -- auto, manual
end

-- 放进字符类 [...] 使用的转义（只转义 % ^ ] -）
local function esc_class(c)
    if not c or c == "" then return "" end
    return (c:gsub("([%%%^%]%-])", "%%%1"))
end

-- 普通模式串位置的单字符转义（最小化：仅非字母数字下划线时转义）
local function esc_pat(c)
    if not c or c == "" then return "" end
    if c:match("[%w_]") then return c end
    return (c:gsub("(%W)", "%%%1"))
end

-- 清洗整串 raw：去掉手动分隔符（如 "'"）
local function clean_raw(ctx, raw)
    if not raw or raw == "" then return "" end
    local _, manual = get_delimiters(ctx)
    if manual and #manual == 1 then
        raw = raw:gsub(esc_pat(manual), "")
    end
    return raw
end

-- 取候选前 n 个字符
local function utf8_head(s, n)
    local i, c = 1, 0
    while i <= #s and c < n do
        local b = s:byte(i)
        i = i + ((b < 0x80) and 1 or ((b < 0xE0) and 2 or ((b < 0xF0) and 3 or 4)))
        c = c + 1
    end
    return s:sub(1, i - 1)
end
-- 生成 target：按分隔符切 preedit/script_text，取前 n 个并去分隔符拼接
local function script_prefix(ctx, n)
    local raw_in    = ctx.input or ""
    local prop_key  = ctx:get_property("sequence_preedit_key") or ""
    local prop_val  = ctx:get_property("sequence_preedit_val") or ""
    local script_txt = ctx:get_script_text() or ""

    local s
    if prop_key == raw_in and prop_val ~= "" then
        s = prop_val
    else
        s = script_txt
    end
    if s == "" then return "" end

    local auto, manual = get_delimiters(ctx)
    local pat = "[^" .. esc_class(auto) .. esc_class(manual) .. "%s]+"

    local parts = {}
    for w in s:gmatch(pat) do parts[#parts + 1] = w end
    if #parts == 0 then return "" end

    local upto = math.min(n, #parts)
    local target = table.concat({ table.unpack(parts, 1, upto) }, "")
    return target
end
-- 对齐“去分隔符后的 raw_clean”与 target；返回消耗长度（基于 raw_clean）
local function eat_len_by_target(ctx, target)
    if target == "" then return 0 end
    local raw = ctx.input or ""
    if raw == "" then return 0 end

    local clean = clean_raw(ctx, raw)
    local i, j, Lc, Lt = 1, 1, #clean, #target
    while i <= Lc and j <= Lt do
        if clean:sub(i, i) ~= target:sub(j, j) then
            return 0
        end
        i, j = i + 1, j + 1
    end
    if j <= Lt then return 0 end
    return i - 1
end

local function set_pending(env, rest)
    env._cpc_pending_rest = rest or ""
end
local function has_pending(env)
    return type(env._cpc_pending_rest) == "string" and env._cpc_pending_rest ~= nil
end
local function take_pending(env)
    local r = env._cpc_pending_rest
    env._cpc_pending_rest = nil
    return r
end

function M.init(env)
    local ctx = env.engine.context

    env._cpc_update_conn = ctx.update_notifier:connect(function(c)
        if not has_pending(env) then return end
        local rest = take_pending(env) or ""

        c.input = rest
        if c.clear_non_confirmed_composition then
            c:clear_non_confirmed_composition()
        end
        if c.caret_pos ~= nil then
            c.caret_pos = #rest
        end
    end)

    env._cpc_key_handler = function(key)

        if not key:ctrl() or key:release() then
            return wanxiang.RIME_PROCESS_RESULTS.kNoop
        end

        local n = DIGIT[key.keycode] or KP[key.keycode]
        if not n then return wanxiang.RIME_PROCESS_RESULTS.kNoop end

        local c = env.engine.context
        if not c:is_composing() then
            return wanxiang.RIME_PROCESS_RESULTS.kNoop
        end

        local cand = c:get_selected_candidate() or c:get_candidate(0)
        if not cand or not cand.text or #cand.text == 0 then
            return wanxiang.RIME_PROCESS_RESULTS.kNoop
        end

        local head = utf8_head(cand.text, n)
        if head == "" then
            return wanxiang.RIME_PROCESS_RESULTS.kNoop
        end

        local target = script_prefix(c, n)
        if target == "" then
            return wanxiang.RIME_PROCESS_RESULTS.kNoop
        end

        local consumed = eat_len_by_target(c, target)
        if consumed == 0 then
            return wanxiang.RIME_PROCESS_RESULTS.kNoop
        end

        local raw_clean = clean_raw(c, c.input or "")
        local rest = raw_clean:sub(consumed + 1)

        env.engine:commit_text(head)
        set_pending(env, rest)
        c:refresh_non_confirmed_composition()

        return wanxiang.RIME_PROCESS_RESULTS.kAccepted
    end
end

function M.fini(env)
    if env._cpc_update_conn then
        env._cpc_update_conn:disconnect()
        env._cpc_update_conn = nil
    end
    env._cpc_key_handler = nil
end

function M.func(key, env)
    if not env._cpc_key_handler then
        return wanxiang.RIME_PROCESS_RESULTS.kNoop
    end
    return env._cpc_key_handler(key)
end

return M
