--
-- Copyright (c) 2021-2025 Zeping Lee
-- Released under the MIT license.
-- Repository: https://github.com/zepinglee/citeproc-lua
--

local citation_module = {}

local context
local element
local ir_node
local output
local node_names
local util

local using_luatex, kpse = pcall(require, "kpse")
if using_luatex then
 context = require("citeproc-context")
 element = require("citeproc-element")
 ir_node = require("citeproc-ir-node")
 output = require("citeproc-output")
 node_names = require("citeproc-node-names")
 util = require("citeproc-util")
else
 context = require("citeproc.context")
 element = require("citeproc.element")
 ir_node = require("citeproc.ir-node")
 output = require("citeproc.output")
 node_names = require("citeproc.node-names")
 util = require("citeproc.util")
end

local Context = context.Context
local IrState = context.IrState
local Element = element.Element
local IrNode = ir_node.IrNode
local Rendered = ir_node.Rendered
local SeqIr = ir_node.SeqIr
local YearSuffix = ir_node.YearSuffix
local GroupVar = ir_node.GroupVar
local Micro = output.Micro
local Formatted = output.Formatted
local PlainText = output.PlainText
local InlineElement = output.InlineElement
local UndefinedCite = output.UndefinedCite
local CiteInline = output.CiteInline
local DisamStringFormat = output.DisamStringFormat
local SortStringFormat = output.SortStringFormat

local Position = util.Position


---@class Citation: Element
---@field disambiguate_add_givenname boolean?
---@field givenname_disambiguation_rule string
---@field disambiguate_add_names boolean?
---@field disambiguate_add_year_suffix boolean?
---@field cite_group_delimiter string
---@field cite_grouping boolean?
---@field collapse string?
---@field year_suffix_delimiter string
---@field after_collapse_delimiter string
---@field near_note_distance integer
---@field style Style
---@field sort Sort
---@field layout Layout
---@field layouts_by_language { [string]: Layout }
---@field name_inheritance Name
local Citation = Element:derive("citation", {
 givenname_disambiguation_rule = "by-cite",
 -- https://github.com/citation-style-language/schema/issues/338
 -- The cite_group_delimiter may be changed to inherit the delimiter in citation > layout.
 cite_group_delimiter = ", ",
 near_note_distance = 5,
})

function Citation:from_node(node, style)

 local o = self:new()
 o.children = {}
 o.style = style
 o.layout = nil
 o.layouts_by_language = {}

 o:process_children_nodes(node)

 -- o.layouts = nil  -- CSL-M extension

 for _, child in ipairs(o.children) do
   local element_name = child.element_name
   if element_name == "layout" then
     if child.locale then
       for _, lang in ipairs(util.split(util.strip(child.locale))) do
         o.layouts_by_language[lang] = child
       end
     else
       o.layout = child
     end
   elseif element_name == "sort" then
     o.sort = child
   end
 end

 -- Disambiguation
 o:set_bool_attribute(node, "disambiguate-add-givenname")
 o:set_attribute(node, "givenname-disambiguation-rule")
 o:set_bool_attribute(node, "disambiguate-add-names")
 o:set_bool_attribute(node, "disambiguate-add-year-suffix")

 -- Cite Grouping
 o:set_attribute(node, "cite-group-delimiter")
 -- In the current citeproc-js implementation and test suite,
 -- cite grouping is activated by setting the cite-group-delimiter
 -- attribute or the collapse attributes on cs:citation.
 -- It may be changed to an independent procedure.
 -- https://github.com/citation-style-language/schema/issues/338
 if node:get_attribute("cite-group-delimiter") then
   o.cite_grouping = true
 else
   o.cite_grouping = false
 end


 -- Cite Collapsing
 o:set_attribute(node, "collapse")
 o:set_attribute(node, "year-suffix-delimiter")
 if not o.year_suffix_delimiter then
   o.year_suffix_delimiter = o.layout.delimiter
 end
 o:set_attribute(node, "after-collapse-delimiter")
 if not o.after_collapse_delimiter then
   o.after_collapse_delimiter = o.layout.delimiter
 end

 -- Note Distance
 o:set_number_attribute(node, "near-note-distance")

 local name_inheritance = node_names.Name:new()
 for key, value in pairs(style.name_inheritance) do
   if value ~= nil then
     name_inheritance[key] = value
   end
 end
 Element.make_name_inheritance(name_inheritance, node)
 o.name_inheritance = name_inheritance

 -- update_mode = "plain" or "numeric" or "position" (or "both"?)

 return o
end

---@param citation CitationData
---@param engine CiteProc
---@return string
function Citation:build_citation_str(citation, engine)
 if engine.registry.requires_sorting then
   engine:sort_bibliography()
 end

 local citation_str = self:build_cluster(citation.citationItems, engine, citation.properties)
 return citation_str
end

-- Formatting is stripped from the author-only and composite renderings
-- of the author name
local function remove_name_formatting(ir)
 if ir._element_name == "name" then
   ir.formatting = nil
 end
 if ir.children then
   for _, child in ipairs(ir.children) do
     remove_name_formatting(child)
   end
 end
end

---@alias CiteId string | number

---@param citation_items CitationItem[]
---@param engine CiteProc
---@param properties CitationProperties
---@return string
function Citation:build_cluster(citation_items, engine, properties)
 properties = properties or {}
 local output_format = engine.output_format
 ---@type CiteIr[]
 local irs = {}
 for _, cite_item in ipairs(citation_items) do
   local ir = self:build_fully_disambiguated_ir(cite_item, output_format, engine, properties)
   table.insert(irs, ir)
 end

 -- Special citation forms
 -- https://citeproc-js.readthedocs.io/en/latest/running.html#special-citation-forms
 self:_apply_special_citation_form(irs, properties, output_format, engine)

 if self.cite_grouping then
   irs = self:group_cites(irs)
 else
   local citation_collapse = self.collapse
   if citation_collapse == "year" or citation_collapse == "year-suffix" or
       citation_collapse == "year-suffix-ranged" then
     irs = self:group_cites(irs)
   end
 end

 if self.collapse then
   self:collapse_cites(irs, engine)
 end

 -- Capitalizing the first term does not happen in the citation of in-text style.
 -- See <https://github.com/Juris-M/citeproc-js/blob/f88a47e6d143ace8a79569388534ff8ad9205da0/src/node_text.js#L190-L199>.
 -- TODO: in-text bibliography
 if engine.style.class == "note" then
   -- Capitalize first
   for i, ir in ipairs(irs) do
     -- local layout_prefix
     -- local layout_affixes = self.layout.affixes
     -- if layout_affixes then
     --   layout_prefix = layout_affixes.prefix
     -- end
     local prefix_inlines = ir.cite_prefix
     if prefix_inlines then
       -- Prefix is inlines
       local prefix_str = output.SortStringFormat:new():output(prefix_inlines, context)
       if (string.match(prefix_str, "[.!?]%s*$")
             -- position_IbidWithPrefixFullStop.txt
             -- `Book A. He said “Please work.” Ibid.`
             or string.match(prefix_str, "[.!?]”%s*$")
           ) and InlineElement.has_space(prefix_inlines) then
         ir:capitalize_first_term()
       end
     else
       local delimiter = self.layout.delimiter
       if i == 1 or not delimiter or string.match(delimiter, "[.!?]%s*$") then
         ir:capitalize_first_term()
       end
     end
   end
 end

 local citation_stream = {}

 local context = Context:new()
 context.engine = engine
 context.style = engine.style
 context.area = self
 context.in_bibliography = false
 context.lang = engine.lang
 context.locale = engine:get_locale(engine.lang)
 context.name_inheritance = self.name_inheritance
 context.format = output_format

 local previous_ir
 for i, ir in ipairs(irs) do
   local cite_prefix = ir.cite_prefix
   local cite_suffix = ir.cite_suffix
   if not ir.collapse_suppressed then
     local cite_inlines = ir:flatten(output_format)
     if #cite_inlines > 0 then
       -- Make sure cite_inlines has outputs contents.
       -- collapse_AuthorCollapseNoDateSorted.txt
       if previous_ir then
         local delimiter = previous_ir.own_delimiter or previous_ir.cite_delimiter
         if delimiter then
           if cite_prefix then
             local left_most_str
             left_most_str = cite_prefix[1]:get_left_most_string()
             if string.match(left_most_str, "^[,.;?!]") then
               delimiter = nil
             end
           end
           if delimiter then
             table.insert(citation_stream, PlainText:new(delimiter))
           end
         end
       end

       if cite_prefix then
         table.insert(citation_stream, Micro:new(cite_prefix))
       end

       -- if context.engine.opt.citation_link then
       cite_inlines = {CiteInline:new(cite_inlines, ir.cite_item)}
       -- end
       util.extend(citation_stream, cite_inlines)

       if cite_suffix then
         table.insert(citation_stream, Micro:new(cite_suffix))
         local cite_delimiter = ir.own_delimiter or ir.cite_delimiter
         if cite_delimiter and string.match(cite_delimiter, "^[,.;?]") then
           -- affix_WithCommas.txt
           local right_most_str = cite_suffix[#cite_suffix]:get_right_most_string()
           if string.match(right_most_str, "[,.;?]%s*$") then
             ir.own_delimiter = string.gsub(cite_delimiter, "^[,.;?]", "")
           end
         end
       end

       previous_ir = ir
     end
   end
 end

 local has_printed_form = true
 if #citation_items == 0 then
   -- bugreports_AuthorOnlyFail.txt
   citation_stream = {PlainText:new("[NO_PRINTED_FORM]")}
   has_printed_form = false
 elseif #citation_stream == 0 then
   -- date_DateNoDateNoTest.txt
   has_printed_form = false
   citation_stream = {PlainText:new("[CSL STYLE ERROR: reference with no printed form.]")}
 elseif #citation_stream == 1 and citation_stream[1]._type == "CiteInline" and
     #citation_stream[1].inlines == 1 and citation_stream[1].inlines[1].value == "[NO_PRINTED_FORM]" then
   has_printed_form = false
 end

 -- Ouput citation affixes
 if has_printed_form then
   if properties.prefix and properties.prefix ~= "" then
     local citation_prefix = util.check_prefix_space_append(properties.prefix)
     local inlines = InlineElement:parse(citation_prefix, context, true)
     table.insert(citation_stream, 1, Micro:new(inlines))
   end
   if properties.suffix and properties.suffix ~= "" then
     local citation_suffix = util.check_suffix_prepend(properties.suffix)
     local inlines = InlineElement:parse(citation_suffix, context, true)
     table.insert(citation_stream, Micro:new(inlines))
   end
 end

 local suppress_layout_affixes = (properties.mode == "author-only"
   or (#citation_items >= 1 and citation_items[1]["author-only"])
   or properties.mode == "cite-year")
 if has_printed_form and context.area.layout.affixes and not suppress_layout_affixes then
   if irs[1].layout_prefix then
     table.insert(citation_stream, 1, PlainText:new(irs[1].layout_prefix))
   end
   if irs[#irs].layout_suffix then
     table.insert(citation_stream, PlainText:new(irs[#irs].layout_suffix))
   end
 end

 if has_printed_form and context.area.layout.formatting then
   citation_stream = {Formatted:new(citation_stream, context.area.layout.formatting)}
 end

 if properties.mode == "composite" then
   local author_ir
   if irs[1] then
     author_ir = irs[1].author_ir
   end
   if author_ir then
     local infix = properties.infix
     if infix then
       if string.match(infix, "^%w") then
         -- discretionary_SingleNarrativeCitation.txt
         infix = " " .. infix
       end
       if string.match(infix, "%w$") then
         infix = infix .. " "
       end
       if infix == "" then
         -- discretionary_AuthorOnlySuppressLocator.txt
         infix = " "
       end
       for i, inline in ipairs(InlineElement:parse(infix, context, true)) do
         table.insert(citation_stream, i, inline)
       end
     else
       table.insert(citation_stream, 1, PlainText:new(" "))
     end

     local author_inlines = author_ir:flatten(output_format)
     for i, inline in ipairs(author_inlines) do
       table.insert(citation_stream, i, inline)
     end
   end
 end

 local str = output_format:output(citation_stream, context)
 str = util.strip(str)

 return str
end

function Citation:sorted_citation_items(items, engine)
 local citation_sort = self.sort
 if not citation_sort then
   return items
 end

 local state = IrState:new()
 local context = Context:new()
 context.engine = engine
 context.style = engine.style
 context.area = self
 context.in_bibliography = false
 context.lang = engine.lang
 context.locale = engine:get_locale(engine.lang)
 context.name_inheritance = self.name_inheritance
 context.format = SortStringFormat:new()
 -- context.id = id
 context.cite = nil
 -- context.reference = self:get_item(id)

 items = citation_sort:sort(items, state, context)
 return items
end


---@class CiteIr: IrNode
---@field cite_item CitationItem
---@field reference table
---@field ir_index any
---@field is_ambiguous boolean
---@field disam_level integer
---@field disam_str string?
---@field cite_delimiter string?
---@field layout_prefix string?
---@field layout_suffix string?
---@field cite_prefix InlineElement[]?
---@field cite_suffix InlineElement[]?

---@param cite_item CitationItem
---@param output_format OutputFormat
---@param engine CiteProc
---@param properties CitationProperties
---@return CiteIr
function Citation:build_fully_disambiguated_ir(cite_item, output_format, engine, properties)
 local cite_ir = self:build_ambiguous_ir(cite_item, output_format, engine)
 cite_ir = self:apply_disambiguate_add_givenname(cite_ir, engine)
 cite_ir = self:apply_disambiguate_add_names(cite_ir, engine)
 cite_ir = self:apply_disambiguate_conditionals(cite_ir, engine)
 cite_ir = self:apply_disambiguate_add_year_suffix(cite_ir, engine)

 if engine.style.class == "note" and cite_item.position_level == Position.First then
   -- Disambiguation should be based on the subsequent form
   -- disambiguate_BasedOnEtAlSubsequent.txt
   cite_item.position_level = Position.Subsequent
   cite_item["first-reference-note-number"] = properties.noteIndex
   local disam_ir = self:build_ambiguous_ir(cite_item, output_format, engine)
   disam_ir = self:apply_disambiguate_add_givenname(disam_ir, engine)
   disam_ir = self:apply_disambiguate_add_names(disam_ir, engine)
   disam_ir = self:apply_disambiguate_conditionals(disam_ir, engine)
   disam_ir = self:apply_disambiguate_add_year_suffix(disam_ir, engine)
   cite_item.position_level = Position.First
   cite_item["first-reference-note-number"] = nil
 end

 return cite_ir
end

---@param cite_item CitationItem
---@param output_format OutputFormat
---@param engine CiteProc
---@return CiteIr
function Citation:build_ambiguous_ir(cite_item, output_format, engine)
 local state = IrState:new(engine.style)
 local context = Context:new()
 context.engine = engine
 context.style = engine.style
 context.area = self
 context.name_inheritance = self.name_inheritance
 context.format = output_format
 context.id = cite_item.id
 context.cite = cite_item
 -- context.reference = self:get_item(cite_item.id)
 context.reference = engine.registry.registry[cite_item.id]

 local active_layout, context_lang = util.get_layout_by_language(self, engine, context.reference)
 context.lang = context_lang
 context.locale = engine:get_locale(context_lang)

 local ir
 if context.reference then
   ir = self:build_ir(engine, state, context, active_layout)
 else
   ir = Rendered:new({UndefinedCite:new({PlainText:new(tostring(cite_item.id))}, cite_item)}, self)
 end

 ir.cite_item = cite_item
 ir.reference = context.reference
 ir.ir_index = #engine.disam_irs + 1
 table.insert(engine.disam_irs, ir)
 ir.is_ambiguous = false
 ir.disam_level = 0

 ir.cite_delimiter = active_layout.delimiter
 if active_layout.affixes then
   ir.layout_prefix = active_layout.affixes.prefix
   ir.layout_suffix = active_layout.affixes.suffix
 end

 if cite_item.prefix and cite_item.prefix ~= "" then
   local cite_prefix = util.check_prefix_space_append(cite_item.prefix)
   ir.cite_prefix = InlineElement:parse(cite_prefix, context, true)
 end
 if cite_item.suffix and cite_item.suffix ~= "" then
   local cite_suffix = util.check_suffix_prepend(cite_item.suffix)
   ir.cite_suffix = InlineElement:parse(cite_suffix, context, true)
 end

 -- Formattings like font-style are ignored for disambiguation.
 local disam_format = DisamStringFormat:new()
 local inlines = ir:flatten(disam_format)
 local disam_str = disam_format:output(inlines, context)
 ir.disam_str = disam_str

 if not engine.cite_irs_by_output[disam_str] then
   engine.cite_irs_by_output[disam_str] = {}
 end

 for ir_index, ir_ in pairs(engine.cite_irs_by_output[disam_str]) do
   if ir_.cite_item.id ~= cite_item.id then
     ir.is_ambiguous = true
     break
   end
 end
 engine.cite_irs_by_output[disam_str][ir.ir_index] = ir

 return ir
end

---@param engine CiteProc
---@param state State
---@param context Context
---@param active_layout Layout
---@return CiteIr
function Citation:build_ir(engine, state, context, active_layout)
 if not active_layout then
   util.error("Missing citation layout.")
 end
 return active_layout:build_ir(engine, state, context)
end

function Citation:apply_disambiguate_add_givenname(cite_ir, engine)
 if self.disambiguate_add_givenname then
   local gn_disam_rule = self.givenname_disambiguation_rule
   if gn_disam_rule == "all-names" or gn_disam_rule == "all-names-with-initials" then
     cite_ir = self:apply_disambiguate_add_givenname_all_names(cite_ir, engine)
   elseif gn_disam_rule == "primary-name" or gn_disam_rule == "primary-name-with-initials" then
     cite_ir = self:apply_disambiguate_add_givenname_primary_name(cite_ir, engine)
   elseif gn_disam_rule == "by-cite" then
     cite_ir = self:apply_disambiguate_add_givenname_by_cite(cite_ir, engine)
   end
 end
 return cite_ir
end

-- TODO: reorganize this code
function Citation:apply_disambiguate_add_givenname_all_names(cite_ir, engine)
 if not cite_ir.person_name_irs or #cite_ir.person_name_irs == 0 then
   return cite_ir
 end

 for _, person_name_ir in ipairs(cite_ir.person_name_irs) do
   local name_output = person_name_ir.name_output

   if not person_name_ir.person_name_index then
     person_name_ir.person_name_index = #engine.person_names + 1
     table.insert(engine.person_names, person_name_ir)
   end

   if not engine.person_names_by_output[name_output] then
     engine.person_names_by_output[name_output] = {}
   end
   engine.person_names_by_output[name_output][person_name_ir.person_name_index] = person_name_ir

   local ambiguous_name_irs = {}
   local ambiguous_same_output_irs = {}

   for _, pn_ir in pairs(engine.person_names_by_output[person_name_ir.name_output]) do
     if pn_ir.full_name ~= person_name_ir.full_name then
       table.insert(ambiguous_name_irs, pn_ir)
     end
     if pn_ir.name_output == person_name_ir.name_output then
       table.insert(ambiguous_same_output_irs, pn_ir)
     end
   end

   while person_name_ir.disam_variants_index < #person_name_ir.disam_variants do
     if #ambiguous_name_irs == 0 then
       break
     end

     for _, pn_ir in ipairs(ambiguous_same_output_irs) do
       -- expand one name
       if pn_ir.disam_variants_index < #pn_ir.disam_variants then
         pn_ir.disam_variants_index = pn_ir.disam_variants_index + 1
         pn_ir.name_output = pn_ir.disam_variants[pn_ir.disam_variants_index]
         pn_ir.inlines = pn_ir.disam_inlines[pn_ir.name_output]

         if not engine.person_names_by_output[pn_ir.name_output] then
           engine.person_names_by_output[pn_ir.name_output] = {}
         end
         engine.person_names_by_output[pn_ir.name_output][pn_ir.person_name_index] = pn_ir
       end
     end

     -- update ambiguous_name_irs and ambiguous_same_output_irs
     ambiguous_name_irs = {}
     ambiguous_same_output_irs = {}
     for _, pn_ir in pairs(engine.person_names_by_output[person_name_ir.name_output]) do
       if pn_ir.full_name ~= person_name_ir.full_name then
         table.insert(ambiguous_name_irs, pn_ir)
       end
       if pn_ir.name_output == person_name_ir.name_output then
         table.insert(ambiguous_same_output_irs, pn_ir)
       end
     end

   end
 end

 -- update cite_ir output
 local disam_format = DisamStringFormat:new()
 local inlines = cite_ir:flatten(disam_format)
 local disam_str = disam_format:output(inlines, nil)
 cite_ir.disam_str = disam_str
 if not engine.cite_irs_by_output[disam_str] then
   engine.cite_irs_by_output[disam_str] = {}
 end
 engine.cite_irs_by_output[disam_str][cite_ir.ir_index] = cite_ir

 -- update ambiguous_cite_irs and ambiguous_same_output_irs
 local ambiguous_cite_irs = {}
 for ir_index, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
   if ir_.cite_item.id ~= cite_ir.cite_item.id then
     table.insert(ambiguous_cite_irs, ir_)
   end
 end
 if #ambiguous_cite_irs == 0 then
   cite_ir.is_ambiguous = false
 end

 return cite_ir
end

function Citation:apply_disambiguate_add_givenname_primary_name(cite_ir, engine)
 if not cite_ir.person_name_irs or #cite_ir.person_name_irs == 0 then
   return cite_ir
 end
 local person_name_ir = cite_ir.person_name_irs[1]
 local name_output = person_name_ir.name_output

 if not person_name_ir.person_name_index then
   person_name_ir.person_name_index = #engine.person_names + 1
   table.insert(engine.person_names, person_name_ir)
 end
 if not engine.person_names_by_output[name_output] then
   engine.person_names_by_output[name_output] = {}
 end
 engine.person_names_by_output[name_output][person_name_ir.person_name_index] = person_name_ir

 local ambiguous_name_irs = {}
 local ambiguous_same_output_irs = {}

 for _, pn_ir in pairs(engine.person_names_by_output[person_name_ir.name_output]) do
   if pn_ir.full_name ~= person_name_ir.full_name then
     table.insert(ambiguous_name_irs, pn_ir)
   end
   if pn_ir.name_output == person_name_ir.name_output then
     table.insert(ambiguous_same_output_irs, pn_ir)
   end
 end

 for _, name_variant in ipairs(person_name_ir.disam_variants) do
   if #ambiguous_name_irs == 0 then
     break
   end

   for _, pn_ir in ipairs(ambiguous_same_output_irs) do
     -- expand one name
     if pn_ir.disam_variants_index < #pn_ir.disam_variants then
       pn_ir.disam_variants_index = pn_ir.disam_variants_index + 1
       pn_ir.name_output = pn_ir.disam_variants[pn_ir.disam_variants_index]
       pn_ir.inlines = pn_ir.disam_inlines[pn_ir.name_output]

       if not engine.person_names_by_output[pn_ir.name_output] then
         engine.person_names_by_output[pn_ir.name_output] = {}
       end
       engine.person_names_by_output[pn_ir.name_output][person_name_ir.person_name_index] = person_name_ir
     end
   end

   -- update ambiguous_name_irs and ambiguous_same_output_irs
   ambiguous_name_irs = {}
   ambiguous_same_output_irs = {}
   for _, pn_ir in pairs(engine.person_names_by_output[person_name_ir.name_output]) do
     if pn_ir.full_name ~= person_name_ir.full_name then
       table.insert(ambiguous_name_irs, pn_ir)
     end
     if pn_ir.name_output == person_name_ir.name_output then
       table.insert(ambiguous_same_output_irs, pn_ir)
     end
   end
 end

 return cite_ir
end

function Citation:apply_disambiguate_add_givenname_by_cite(cite_ir, engine)
 if not cite_ir.is_ambiguous then
   return cite_ir
 end
 if not cite_ir.person_name_irs or #cite_ir.person_name_irs == 0 then
   return cite_ir
 end

 local disam_format = DisamStringFormat:new()

 local ambiguous_cite_irs = {}
 local ambiguous_same_output_irs = {}

 for ir_index, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
   if ir_.cite_item.id ~= cite_ir.cite_item.id then
     table.insert(ambiguous_cite_irs, ir_)
   end
   if ir_.disam_str == cite_ir.disam_str then
     table.insert(ambiguous_same_output_irs, ir_)
   end
 end

 for i, person_name_ir in ipairs(cite_ir.person_name_irs) do
   if #ambiguous_cite_irs == 0 then
     cite_ir.is_ambiguous = false
     break
   end

   while person_name_ir.disam_variants_index < #person_name_ir.disam_variants do

     local is_different_name = false
     for _, ir_ in ipairs(ambiguous_cite_irs) do
       if ir_.person_name_irs[i] then
         if ir_.person_name_irs[i].full_name ~= person_name_ir.full_name then
           is_different_name = true
           break
         end
       end
     end
     if not is_different_name then
       break
     end

     for _, ir_ in ipairs(ambiguous_same_output_irs) do
       local person_name_ir_ = ir_.person_name_irs[i]
       if person_name_ir_ then
         if person_name_ir_.disam_variants_index < #person_name_ir_.disam_variants then
           person_name_ir_.disam_variants_index = person_name_ir_.disam_variants_index + 1
           local disam_variant = person_name_ir_.disam_variants[person_name_ir_.disam_variants_index]
           person_name_ir_.name_output = disam_variant
           person_name_ir_.inlines = person_name_ir_.disam_inlines[disam_variant]
           -- Update cite ir output
           local inlines = ir_:flatten(disam_format)
           local disam_str = disam_format:output(inlines, nil)
           ir_.disam_str = disam_str
           if not engine.cite_irs_by_output[disam_str] then
             engine.cite_irs_by_output[disam_str] = {}
           end
           engine.cite_irs_by_output[disam_str][ir_.ir_index] = ir_
         end
       end
     end

     -- update ambiguous_cite_irs and ambiguous_same_output_irs
     ambiguous_cite_irs = {}
     ambiguous_same_output_irs = {}
     for ir_index, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
       if ir_.cite_item.id ~= cite_ir.cite_item.id then
         table.insert(ambiguous_cite_irs, ir_)
       end
       if ir_.disam_str == cite_ir.disam_str then
         table.insert(ambiguous_same_output_irs, ir_)
       end
     end

     if #ambiguous_cite_irs == 0 then
       cite_ir.is_ambiguous = false
       return cite_ir
     end

   end
 end

 return cite_ir
end

local function find_first_name_ir(ir)
 if ir._type == "NameIr" then
   return ir
 end
 if ir.children then
   for _, child in ipairs(ir.children) do
     local name_ir = find_first_name_ir(child)
     if name_ir then
       return name_ir
     end
   end
 end
 return nil
end

function Citation:apply_disambiguate_add_names(cite_ir, engine)
 if not self.disambiguate_add_names then
   return cite_ir
 end

 if not cite_ir.name_ir then
   cite_ir.name_ir = find_first_name_ir(cite_ir)
 end
 local name_ir = cite_ir.name_ir

 if not cite_ir.is_ambiguous then
   return cite_ir
 end

 if not name_ir or not name_ir.et_al_abbreviation then
   return cite_ir
 end

 local disam_format = DisamStringFormat:new()

 while cite_ir.is_ambiguous do
   if #cite_ir.name_ir.hidden_name_irs == 0 then
     break
   end

   local ambiguous_cite_irs = {}
   local ambiguous_same_output_irs = {}
   for _, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
     if ir_.cite_item.id ~= cite_ir.cite_item.id then
       table.insert(ambiguous_cite_irs, ir_)
     end
     if ir_.disam_str == cite_ir.disam_str then
       table.insert(ambiguous_same_output_irs, ir_)
     end
   end

   if #ambiguous_cite_irs == 0 then
     cite_ir.is_ambiguous = false
     break
   end

   -- check if the cite can be (fully) disambiguated by adding names
   local can_be_disambuguated = false
   for _, ir_ in ipairs(ambiguous_cite_irs) do
     if ir_.name_ir.full_name_str ~= cite_ir.name_ir.full_name_str then
       can_be_disambuguated = true
       break
     end
   end
   if not can_be_disambuguated then
     break
   end

   for _, ir_ in ipairs(ambiguous_same_output_irs) do
     local added_person_name_ir = ir_.name_ir.name_inheritance:expand_one_name(ir_.name_ir)
     if added_person_name_ir then
       table.insert(ir_.person_name_irs, added_person_name_ir)

       if not added_person_name_ir.person_name_index then
         added_person_name_ir.person_name_index = #engine.person_names + 1
         table.insert(engine.person_names, added_person_name_ir)
       end
       local name_output = added_person_name_ir.name_output
       if not engine.person_names_by_output[name_output] then
         engine.person_names_by_output[name_output] = {}
       end
       engine.person_names_by_output[name_output][added_person_name_ir.person_name_index] = added_person_name_ir

       -- Update ir output
       local inlines = ir_:flatten(disam_format)
       local disam_str = disam_format:output(inlines, nil)
       ir_.disam_str = disam_str
       if not engine.cite_irs_by_output[disam_str] then
         engine.cite_irs_by_output[disam_str] = {}
       end
       engine.cite_irs_by_output[disam_str][ir_.ir_index] = ir_
     end
   end


   if self.disambiguate_add_givenname then
     local gn_disam_rule = self.givenname_disambiguation_rule
     if gn_disam_rule == "all-names" or gn_disam_rule == "all-names-with-initials" then
       cite_ir = self:apply_disambiguate_add_givenname_all_names(cite_ir, engine)
     elseif gn_disam_rule == "by-cite" then
       cite_ir = self:apply_disambiguate_add_givenname_by_cite(cite_ir, engine)
     end
   end

   cite_ir.is_ambiguous = self:check_ambiguity(cite_ir, engine)

 end

 return cite_ir
end

function Citation:collect_irs_with_disambiguate_branch(ir)
 local irs_with_disambiguate_branch = {}
 if ir.children then
   for i, child_ir in ipairs(ir.children) do
     if child_ir.disambiguate_branch_ir then
       table.insert(irs_with_disambiguate_branch, child_ir)
     elseif child_ir.children then
       util.extend(irs_with_disambiguate_branch,
         self:collect_irs_with_disambiguate_branch(child_ir))
     end
   end
 end
 return irs_with_disambiguate_branch
end

function Citation:apply_disambiguate_conditionals(cite_ir, engine)
 cite_ir.irs_with_disambiguate_branch = self:collect_irs_with_disambiguate_branch(cite_ir)

 local disam_format = DisamStringFormat:new()

 while cite_ir.is_ambiguous do
   if #cite_ir.irs_with_disambiguate_branch == 0 then
     break
   end

   -- update ambiguous_same_output_irs
   local ambiguous_same_output_irs = {}
   for _, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
     if ir_.disam_str == cite_ir.disam_str then
       table.insert(ambiguous_same_output_irs, ir_)
     end
   end

   for _, ir_ in ipairs(ambiguous_same_output_irs) do
     if #ir_.irs_with_disambiguate_branch > 0 then
       -- Disambiguation is incremental
       -- disambiguate_IncrementalExtraText.txt
       ---@type SeqIr
       local condition_ir = ir_.irs_with_disambiguate_branch[1]
       condition_ir.children[1] = condition_ir.disambiguate_branch_ir
       -- condition_ir.person_name_irs are no longer used and we ignore them
       condition_ir.group_var = condition_ir.disambiguate_branch_ir.group_var
       table.remove(ir_.irs_with_disambiguate_branch, 1)
       -- disambiguate_DisambiguateTrueReflectedInBibliography.txt
       ir_.reference.disambiguate = true

       -- Update ir output
       local inlines = ir_:flatten(disam_format)
       local disam_str = disam_format:output(inlines, nil)
       ir_.disam_str = disam_str
       if not engine.cite_irs_by_output[disam_str] then
         engine.cite_irs_by_output[disam_str] = {}
       end
       engine.cite_irs_by_output[disam_str][ir_.ir_index] = ir_
     end
   end

   cite_ir.is_ambiguous = self:check_ambiguity(cite_ir, engine)
 end
 return cite_ir
end

function Citation:check_ambiguity(cite_ir, engine)
 local is_ambiguous = false
 for _, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
   if ir_.cite_item.id ~= cite_ir.cite_item.id then
     is_ambiguous = true
   end
 end
 return is_ambiguous
end

function Citation:get_ambiguous_cite_irs(cite_ir, engine)
 local res = {}
 for _, ir in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
   if ir.cite_item.id ~= cite_ir.cite_item.id then
     table.insert(res, ir)
   end
 end
 return res
end

function Citation:get_ambiguous_same_output_cite_irs(cite_ir, engine)
 -- This includes the cite_ir itself.
 local res = {}
 for _, ir in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
   if ir.disam_str == cite_ir.disam_str then
     table.insert(res, ir)
   end
 end
 return res
end

function Citation:apply_disambiguate_add_year_suffix(cite_ir, engine)
 if not cite_ir.is_ambiguous or not self.disambiguate_add_year_suffix then
   return cite_ir
 end

 local ambiguous_same_output_irs = self:get_ambiguous_same_output_cite_irs(cite_ir, engine)

 table.sort(ambiguous_same_output_irs, function (a, b)
   -- return a.ir_index < b.ir_index
   return a.reference["citation-number"] < b.reference["citation-number"]
 end)

 local disam_format = DisamStringFormat:new()

 for _, ir_ in ipairs(ambiguous_same_output_irs) do
   ir_.reference.year_suffix_number = nil
 end

 -- TODO: clear year-suffix after updateItems
 local year_suffix_number = 0
 for _, ir_ in ipairs(ambiguous_same_output_irs) do
   if not ir_.reference.year_suffix_number then
     year_suffix_number = year_suffix_number + 1
     ir_.reference.year_suffix_number = year_suffix_number
     ir_.reference["year-suffix"] = self:render_year_suffix(year_suffix_number)
   end

   if not ir_.year_suffix_irs then
     ir_.year_suffix_irs = ir_:collect_year_suffix_irs()
     if #ir_.year_suffix_irs == 0 then
       -- The style does not have a "year-suffix" variable.
       -- Then the year-suffix is appended the first year rendered through cs:date or citation-label
       local year_ir = ir_:find_first_year_ir()
       if year_ir then
         local year_suffix_ir = YearSuffix:new({}, self)
         table.insert(year_ir.children, year_suffix_ir)
         table.insert(ir_.year_suffix_irs, year_suffix_ir)
       else
         util.warning("No date variable for year-suffix")
       end
     end
   end

   -- Render all the year-suffix irs.
   for _, year_suffix_ir in ipairs(ir_.year_suffix_irs) do
     year_suffix_ir.inlines = {PlainText:new(ir_.reference["year-suffix"])}
     year_suffix_ir.year_suffix_number = ir_.reference.year_suffix_number
     year_suffix_ir.group_var = GroupVar.Important
   end

   -- DisamStringFormat doesn't render YearSuffix and this can be skipped.
   -- local inlines = ir_:flatten(disam_format)
   -- local disam_str = disam_format:output(inlines, nil)
   -- ir_.disam_str = disam_str
   -- if not engine.cite_irs_by_output[disam_str] then
   --   engine.cite_irs_by_output[disam_str] = {}
   -- end
   -- engine.cite_irs_by_output[disam_str][ir_.ir_index] = ir_
 end

 cite_ir.is_ambiguous = false

 return cite_ir
end

function Citation:render_year_suffix(year_suffix_number)
 if year_suffix_number <= 0 then
   return nil
 end
 local year_suffix = ""
 while year_suffix_number > 0 do
   local i = (year_suffix_number - 1) % 26
   year_suffix = string.char(i + 97) .. year_suffix
   year_suffix_number = (year_suffix_number - 1) // 26
 end
 return year_suffix
end

local function find_first(ir, check)
 if check(ir) then
   return ir
 end
 if ir.children then
   for _, child in ipairs(ir.children) do
     local target_ir = find_first(child, check)
     if target_ir then
       return target_ir
     end
   end
 end
 return nil
end

-- Find the first rendering element and it should be produced by and names element
local function find_first_names_ir(ir)
 if ir.first_names_ir then
   return ir.first_names_ir
 end

 local first_rendering_ir = find_first(ir, function (ir_)
   return (ir_._element_name == "text"
         or ir_._element_name == "date"
         or ir_._element_name == "number"
         or ir_._element_name == "names"
         or ir_._element_name == "label")
       and ir_.group_var ~= GroupVar.Missing
 end)
 local first_names_ir
 if first_rendering_ir and first_rendering_ir._element_name == "names" then
   first_names_ir = first_rendering_ir
 end
 if first_names_ir then
   local disam_format = DisamStringFormat:new()
   local inlines = first_names_ir:flatten(disam_format)
   first_names_ir.disam_str = disam_format:output(inlines, nil)
 end
 ir.first_names_ir = first_names_ir
 return first_names_ir

end

function Citation:_apply_special_citation_form(irs, properties, output_format, engine)
 if properties.mode then
   if properties.mode == "author-only" then
     for _, ir in ipairs(irs) do
       self:_apply_citation_mode_author_only(ir)
     end
   elseif properties.mode == "suppress-author" then
     -- suppress-author mode does not work in note style
     -- discretionary_FirstReferenceNumberWithIntext.txt
     if engine.style.class ~= "note" then
       for _, ir in ipairs(irs) do
         self:_apply_suppress_author(ir)
       end
     end

   elseif properties.mode == "cite-year" then
     if engine.style.class ~= "note" then
       for i, ir in ipairs(irs) do
         self:_apply_suppress_author(ir)
         local cite_str = output_format:output(ir:flatten(output_format), nil)
         if ir.reference and ir.reference.issued and ir.reference.issued["date-parts"] then
           local year = tostring(ir.reference.issued["date-parts"][1][1])
           if not string.match(cite_str, year) then
             irs[i] = Rendered:new({PlainText:new(year)}, self)
           end
         end
       end
     end

   elseif properties.mode == "composite" then
     self:_apply_composite(irs[1], output_format, engine)

   end

 else
   for _, ir in ipairs(irs) do
     if ir.cite_item["author-only"] then
       self:_apply_cite_author_only(ir)
     elseif ir.cite_item["suppress-author"] then
       self:_apply_suppress_author(ir)
     end
   end
 end
end

function Citation:_apply_citation_mode_author_only(ir)
 -- Used in pr
 local author_ir = find_first_names_ir(ir)

 if author_ir then
   remove_name_formatting(author_ir)
   ir.children = {author_ir}
 else
   ir.children = {Rendered:new({PlainText:new("[NO_PRINTED_FORM]")}, self)}
 end
 return ir
end

-- Citation flags with makeCitationCluster
-- In contrast to Citation flags with processCitationCluster, this funciton
-- looks for the first rendering element instead of names element.
-- See discretionary_AuthorOnly.txt
function Citation:_apply_cite_author_only(ir)
 local author_ir = find_first(ir, function (ir_)
   return (ir_._element_name == "text"
         or ir_._element_name == "date"
         or ir_._element_name == "number"
         or ir_._element_name == "names"
         or ir_._element_name == "label")
       and ir_.group_var ~= GroupVar.Missing
 end)

 if author_ir then
   remove_name_formatting(author_ir)
   ir.children = {author_ir}
 else
   ir.children = {Rendered:new({PlainText:new("[NO_PRINTED_FORM]")}, self)}
 end
 return ir
end

function Citation:_apply_suppress_author(ir)
 local author_ir = find_first_names_ir(ir)
 if author_ir then
   author_ir.collapse_suppressed = true
 end
 return ir
end

function Citation:_apply_composite(ir, output_format, engine)
 -- local first_names_ir = find_first_names_ir(ir)

 local first_names_ir = find_first_names_ir(ir)
 if first_names_ir and engine.style.class ~= "note" then
   first_names_ir.collapse_suppressed = true
 end

 local author_ir
 if engine.style.intext then
   local properties = {mode = "author-only"}
   author_ir = engine.style.intext:build_fully_disambiguated_ir(ir.cite_item, output_format, engine, properties)
 elseif first_names_ir then
   author_ir = first_names_ir
 end

 if author_ir then
   remove_name_formatting(author_ir)
   ir.author_ir = author_ir
 else
   ir.author_ir = Rendered:new({PlainText:new("[NO_PRINTED_FORM]")}, self)
 end

 return ir
end

function Citation:group_cites(irs)
 local disam_format = DisamStringFormat:new()
 for _, ir in ipairs(irs) do
   local first_names_ir = ir.first_names_ir
   if not first_names_ir then
     first_names_ir = find_first(ir, function (ir_)
       return ir_._element_name == "names" and ir_.group_var ~= GroupVar.Missing
     end)
     if first_names_ir then
       local inlines = first_names_ir:flatten(disam_format)
       first_names_ir.disam_str = disam_format:output(inlines, nil)
     end
     ir.first_names_ir = first_names_ir
   end
 end

 local irs_by_name = {}
 local name_list = {}

 for _, ir in ipairs(irs) do
   local name_str = ""
   if ir.first_names_ir then
     name_str = ir.first_names_ir.disam_str
   end
   if not irs_by_name[name_str] then
     irs_by_name[name_str] = {}
     table.insert(name_list, name_str)
   end
   table.insert(irs_by_name[name_str], ir)
 end

 local grouped = {}
 for _, name_str in ipairs(name_list) do
   local irs_with_same_name = irs_by_name[name_str]
   for i, ir in ipairs(irs_with_same_name) do
     if i < #irs_with_same_name then
       ir.own_delimiter = self.cite_group_delimiter
     end
     table.insert(grouped, ir)
   end
 end
 return grouped
end

function Citation:collapse_cites(irs, engine)
 if self.collapse == "citation-number" then
   self:collapse_cites_by_citation_number(irs, engine)
 elseif self.collapse == "year" then
   self:collapse_cites_by_year(irs)
 elseif self.collapse == "year-suffix" then
   self:collapse_cites_by_year_suffix(irs)
 elseif self.collapse == "year-suffix-ranged" then
   self:collapse_cites_by_year_suffix_ranged(irs)
 end
end

---@param irs CiteIr
---@param engine CiteProc
function Citation:collapse_cites_by_citation_number(irs, engine)
 local cite_groups = {}
 local current_group = {}
 local previous_citation_number
 for i, ir in ipairs(irs) do
   local citation_number
   local only_citation_number_ir = self:get_only_citation_number(ir)
   if only_citation_number_ir then
     -- Other irs like locators are not rendered.
     -- collapse_CitationNumberRangesWithAffixesGrouped.txt
     citation_number = only_citation_number_ir.citation_number
   end
   if i == 1 then
     table.insert(current_group, ir)
   elseif citation_number and previous_citation_number and
       previous_citation_number + 1 == citation_number then
     table.insert(current_group, ir)
   else
     table.insert(cite_groups, current_group)
     current_group = {ir}
   end
   previous_citation_number = citation_number
 end
 table.insert(cite_groups, current_group)

 local locale = engine:get_locale(engine.lang)
 local citation_range_delimiter = locale:get_simple_term("citation-range-delimiter")
 if not citation_range_delimiter then
   citation_range_delimiter = util.unicode["en dash"]
 end

 for _, cite_group in ipairs(cite_groups) do
   if #cite_group >= 3 then
     cite_group[1].own_delimiter = citation_range_delimiter
     for i = 2, #cite_group - 1 do
       cite_group[i].collapse_suppressed = true
     end
     cite_group[#cite_group].own_delimiter = self.after_collapse_delimiter
   end
 end
end

function Citation:get_only_citation_number(ir)
 if ir.citation_number then
   return ir
 end
 if not ir.children then
   return nil
 end
 local only_citation_number_ir
 for _, child in ipairs(ir.children) do
   if child.group_var ~= GroupVar.Missing then
     local citation_number_ir = self:get_only_citation_number(child)
     if citation_number_ir then
       if only_citation_number_ir then
         return nil
       else
         only_citation_number_ir = citation_number_ir
       end
     else
       return false
     end
   end
 end
 return only_citation_number_ir
end

function Citation:collapse_cites_by_year(irs)
 local cite_groups = {{}}
 local previous_name_str
 for i, ir in ipairs(irs) do
   local name_str
   if ir.first_names_ir then
     name_str = ir.first_names_ir.disam_str
   end
   if i == 1 then
     table.insert(cite_groups[#cite_groups], ir)
   elseif name_str and name_str == previous_name_str then
     -- ir.first_names_ir was set in the cite grouping stage
     -- TODO: and not previous cite suffix
     table.insert(cite_groups[#cite_groups], ir)
   else
     table.insert(cite_groups, {ir})
   end
   previous_name_str = name_str
 end

 for _, cite_group in ipairs(cite_groups) do
   if #cite_group > 1 then
     for i, cite_ir in ipairs(cite_group) do
       if i > 1 and cite_ir.first_names_ir then
         cite_ir.first_names_ir.collapse_suppressed = true
       end
       if i == #cite_group then
         cite_ir.own_delimiter = self.after_collapse_delimiter
       elseif i < #cite_group then
         if self.style.class == "in-text" then
           if cite_ir.cite_item.locator then
             -- Special hack
             cite_ir.own_delimiter = self.after_collapse_delimiter
           else
             cite_ir.own_delimiter = self.cite_group_delimiter or self.layout.delimiter
           end
         else
           -- In note style, the layout delimiter is tried first before falling back to the default.
           -- See <https://github.com/citation-style-language/test-suite/issues/56>
           cite_ir.own_delimiter = self.layout.delimiter or self.cite_group_delimiter
         end
       end
     end
   end
 end
end

local function find_rendered_year_suffix(ir)
 if ir._type == "YearSuffix" then
   return ir
 end
 if ir.children then
   for _, child in ipairs(ir.children) do
     if child.group_var ~= GroupVar.Missing then
       local year_suffix = find_rendered_year_suffix(child)
       if year_suffix then
         return year_suffix
       end
     end
   end
 end
 return nil
end

function Citation:collapse_cites_by_year_suffix(irs)
 self:collapse_cites_by_year(irs)
 -- Group by disam_str
 -- The year-suffix is ommitted in DisamStringFormat
 local cite_groups = {{}}
 local previous_ir
 local previous_year_suffix
 for i, ir in ipairs(irs) do
   local year_suffix = find_rendered_year_suffix(ir)
   ir.rendered_year_suffix_ir = year_suffix
   if i == 1 then
     table.insert(cite_groups[#cite_groups], ir)
   elseif year_suffix and previous_ir.disam_str == ir.disam_str and previous_year_suffix then
     -- TODO: and not previous cite suffix
     table.insert(cite_groups[#cite_groups], ir)
   else
     table.insert(cite_groups, {ir})
   end
   previous_ir = ir
   previous_year_suffix = year_suffix
 end

 for _, cite_group in ipairs(cite_groups) do
   if #cite_group > 1 then
     for i, cite_ir in ipairs(cite_group) do
       if i > 1 then
         -- cite_ir.children = {cite_ir.rendered_year_suffix_ir}
         -- Set the collapse_suppressed flag rather than removing the child irs.
         -- This leaves the disamb ir structure unchanged.
         self:suppress_ir_except_child(cite_ir, cite_ir.rendered_year_suffix_ir)
       end
       if i < #cite_group then
         if self.cite_grouping then
           -- In the current citeproc-js impplementation, explicitly set
           -- cite-group-delimiter takes precedence over year-suffix-delimiter.
           -- May be changed in the future.
           -- https://github.com/citation-style-language/test-suite/issues/50
           cite_ir.own_delimiter = self.cite_group_delimiter
         else
           cite_ir.own_delimiter = self.year_suffix_delimiter
         end
       elseif i == #cite_group then
         cite_ir.own_delimiter = self.after_collapse_delimiter
       end
     end
   end
 end
end

function Citation:suppress_ir_except_child(ir, target)
 if ir == target then
   ir.collapse_suppressed = false
   return false
 end
 ir.collapse_suppressed = true
 if ir.children then
   for _, child in ipairs(ir.children) do
     if child.group_var ~= GroupVar.Missing and not child.collapse_suppressed then
       if not self:suppress_ir_except_child(child, target) then
         ir.collapse_suppressed = false
       end
     end
   end
 end
 return ir.collapse_suppressed
end

function Citation:collapse_cites_by_year_suffix_ranged(irs)
 self:collapse_cites_by_year_suffix(irs)
 -- Group by disam_str
 local cite_groups = {{}}
 local previous_ir
 local previous_year_suffix
 for i, ir in ipairs(irs) do
   local year_suffix_ir = find_rendered_year_suffix(ir)
   ir.rendered_year_suffix_ir = year_suffix_ir
   if i == 1 then
     table.insert(cite_groups[#cite_groups], ir)
   elseif year_suffix_ir and previous_ir.disam_str == ir.disam_str and previous_year_suffix and
       year_suffix_ir.year_suffix_number == previous_year_suffix.year_suffix_number + 1 then
     -- TODO: and not previous cite suffix
     table.insert(cite_groups[#cite_groups], ir)
   else
     table.insert(cite_groups, {ir})
   end
   previous_ir = ir
   previous_year_suffix = year_suffix_ir
 end

 for _, cite_group in ipairs(cite_groups) do
   if #cite_group > 2 then
     for i, cite_ir in ipairs(cite_group) do
       if i == 1 then
         cite_ir.own_delimiter = util.unicode["en dash"]
       elseif i < #cite_group then
         cite_ir.collapse_suppressed = true
       end
     end
   end
 end
end


local InText = Citation:derive("intext", {
 givenname_disambiguation_rule = "by-cite",
 cite_group_delimiter = ", ",
 near_note_distance = 5,
})


citation_module.Citation = Citation
citation_module.InText = InText


return citation_module