if not WeakAuras.IsLibsOK() then return end
local AddonName = ...
local Private = select(2, ...)

local WeakAuras = WeakAuras
local L = WeakAuras.L

-- Lua APIs
local time, format, floor = time, format, floor

-- WoW APIs
local GetLocale = GetLocale

local TIME_UTIL_WHITE_SPACE_STRIPPABLE = not (GetLocale() == "deDE" or GetLocale() == "ruRU");
local SECONDS_PER_MIN = 60;
local SECONDS_PER_HOUR = 60 * SECONDS_PER_MIN;
local SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
local SECONDS_PER_MONTH = 30 * SECONDS_PER_DAY;
local SECONDS_PER_YEAR = 12 * SECONDS_PER_MONTH;

function SecondsToMinutes(seconds)
  return seconds / SECONDS_PER_MIN;
end

function MinutesToSeconds(minutes)
  return minutes * SECONDS_PER_MIN;
end

function HasTimePassed(testTime, amountOfTime)
  return ((time() - testTime) >= amountOfTime);
end

SecondsFormatter = {};

SecondsFormatterConstants =
{
  ZeroApproximationThreshold = 0,
  ConvertToLower = true,
  DontConvertToLower = false,
  RoundUpLastUnit = true,
  DontRoundUpLastUnit = false,
  RoundUpIntervals = true,
  DontRoundUpIntervals = false,
}

SecondsFormatter.Abbreviation =
{
  None = 1, -- seconds, minutes, hours...
  Truncate = 2, -- sec, min, hr...
  OneLetter = 3, -- s, m, h...
}

SecondsFormatter.Interval = {
  Seconds = 1,
  Minutes = 2,
  Hours = 3,
  Days = 4,
}

SecondsFormatter.IntervalDescription = {
  [SecondsFormatter.Interval.Seconds] = {seconds = 1, formatString = { L["D_SECONDS"], L["SECONDS_ABBR"], L["SECOND_ONELETTER_ABBR"]}},
  [SecondsFormatter.Interval.Minutes] = {seconds = SECONDS_PER_MIN, formatString = {L["D_MINUTES"], L["MINUTES_ABBR"], L["MINUTE_ONELETTER_ABBR"]}},
  [SecondsFormatter.Interval.Hours] = {seconds = SECONDS_PER_HOUR, formatString = {L["D_HOURS"], L["HOURS_ABBR"], L["HOUR_ONELETTER_ABBR"]}},
  [SecondsFormatter.Interval.Days] = {seconds = SECONDS_PER_DAY, formatString = {L["D_DAYS"], L["DAYS_ABBR"], L["DAY_ONELETTER_ABBR"]}},
}

--[[ Seconds formatter to standardize representations of seconds. When adding a new formatter
please consider if a prexisting formatter suits your needs, otherwise, before adding a new formatter,
consider adding it to a file appropriate to it's intended use. For example, "WorldQuestsSecondsFormatter"
could be added to QuestUtil.h so it's immediately apparent the scenarios the formatter is appropriate.]]

SecondsFormatterMixin = {}
-- defaultAbbreviation: the default abbreviation for the format. Can be overrridden in SecondsFormatterMixin:Format()
-- approximationSeconds: threshold for representing the seconds as an approximation (ex. "< 2 hours").
-- roundUpLastUnit: determines if the last unit in the output format string is ceiled (floored by default).
-- convertToLower: converts the format string to lowercase.
-- roundUpIntervals: determines if units can be promoted to a higher interval after (ex. 60m -> 1h).
function SecondsFormatterMixin:Init(approximationSeconds, defaultAbbreviation, roundUpLastUnit, convertToLower, roundUpIntervals)
  self:SetApproximationSeconds(approximationSeconds or 0);
  self:SetMinInterval(SecondsFormatter.Interval.Seconds);
  self:SetDefaultAbbreviation(defaultAbbreviation or SecondsFormatter.Abbreviation.None);
  self:SetCanRoundUpLastUnit(roundUpLastUnit or SecondsFormatterConstants.DontRoundUpLastUnit);
  self:SetCanRoundUpIntervals(roundUpIntervals or SecondsFormatterConstants.DontRoundUpIntervals);
  self:SetDesiredUnitCount(2);
  self:SetStripIntervalWhitespace(false);
  self:SetConvertToLower(convertToLower or false);
end

function SecondsFormatterMixin:SetStripIntervalWhitespace(strip)
  self.stripIntervalWhitespace = strip;
end

function SecondsFormatterMixin:GetStripIntervalWhitespace()
  return self.stripIntervalWhitespace;
end

function SecondsFormatterMixin:SetConvertToLower(convertToLower)
  self.convertToLower = convertToLower;
end

function SecondsFormatterMixin:GetMaxInterval()
  return #SecondsFormatter.IntervalDescription;
end

function SecondsFormatterMixin:GetIntervalDescription(interval)
  return SecondsFormatter.IntervalDescription[interval];
end

function SecondsFormatterMixin:GetIntervalSeconds(interval)
  local intervalDescription = self:GetIntervalDescription(interval);
  return intervalDescription and intervalDescription.seconds or nil;
end

function SecondsFormatterMixin:CanApproximate(seconds)
  return (seconds > 0 and seconds < self:GetApproximationSeconds());
end

function SecondsFormatterMixin:SetDefaultAbbreviation(defaultAbbreviation)
  self.defaultAbbreviation = defaultAbbreviation;
end

function SecondsFormatterMixin:GetDefaultAbbreviation()
  return self.defaultAbbreviation;
end

function SecondsFormatterMixin:SetApproximationSeconds(approximationSeconds)
  self.approximationSeconds = approximationSeconds;
end

function SecondsFormatterMixin:GetApproximationSeconds()
  return self.approximationSeconds;
end

function SecondsFormatterMixin:SetCanRoundUpLastUnit(roundUpLastUnit)
  self.roundUpLastUnit = roundUpLastUnit;
end

function SecondsFormatterMixin:CanRoundUpLastUnit()
  return self.roundUpLastUnit;
end

function SecondsFormatterMixin:SetCanRoundUpIntervals(roundUpIntervals)
  self.roundUpIntervals = roundUpIntervals;
end

function SecondsFormatterMixin:CanRoundUpIntervals()
  return self.roundUpIntervals;
end

function SecondsFormatterMixin:SetDesiredUnitCount(unitCount)
  self.unitCount = unitCount;
end

function SecondsFormatterMixin:GetDesiredUnitCount(seconds)
  -- seconds ignored in base implementation, but instances of this mixin can override this function
  return self.unitCount;
end

function SecondsFormatterMixin:SetMinInterval(interval)
  self.minInterval = interval;
end

function SecondsFormatterMixin:GetMinInterval(seconds)
  -- seconds ignored in base implementation, but instances of this mixin can override this function
  return self.minInterval;
end

function SecondsFormatterMixin:GetFormatString(interval, abbreviation, convertToLower)
  local intervalDescription = self:GetIntervalDescription(interval);
  local formatString = intervalDescription.formatString[abbreviation];
  if convertToLower then
      formatString = formatString:lower();
  end
  local strip = TIME_UTIL_WHITE_SPACE_STRIPPABLE and self:GetStripIntervalWhitespace();
  return strip and formatString:gsub(" ", "") or formatString;
end

function SecondsFormatterMixin:FormatZero(abbreviation, toLower)
  local minInterval = self:GetMinInterval(seconds);
  local formatString = self:GetFormatString(minInterval, abbreviation);
  return formatString:format(0);
end

function SecondsFormatterMixin:FormatMillseconds(millseconds, abbreviation)
  return self:Format(millseconds/1000, abbreviation);
end

function SecondsFormatterMixin:Format(seconds, abbreviation)
  if (seconds == nil) then
      return "";
  end

  seconds = math.ceil(seconds);
  abbreviation = abbreviation or self:GetDefaultAbbreviation();

  if (seconds <= 0) then
      return self:FormatZero(abbreviation);
  end

  local minInterval = self:GetMinInterval(seconds);
  local maxInterval = self:GetMaxInterval();

  if (self:CanApproximate(seconds)) then
      local interval = math.max(minInterval, SecondsFormatter.Interval.Minutes);
      while (interval < maxInterval) do
          local nextInterval = interval + 1;
          if (seconds > self:GetIntervalSeconds(nextInterval)) then
              interval = nextInterval;
          else
              break;
          end
      end

      local formatString = self:GetFormatString(interval, abbreviation, self.convertToLower);
      local unit = formatString:format(math.ceil(seconds / self:GetIntervalSeconds(interval)));
      return string.format(L["LESS_THAN_OPERAND"], unit);
  end

  local output = "";
  local appendedCount = 0;
  local desiredCount = self:GetDesiredUnitCount(seconds);
  local convertToLower = self.convertToLower;

  local intervalUnits = {};
  for interval, value in pairs(SecondsFormatter.Interval) do
    intervalUnits[value] = 0;
  end

  local currentInterval = maxInterval;
  while ((appendedCount < desiredCount) and (currentInterval >= minInterval)) do
      local intervalDescription = self:GetIntervalDescription(currentInterval);
      local intervalSeconds = intervalDescription.seconds;
      if (seconds >= intervalSeconds) then
          appendedCount = appendedCount + 1;

          local quotient = seconds / intervalSeconds;
          if (quotient > 0) then
            if (self:CanRoundUpLastUnit() and ((minInterval == currentInterval) or (appendedCount == desiredCount))) then
              intervalUnits[currentInterval] = math.ceil(quotient);
            else
              intervalUnits[currentInterval] = math.floor(quotient);
              end
          else
            break;
          end

          seconds = math.fmod(seconds, intervalSeconds);
      end

      currentInterval = currentInterval - 1;
  end

  if self:CanRoundUpIntervals() then
    -- Intervals are promoted to a higher interval if possible (60m -> 1h) so that a value isn't expressed
    -- as 1h 60m as a result of individual interval round-up. This promotion can only happen within the
    -- min interval, max interval band so that we don't promote to an interval we don't intend to display.
    for interval, value in ipairs(intervalUnits) do
      local intervalDescription = self:GetIntervalDescription(interval);
      local intervalSeconds = intervalDescription.seconds;

      if value == intervalSeconds then
        local nextInterval = interval + 1;
        if nextInterval <= maxInterval then
          intervalUnits[nextInterval] = intervalUnits[nextInterval] + 1;
          intervalUnits[interval] = 0;
        end
      end
    end
  end

  for interval, value in ipairs_reverse(intervalUnits) do
    if value > 0 then
      if (output ~= "") then
        output = output..L["TIME_UNIT_DELIMITER"];
      end

      local formatString = self:GetFormatString(interval, abbreviation, convertToLower);
      output = output..formatString:format(value);
    end
  end

  -- Return the zero format if an acceptable representation couldn't be formed.
  if (output == "") then
      return self:FormatZero(abbreviation);
  end

  return output;
end

function ConvertSecondsToUnits(timestamp)
  timestamp = math.max(timestamp, 0);
  local days = math.floor(timestamp / SECONDS_PER_DAY);
  timestamp = timestamp - (days * SECONDS_PER_DAY);
  local hours = math.floor(timestamp / SECONDS_PER_HOUR);
  timestamp = timestamp - (hours * SECONDS_PER_HOUR);
  local minutes = math.floor(timestamp / SECONDS_PER_MIN);
  timestamp = timestamp - (minutes * SECONDS_PER_MIN);
  local seconds = math.floor(timestamp);
  local milliseconds = timestamp - seconds;
  return {
      days=days,
      hours=hours,
      minutes=minutes,
      seconds=seconds,
      milliseconds=milliseconds,
  }
end

function SecondsToClock(seconds, displayZeroHours)
  local units = ConvertSecondsToUnits(seconds);
  if units.hours > 0 or displayZeroHours then
      return format(L["HOURS_MINUTES_SECONDS"], units.hours, units.minutes, units.seconds);
  else
      return format(L["MINUTES_SECONDS"], units.minutes, units.seconds);
  end
end

-- Deprecated. See SecondsFormatter for intended replacement
--[[function SecondsToTime(seconds, noSeconds, notAbbreviated, maxCount, roundUp)
  local time = "";
  local count = 0;
  local tempTime;
  seconds = roundUp and math.ceil(seconds) or math.floor(seconds);
  maxCount = maxCount or 2;

  -- When limited to a single term, use a higher threshold of 1.5 min/hr/day.
  -- If there are at least 2 terms, the higher threshold is unnecessary.
  local threshold = maxCount > 1 and 1.0 or 1.5

  if ( seconds >= SECONDS_PER_DAY * threshold ) then
      count = count + 1;
      if ( count == maxCount and roundUp ) then
          tempTime = math.ceil(seconds / SECONDS_PER_DAY);
      else
          tempTime = math.floor(seconds / SECONDS_PER_DAY);
      end
      if ( notAbbreviated ) then
          time = L["D_DAYS"]:format(tempTime);
      else
          time = L["DAYS_ABBR"]:format(tempTime);
      end
      seconds = seconds % SECONDS_PER_DAY;
  end
  if ( count < maxCount and seconds >= SECONDS_PER_HOUR * threshold ) then
      count = count + 1;
      if ( time ~= "" ) then
          time = time..L["TIME_UNIT_DELIMITER"];
      end
      if ( count == maxCount and roundUp ) then
          tempTime = math.ceil(seconds / SECONDS_PER_HOUR);
      else
          tempTime = math.floor(seconds / SECONDS_PER_HOUR);
      end
      if ( notAbbreviated ) then
          time = time..L["D_HOURS"]:format(tempTime);
      else
          time = time..L["HOURS_ABBR"]:format(tempTime);
      end
      seconds = seconds % SECONDS_PER_HOUR;
  end
  if ( count < maxCount and seconds >= SECONDS_PER_MIN * threshold ) then
      count = count + 1;
      if ( time ~= "" ) then
          time = time..L["TIME_UNIT_DELIMITER"];
      end
      if ( count == maxCount and roundUp ) then
          tempTime = math.ceil(seconds / SECONDS_PER_MIN);
      else
          tempTime = math.floor(seconds / SECONDS_PER_MIN);
      end
      if ( notAbbreviated ) then
          time = time..L["D_MINUTES"]:format(tempTime);
      else
          time = time..L["MINUTES_ABBR"]:format(tempTime);
      end
      seconds = seconds % SECONDS_PER_MIN;
  end
  if ( count < maxCount and seconds > 0 and not noSeconds ) then
      if ( time ~= "" ) then
          time = time..L["TIME_UNIT_DELIMITER"];
      end
      if ( notAbbreviated ) then
          time = time..L["D_SECONDS"]:format(seconds);
      else
          time = time..L["SECONDS_ABBR"]:format(seconds);
      end
  end
  return time;
end]]

-- Deprecated. See SecondsFormatter for intended replacement
function MinutesToTime(mins, hideDays)
  local time = "";
  local count = 0;
  local tempTime;
  -- only show days if hideDays is false
  if ( mins > 1440 and not hideDays ) then
      tempTime = floor(mins / 1440);
      time = L["TIME_UNIT_DELIMITER"] .. format(L["DAYS_ABBR"], tempTime);
      mins = mins % 1440;
      count = count + 1;
  end
  if ( mins > 60  ) then
      tempTime = floor(mins / 60);
      time = time .. L["TIME_UNIT_DELIMITER"] .. format(L["HOURS_ABBR"], tempTime);
      mins = mins % 60;
      count = count + 1;
  end
  if ( count < 2 ) then
      tempTime = mins;
      time = time .. L["TIME_UNIT_DELIMITER"] .. format(L["MINUTES_ABBR"], tempTime);
      count = count + 1;
  end
  return time;
end

-- Deprecated. See SecondsFormatter for intended replacement
--[[function SecondsToTimeAbbrev(seconds, thresholdOverride)
  local tempTime;
  local threshold = 1.5;
  if thresholdOverride then
      threshold = thresholdOverride;
  end

  if ( seconds >= SECONDS_PER_DAY * threshold ) then
      tempTime = ceil(seconds / SECONDS_PER_DAY);
      return L["DAY_ONELETTER_ABBR"], tempTime;
  end
  if ( seconds >= SECONDS_PER_HOUR * threshold ) then
      tempTime = ceil(seconds / SECONDS_PER_HOUR);
      return L["HOUR_ONELETTER_ABBR"], tempTime;
  end
  if ( seconds >= SECONDS_PER_MIN * threshold ) then
      tempTime = ceil(seconds / SECONDS_PER_MIN);
      return L["MINUTE_ONELETTER_ABBR"], tempTime;
  end
  return L["SECOND_ONELETTER_ABBR"], seconds;
end]]

function FormatShortDate(day, month, year)
  local LOCALE_enGB = (GetLocale() == "enUS") or (GetLocale() == "enGB")
  if (year) then
      if (LOCALE_enGB) then
          return L["SHORTDATE_EU"]:format(day, month, year);
      else
          return L["SHORTDATE"]:format(day, month, year);
      end
  else
      if (LOCALE_enGB) then
          return L["SHORTDATENOYEAR_EU"]:format(day, month);
      else
          return L["SHORTDATENOYEAR"]:format(day, month);
      end
  end
end

function BreakUpLargeNumbers(value)
  local retString = "";
  if ( value < 1000 ) then
      if ( (value - math.floor(value)) == 0) then
      return value;
      end
      local decimal = (math.floor(value*100));
      retString = string.sub(decimal, 1, -3);
      retString = retString..L["DECIMAL_SEPERATOR"];
      retString = retString..string.sub(decimal, -2);
      return retString;
  end

  value = math.floor(value);
  retString = value;
  return retString;
end

if GetLocale() ~= "zhCN" or GetLocale() ~= "zhTW" or GetLocale() ~= "koKR" then -- non Asian locales use different number abbreviations
  function AbbreviateLargeNumbers(value)
    local strLen = strlen(value);
    local retString = value;
    if ( strLen > 8 ) then
      retString = string.sub(value, 1, -7)..L["SECOND_NUMBER_CAP"];
    elseif ( strLen > 5 ) then
      retString = string.sub(value, 1, -4)..L["FIRST_NUMBER_CAP"];
    elseif (strLen > 3 ) then
      retString = BreakUpLargeNumbers(value);
    end
    return retString;
  end
else -- Asian locales use different number abbreviations
  function AbbreviateLargeNumbers(value)
    local strLen = strlen(value);
    local retString = value;
    if ( strLen >= 11 ) then
      retString = string.sub(value, 1, -8)..L["SECOND_NUMBER_CAP"];
    elseif ( strLen >= 9 ) then
      retString = string.sub(value, 1, -9).."."..string.sub(value, -8, -7)..L["SECOND_NUMBER_CAP"];
    elseif ( strLen >= 7 ) then
      retString = string.sub(value, 1, -5)..L["FIRST_NUMBER_CAP"];
    elseif (strLen > 3 ) then
      retString = BreakUpLargeNumbers(value);
    end
    return retString;
  end
end

if GetLocale() ~= "zhCN" or GetLocale() ~= "zhTW" or GetLocale() ~= "koKR" then -- Asian locales use different number abbreviations
  NUMBER_ABBREVIATION_DATA = {
    -- Order these from largest to smallest
    -- (significandDivisor and fractionDivisor should multiply to be equal to breakpoint)
    { breakpoint = 10000000000000,	abbreviation = L["FOURTH_NUMBER_CAP_NO_SPACE"],		significandDivisor = 1000000000000,	fractionDivisor = 1 },
    { breakpoint = 1000000000000,	abbreviation = L["FOURTH_NUMBER_CAP_NO_SPACE"],		significandDivisor = 100000000000,	fractionDivisor = 10 },
    { breakpoint = 10000000000,		abbreviation = L["THIRD_NUMBER_CAP_NO_SPACE"],		significandDivisor = 1000000000,	fractionDivisor = 1 },
    { breakpoint = 1000000000,		abbreviation = L["THIRD_NUMBER_CAP_NO_SPACE"],		significandDivisor = 100000000,	fractionDivisor = 10 },
    { breakpoint = 10000000,		abbreviation = L["SECOND_NUMBER_CAP_NO_SPACE"],		significandDivisor = 1000000,	fractionDivisor = 1 },
    { breakpoint = 1000000,			abbreviation = L["SECOND_NUMBER_CAP_NO_SPACE"],		significandDivisor = 100000,		fractionDivisor = 10 },
    { breakpoint = 10000,			abbreviation = L["FIRST_NUMBER_CAP_NO_SPACE"],		significandDivisor = 1000,		fractionDivisor = 1 },
    { breakpoint = 1000,			abbreviation = L["FIRST_NUMBER_CAP_NO_SPACE"],		significandDivisor = 100,		fractionDivisor = 10 },
  };
else
  NUMBER_ABBREVIATION_DATA = {
    -- Order these from largest to smallest.
    { breakpoint = 1000000000,	abbreviation = L["SECOND_NUMBER_CAP_NO_SPACE"],	significandDivisor = 100000000,	fractionDivisor = 1 },
    { breakpoint = 100000000,	abbreviation = L["SECOND_NUMBER_CAP_NO_SPACE"],	significandDivisor = 10000000,	fractionDivisor = 10 },
    { breakpoint = 100000,		abbreviation = L["FIRST_NUMBER_CAP_NO_SPACE"],	significandDivisor = 10000,		fractionDivisor = 1 },
    { breakpoint = 10000,		abbreviation = L["FIRST_NUMBER_CAP_NO_SPACE"],	significandDivisor = 1000,		fractionDivisor = 10 },
  };
end

function GetLocalizedNumberAbbreviationData()
  return NUMBER_ABBREVIATION_DATA;
end

function AbbreviateNumbers(value)
  for i, data in ipairs(GetLocalizedNumberAbbreviationData()) do
    if value >= data.breakpoint then
      local finalValue = math.floor(value / data.significandDivisor) / data.fractionDivisor;
      return finalValue .. data.abbreviation;
    end
  end
  return tostring(value);
end
