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

local context = {}

local unicode
local LocalizedQuotes
local util

if kpse 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 any
---@field cite_id string?
---@field style any Style
---@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 = {
    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
--   util.debug(name)
-- 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)
  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


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
