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

local names_module = {}

local unicode
local element
local ir_node
local output
local util

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

local NameIr = ir_node.NameIr
local PersonNameIr = ir_node.PersonNameIr
local SeqIr = ir_node.SeqIr
local Rendered = ir_node.Rendered
local GroupVar = ir_node.GroupVar
local InlineElement = output.InlineElement
local PlainText = output.PlainText
local SortStringFormat = output.SortStringFormat

local Element = element.Element

local Position = util.Position


---@class Names: Element
---@field variable string
---@field delimiter string?
---@field name Name
---@field et_al EtAl
---@field label Label
---@field substitute Substitute
local Names = Element:derive("names")

---@class Name: Element
---@field and string?
---@field delimiter string?
---@field delimiter_precedes_et_al string?
---@field delimiter_precedes_last string?
---@field et_al_min integer?
---@field et_al_use_first integer?
---@field et_al_subsequent_min integer?
---@field et_al_subsequent_use_first integer?
---@field et_al_subsequent_use_last integer?
---@field form string?
---@field initialize boolean?
---@field initialize_with string?
---@field name_as_sort_order string?
---@field sort_separator string?
---@field prefix string?
---@field suffix string?
---@field font_style string?
---@field font_variant string?
---@field font_weight string?
---@field text_decoration string?
---@field vertical_align string?
---@field family NamePart
---@field given NamePart
local Name = Element:derive("name", {
 delimiter = ", ",
 delimiter_precedes_et_al = "contextual",
 delimiter_precedes_last = "contextual",
 et_al_use_last = false,
 form = "long",
 initialize = true,
 sort_separator = ", ",
})

---@class NamePart: Element
---@field name string
---@field text_case string?
---@field prefix string?
---@field suffix string?
local NamePart = Element:derive("name-part")

---@class EtAl: Element
local EtAl = Element:derive("et-al")

---@class Substitute: Element
local Substitute = Element:derive("substitute")


-- [Names](https://docs.citationstyles.org/en/stable/specification.html#names)
function Names:new()
 local o = Element.new(self)
 o.name = nil
 o.et_al = nil
 o.substitute = nil
 o.label = nil
 return o
end

function Names:from_node(node)
 local o = Names:new()
 o:set_attribute(node, "variable")
 o.name = nil
 o.et_al = nil
 o.substitute = nil
 o.label = nil
 o.children = {}
 o:process_children_nodes(node)
 for _, child in ipairs(o.children) do
   local element_name = child.element_name
   if element_name == "name" then
     o.name = child
   elseif element_name == "et-al" then
     o.et_al = child
   elseif element_name == "substitute" then
     o.substitute = child
   elseif element_name == "label" then
     o.label = child
     if o.name then
       child.after_name = true
     end
   else
     util.warning(string.format("Unknown element '%s'.", element_name))
   end
 end
 o:get_delimiter_attribute(node)
 o:set_affixes_attributes(node)
 o:set_display_attribute(node)
 o:set_formatting_attributes(node)
 o:set_text_case_attribute(node)
 return o
end

function Names:build_ir(engine, state, context)
 -- names_inheritance: names and name attributes inherited from cs:style
 --   and cs:citation or cs:bibliography
 -- name_override: names, name, et-al, label elements inherited in substitute element
 local names_inheritance = Names:new()
 names_inheritance.delimiter = context.name_inheritance.names_delimiter

 names_inheritance.variable = self.variable

 for _, attr in ipairs({"delimiter", "affixes", "formatting", "display"}) do
   if self[attr] then
     names_inheritance[attr] = util.clone(self[attr])
   elseif state.name_override and state.name_override[attr] then
     names_inheritance[attr] = util.clone(state.name_override[attr])
   end
 end

 if self.name then
   names_inheritance.name = util.clone(context.name_inheritance)
   for key, value in pairs(self.name) do
     names_inheritance.name[key] = util.clone(value)
   end
 else
   if state.name_override then
     names_inheritance.name = util.clone(state.name_override.name)
   else
     names_inheritance.name = util.clone(context.name_inheritance)
   end
 end

 if self.et_al then
   names_inheritance.et_al = util.clone(self.et_al)
 elseif state.name_override then
   names_inheritance.et_al = util.clone(state.name_override.et_al)
 else
   names_inheritance.et_al = EtAl:new()
 end

 if self.label then
   names_inheritance.label = util.clone(self.label)
 elseif state.name_override then
   names_inheritance.label = util.clone(state.name_override.label)
 end

 if context.cite then
   local position_level = context.cite.position or context.cite.position_level
   if position_level and position_level >= Position.Subsequent then
     if names_inheritance.name.et_al_subsequent_min then
       names_inheritance.name.et_al_min = names_inheritance.name.et_al_subsequent_min
     end
     if names_inheritance.name.et_al_subsequent_use_first then
       names_inheritance.name.et_al_use_first = names_inheritance.name.et_al_subsequent_use_first
     end
   end
 end

 local irs = {}
 local num_names = 0

 local index_by_variable = {}

 -- The names element may have an empty variable attribute.
 -- substitute_SubstituteOnlyOnceString.txt
 if names_inheritance.variable then
   for _, variable in ipairs(util.split(names_inheritance.variable)) do
     local name_ir = names_inheritance.name:build_ir(variable, names_inheritance.et_al, names_inheritance.label,
       engine, state, context)
     if name_ir and names_inheritance.name.form == "count" then
       num_names = num_names + name_ir.name_count
     end
     if name_ir and name_ir.group_var ~= GroupVar.Missing then
       table.insert(irs, name_ir)
       index_by_variable[variable] = #irs
     end
   end
 end

 -- editor & translator
 local editor_ir = irs[index_by_variable.editor]
 local translator_ir = irs[index_by_variable.translator]
 if editor_ir and translator_ir then
   local editor_name_ir
   local editor_label_ir
   local translator_name_ir
   local translator_label_ir
   for _, ir in ipairs(editor_ir.children) do
     if ir._type == "NameIr" then
       editor_name_ir = ir
     elseif ir._element_name == "label" then
       editor_label_ir = ir
     end
   end
   for _, ir in ipairs(translator_ir.children) do
     if ir._type == "NameIr" then
       translator_name_ir = ir
     elseif ir._element_name == "label" then
       translator_label_ir = ir
     end
   end

   if editor_name_ir.full_name_str == translator_name_ir.full_name_str then
     local names = context:get_variable("editor")
     local editor_translator_label_ir = names_inheritance.name:build_name_label(names_inheritance.label,
       "editortranslator", names, context)
     if editor_translator_label_ir then
       local first_index = index_by_variable.editor
       local second_index = index_by_variable.translator
       if first_index > second_index then
         local tmp = first_index
         first_index = second_index
         second_index = tmp
       end
       table.remove(irs, second_index)
       for i, ir in ipairs(irs[first_index].children) do
         if ir._element_name == "label" then
           irs[first_index].children[i] = editor_translator_label_ir
           break
         end
       end
     end
   end
 end

 if names_inheritance.name.form == "count" then
   if num_names > 0 then
     local ir = Rendered:new({PlainText:new(tostring(num_names))}, self)
     ir.name_count = num_names
     ir.group_var = GroupVar.Important
     ir = NameIr:new({ir}, self)
     ir.name_count = num_names
     ir.group_var = GroupVar.Important
     return ir
   end
 else
   if #irs > 0 then
     local ir = SeqIr:new(irs, self)
     ir.group_var = GroupVar.Important
     ir.delimiter = names_inheritance.delimiter
     ir.formatting = util.clone(names_inheritance.formatting)
     ir.affixes = util.clone(names_inheritance.affixes)
     ir.display = names_inheritance.display
     return ir
   end
 end

 if self.substitute then
   local new_state = util.clone(state)
   new_state.name_override = names_inheritance
   for _, substitution in ipairs(self.substitute.children) do
     local ir = substitution:build_ir(engine, new_state, context)
     if ir and (ir.group_var == GroupVar.Important or ir.group_var == GroupVar.Plain) then
       if not ir.person_name_irs or #ir.person_name_irs == 0 then
         -- In case of a <text variable="title"/> in <substitute>
         local name_count = ir.name_count
         ir = NameIr:new({ir}, self)
         ir.name_count = name_count  -- sort_AguStyle.txt
         ir.group_var = GroupVar.Important
       end
       return ir
     end
   end
 end

 local ir = Rendered:new({}, self)
 ir.group_var = GroupVar.Missing
 return ir

end


function Names:substitute_single_field(result, context)
 if not result then
   return nil
 end
 if context.build.first_rendered_names and #context.build.first_rendered_names == 0 then
   context.build.first_rendered_names[1] = result
 end
 result = self:substitute_names(result, context)
 return result
end

function Names:substitute_names(result, context)
 if not context.build.first_rendered_names then
   return result
 end
 local name_strings = {}
 local match_all

 if #context.build.first_rendered_names > 0 then
   match_all = true
 else
   match_all = false
 end
 for i, text in ipairs(context.build.first_rendered_names) do
   local str = text:render(context.engine.formatter, context)
   name_strings[i] = str
   if context.build.preceding_first_rendered_names and str ~= context.build.preceding_first_rendered_names[i] then
     match_all = false
   end
 end

 if context.build.preceding_first_rendered_names then
   local sub_str = context.options["subsequent-author-substitute"]
   local sub_rule = context.options["subsequent-author-substitute-rule"]

   if sub_rule == "complete-all" then
     if match_all then
       if sub_str == "" then
         result = nil
       else
         result.contents = {sub_str}
       end
     end

   elseif sub_rule == "complete-each" then
     -- In-place substitution
     if match_all then
       for _, text in ipairs(context.build.first_rendered_names) do
         text.contents = {sub_str}
       end
       -- FIXME: Resolve the undefined concat() method.
       result = self:concat(context.build.first_rendered_names, context)
     end

   elseif sub_rule == "partial-each" then
     for i, text in ipairs(context.build.first_rendered_names) do
       if name_strings[i] == context.build.preceding_first_rendered_names[i] then
         text.contents = {sub_str}
       else
         break
       end
     end
     result = self:concat(context.build.first_rendered_names, context)

   elseif sub_rule == "partial-first" then
     if name_strings[1] == context.build.preceding_first_rendered_names[1] then
       context.build.first_rendered_names[1].contents = {sub_str}
     end
     result = self:concat(context.build.first_rendered_names, context)
   end
 end

 if #context.build.first_rendered_names > 0 then
   context.build.first_rendered_names = nil
 end
 context.build.preceding_first_rendered_names = name_strings
 return result
end

-- [Name](https://docs.citationstyles.org/en/stable/specification.html#name)
function Name:new()
 local o = Element.new(self, "name")

 o.family = NamePart:new("family")
 o.given = NamePart:new("given")
 return o
end

function Name:from_node(node)
 local o = Name:new()
 o:set_attribute(node, "and")
 o:get_delimiter_attribute(node)
 o:set_attribute(node, "delimiter-precedes-et-al")
 o:set_attribute(node, "delimiter-precedes-last")
 o:set_number_attribute(node, "et-al-min")
 o:set_number_attribute(node, "et-al-use-first")
 o:set_number_attribute(node, "et-al-subsequent-min")
 o:set_number_attribute(node, "et-al-subsequent-use-first")
 o:set_bool_attribute(node, "et-al-use-last")
 o:set_attribute(node, "form")
 o:set_bool_attribute(node, "initialize")
 o:set_attribute(node, "initialize-with")
 o:set_attribute(node, "name-as-sort-order")
 o:set_attribute(node, "sort-separator")
 o:set_affixes_attributes(node)
 o:set_formatting_attributes(node)
 o:process_children_nodes(node)
 for _, child in ipairs(o.children) do
   if child.name == "family" then
     o.family = child
   elseif child.name == "given" then
     o.given = child
   end
 end
 if not o.family then
   o.family = NamePart:new()
   o.family.name = "family"
 end
 if not o.given then
   o.given = NamePart:new()
   o.family.name = "given"
 end
 return o
end


function Name:build_name_label(label, variable, names, context)
 local is_plural = (label.plural == "always" or (label.plural == "contextual" and #names > 1))
 local label_term = context.locale:get_simple_term(variable, label.form, is_plural)
 if not label_term or label_term == "" then
   return nil
 end
 local inlines = label:render_text_inlines(label_term, context)
 local label_ir = Rendered:new(inlines, label)
 return label_ir
end


function Name:build_ir(variable, et_al, label, engine, state, context)
 -- Returns NameIR
 local names
 if not state.suppressed[variable] then
   names = context:get_variable(variable)
 end
 if not names then
   return nil
 end

 if context.sort_key then
   self.delimiter = "   "
   self.name_as_sort_order = "all"
   if context.sort_key.names_min then
     self.et_al_min = context.sort_key.names_min
   end
   if context.sort_key.names_use_first then
     self.et_al_use_first = context.sort_key.names_use_first
   end
   if context.sort_key.names_use_last then
     self.et_al_use_last = context.sort_key.names_use_last
   end
   et_al = nil
   label = nil
 end

 local et_al_abbreviation = self.et_al_min and self.et_al_use_first and #names >= self.et_al_min and
     #names > self.et_al_use_first
 local use_last = et_al_abbreviation and self.et_al_use_last and self.et_al_use_first <= self.et_al_min - 2

 if self.form == "count" then
   local count
   if et_al_abbreviation then
     count = self.et_al_use_first
   else
     count = #names
   end
   local ir = Rendered:new({PlainText:new(tostring(count))}, {})
   ir.name_count = count
   ir.group_var = GroupVar.Important
   return ir
 end

 -- TODO: only build names as needed
 local full_name_irs = {}
 local full_name_str = ""
 for i, name_var in ipairs(names) do
   local person_name_ir = self:build_person_name_ir(name_var, i == 1, context)
   table.insert(full_name_irs, person_name_ir)

   local name_variants = person_name_ir.disam_variants
   if full_name_str ~= "" then
     full_name_str = full_name_str .. "     "
   end
   full_name_str = full_name_str .. name_variants[#name_variants]
 end

 local person_name_irs  -- TODO: rename to rendered_name_irs
 local hidden_name_irs
 if et_al_abbreviation then
   person_name_irs = util.slice(full_name_irs, 1, self.et_al_use_first)
   hidden_name_irs = util.slice(full_name_irs, self.et_al_use_first + 1, #full_name_irs)
   if use_last then
     table.insert(person_name_irs, full_name_irs[#full_name_irs])
     table.remove(hidden_name_irs, #hidden_name_irs)
   end
 else
   person_name_irs = util.slice(full_name_irs, 1, #full_name_irs)
   hidden_name_irs = {}
 end


 local and_term_ir
 if not context.sort_key then
   -- sort_WithAndInOneEntry.txt
   local and_term
   if self["and"] == "text" then
     and_term = context.locale:get_simple_term("and")
   elseif self["and"] == "symbol" then
     and_term = "&"
   end
   if and_term then
     and_term_ir = Rendered:new({PlainText:new(and_term .. " ")}, {})
   end
 end

 local et_al_ir
 if et_al and et_al_abbreviation and not use_last then
   et_al_ir = et_al:build_ir(engine, state, context)
 end

 local irs = self:join_person_name_irs(person_name_irs, and_term_ir, et_al_ir, use_last)

 local ir = NameIr:new(irs, self)

 ir.name_inheritance = self
 ir.name_variable = names
 ir.and_term_ir = and_term_ir
 ir.et_al_ir = et_al_ir
 ir.et_al_abbreviation = et_al_abbreviation
 ir.use_last = use_last

 ir.full_name_irs = full_name_irs
 ir.full_name_str = full_name_str
 ir.person_name_irs = person_name_irs
 ir.hidden_name_irs = hidden_name_irs

 -- etal_UseZeroFirst.txt: et-al-use-first="0"
 if #irs == 0 then
   ir.group_var = GroupVar.Missing
   return ir
 else
   ir.group_var = GroupVar.Important
 end

 irs = {ir}

 if label then
   local label_ir = self:build_name_label(label, variable, names, context);
   if label_ir then
     if label.after_name then
       table.insert(irs, label_ir)
     else
       table.insert(irs, 1, label_ir)
     end
   end
 end

 ir = SeqIr:new(irs, self)

 -- Suppress substituted name variable
 if state.name_override and not context.sort_key then
   state.suppressed[variable] = true
 end

 return ir
end

function Name:build_person_name_ir(name, is_first, context)
 local is_latin = util.has_romanesque_char(name.family)
 local is_inverted = (name.family and name.family ~= "" and is_latin and
   (self.name_as_sort_order == "all" or (self.name_as_sort_order == "first" and is_first)))

 local inlines = self:render_person_name(name, is_first, is_latin, is_inverted, context)
 local person_name_ir = PersonNameIr:new(inlines, self)

 -- discretionary_ExampleSeveralAuthorsWithIntext.txt
 person_name_ir.formatting = self.formatting
 person_name_ir.affixes = self.affixes

 person_name_ir.is_inverted = is_inverted

 local output_format = SortStringFormat:new()
 person_name_ir.name_output = output_format:output(inlines)
 person_name_ir.disam_variants_index = 1

 person_name_ir.disam_variants = {person_name_ir.name_output}
 person_name_ir.disam_inlines = {inlines}

 if context.area.disambiguate_add_givenname and not context.sort_key then
   local disam_name = util.clone(self)
   if disam_name.form == "short" then
     disam_name.form = "long"
     if disam_name.initialize and disam_name.initialize_with then
       local name_inlines = disam_name:render_person_name(name, is_first, is_latin, is_inverted, context)
       local disam_variant = output_format:output(name_inlines)
       local last_variant = person_name_ir.disam_variants[#person_name_ir.disam_variants]
       if disam_variant ~= last_variant then
         table.insert(person_name_ir.disam_variants, disam_variant)
         person_name_ir.disam_inlines[disam_variant] = name_inlines
       end
     end
   end

   local givenname_disambiguation_rule = context.area.givenname_disambiguation_rule
   local only_initials = (givenname_disambiguation_rule == "all-names-with-initials" or
     givenname_disambiguation_rule == "primary-name-with-initials")
   if disam_name.initialize and not only_initials then
     disam_name.initialize = false
     local name_inlines = disam_name:render_person_name(name, is_first, is_latin, is_inverted, context)
     local disam_variant = output_format:output(name_inlines)
     local last_variant = person_name_ir.disam_variants[#person_name_ir.disam_variants]
     if disam_variant ~= last_variant then
       table.insert(person_name_ir.disam_variants, disam_variant)
       person_name_ir.disam_inlines[disam_variant] = name_inlines
     end
   end

   context.sort_key = true
   local full_name_inlines = disam_name:render_person_name(name, is_first, is_latin, is_inverted, context)
   -- full_name is used for comparison in disambiguation
   person_name_ir.full_name = output_format:output(full_name_inlines)
   context.sort_key = false
 end

 return person_name_ir
end

function Name:render_person_name(name, is_first, is_latin, is_inverted, context)
 -- Return: inlines
 -- TODO
 local is_sort = context.sort_key
 local demote_ndp = (context.style.demote_non_dropping_particle == "display-and-sort" or
   (is_sort and context.style.demote_non_dropping_particle == "sort-only"))

 local name_part_tokens = self:get_display_order(name, self.form, is_latin, is_sort, is_inverted, demote_ndp)

 local inlines = {}
 for i, token in ipairs(name_part_tokens) do
   if token == "family" or token == "ndp-family" or token == "dp-ndp-family-suffix" then
     local family_inlines = self:render_family(name, token, context)
     util.extend(inlines, family_inlines)

   elseif token == "given" or token == "given-dp" or token == "given-dp-ndp" then
     local given_inlines = self:render_given(name, token, context)
     util.extend(inlines, given_inlines)

   elseif token == "dp" or token == "dp-ndp" then
     local particle_inlines = self:render_particle(name, token, context)
     util.extend(inlines, particle_inlines)

   elseif token == "suffix" then
     local text = name.suffix or ""
     util.extend(inlines, InlineElement:parse(text, context))

   elseif token == "literal" then
     local literal_inlines = self.family:format_text_case(name.literal, context)
     util.extend(inlines, literal_inlines)

   elseif token == "space" then
     table.insert(inlines, PlainText:new(" "))

   elseif token == "wide-space" then
     table.insert(inlines, PlainText:new("   "))

   elseif token == "sort-separator" then
     table.insert(inlines, PlainText:new(self.sort_separator))
   end
 end
 return inlines
end

-- Name-part Order
-- https://docs.citationstyles.org/en/stable/specification.html#name-part-order
function Name:get_display_order(name, form, is_latin, is_sort, is_inverted, demote_ndp)
 if is_sort then
   if not name.family then
     -- The literal is compared with the literal
     if self.form == "long" then
       return {"literal", "wide-space", "wide-space", "wide-space"}
     else
       return {"literal", "wide-space"}
     end
   end

   if not is_latin then
     if form == "long" and name.given then
       return {"family", "given"}
     else
       return {"family"}
     end
   end

   if self.form == "long" then
     if demote_ndp then
       return {"family", "wide-space", "dp-ndp", "wide-space", "given", "wide-space", "suffix"}
     else
       return {"ndp-family", "wide-space", "dp", "wide-space", "given", "wide-space", "suffix"}
     end
   else
     if demote_ndp then
       return {"family", "wide-space", "dp-ndp"}
     else
       return {"ndp-family", "wide-space", "dp"}
     end
   end
 end

 if not name.family then
   if name.literal then
     return {"literal"}
   else
     util.error("Invalid name")
   end
 end

 if not is_latin then
   if form == "long" and name.given then
     return {"family", "given"}
   else
     return {"family"}
   end
 end

 if form == "short" then
   return {"ndp-family"}
 end

 local ndp = name["non-dropping-particle"]
 local dp = name["dropping-particle"]

 local name_part_tokens = {"family"}
 if name.given then
   if is_inverted then
     if demote_ndp then
       name_part_tokens = {"family", "sort-separator", "given-dp-ndp"}
     else
       name_part_tokens = {"ndp-family", "sort-separator", "given-dp"}
     end
   else
     name_part_tokens = {"given", "space", "dp-ndp-family-suffix"}
   end
 else
   if is_inverted then
     if demote_ndp then
       if ndp or dp then
         name_part_tokens = {"family", "sort-separator", "dp-ndp"}
       else
         name_part_tokens = {"family"}
       end
     else
       name_part_tokens = {"ndp-family"}
     end
   else
     name_part_tokens = {"dp-ndp-family-suffix"}
   end
 end

 if name.suffix and is_inverted then
   if is_inverted or name["comma-suffix"] then
     table.insert(name_part_tokens, "sort-separator")
     table.insert(name_part_tokens, "suffix")
   elseif string.match(name.suffix, "^%p") then
     table.insert(name_part_tokens, "sort-separator")
     table.insert(name_part_tokens, "suffix")
   else
     table.insert(name_part_tokens, "space")
     table.insert(name_part_tokens, "suffix")
   end
 end

 return name_part_tokens
end

function Name:render_family(name, token, context)
 local inlines = {}
 local name_part

 if token == "dp-ndp-family-suffix" then
   local dp_part = name["dropping-particle"]
   if dp_part then
     name_part = dp_part
     local dp_inlines = self.given:format_text_case(dp_part, context)
     util.extend(inlines, dp_inlines)
   end
 end

 if token == "dp-ndp-family-suffix" or token == "ndp-family" then
   local ndp_part = name["non-dropping-particle"]
   if ndp_part then
     if context.sort_key then
       ndp_part = self:format_sort_particle(ndp_part)
     end
     if #inlines > 0 then
       table.insert(inlines, PlainText:new(" "))
     end
     name_part = ndp_part
     local ndp_inlines = self.family:format_text_case(ndp_part, context)
     util.extend(inlines, ndp_inlines)
   end
 end

 local family = name.family
 if context.sort_key then
   -- Remove brackets for sorting: sort_NameVariable.txt
   family = string.gsub(family, "[%[%]]", "")
 end

 local family_inlines = self.family:format_text_case(family, context)
 if #inlines > 0 then
   if not string.match(name_part, "^%l'$") and
       not string.match(name_part, "^%l’$") and
       not util.endswith(name_part, "-") then
     table.insert(inlines, PlainText:new(" "))
   end
 end
 util.extend(inlines, family_inlines)

 if token == "dp-ndp-family-suffix" then
   local suffix_part = name.suffix
   if suffix_part then
     if name["comma-suffix"] or util.startswith(suffix_part, "!") then
       -- force use sort-separator exclamation prefix: magic_NameSuffixWithComma.txt
       -- "! Jr." => "Jr."
       table.insert(inlines, PlainText:new(self.sort_separator))
       suffix_part = string.gsub(suffix_part, "^%p%s*", "")
     else
       table.insert(inlines, PlainText:new(" "))
     end
     table.insert(inlines, PlainText:new(suffix_part))
   end
 end

 inlines = self.family:affixed(inlines)
 return inlines
end

function Name:render_given(name, token, context)
 local given = name.given

 if context.sort_key then
   -- The empty given name is needed for evaluate the sort key.
   if not given then
     return {PlainText:new("")}
   end
   -- Remove brackets for sorting: sort_NameVariable.txt
   given = string.gsub(given, "[%[%]]", "")
 end

 if self.initialize_with then
   given = self:initialize_name(given, self.initialize_with, context.style.initialize_with_hyphen)
 end
 local inlines = self.given:format_text_case(given, context)

 if token == "given-dp" or token == "given-dp-ndp" then
   local name_part = name["dropping-particle"]
   if name_part then
     table.insert(inlines, PlainText:new(" "))
     local dp_inlines = self.given:format_text_case(name_part, context)
     util.extend(inlines, dp_inlines)
   end
 end

 if token == "given-dp-ndp" then
   local name_part = name["non-dropping-particle"]
   if name_part then
     table.insert(inlines, PlainText:new(" "))
     local ndp_inlines = self.family:format_text_case(name_part, context)
     util.extend(inlines, ndp_inlines)
   end
 end

 inlines = self.given:affixed(inlines)
 return inlines
end

-- sort_LeadingApostropheOnNameParticle.txt
-- "’t " => "t"
function Name:format_sort_particle(particle)
 particle = string.gsub(particle, "^'", "")
 particle = string.gsub(particle, "^’", "")
 return particle
end

function Name:render_particle(name, token, context)
 local inlines = {}

 local dp_part = name["dropping-particle"]
 if dp_part then
   dp_part = self:format_sort_particle(dp_part)
   local dp_inlines = self.given:format_text_case(dp_part, context)
   util.extend(inlines, dp_inlines)
 end

 if token == "dp-ndp" then
   local ndp_part = name["non-dropping-particle"]
   if ndp_part then
     if #inlines > 0 then
       table.insert(inlines, PlainText:new(" "))
     end
     ndp_part = self:format_sort_particle(ndp_part)
     local ndp_inlines = self.family:format_text_case(ndp_part, context)
     util.extend(inlines, ndp_inlines)
   end
 end

 return inlines
end

function Name:_check_delimiter(delimiter_attribute, num_first_names, inverted)
 -- `delimiter-precedes-et-al` and `delimiter-precedes-last`
 if delimiter_attribute == "always" then
   return true
 elseif delimiter_attribute == "never" then
   return false
 elseif delimiter_attribute == "contextual" then
   if num_first_names > 1 then
     return true
   else
     return false
   end
 elseif delimiter_attribute == "after-inverted-name" then
   if inverted then
     return true
   else
     return false
   end
 end
 return false
end

-- TODO: initialize name with markups
--   name_InTextMarkupInitialize.txt
--   name_InTextMarkupNormalizeInitials.txt
function Name:initialize_name(given, with, initialize_with_hyphen)
 if not given or given == "" then
   return ""
 end

 if initialize_with_hyphen == false then
   given = string.gsub(given, "-", " ")
 end

 -- Split the given name to name_list (e.g., {"John", "M." "E"})
 -- Compound names are splitted too but are marked in punc_list.
 local name_list = {}
 local punct_list = {}
 local last_position = 1
 for name, pos in string.gmatch(given, "([^-.%s]+[-.%s]+)()") do
   table.insert(name_list, string.match(name, "^[^-%s]+"))
   if string.match(name, "%-") then
     table.insert(punct_list, "-")
   else
     table.insert(punct_list, "")
   end
   last_position = pos
 end
 if last_position <= #given then
   table.insert(name_list, util.strip(string.sub(given, last_position)))
   table.insert(punct_list, "")
 end

 for i, name in ipairs(name_list) do
   local is_particle = false
   local is_abbreviation = false

   local first_letter = utf8.char(utf8.codepoint(name))
   if unicode.islower(first_letter) then
     is_particle = true
   elseif #name == 1 then
     is_abbreviation = true
   else
     local abbreviation = string.match(name, "^([^.]+)%.$")
     if abbreviation then
       is_abbreviation = true
       name = abbreviation
     end
   end

   if is_particle then
     name_list[i] = name .. " "
     if i > 1 and not string.match(name_list[i - 1], "%s$") then
       name_list[i - 1] = name_list[i - 1] .. " "
     end
   elseif is_abbreviation then
     name_list[i] = name .. with
   else
     if self.initialize then
       if unicode.isupper(name) then
         name = first_letter
       else
         -- Long abbreviation: "TSerendorjiin" -> "Ts."
         local abbreviation = ""
         for _, c in utf8.codes(name) do
           local char = utf8.char(c)
           local lower = unicode.lower(char)
           if lower == char then
             break
           end
           if abbreviation == "" then
             abbreviation = char
           else
             abbreviation = abbreviation .. lower
           end
         end
         name = abbreviation
       end
       name_list[i] = name .. with
     else
       name_list[i] = name .. " "
     end
   end

   -- Handle the compound names
   if i > 1 and punct_list[i - 1] == "-" then
     if is_particle then  -- special case "Guo-ping"
       name_list[i] = ""
     else
       name_list[i - 1] = util.rstrip(name_list[i - 1])
       name_list[i] = "-" .. name_list[i]
     end
   end
 end

 local res = util.concat(name_list, "")
 res = util.strip(res)
 return res

end

function Name:join_person_name_irs(rendered_name_irs, and_term_ir, et_al_ir, use_last)
 local first_items = rendered_name_irs
 local last_item
 if et_al_ir then
   first_items = rendered_name_irs
   last_item = et_al_ir
 elseif #rendered_name_irs > 1 then
   first_items = util.slice(rendered_name_irs, 1, #rendered_name_irs - 1)
   last_item = rendered_name_irs[#rendered_name_irs]
 end

 local irs = {}

 for i, person_name_ir in ipairs(first_items) do
   if i > 1 then
     table.insert(irs, Rendered:new({PlainText:new(self.delimiter)}, self))
   end
   table.insert(irs, person_name_ir)
 end

 if last_item then
   if use_last then
     local delimiter = self.delimiter .. util.unicode["horizontal ellipsis"] .. " "
     table.insert(irs, Rendered:new({PlainText:new(delimiter)}, self))
     table.insert(irs, last_item)
   elseif et_al_ir then
     if #first_items > 0 then
       local inverted = first_items[#first_items].is_inverted
       local use_delimiter = self:_check_delimiter(self.delimiter_precedes_et_al, #first_items, inverted)
       if use_delimiter then
         table.insert(irs, Rendered:new({PlainText:new(self.delimiter)}, self))
       elseif not et_al_ir.starts_with_cjk then
         -- name_EtAlWithCombined.txt
         table.insert(irs, Rendered:new({PlainText:new(" ")}, self))
       end
       table.insert(irs, last_item)
     end
   else
     local inverted = first_items[#first_items].is_inverted
     local use_delimiter = self:_check_delimiter(self.delimiter_precedes_last, #first_items, inverted)
     if use_delimiter or not and_term_ir then
       table.insert(irs, Rendered:new({PlainText:new(self.delimiter)}, self))
     else
       table.insert(irs, Rendered:new({PlainText:new(" ")}, self))
     end
     if and_term_ir and not et_al_ir then
       table.insert(irs, and_term_ir)
     end
     table.insert(irs, last_item)
   end
 end

 return irs
end

-- For use in disambiguate-add-names
function Name:expand_one_name(name_ir)
 local rendered_name_irs = name_ir.person_name_irs
 local hidden_name_irs = name_ir.hidden_name_irs
 if #hidden_name_irs == 0 then
   return nil
 end
 local person_name_ir_to_add = hidden_name_irs[1]
 if name_ir.use_last then
   table.insert(rendered_name_irs, #rendered_name_irs, person_name_ir_to_add)
 else
   table.insert(rendered_name_irs, person_name_ir_to_add)
 end
 table.remove(hidden_name_irs, 1)
 if #hidden_name_irs == 0 then
   if name_ir.et_al_abbreviation then
     name_ir.et_al_abbreviation = false
   end
   if name_ir.use_last then
     name_ir.use_last = false
   end
 end

 local and_term_ir = name_ir.and_term_ir
 local et_al_ir
 if name_ir.et_al_abbreviation then
   et_al_ir = name_ir.et_al_ir
 end
 local use_last = name_ir.use_last

 name_ir.children = self:join_person_name_irs(rendered_name_irs, and_term_ir, et_al_ir, use_last)
 return person_name_ir_to_add
end


-- [Name-part](https://docs.citationstyles.org/en/stable/specification.html#name-part-formatting)
function NamePart:new(name)
 local o = Element.new(self)
 o.name = name
 return o
end

function NamePart:from_node(node)
 local o = NamePart:new()
 o:set_attribute(node, "name")
 o:set_formatting_attributes(node)
 o:set_text_case_attribute(node)
 o:set_affixes_attributes(node)
 return o
end

function NamePart:format_text_case(text, context)
 local output_format = context.format
 local inlines = InlineElement:parse(text, context)
 local is_english = context:is_english()
 -- if not output_format then
 --   print(debug.traceback())
 --   assert(output_format)
 -- end
 output_format:apply_text_case(inlines, self.text_case, is_english)

 inlines = output_format:with_format(inlines, self.formatting)
 return inlines
end

function NamePart:affixed(inlines)
 if self.affixes then
   if self.affixes.prefix then
     table.insert(inlines, 1, PlainText:new(self.affixes.prefix))
   end
   if self.affixes.suffix then
     table.insert(inlines, PlainText:new(self.affixes.suffix))
   end
 end
 return inlines
end


-- [Et-al](https://docs.citationstyles.org/en/stable/specification.html#et-al)
EtAl.term = "et-al"

function EtAl:from_node(node)
 local o = EtAl:new()
 o:set_attribute(node, "term")
 o:set_formatting_attributes(node)
 return o
end

function EtAl:build_ir(engine, state, context)
 local term = context.locale:get_simple_term(self.term)
 if not term then
   return term
 end
 local inlines = InlineElement:parse(term, context)
 if #inlines == 0 then
   return nil
 end

 inlines = context.format:with_format(inlines, self.formatting)

 local ir = Rendered:new(inlines, self)

 if util.is_cjk_char(utf8.codepoint(term, 1)) then
   ir.starts_with_cjk = true
 end

 return ir
end

function Substitute:from_node(node)
 local o = Substitute:new()
 o:process_children_nodes(node)
 return o
end


names_module.Names = Names
names_module.Name = Name
names_module.NamePart = NamePart
names_module.EtAl = EtAl
names_module.Substitute = Substitute

return names_module