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

local date_module = {}

local element = require("citeproc-element")
local util = require("citeproc-util")


local Date = element.Element:new()

function Date:render (item, context)
  self:debug_info(context)
  context = self:process_context(context)

  if context.sorting then
    return self:render_sort_key(item, context)
  end

  local variable_name = context.options["variable"]

  local is_locale_date
  if variable_name then
    context.variable = variable_name
    is_locale_date = false
  else
    variable_name = context.variable
    is_locale_date = true
  end

  local date = self:get_variable(item, variable_name, context)
  if not date then
    return nil
  end

  local res = nil
  local form = context.options["form"]
  if form and not is_locale_date then
    for _, date_part in ipairs(self:query_selector("date-part")) do
      local name = date_part:get_attribute("name")
      if not context.date_part_attributes then
        context.date_part_attributes = {}
      end
      if not context.date_part_attributes[name] then
        context.date_part_attributes[name] = {}
      end

      for attr, value in pairs(date_part._attr) do
        if attr ~= name then
          context.date_part_attributes[name][attr] = value
        end
      end
    end
    res = self:get_locale_date(context, form):render(item, context)
  else
    if not date["date-parts"] or #date["date-parts"] == 0 then
      local literal = date["literal"]
      if literal then
        res = literal
      else
        local raw = date["raw"]
        if raw then
          res = raw
        end
      end

    else
      if #date["date-parts"] == 1 then
        res = self:_render_single_date(date, context)
      elseif #date["date-parts"] == 2 then
        res = self:_render_date_range(date, context)
      end
    end
  end

  table.insert(context.variable_attempt, res ~= nil)

  res = self:format(res, context)
  res = self:wrap(res, context)
  return res
end

function Date:get_locale_date(context, form)
  local date = nil
  local style = context.style
  local query = string.format("date[form=\"%s\"]", form)
  for _, locale in ipairs(style:get_locales()) do
    date = locale:query_selector(query)[1]
    if date then
      break
    end
  end
  if not date then
    error(string.format("Failed to find '%s'", query))
  end
  return date
end

function Date:render_sort_key (item, context)
  local variable_name = context.options["variable"]
  local date = self:get_variable(item, variable_name, context)
  if not date or not date["date-parts"] then
    return nil
  end
  local show_parts = {
    year = false,
    month = false,
    day = false,
  }
  if self:get_attribute("form") then
    local date_parts = self:get_attribute("date-parts") or "year-month-day"
    for _, dp_name in ipairs(util.split(date_parts, "%-")) do
      show_parts[dp_name] = true
    end
  else
    for _, child in ipairs(self:query_selector("date-part")) do
      show_parts[child:get_attribute("name")] = true
    end
  end
  local res = ""
  for _, date_parts in ipairs(date["date-parts"]) do
    for i, dp_name in ipairs({"year", "month", "day"}) do
      local value = date_parts[i]
      if not value or not show_parts[dp_name] then
        value = 0
      end
      if i == 1 then
        res = res .. string.format("%05d", value + 10000)
      else
        res = res .. string.format("%02d", value)
      end
    end
  end
  return res
end

function Date:_render_single_date (date, context)
  local show_parts = self:_get_show_parts(context)

  local output = {}
  for _, child in ipairs(self:query_selector("date-part")) do
    if show_parts[child:get_attribute("name")] then
      table.insert(output, child:render(date, context))
    end
  end
  return self:concat(output, context)
end

function Date:_render_date_range (date, context)
  local show_parts = self:_get_show_parts(context)
  local part_index = {}

  local largest_diff_part = nil
  for i, name in ipairs({"year", "month", "day"}) do
    part_index[name] = i
    local part_value1 = date["date-parts"][1][i]
    if show_parts[name] and part_value1 then
      if not largest_diff_part then
        largest_diff_part = name
      end
    end
  end

  local date_parts = {}
  for _, date_part in ipairs(self:query_selector("date-part")) do
    if show_parts[date_part:get_attribute("name")] then
      table.insert(date_parts, date_part)
    end
  end

  local diff_begin = 0
  local diff_end = #date_parts
  local range_delimiter = nil

  for i, date_part in ipairs(date_parts) do
    local name = date_part:get_attribute("name")
    if name == largest_diff_part then
      range_delimiter = date_part:get_attribute("range-delimiter")
      if not range_delimiter then
        range_delimiter = util.unicode["en dash"]
      end
    end

    local index = part_index[name]
    local part_value1 = date["date-parts"][1][index]
    local part_value2 = date["date-parts"][2][index]
    if part_value1 and part_value1 ~= part_value2 then
      if diff_begin == 0 then
        diff_begin = i
      end
      diff_end = i
    end
  end

  local same_prefix = {}
  local range_begin = {}
  local range_end = {}
  local same_suffix = {}

  local no_suffix_context = self:process_context(context)
  no_suffix_context.options["suffix"] = nil

  for i, date_part in ipairs(date_parts) do
    local res = nil
    if i == diff_end then
      res = date_part:render(date, no_suffix_context, true)
    else
      res = date_part:render(date, context)
    end
    if i < diff_begin then
      table.insert(same_prefix, res)
    elseif i <= diff_end then
      table.insert(range_begin, res)
      table.insert(range_end, date_part:render(date, context, false, true))
    else
      table.insert(same_suffix, res)
    end
  end

  local prefix_output = self:concat(same_prefix, context) or ""
  local range_begin_output = self:concat(range_begin, context) or ""
  local range_end_output = self:concat(range_end, context) or ""
  local suffix_output = self:concat(same_suffix, context)
  local range_output = range_begin_output .. range_delimiter .. range_end_output

  local res = self:concat({prefix_output, range_output, suffix_output}, context)

  return res
end

function Date:_get_show_parts (context)
  local show_parts = {}
  local date_parts = context.options["date-parts"] or "year-month-day"
  for _, date_part in ipairs(util.split(date_parts, "%-")) do
    show_parts[date_part] = true
  end
  return show_parts
end


local DatePart = element.Element:new()

DatePart.render = function (self, date, context, last_range_begin, range_end)
  self:debug_info(context)
  context = self:process_context(context)
  local name = context.options["name"]
  local range_delimiter = context.options["range-delimiter"] or false

  -- The attributes set on cs:date-part elements of a cs:date with form
  -- attribute override those specified for the localized date formats
  if context.date_part_attributes then
    local context_attributes = context.date_part_attributes[name]
    if context_attributes then
      for attr, value in pairs(context_attributes) do
        context.options[attr] = value
      end
    end
  end

  if last_range_begin then
    context.options["suffix"] = ""
  end

  local date_parts_index = 1
  if range_end then
    date_parts_index = 2
  end

  local res = nil
  if name == "day" then
    local day = date["date-parts"][date_parts_index][3]
    if not day then
      return nil
    end
    day = tonumber(day)
    -- range open
    if day == 0 then
      return nil
    end
    local form = context.options["form"] or "numeric"

    if form == "ordinal" then
      local option = self:get_locale_option("limit-day-ordinals-to-day-1")
      if option and option ~= "false" and day > 1 then
        form = "numeric"
      end
    end
    if form == "numeric" then
      res = tostring(day)
    elseif form == "numeric-leading-zeros" then
      -- TODO: day == nil?
      if not day then
        return nil
      end
      res = string.format("%02d", day)
    elseif form == "ordinal" then
      res = util.to_ordinal(day)
    end

  elseif name == "month" then
    local form = context.options["form"] or "long"

    local month = date["date-parts"][date_parts_index][2]
    if month then
      month = tonumber(month)
      -- range open
      if month == 0 then
        return nil
      end
    end

    if form == "long" or form == "short" then
      local term_name = nil
      if month then
        if month >= 1 and month <= 12 then
          term_name = string.format("month-%02d", month)
        elseif month >= 13 and month <= 24 then
          local season = month % 4
          if season == 0 then
            season = 4
          end
          term_name = string.format("season-%02d", season)
        else
          util.warning("Invalid month value")
          return nil
        end
      else
        local season = date["season"]
        if season then
          season = tonumber(season)
          term_name = string.format("season-%02d", season)
        else
          return nil
        end
      end
      res = self:get_term(term_name, form):render(context)
    elseif form == "numeric" then
      res = tostring(month)
    elseif form == "numeric-leading-zeros" then
      -- TODO: month == nil?
      if not month then
        return nil
      end
      res = string.format("%02d", month)
    end
    res = self:strip_periods(res, context)

  elseif name == "year" then
    local year = date["date-parts"][date_parts_index][1]
    if year then
      year = tonumber(year)
      -- range open
      if year == 0 then
        return nil
      end
      local form = context.options["form"] or "long"
      if form == "long" then
        year = tonumber(year)
        if year < 0 then
          res = tostring(-year) .. self:get_term("bc"):render(context)
        elseif year < 1000 then
          res = tostring(year) .. self:get_term("ad"):render(context)
        else
          res = tostring(year)
        end
      elseif form == "short" then
        res = string.sub(tostring(year), -2)
      end
    end
  end
  res = self:case(res, context)
  res = self:format(res, context)
  res = self:wrap(res, context)
  res = self:display(res, context)
  return res
end


date_module.Date = Date
date_module.DatePart = DatePart

return date_module
