--[[
affiliation-blocks – generate title components
Copyright © 2017–2021 Albert Krewinkel
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
]]

local from_utils = require("utils")
local has_key = from_utils.has_key
local List = require("pandoc.List")
local utils = require("pandoc.utils")
local stringify = utils.stringify

local M = {}

local default_marks = {
  corresponding_author = FORMAT == "latex" and { pandoc.RawInline("latex", "*") } or { pandoc.Str("*") },
  equal_contributor = FORMAT == "latex" and { pandoc.RawInline("latex", "\\#") } or { pandoc.Str("#") },
}

local function get_orcid_mark(orcid_value)
  if not orcid_value then
    return {}
  end
  local orcid_str
  if type(orcid_value) == "string" then
    orcid_str = orcid_value
  elseif type(orcid_value) == "table" then
    if orcid_value.text then
      orcid_str = orcid_value.text
    elseif orcid_value[1] and orcid_value[1].text then
      orcid_str = orcid_value[1].text
    else
      return {}
    end
  else
    return {}
  end
  orcid_str = orcid_str:gsub("[%-%s]", "")
  if FORMAT == "latex" then
    return { pandoc.RawInline("latex", "\\orcidlink{" .. orcid_str .. "}") }
  elseif FORMAT:match("docx") then
    local orcid_url = "https://orcid.org/" .. orcid_str
    return {
      pandoc.Str(" "),
      pandoc.Link("ID", orcid_url),
    }
  else
    local orcid_url = "https://orcid.org/" .. orcid_str
    return { pandoc.Link(pandoc.Str(""), orcid_url, "", { class = "orcid" }) }
  end
end

M.default_marks = default_marks

local function is_equal_contributor(author)
  if has_key(author, "attributes") then
    return author.attributes["equal-contributor"]
  end
  return nil
end

local function create_equal_contributors_block(authors, mark)
  local has_equal_contribs = List:new(authors):find_if(is_equal_contributor)
  if not has_equal_contribs then
    return nil
  end
  local contributors = {
    pandoc.Superscript(mark("equal_contributor")),
    pandoc.Space(),
    pandoc.Str("These authors contributed equally to this work."),
  }
  return List:new({ pandoc.Para(contributors) })
end
M.create_equal_contributors_block = create_equal_contributors_block

local function intercalate(lists, elem)
  local result = List:new({})
  for i = 1, (#lists - 1) do
    result:extend(lists[i])
    result:extend(elem)
  end
  if #lists > 0 then
    result:extend(lists[#lists])
  end
  return result
end

local function is_corresponding_author(author)
  if has_key(author, "attributes") then
    if author.attributes["corresponding"] then
      return author.email
    end
  end
  return nil
end
M.is_corresponding_author = is_corresponding_author

local function create_correspondence_blocks(authors, mark)
  local corresponding_authors = List:new({})
  for _, author in ipairs(authors) do
    if is_corresponding_author(author) then
      local mailto = "mailto:" .. stringify(author.email)
      local author_with_mail = List:new(
        author.name.literal
          .. List:new({ pandoc.Space(), pandoc.Str("(") })
          .. author.email
          .. List:new({ pandoc.Str(")") })
      )
      local link = pandoc.Link(author_with_mail, mailto)
      table.insert(corresponding_authors, { link })
    end
  end
  if #corresponding_authors == 0 then
    return nil
  end
  local correspondence = List:new({
    pandoc.Superscript(mark("corresponding_author")),
    pandoc.Space(),
    pandoc.Str("Corresponding to:"),
    pandoc.Space(),
  })
  local sep = List:new({ pandoc.Str(","), pandoc.Space() })
  return { pandoc.Para(correspondence .. intercalate(corresponding_authors, sep)) }
end

M.create_correspondence_blocks = create_correspondence_blocks

local function author_inline_generator(get_mark, meta)
  return function(author)
    local author_marks = List:new({})
    if has_key(author, "orcid") then
      author_marks[#author_marks + 1] = get_orcid_mark(author.orcid)
    end
    local affilstyle = meta and meta.affilstyle and stringify(meta.affilstyle) or "alphabeta"
    for _, idx in ipairs(author.affiliations) do
      local idx_num = tonumber(stringify(idx)) -- Convert MetaString/MetaInlines to number
      if not idx_num then
        error("Invalid affiliation index: " .. tostring(idx))
      end
      local idx_str
      if affilstyle == "number" then
        idx_str = tostring(idx_num)
      else
        if idx_num > 26 then
          error("Too many affiliations: only up to 26 (a-z) are supported")
        end
        idx_str = string.char(96 + idx_num)
      end
      author_marks[#author_marks + 1] = { pandoc.Str(idx_str) }
    end
    if has_key(author, "attributes") then
      if author.attributes["equal-contributor"] then
        author_marks[#author_marks + 1] = get_mark("equal_contributor")
      end
    end
    if is_corresponding_author(author) then
      author_marks[#author_marks + 1] = get_mark("corresponding_author")
    end
    if FORMAT:match("latex") then
      author.name.literal[#author.name.literal + 1] = pandoc.Superscript(intercalate(author_marks, { pandoc.Str(",") }))
      return author
    else
      local res = List.clone(author.name.literal)
      res[#res + 1] = pandoc.Superscript(intercalate(author_marks, { pandoc.Str(",") }))
      return res
    end
  end
end
M.author_inline_generator = author_inline_generator

local function create_authors_inlines(authors, mark, meta)
  local inlines_generator = author_inline_generator(mark, meta)
  local inlines = List:new(authors):map(inlines_generator)
  local and_str = List:new({ pandoc.Space(), pandoc.Str("and"), pandoc.Space() })
  local last_author = inlines[#inlines]
  inlines[#inlines] = nil
  local result = intercalate(inlines, { pandoc.Str(","), pandoc.Space() })
  if #authors > 1 then
    if #authors == 2 then
      result:extend(and_str)
    else
      result:extend(List:new({ pandoc.Str(",") }) .. and_str)
    end
  end
  result:extend(last_author)
  return result
end
M.create_authors_inlines = create_authors_inlines

local function create_affiliations_blocks_alphabeta(affiliations, meta)
  local affilstyle = meta and meta.affilstyle and stringify(meta.affilstyle) or "alphabeta"
  local affil_lines = List:new(affiliations):map(function(affil, i)
    if affilstyle == "number" then
      num_inlines = pandoc.List:new({
        pandoc.Superscript(pandoc.Str(tostring(i))),
        pandoc.Space(),
      })
    else
      num_inlines = pandoc.List:new({
        pandoc.Superscript(pandoc.Str(string.char(96 + i))),
        pandoc.Space(),
      })
    end
    local name_inlines = type(affil.name) == "table" and affil.name or { pandoc.Str(tostring(affil.name)) }
    local city_inlines = type(affil.city) == "table" and affil.city or { pandoc.Str(tostring(affil.city)) }
    local postcode_inlines = type(affil["postal-code"]) == "table" and affil["postal-code"]
      or { pandoc.Str(tostring(affil["postal-code"])) }
    local country_inlines = type(affil.country) == "table" and affil.country
      or { pandoc.Str(tostring(affil.country or affil["postal-code"])) }
    return num_inlines
      :extend(name_inlines)
      :extend({ pandoc.Str(", ") })
      :extend(city_inlines)
      :extend({ pandoc.Space() })
      :extend(postcode_inlines)
      :extend({ pandoc.Str(", ") })
      :extend(country_inlines)
      :extend({ pandoc.Str(".") })
  end)
  local combined_inlines = pandoc.List:new()
  for i, line in ipairs(affil_lines) do
    combined_inlines:extend(line)
    if i < #affil_lines then
      combined_inlines:extend({ pandoc.LineBreak() })
    end
  end
  return { pandoc.Para(combined_inlines) }
end

local function create_affiliations_blocks_number(affiliations, meta)
  local affilstyle = meta and meta.affilstyle and stringify(meta.affilstyle) or "alphabeta"
  local affil_lines = List:new(affiliations):map(function(affil, i)
    local num_inlines = pandoc.List:new({
      pandoc.Superscript(pandoc.Str(tostring(i))),
      pandoc.Space(),
    })
    local name_inlines = type(affil.name) == "table" and affil.name or { pandoc.Str(tostring(affil.name)) }
    local city_inlines = type(affil.city) == "table" and affil.city or { pandoc.Str(tostring(affil.city)) }
    local postcode_inlines = type(affil["postal-code"]) == "table" and affil["postal-code"]
      or { pandoc.Str(tostring(affil["postal-code"])) }
    local country_inlines = type(affil.country) == "table" and affil.country
      or { pandoc.Str(tostring(affil.country or affil["postal-code"])) }
    return num_inlines
      :extend(name_inlines)
      :extend({ pandoc.Str(", ") })
      :extend(city_inlines)
      :extend({ pandoc.Space() })
      :extend(postcode_inlines)
      :extend({ pandoc.Str(", ") })
      :extend(country_inlines)
      :extend({ pandoc.Str(".") })
  end)
  local combined_inlines = pandoc.List:new()
  for i, line in ipairs(affil_lines) do
    combined_inlines:extend(line)
    if i < #affil_lines then
      combined_inlines:extend({ pandoc.LineBreak() })
    end
  end
  return { pandoc.Para(combined_inlines) }
end

M.create_affiliations_blocks = create_affiliations_blocks_alphabeta

function Meta(meta)
  local affilstyle = meta and meta.affilstyle and stringify(meta.affilstyle) or "alphabeta"
  M.create_affiliations_blocks = affilstyle == "number" and create_affiliations_blocks_number
    or create_affiliations_blocks_alphabeta
  if meta.authors then
    meta.author = create_authors_inlines(meta.authors, M.default_marks, meta)
  end
  if meta.affiliations then
    meta.institute = M.create_affiliations_blocks(meta.affiliations, meta)
  end
  if meta.authors then
    local equal_contributors = create_equal_contributors_block(meta.authors, M.default_marks)
    if equal_contributors then
      meta["equal-contributors"] = equal_contributors
    end
    local correspondence = create_correspondence_blocks(meta.authors, M.default_marks)
    if correspondence then
      meta.correspondence = correspondence
    end
  end
  return meta
end

return M
