/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Functions for formatting relative dates.  Such as "3 days ago"
 * "3 hours ago", "14 minutes ago", "12 days ago", "Today", "Yesterday".
 *
 * Closure's I18N formatter for relative dates and times is by default to
 * format strings function. It provides plural forms and many locales
 * using standard data from the Common Data Locale Repository (CLDR).
 */

goog.provide('goog.date.relative');
goog.provide('goog.date.relative.TimeDeltaFormatter');
goog.provide('goog.date.relative.Unit');

goog.require('goog.i18n.DateTimeFormat');
goog.require('goog.i18n.DateTimePatterns');
goog.require('goog.i18n.RelativeDateTimeFormat');
goog.requireType('goog.date.DateTime');

goog.scope(function() {
'use strict';
// For referencing this module.
var RelativeDateTimeFormat =
    goog.module.get('goog.i18n.RelativeDateTimeFormat');

/**
 * Number of milliseconds in a minute.
 * @type {number}
 * @private
 */
goog.date.relative.MINUTE_MS_ = 60000;


/**
 * Number of milliseconds in a day.
 * @type {number}
 * @private
 */
goog.date.relative.DAY_MS_ = 86400000;


/**
 * Limit on number of days in past or future for formatting.
 * Since the timestamp is in milliseconds, the difference in days
 * is limited (10^9 milliseconds = 11.6 days.)
 * @type {number}
 * @private
 */
goog.date.relative.FORTNIGHT_ = 14;


/**
 * Unicode UTF-16 surrogate range minimum
 * @type {number}
 * @private
 */
goog.date.relative.SURROGATE_LOW_ = 0xd800;


/**
 * Unicode UTF-16 surrogate range maximum
 * @type {number}
 * @private
 */
goog.date.relative.SURROGATE_HIGH_ = 0xdfff;


/**
 * Enumeration used to identify time units internally.
 * @enum {number}
 */
goog.date.relative.Unit = {
  MINUTES: 0,
  HOURS: 1,
  DAYS: 2
};


/**
 * Full date formatter.
 * @type {?goog.i18n.DateTimeFormat}
 * @private
 */
goog.date.relative.fullDateFormatter_;


/**
 * Short time formatter.
 * @type {?goog.i18n.DateTimeFormat}
 * @private
 */
goog.date.relative.shortTimeFormatter_;


/**
 * Month-date formatter.
 * @type {?goog.i18n.DateTimeFormat}
 * @private
 */
goog.date.relative.monthDateFormatter_;


/**
 * Casing mode: default true for backward compatibility
 * True causes formatDay to capitalize first character of
 * the returned string.
 * If false, the string is not changed.
 * @type {boolean}
 * @private
 */
goog.date.relative.casingMode_ = true;


/**
 * Handles formatting of time deltas.
 * @private {?goog.date.relative.TimeDeltaFormatter}
 */
goog.date.relative.formatTimeDelta_;


/**
 * Caller-settable function for formatting time. Default is internal
 * formatting using goog.i18n.RelativeDateTimeFormat
 * @typedef {function(number, boolean, !goog.date.relative.Unit): string}
 */
goog.date.relative.TimeDeltaFormatter;


/**
 * Sets a different formatting function for time deltas ("3 days ago").
 * While its visibility is public, this function is Closure-internal and should
 * not be used in application code.
 * @param {!goog.date.relative.TimeDeltaFormatter} formatter The function to use
 *     for formatting time deltas (i.e. relative times).
 */
goog.date.relative.setTimeDeltaFormatter = function(formatter) {
  'use strict';
  goog.date.relative.formatTimeDelta_ = formatter;
};


/**
 * Sets casing mode to a boolean.
 * If true, the first letter of day formats ("today", "yesterday", "tommorow")
 * is capitalized using locale-aware toUpper.
 * If false, no casing is done on basic data.
 * @param {boolean} capitalizeMode
 */
goog.date.relative.setCasingMode = function(capitalizeMode) {
  'use strict';
  goog.date.relative.casingMode_ = capitalizeMode;
};


/**
 * Converts first letter of a string to upper case.
 * @param {string} text
 * @return {string}
 * @package Visible for testing
 */
goog.date.relative.upcase = function(text) {
  'use strict';
  // Note: Casing is harder than just handling the first character, so
  // this is an approximation.

  var codepointLength = 1;
  // Check for surrogate values.
  var codePoint0 = text.charCodeAt(0);
  if (codePoint0 >= goog.date.relative.SURROGATE_LOW_ &&
      codePoint0 <= goog.date.relative.SURROGATE_HIGH_) {
    // It's a surrogate.
    codepointLength = 2;
  }
  text = text.substring(0, codepointLength).toLocaleUpperCase() +
      text.substring(codepointLength);
  return text;
};


/**
 * Returns string with "sentence casing" for the input string, i.e.,
 * Finds Day unit in relative date time compatible values, if available.
 * then formats the result using that data.
 * If codepoints are surrogate code points, returns the string unchanged.
 * If no relative non-numeric data is available, returns null.
 *
 * @param {number} dayOffset Offset of day unit for lookup in rdtf symbols data.
 * @return {string|null}
 * @private
 */
goog.date.relative.relativeCasedString_ = function(dayOffset) {
  'use strict';
  var rdtf_formatter =
      new RelativeDateTimeFormat(RelativeDateTimeFormat.NumericOption.AUTO);

  var result =
      rdtf_formatter.format(dayOffset, RelativeDateTimeFormat.Unit.DAY);

  // Check for a digit in expected Auto results, which implies a Numeric
  // result was actually returned.
  // Limitation: This checks only for ASCII, Arabic, ArabicExtended digits.
  if (!result || result.match(/[0-9\u0660-\u0669\u06f0-\u06f9]/g)) {
    return null;
  }

  if (goog.date.relative.casingMode_) {
    return goog.date.relative.upcase(result);
  }
  return result;
};


/**
 * Returns a date in month format, e.g. Mar 15.
 * @param {!Date} date The date object.
 * @return {string} The formatted string.
 * @private
 */
goog.date.relative.formatMonth_ = function(date) {
  'use strict';
  if (!goog.date.relative.monthDateFormatter_) {
    goog.date.relative.monthDateFormatter_ =
        new goog.i18n.DateTimeFormat(goog.i18n.DateTimePatterns.MONTH_DAY_ABBR);
  }
  return goog.date.relative.monthDateFormatter_.format(date);
};


/**
 * Returns a date in short-time format, e.g. 2:50 PM.
 * @param {!Date|!goog.date.DateTime} date The date object.
 * @return {string} The formatted string.
 * @private
 */
goog.date.relative.formatShortTime_ = function(date) {
  'use strict';
  if (!goog.date.relative.shortTimeFormatter_) {
    goog.date.relative.shortTimeFormatter_ = new goog.i18n.DateTimeFormat(
        goog.i18n.DateTimeFormat.Format.SHORT_TIME);
  }
  return goog.date.relative.shortTimeFormatter_.format(date);
};


/**
 * Returns a date in full date format, e.g. Tuesday, March 24, 2009.
 * @param {!Date|!goog.date.DateTime} date The date object.
 * @return {string} The formatted string.
 * @private
 */
goog.date.relative.formatFullDate_ = function(date) {
  'use strict';
  if (!goog.date.relative.fullDateFormatter_) {
    goog.date.relative.fullDateFormatter_ =
        new goog.i18n.DateTimeFormat(goog.i18n.DateTimeFormat.Format.FULL_DATE);
  }
  return goog.date.relative.fullDateFormatter_.format(date);
};


/**
 * Formats quantity and relative unit using i18n.relativedatetimeformat.
 * Converts absolute quantity and unit to relative date time compatible values,
 * then formats the result using that data.
 *
 * @param {number} absQuantity
 * @param {boolean} futureFlag
 * @param {!goog.date.relative.Unit} relUnit
 * @return {string}
 * @private
 */
goog.date.relative.rdtformat_ = function(absQuantity, futureFlag, relUnit) {
  'use strict';
  // Convert absolute value to negative for past, non-negative for future.
  var quantity = futureFlag ? absQuantity : -absQuantity;

  var rdtfFormatter = new RelativeDateTimeFormat();

  var rdtfUnit;
  switch (relUnit) {
    case goog.date.relative.Unit.MINUTES:
      rdtfUnit = RelativeDateTimeFormat.Unit.MINUTE;
      break;
    case goog.date.relative.Unit.HOURS:
      rdtfUnit = RelativeDateTimeFormat.Unit.HOUR;
      break;
    default:
    case goog.date.relative.Unit.DAYS:
      rdtfUnit = RelativeDateTimeFormat.Unit.DAY;
      break;
  }
  // Use locale-aware relatve date time formatter, compatible with ICU4C/ICU4J.
  return rdtfFormatter.format(quantity, rdtfUnit);
};


/**
 * Accepts a timestamp in milliseconds and outputs a relative time in the form
 * of "1 hour ago", "1 day ago", "in 1 hour", "in 2 days" etc.  If the date
 * delta is over 2 weeks, then the output string will be empty.
 * @param {number} dateMs Date in milliseconds.
 * @return {string} The formatted date.
 */
goog.date.relative.format = function(dateMs) {
  'use strict';
  var now = goog.now();
  var delta = Math.floor((now - dateMs) / goog.date.relative.MINUTE_MS_);

  var future = false;

  if (delta < 0) {
    future = true;
    delta *= -1;
  }

  if (delta < 60) {  // Minutes.
    return goog.date.relative.formatTimeDelta_(
        delta, future, goog.date.relative.Unit.MINUTES);

  } else {
    delta = Math.floor(delta / 60);
    if (delta < 24) {  // Hours.
      return goog.date.relative.formatTimeDelta_(
          delta, future, goog.date.relative.Unit.HOURS);

    } else {
      // We can be more than 24 hours apart but still only 1 day apart, so we
      // compare the closest time from today against the target time to find
      // the number of days in the delta.
      var midnight = new Date(goog.now());
      midnight.setHours(0);
      midnight.setMinutes(0);
      midnight.setSeconds(0);
      midnight.setMilliseconds(0);

      // Convert to days ago.
      delta =
          Math.ceil((midnight.getTime() - dateMs) / goog.date.relative.DAY_MS_);

      if (future) {
        delta *= -1;
      }

      // Uses days for less than 2-weeks.
      if (delta < goog.date.relative.FORTNIGHT_) {
        return goog.date.relative.formatTimeDelta_(
            delta, future, goog.date.relative.Unit.DAYS);

      } else {
        // For messages older than 2 weeks do not show anything.  The client
        // should decide the date format to show.
        return '';
      }
    }
  }
};


/**
 * Accepts a timestamp in milliseconds and outputs a relative time in the form
 * of "1 hour ago", "1 day ago".  All future times will be returned as 0 minutes
 * ago.
 *
 * This is provided for compatibility with users of the previous incarnation of
 * the above {@see #format} method who relied on it protecting against
 * future dates.
 *
 * @param {number} dateMs Date in milliseconds.
 * @return {string} The formatted date.
 */
goog.date.relative.formatPast = function(dateMs) {
  'use strict';
  var now = goog.now();
  if (now < dateMs) {
    dateMs = now;
  }
  return goog.date.relative.format(dateMs);
};


/**
 * Accepts a timestamp in milliseconds and outputs a relative day. i.e. "Today",
 * "Yesterday", "Tomorrow", or "Sept 15".
 *
 * @param {number} dateMs Date in milliseconds.
 * @param {function(!Date):string=} opt_formatter Formatter for the date.
 *     Defaults to form 'MMM dd'.
 * @return {string} The formatted date.
 */
goog.date.relative.formatDay = function(dateMs, opt_formatter) {
  'use strict';
  var today = new Date(goog.now());
  const originalTimezoneOffset = today.getTimezoneOffset();

  today.setHours(0);
  today.setMinutes(0);
  today.setSeconds(0);
  today.setMilliseconds(0);

  // It is possible for the time zone to differ between 00:00 and HH:MM on a
  // given day if daylight saving time ended on that day some time before HH:MM.
  // In this case, the number of hours between 00:MM and HH:MM is not HH. It is
  // HH + 1. In most cases this doesn't matter, but if the current date-time is
  // 23:MM in PST on the day daylight saving time ended (e.g. Nov 7, 2021) and
  // `dateMs` is in that same hour, then without correction, the number of hours
  // between 00:00 and 23:MM would be calculated as 24+ hours, causing
  // date-times corresponding to 'Today' to be formatted as 'Tomorrow' (e.g.
  // b/205512072). Here we correct the offset by computing the difference
  // between today's original time zone and the time zone at 00:00.
  const timezoneOffsetCorrection =
      (today.getTimezoneOffset() - originalTimezoneOffset) *
      goog.date.relative.MINUTE_MS_;

  let dayOffset = (dateMs - today.getTime() + timezoneOffsetCorrection) /
      goog.date.relative.DAY_MS_;

  dayOffset = Math.floor(dayOffset);

  var relativeResult = goog.date.relative.relativeCasedString_(dayOffset);

  if (relativeResult) {
    // Return the non-numeric answer such as "ayer" or "tomorrow".
    return relativeResult;
  }

  // Use specialized formatting such as day and month when no
  // special form for the offset is available.
  var formatFunction = opt_formatter || goog.date.relative.formatMonth_;
  return formatFunction(new Date(dateMs));
};


/**
 * Formats a date, adding the relative date in parenthesis.  If the date is less
 * than 24 hours then the time will be printed, otherwise the full-date will be
 * used.  Examples:
 *   2:20 PM (1 minute ago)
 *   Monday, February 27, 2009 (4 days ago)
 *   Tuesday, March 20, 2005    // Too long ago for a relative date.
 *
 * @param {!Date|!goog.date.DateTime} date A date object.
 * @param {string=} opt_shortTimeMsg An optional short time message can be
 *     provided if available, so that it's not recalculated in this function.
 * @param {string=} opt_fullDateMsg An optional date message can be
 *     provided if available, so that it's not recalculated in this function.
 * @return {string} The date string in the above form.
 */
goog.date.relative.getDateString = function(
    date, opt_shortTimeMsg, opt_fullDateMsg) {
  'use strict';
  return goog.date.relative.getDateString_(
      date, goog.date.relative.format, opt_shortTimeMsg, opt_fullDateMsg);
};


/**
 * Formats a date, adding the relative date in parenthesis.   Functions the same
 * as #getDateString but ensures that the date is always seen to be in the past.
 * If the date is in the future, it will be shown as 0 minutes ago.
 *
 * This is provided for compatibility with users of the previous incarnation of
 * the above {@see #getDateString} method who relied on it protecting against
 * future dates.
 *
 * @param {Date|goog.date.DateTime} date A date object.
 * @param {string=} opt_shortTimeMsg An optional short time message can be
 *     provided if available, so that it's not recalculated in this function.
 * @param {string=} opt_fullDateMsg An optional date message can be
 *     provided if available, so that it's not recalculated in this function.
 * @return {string} The date string in the above form.
 */
goog.date.relative.getPastDateString = function(
    date, opt_shortTimeMsg, opt_fullDateMsg) {
  'use strict';
  return goog.date.relative.getDateString_(
      date, goog.date.relative.formatPast, opt_shortTimeMsg, opt_fullDateMsg);
};


/**
 * Formats a date, adding the relative date in parenthesis.  If the date is less
 * than 24 hours then the time will be printed, otherwise the full-date will be
 * used.  Examples:
 *   2:20 PM (1 minute ago)
 *   Monday, February 27, 2009 (4 days ago)
 *   Tuesday, March 20, 2005    // Too long ago for a relative date.
 *
 * @param {Date|goog.date.DateTime} date A date object.
 * @param {function(number) : string} relativeFormatter Function to use when
 *     formatting the relative date.
 * @param {string=} opt_shortTimeMsg An optional short time message can be
 *     provided if available, so that it's not recalculated in this function.
 * @param {string=} opt_fullDateMsg An optional date message can be
 *     provided if available, so that it's not recalculated in this function.
 * @return {string} The date string in the above form.
 * @private
 */
goog.date.relative.getDateString_ = function(
    date, relativeFormatter, opt_shortTimeMsg, opt_fullDateMsg) {
  'use strict';
  var dateMs = date.getTime();

  var relativeDate = relativeFormatter(dateMs);

  if (relativeDate) {
    relativeDate = ' (' + relativeDate + ')';
  }

  var delta = Math.floor((goog.now() - dateMs) / goog.date.relative.MINUTE_MS_);
  if (delta < 60 * 24) {
    // TODO(user): this call raises an exception if date is a goog.date.Date.
    return (opt_shortTimeMsg || goog.date.relative.formatShortTime_(date)) +
        relativeDate;
  } else {
    return (opt_fullDateMsg || goog.date.relative.formatFullDate_(date)) +
        relativeDate;
  }
};
});  // End of scope for RelativeDateTimeFormat.

// Set default formatter for date/time.
goog.date.relative.setTimeDeltaFormatter(goog.date.relative.rdtformat_);
