-- Rewrite Quarto book float numbers for the Chinese manuscript.
-- Built-in fig/tbl prefixes are "图" / "表". Quarto then inserts NBSP
-- and a chapter.counter such as "3.1" or "3.1a", which renders as
-- "图 3.1". This filter emits "图3-1" / "表3-1a".
-- title-delim only sits between the number and the caption text, so
-- it cannot remove the prefix gap.
-- Also drop the spaces Pandoc keeps around @fig / @tbl when the
-- neighbor is CJK or Chinese punctuation. Keep those spaces in the
-- qmd source; "@" must not be glued to the preceding character.
-- Idempotent. Run at post-render so both body refs and captions exist.
-- Lua patterns are byte-wise: do not put CJK inside [].

local FIG = "\229\155\190" -- U+56FE 图
local TBL = "\232\161\168" -- U+8868 表
local NBSP = "\194\160" -- U+00A0

local function is_nbsp_str(text)
  return text == "\u{A0}" or text == NBSP or text == " "
end

local function is_gap(el)
  if el == nil then
    return false
  end
  if el.t == "Space" or el.t == "SoftBreak" then
    return true
  end
  if el.t == "Str" and is_nbsp_str(el.text) then
    return true
  end
  if el.t == "RawInline" then
    local t = el.text
    return t == "&nbsp;" or t == "&#160;" or t == "&#xa0;" or is_nbsp_str(t)
  end
  return false
end

local function is_float_mark(ch)
  return ch == FIG or ch == TBL or ch == "图" or ch == "表"
end

local function utf8_first(text)
  if not text or text == "" then
    return ""
  end
  return pandoc.text.sub(text, 1, 1)
end

local function utf8_last(text)
  if not text or text == "" then
    return ""
  end
  local n = pandoc.text.len(text)
  return pandoc.text.sub(text, n, n)
end

local function codepoint(ch)
  if not ch or ch == "" then
    return nil
  end
  local ok, cp = pcall(utf8.codepoint, ch)
  if ok then
    return cp
  end
  return nil
end

local function is_cjk_class(ch)
  local cp = codepoint(ch)
  if not cp then
    return false
  end
  return (cp >= 0x2E80 and cp <= 0x9FFF)
    or (cp >= 0xF900 and cp <= 0xFAFF)
    or (cp >= 0xFF00 and cp <= 0xFFEF)
    or (cp >= 0x3000 and cp <= 0x303F)
end

local function is_tight_before_xref(ch)
  return is_cjk_class(ch) or ch == "(" or ch == "[" or ch == "（" or ch == "【"
end

local function is_tight_after_xref(ch)
  return is_cjk_class(ch)
    or ch == ")" or ch == "]" or ch == "）" or ch == "】"
    or ch == "." or ch == "," or ch == ";" or ch == ":"
    or ch == "。" or ch == "，" or ch == "；" or ch == "："
    or ch == "、"
end

-- Head text plus a trailing 图/表. "与表" -> "与", "表". "图3-1" -> nil.
local function split_trailing_mark(text)
  if not text or text == "" then
    return nil
  end
  local last = utf8_last(text)
  if not is_float_mark(last) then
    return nil
  end
  local n = pandoc.text.len(text)
  if n == 1 then
    return "", last
  end
  local head = pandoc.text.sub(text, 1, n - 1)
  local i = 1
  local hn = pandoc.text.len(head)
  -- Allow spaces already stuck to the mark; the number still follows.
  while i <= hn do
    local ch = pandoc.text.sub(head, hn - i + 1, hn - i + 1)
    if ch ~= " " and ch ~= "\u{A0}" and ch ~= "\t" then
      break
    end
    i = i + 1
  end
  if i > 1 then
    head = pandoc.text.sub(head, 1, hn - (i - 1))
  end
  return head, last
end

local function rewrite_embedded(text)
  local pat_gap = "[" .. NBSP .. " ]*"
  local pat = ")(" .. pat_gap .. ")(%d+)%.(%d+)([A-Za-z]?)"
  local function repl(prefix, _gap, chap, idx, suf)
    return prefix .. chap .. "-" .. idx .. (suf or "")
  end
  text = text:gsub("(" .. FIG .. pat, repl)
  text = text:gsub("(" .. TBL .. pat, repl)
  return text
end

-- "3.1" / "3.1a" / "3.1 其余" -> "3-1" / "3-1a" / "3-1 其余"
local function take_number(text)
  if not text then
    return nil
  end
  local chap, idx, suf, tail = text:match("^(%d+)%.(%d+)([A-Za-z]?)(.*)$")
  if not chap then
    return nil
  end
  return chap .. "-" .. idx .. (suf or "") .. (tail or "")
end

local function is_cn_xref(el)
  if el == nil or el.t ~= "Link" then
    return false
  end
  local s = pandoc.utils.stringify(el)
  if not is_float_mark(utf8_first(s)) then
    return false
  end
  return s:match("%d+[-%.]%d+") ~= nil
end

local function tighten_inlines(inlines)
  local src = pandoc.List(inlines)
  for _, el in ipairs(src) do
    if el.t == "Str" then
      el.text = rewrite_embedded(el.text)
    end
  end

  local merged = pandoc.List()
  local i = 1
  while i <= #src do
    local a = src[i]
    local b = src[i + 1]
    local c = src[i + 2]
    local head, mark = nil, nil
    if a and a.t == "Str" then
      head, mark = split_trailing_mark(a.text)
    end
    if mark and is_gap(b) and c and c.t == "Str" then
      local rewritten = take_number(c.text)
      if rewritten then
        merged:insert(pandoc.Str(head .. mark .. rewritten))
        i = i + 3
      else
        merged:insert(a)
        i = i + 1
      end
    elseif mark and b and b.t == "Str" then
      local rewritten = take_number(b.text)
      if rewritten then
        merged:insert(pandoc.Str(head .. mark .. rewritten))
        i = i + 2
      else
        merged:insert(a)
        i = i + 1
      end
    else
      if a then
        merged:insert(a)
      end
      i = i + 1
    end
  end

  local out = pandoc.List()
  i = 1
  while i <= #merged do
    local a = merged[i]
    local b = merged[i + 1]
    local c = merged[i + 2]
    if a and a.t == "Str" and is_gap(b) and is_cn_xref(c)
      and is_tight_before_xref(utf8_last(a.text)) then
      out:insert(a)
      out:insert(c)
      i = i + 3
      local d = merged[i]
      local e = merged[i + 1]
      if is_gap(d) and e and e.t == "Str" and is_tight_after_xref(utf8_first(e.text)) then
        out:insert(e)
        i = i + 2
      end
    elseif is_cn_xref(a) and is_gap(b) and c and c.t == "Str"
      and is_tight_after_xref(utf8_first(c.text)) then
      out:insert(a)
      out:insert(c)
      i = i + 3
    elseif is_gap(a) and is_cn_xref(b) then
      local prev = out[#out]
      if prev and prev.t == "Str" and is_tight_before_xref(utf8_last(prev.text)) then
        out:insert(b)
        i = i + 2
        local d = merged[i]
        local e = merged[i + 1]
        if is_gap(d) and e and e.t == "Str" and is_tight_after_xref(utf8_first(e.text)) then
          out:insert(e)
          i = i + 2
        end
      else
        out:insert(a)
        i = i + 1
      end
    else
      if a then
        out:insert(a)
      end
      i = i + 1
    end
  end
  return out
end

function Inlines(inlines)
  return tighten_inlines(inlines)
end
