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

local style_module = {}

local dom = require("luaxml-domobject")

local Element = require("citeproc-element").Element
local IrNode = require("citeproc-ir-node").IrNode
local Rendered = require("citeproc-ir-node").Rendered
local SeqIr = require("citeproc-ir-node").SeqIr
local PlainText = require("citeproc-output").PlainText
local DisamStringFormat = require("citeproc-output").DisamStringFormat
local util = require("citeproc-util")


local Style = Element:derive("style")

function Style:new()
  local o = {
    children = {},
    macros = {},
    locales = {},
    initialize_with_hyphen = true,
    demote_non_dropping_particle = "display-and-sort",
  }
  setmetatable(o, self)
  self.__index = self
  return o
end

function Style:parse(xml_str)
  local csl_xml = dom.parse(xml_str)
  if not csl_xml then
    error("Failed to parse CSL style.")
  end
  local style_node = csl_xml:get_path("style")[1]
  if not csl_xml then
    error('Element "style" not found.')
  end
  return Style:from_node(style_node)
end

local function make_name_inheritance(name, node)
  name:set_attribute(node, "and")
  name:set_attribute(node, "delimiter-precedes-et-al")
  name:set_attribute(node, "delimiter-precedes-last")
  name:set_number_attribute(node, "et-al-min")
  name:set_number_attribute(node, "et-al-use-first")
  name:set_number_attribute(node, "et-al-subsequent-min")
  name:set_number_attribute(node, "et-al-subsequent-use-first")
  name:set_bool_attribute(node, "et-al-use-last")
  name:set_bool_attribute(node, "initialize")
  name:set_attribute(node, "initialize-with")
  name:set_attribute(node, "name-as-sort-order")
  name:set_attribute(node, "sort-separator")
  local delimiter = node:get_attribute("name-delimiter")
  if delimiter then
    name.delimiter = delimiter
  end
  local form = node:get_attribute("name-form")
  if form then
    name.form = form
  end
  local names_delimiter = node:get_attribute("names-delimiter")
  if names_delimiter then
    name.names_delimiter = names_delimiter
  end
end

function Style:from_node(node)
  local o = Style:new()

  o:set_attribute(node, "class")
  o:set_attribute(node, "default-locale")
  o:set_attribute(node, "version")

  -- Global Options
  o.initialize_with_hyphen = true
  o:set_bool_attribute(node, "initialize-with-hyphen")
  o:set_attribute(node, "page-range-format")
  o:set_attribute(node, "demote-non-dropping-particle")

  -- Inheritable Name Options
  -- https://docs.citationstyles.org/en/stable/specification.html#inheritable-name-options
  o.name_inheritance = require("citeproc-node-names").Name:new()
  make_name_inheritance(o.name_inheritance, node)

  if o.page_range_format == "chicago" then
    if o.version < "1.1" then
      o.page_range_format = "chicago-15"
    else
      o.page_range_format = "chicago-16"
    end
  end

  o.macros = {}
  o.locales = {}

  o.children = {}
  o:process_children_nodes(node)

  for _, child in ipairs(o.children) do
    local element_name = child.element_name
    if element_name == "info" then
      o.info = child
    elseif element_name == "citation" then
      o.citation = child
    elseif element_name == "bibliography" then
      o.bibliography = child
    elseif element_name == "macro" then
      o.macros[child.name] = child
    elseif element_name == "locale" then
      local xml_lang = child.xml_lang or "@generic"
      o.locales[xml_lang] = child
    end
  end

  return o
end


Style._default_options = {
  ["initialize-with-hyphen"] = true,
  ["page-range-format"] = nil,
  ["demote-non-dropping-particle"] = "display-and-sort",
}

function Style:set_lang(lang, force_lang)
  local default_locale = self:get_attribute("default-locale")
  if lang then
    if default_locale and not force_lang then
      self.lang = default_locale
    end
  else
    self.lang = default_locale or "en-US"
  end
end


local Info = Element:derive("info")


function Info:from_node(node)
  local o = Info:new()

  -- o.authors = nil
  -- o.contributors = nil
  o.categories = {}
  o.id = nil
  -- o.issn = nil
  -- o.eissn = nil
  -- o.issnl = nil
  o.links = {
    independent_parent = nil,
  }
  -- o.published = nil
  -- o.rights = nil
  -- o.summary = nil
  o.title = nil
  -- o.title_short = nil
  o.updated = nil

  for _, child in ipairs(node:get_children()) do
    if child:is_element() then
      local element_name = child:get_element_name()
      if element_name == "category" then
        local citation_format = child:get_attribute("citation-format")
        if citation_format then
          o.categories.citation_format = citation_format
        end

      elseif element_name == "id" then
        o.id = child:get_text()

      elseif element_name == "link" then
        local href = child:get_attribute("href")
        local rel = child:get_attribute("rel")
        if href and rel == "independent-parent" then
          o.links.independent_parent = href
        end

      elseif element_name == "title" then
        o.title = child:get_text()

      elseif element_name == "updated" then
        o.updated = child:get_text()

      end
    end
  end

  return o
end


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 citaion > layout.
  cite_group_delimiter = ", ",
  near_note_distance = 5,
})

function Citation:from_node(node, style)

  local o = Citation:new()
  o.children = {}

  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
      o.layout = child
    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 = require("citeproc-node-names").Name:new()
  for key, value in pairs(style.name_inheritance) do
    if value ~= nil then
      name_inheritance[key] = value
    end
  end
  make_name_inheritance(name_inheritance, node)
  o.name_inheritance = name_inheritance

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

  return o
end

function Citation:build_ir(engine, state, context)
  if not self.layout then
    util.error("Missing citation layout.")
  end
  return self.layout:build_ir(engine, state, context)
end


local Bibliography = Element:derive("bibliography", {
  hanging_indent = false,
  line_spacing = 1,
  entry_spacing = 1,
  subsequent_author_substitute_rule = "complete-all"
})

function Bibliography:from_node(node, style)
  local o = Bibliography:new()
  o.children = {}

  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
      o.layout = child
    elseif element_name == "sort" then
      o.sort = child
    end
  end

  -- Whitespace
  o:set_bool_attribute(node, "hanging-indent")
  o:set_attribute(node, "second-field-align")
  o:set_number_attribute(node, "line-spacing")
  o:set_number_attribute(node, "entry-spacing")

  -- Reference Grouping
  o:set_attribute(node, "subsequent-author-substitute")
  o:set_attribute(node, "subsequent-author-substitute-rule")

  local name_inheritance = require("citeproc-node-names").Name:new()
  for key, value in pairs(style.name_inheritance) do
    if value ~= nil then
      name_inheritance[key] = value
    end
  end
  make_name_inheritance(name_inheritance, node)
  o.name_inheritance = name_inheritance

  return o
end

function Bibliography:build_ir(engine, state, context)
  if not self.layout then
    util.error("Missing bibliography layout.")
  end
  local ir = self.layout:build_ir(engine, state, context)
  -- util.debug(ir)
  if self.second_field_align == "flush" and #ir.children >= 2 then
    ir.children[1].display = "left-margin"
    local right_inline_ir = SeqIr:new(util.slice(ir.children, 2), self)
    right_inline_ir.display = "right-inline"
    if ir.affixes then
      right_inline_ir.affixes = ir.affixes
      right_inline_ir.formatting = ir.formatting
      ir.affixes = nil
      ir.formatting = nil
    end
    ir.children = {ir.children[1], right_inline_ir}
  end

  if self.subsequent_author_substitute then
    self:substitute_subsequent_authors(engine, ir)
  end

  if not ir then
    ir = Rendered:new(PlainText:new("[CSL STYLE ERROR: reference with no printed form.]"), self)
  end
  return ir
end

function Bibliography:substitute_subsequent_authors(engine, ir)
  ir.first_name_ir = self:find_first_name_ir(ir)  -- should be a SeqIr wiht _element = "names"
  if not ir.first_name_ir then
    engine.previous_bib_names_ir = nil
    return
  end
  if self.subsequent_author_substitute_rule == "complete-all" then
    self:substitute_subsequent_authors_complete_all(engine, ir)
  elseif self.subsequent_author_substitute_rule == "complete-each" then
    self:substitute_subsequent_authors_complete_each(engine, ir)
  elseif self.subsequent_author_substitute_rule == "partial-each" then
    self:substitute_subsequent_authors_partial_each(engine, ir)
  elseif self.subsequent_author_substitute_rule == "partial-first" then
    self:substitute_subsequent_authors_partial_first(engine, ir)
  end
  engine.previous_bib_names_ir = ir.first_name_ir
end

function Bibliography:find_first_name_ir(ir)
  if ir._type == "NameIr" then
    return ir
  elseif ir.children then
    for _, child_ir in ipairs(ir.children) do
      local first_name_ir = self:find_first_name_ir(child_ir)
      if first_name_ir then
        return first_name_ir
      end
    end
  end
  return nil
end

function Bibliography:substitute_subsequent_authors_complete_all(engine, ir)
  local bib_names_str = ""

  if #ir.first_name_ir.person_name_irs > 0 then
    for _, person_name_ir in ipairs(ir.first_name_ir.person_name_irs) do
      if bib_names_str ~= "" then
        bib_names_str = bib_names_str .. "     "
      end
      local name_variants = person_name_ir.disam_variants
      bib_names_str = bib_names_str .. name_variants[#name_variants]
    end
  else
    -- In case of a <text variable="title"/> in <substitute>
    local disam_format = DisamStringFormat:new()
    local inlines = ir.first_name_ir:flatten(disam_format)
    bib_names_str = disam_format:output(inlines)
  end
  ir.first_name_ir.bib_names_str = bib_names_str

  if engine.previous_bib_names_ir and
      engine.previous_bib_names_ir.bib_names_str == bib_names_str then
    local text = self.subsequent_author_substitute
    if text == "" then
      ir.first_name_ir.children = {}
      ir.first_name_ir.group_var = "missing"
    else
      -- the output of label is not substituted
      -- util.debug(ir.first_name_ir)
      ir.first_name_ir.children = {Rendered:new({PlainText:new(text)}, self)}
    end
  end
end

function Bibliography:substitute_subsequent_authors_complete_each(engine, ir)
  local bib_names_str = ""

  if #ir.first_name_ir.person_name_irs > 0 then
    for _, person_name_ir in ipairs(ir.first_name_ir.person_name_irs) do
      if bib_names_str ~= "" then
        bib_names_str = bib_names_str .. "     "
      end
      local name_variants = person_name_ir.disam_variants
      bib_names_str = bib_names_str .. name_variants[#name_variants]
    end
  else
    -- In case of a <text variable="title"/> in <substitute>
    local disam_format = DisamStringFormat:new()
    local inlines = ir.first_name_ir:flatten(disam_format)
    bib_names_str = disam_format:output(inlines)
  end
  ir.first_name_ir.bib_names_str = bib_names_str

  if engine.previous_bib_names_ir and
      engine.previous_bib_names_ir.bib_names_str == bib_names_str then
    local text = self.subsequent_author_substitute
    if #ir.first_name_ir.person_name_irs > 0 then
      for _, person_name_ir in ipairs(ir.first_name_ir.person_name_irs) do
        person_name_ir.inlines = {PlainText:new(text)}
      end
    else
      -- In case of a <text variable="title"/> in <substitute>
      if text == "" then
        ir.first_name_ir.children = {}
        ir.first_name_ir.group_var = "missing"
      else
        ir.first_name_ir.children = {Rendered:new({PlainText:new(text)}, self)}
      end
    end
  end
end

function Bibliography:substitute_subsequent_authors_partial_each(engine, ir)
  local bib_names_str = ""

  if #ir.first_name_ir.person_name_irs > 0 then
    if engine.previous_bib_names_ir then
      for i, person_name_ir in ipairs(ir.first_name_ir.person_name_irs) do
        local prev_name_ir = engine.previous_bib_names_ir.person_names[i]
        if prev_name_ir then
          local prev_name_variants = prev_name_ir.disam_variants
          local prev_full_name_str = prev_name_variants[#prev_name_variants]
          local name_variants = person_name_ir.disam_variants
          local full_name_str = name_variants[#name_variants]
          if prev_full_name_str == full_name_str then
            local text = self.subsequent_author_substitute
            person_name_ir.inlines = {PlainText:new(text)}
          else
            break
          end
        end
      end
    end
  else
    -- In case of a <text variable="title"/> in <substitute>
    local disam_format = DisamStringFormat:new()
    local inlines = ir.first_name_ir:flatten(disam_format)
    bib_names_str = disam_format:output(inlines)
    ir.first_name_ir.bib_names_str = bib_names_str
    if engine.previous_bib_names_ir and
        engine.previous_bib_names_ir.bib_names_str == bib_names_str then
      local text = self.subsequent_author_substitute
      if text == "" then
        ir.first_name_ir.children = {}
        ir.first_name_ir.group_var = "missing"
      else
        ir.first_name_ir.children = {Rendered:new({PlainText:new(text)}, self)}
      end
    end
  end
end

function Bibliography:substitute_subsequent_authors_partial_first(engine, ir)
end

local Macro = Element:derive("macro")

function Macro:from_node(node)
  local o = Macro:new()
  o.children = {}
  o:set_attribute(node, "name")
  o:process_children_nodes(node)
  return o
end

function Macro:build_ir(engine, state, context)
  local ir = self:build_group_ir(engine, state, context)
  return ir
end


style_module.Style = Style
style_module.Citation = Citation
style_module.Bibliography = Bibliography


return style_module
