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

local context = {}

local unicode
local LocalizedQuotes
local util

local using_luatex, _ = pcall(require, "kpse")
if using_luatex then
 unicode = require("citeproc-unicode")
 LocalizedQuotes = require("citeproc-output").LocalizedQuotes
 util = require("citeproc-util")
else
 unicode = require("citeproc.unicode")
 LocalizedQuotes = require("citeproc.output").LocalizedQuotes
 util = require("citeproc.util")
end


---@class Context
---@field reference ItemData?
---@field format OutputFormat
---@field cite_id string?
---@field engine CiteProc
---@field style any Style
---@field lang LanguageCode
---@field locale Locale?
---@field name_citation any
---@field names_delimiter any
---@field position any
---@field disamb_pass any
---@field cite CitationItem?
---@field bib_number integer?
---@field in_bibliography boolean
---@field sort_key any
---@field year_suffix any
local Context = {
 reference = nil,
 format = nil,
 cite_id = nil,
 style = nil,
 locale = nil,
 name_citation = nil,
 names_delimiter = nil,

 position = nil,

 disamb_pass = nil,

 cite = nil,
 bib_number = nil,

 in_bibliography = false,
 sort_key = nil,

 year_suffix = nil,
}

function Context:new()
 local o = {
   lang = "en-US",
   in_bibliography = false,
 }
 setmetatable(o, self)
 self.__index = self
 return o
end

function Context:get_variable(name, form)
 local variable_type = util.variable_types[name]
 if variable_type == "number" then
   return self:get_number(name)
   -- elseif variable_type == "date" then
   --   return self:get_date(name)
 elseif variable_type == "name" then
   return self:get_name(name)
 else
   return self:get_ordinary(name, form)
 end
end

---@param name string
---@return string | number?
function Context:get_number(name)
 if name == "locator" then
   if self.cite then
     return self.cite.locator
   end
 elseif name == "citation-number" then
   -- return self.bib_number
   return self.reference["citation-number"]
 elseif name == "first-reference-note-number" then
   if self.cite then
     return self.cite["first-reference-note-number"]
   end
 elseif name == "page-first" then
   return self.page_first(self.reference.page)
 else
   return self.reference[name]
 end
 return nil
end

function Context:get_ordinary(name, form)
 local res = nil
 local variable_name = name
 if form and form ~= "long" then
   variable_name = variable_name .. "-" .. form
 end

 if variable_name == "locator" or variable_name == "label" and self.cite then
   res = self.cite[variable_name]
 elseif variable_name == "citation-label" then
   res = self.reference["citation-label"]
   if not res then
     res = util.get_citation_label(self.reference)
     self.reference["citation-label"] = res
   end
 else
   res = self.reference[variable_name]
 end
 if res then
   return res
 end

 if variable_name == "container-title-short" then
   res = self.reference["journalAbbreviation"]
   if res then
     return res
   end
 end

 if form then
   res = self.reference[name]
   if res then
     return res
   end
 end

 -- if name == "title-short" or name == "container-title-short" then
 --   variable_name = string.gsub(name, "%-short$", "")
 --   res = self.reference[variable_name]
 -- end

 return res
end

-- TODO: optimize: process only once
-- TODO: organize the name parsing code
function Context:get_name(variable_name)
 local names = self.reference[variable_name]
 if names then
   for _, name in ipairs(names) do
     if name.family == "" then
       name.family = nil
     end
     if name.given == "" then
       name.given = nil
     end
     if name.given and not name.family then
       name.family = name.given
       name.given = nil
     end
     self:parse_name_suffix(name)
     self:split_ndp_family(name)
     -- self:split_given_ndp(name)
     self:split_given_dp(name)
   end
 end
 return names
end

function Context:parse_name_suffix(name)
 if not name.suffix and name.family and string.match(name.family, ",") then
   local words = util.split(name.family, ",%s*")
   name.suffix = words[#words]
   name.family = table.concat(util.slice(words, 1, -2), ", ")
 end
 if not name.suffix and name.given and string.match(name.given, ",") then
   -- Split name suffix: magic_NameSuffixNoComma.txt
   -- "John, III" => given: "John", suffix: "III"
   local words = util.split(name.given, ",%s*")
   name.suffix = words[#words]
   name.given = table.concat(util.slice(words, 1, -2), ", ")
 end
end

function Context:split_ndp_family(name)
 if name["non-dropping-particle"] or not name.family then
   return
 end
 if util.startswith(name.family, '"') and util.endswith(name.family, '"') then
   -- Stop parsing family name if surrounded by quotation marks
   -- bugreports_parseName.txt
   name.family = string.gsub(name.family, '"', "")
   return
 end
 local ndp_parts = {}
 local family_parts = {}
 local parts = util.split(name.family)
 for i, part in ipairs(parts) do
   local ndp, family
   -- d'Aubignac
   ndp, family = string.match(part, "^(%l')(.+)$")
   if ndp and family then
     table.insert(ndp_parts, ndp)
     parts[i] = family
   else
     ndp, family = string.match(part, "^(%l’)(.+)$")
     if ndp and family then
       table.insert(ndp_parts, ndp)
       parts[i] = family
     else
       -- al-Aswānī
       ndp, family = string.match(part, "^(%l+%-)(.+)$")
       if ndp and family then
         table.insert(ndp_parts, ndp)
         parts[i] = family
       elseif i < #parts and unicode.islower(part) then
         table.insert(ndp_parts, part)
       end
     end
   end
   if ndp or i == #parts then
     for j = i, #parts do
       table.insert(family_parts, parts[j])
     end
     break
   end
   if not unicode.islower(part) then
     for j = i, #parts do
       table.insert(family_parts, parts[j])
     end
     break
   end
 end
 if #ndp_parts > 0 then
   name["non-dropping-particle"] = table.concat(ndp_parts, " ")
   name.family = table.concat(family_parts, " ")
 end
end

function Context:split_given_dp(name)
 if name["dropping-particle"] or not name.given then
   return
 end
 local dp_parts = {}
 local given_parts = {}
 local parts = util.split(name.given)
 for i = #parts, 1, -1 do
   local part = parts[i]
   if i == 1 or not unicode.islower(part) then
     for j = 1, i do
       table.insert(given_parts, parts[j])
     end
     break
   end
   -- name_ParsedDroppingParticleWithApostrophe.txt
   -- given: "François Hédelin d'" =>
   -- given: "François Hédelin", dropping-particle: "d'"
   if string.match(part, "^%l+'?$") or string.match(part, "^%l+’$") then
     table.insert(dp_parts, 1, part)
   end
 end
 if #dp_parts > 0 then
   name["dropping-particle"] = table.concat(dp_parts, " ")
   name.given = table.concat(given_parts, " ")
 end
end

-- function Context:split_given_ndp(name)
--   if name["non-dropping-particle"] or not name.given then
--     return
--   end

--   if not (string.match(name.given, "%l'$") or string.match(name.given, "%l’$")) then
--     return
--   end

--   local words = util.split(name.given)
--   if #words < 2 then
--     return
--   end
--   local last_word = words[#words]
--   if util.endswith(last_word, "'") or util.endswith(last_word, util.unicode["apostrophe"]) then
--     name["non-dropping-particle"] = last_word
--     name.given = table.concat(util.slice(words, 1, -2), " ")
--   end
-- end

function Context:get_localized_date(form)
 return self.locale.dates[form]
end

function Context:get_macro(name)
 local res = self.style.macros[name]
 if not res then
   util.error(string.format("Undefined macro '%s'", name))
 end
 return res
end

function Context:get_simple_term(name, form, plural)
 -- assert(self.locale)
 return self.locale:get_simple_term(name, form, plural)
end

---@return LocalizedQuotes
function Context:get_localized_quotes()
 return LocalizedQuotes:new(
   self:get_simple_term("open-quote"),
   self:get_simple_term("close-quote"),
   self:get_simple_term("open-inner-quote"),
   self:get_simple_term("close-inner-quote"),
   self.locale.style_options.punctuation_in_quote
 )
end

---@param page string|number
---@return string?
function Context.page_first(page)
 if not page then
   return nil
 end
 page = tostring(page)
 local page_first = util.split(page, "%s*[&,-]%s*")[1]
 return util.split(page_first, util.unicode["en dash"])[1]
end

-- https://docs.citationstyles.org/en/stable/specification.html#non-english-items
function Context:is_english()
 local language = self:get_variable("language")
 if util.startswith(self.engine.lang, "en") then
   if not language or util.startswith(language, "en") then
     return true
   else
     return false
   end
 else
   if language and util.startswith(language, "en") then
     return true
   else
     return false
   end
 end
end


---@class IrState
---@field macro_stack string[]
---@field suppressed {[string]: boolean}
local IrState = {}

function IrState:new(style, cite_id, cite, reference)
 local o = {
   macro_stack = {},
   suppressed = {},
 }
 setmetatable(o, self)
 self.__index = self
 return o
end

function IrState:push_macro(macro_name)
 for _, name in ipairs(macro_name) do
   if name == macro_name then
     util.error(string.format("Recursive macro '%s'.", macro_name))
   end
   table.insert(self.macro_stack, macro_name)
 end
end

function IrState:pop_macro(macro_name)
 table.remove(self.macro_stack)
end


context.Context = Context
context.IrState = IrState

return context