-- Drop the NBSP Quarto inserts between a Chinese float prefix and its number.
-- Also drop the parse-time spaces around @fig/@tbl after resolve, so
-- "（见 @fig-id ）" becomes "（见图2-6）" rather than "（见 图2-6 ）".
-- Built-in fig/tbl always call nbspString() (format.lua titlePrefix, refs.lua
-- add_ref_prefix). title-delim only sits between the number and the caption
-- text, so fig-title: "\u56fe2-" still renders as "\u56fe2- 13".
-- Lua patterns are byte-wise: do not put CJK in []. Use pandoc.text.
-- Idempotent. Leaves non 图N-/表N- prefixes unchanged.

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_cn_float_prefix(text)
  local first = pandoc.text.sub(text, 1, 1)
  if first ~= FIG and first ~= TBL then
    -- fallback if the file encoding stored the literals as UTF-8 CJK
    if first ~= "图" and first ~= "表" then
      return false
    end
  end
  local rest = pandoc.text.sub(text, 2)
  return rest:match("^%d+%-") ~= nil
end

local function tighten_str(text)
  -- 图N-<nbsp/space>digits  /  表N-<nbsp/space>digits
  text = text:gsub("(" .. FIG .. "%d+%-)[" .. NBSP .. " ]+(%d+)", "%1%2")
  text = text:gsub("(" .. TBL .. "%d+%-)[" .. NBSP .. " ]+(%d+)", "%1%2")
  return text
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
  return pandoc.text.sub(text, pandoc.text.len(text), pandoc.text.len(text))
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

-- CJK / fullwidth / CJK punctuation: drop the parse-time space next to @fig
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 == "["
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 == ":"
end

local function is_cn_xref(el)
  if el == nil or el.t ~= "Link" then
    return false
  end
  if el.classes and el.classes:includes("quarto-xref") then
    return true
  end
  local s = pandoc.utils.stringify(el)
  local first = utf8_first(s)
  if first ~= FIG and first ~= TBL and first ~= "图" and first ~= "表" 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 = tighten_str(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]
    if a and a.t == "Str" and is_cn_float_prefix(a.text)
      and is_gap(b)
      and c and c.t == "Str" and c.text:match("^%d+") then
      merged:insert(pandoc.Str(a.text .. c.text))
      i = i + 3
    else
      merged:insert(a)
      i = i + 1
    end
  end
  -- Source keeps "见 @fig-id " so Pandoc can parse the cite; drop those
  -- spaces after resolve when the neighbor is CJK / Chinese punctuation.
  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
      -- Previous Str already emitted (e.g. "）。灌溉（" + Space + Link)
      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
      out:insert(a)
      i = i + 1
    end
  end
  return out
end

function Pandoc(doc)
  -- Must run at post-render: in-text @fig is resolved in the quarto
  -- stage, but caption prefixes are prepended only during render.
  return doc:walk({
    Inlines = function(inlines)
      return pandoc.Inlines(tighten_inlines(inlines))
    end
  })
end
