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

/**
 * @fileoverview RelativeDateTimeFormat provides methods to format simple
 * relative dates and times into a string in a user friendly way and a locale
 * sensitive manner. Numeric quantities are supported with negative values
 * indicating the past, zero meaning now, and positive for the future. Specific
 * named times such as "tomorrow" are returned and correctly pluralized results
 * are given for relative times without specific names such as "in 5 days" or "3
 * weeks ago". The result is localized according to current locale value.
 *
 * Similar to the ICU4J class com/ibm/icu/text/RelativeDateTimeFormatter:
 * http://icu-project.org/apiref/icu4j/com/ibm/icu/text/RelativeDateTimeFormatter.html
 */

goog.module('goog.i18n.RelativeDateTimeFormat');

// For referencing goog.i18n.USE_ECMASCRIPT_I18N_RDTF to determine compile-time
// choice of ECMAScript vs. JavaScript implementation and data.
const LocaleFeature = goog.require('goog.i18n.LocaleFeature');

const MessageFormat = goog.require('goog.i18n.MessageFormat');
const asserts = goog.require('goog.asserts');
const relativeDateTimeSymbols = goog.require('goog.i18n.relativeDateTimeSymbols');

/**
 * @constructor
 * @param {!RelativeDateTimeFormat.NumericOption=} opt_numeric
 *     This optional string determines if formatted output is always
 *     the numeric formatting rather than available relative strings.
 *     ALWAYS (default) forces numeric results in all uses of this instance.
 *     AUTO mode uses available relative strings such as "tomorrow", falling
 * back to numeric.
 * @param {!RelativeDateTimeFormat.Style=} opt_style
 *     This optional value determines the style of the relative time output.
 *     Values include LONG, SHORT, NARROW. Default is LONG.
 *     as part of the resulting formatted string
 * @param {!relativeDateTimeSymbols.RelativeDateTimeSymbols=}
 *     opt_relativeDateTimeSymbols This optional value can be used to set the
 *     data for to use for this instance rather than obtaining from
 *     relativedatetimesymbols.
 * @final
 */
let RelativeDateTimeFormat = function(
    opt_numeric, opt_style, opt_relativeDateTimeSymbols) {
  /**
   * Records if the implementation is ECMAScript
   * @private @type {boolean}
   */
  this.nativeMode_ = false;

  if (!LocaleFeature.USE_ECMASCRIPT_I18N_RDTF) {
    asserts.assert(
        opt_relativeDateTimeSymbols ||
            relativeDateTimeSymbols.getRelativeDateTimeSymbols(),
        'goog.i18n.RelativeDateTimeSymbols requires symbols ECMAScript mode');
    /**
     * RelativeDateTimeSymbols object for locale data required by the formatter.
     * @private @const {?relativeDateTimeSymbols.RelativeDateTimeSymbols}
     */
    this.rdtfSymbols_ = !LocaleFeature.USE_ECMASCRIPT_I18N_RDTF ?
        (opt_relativeDateTimeSymbols ||
         relativeDateTimeSymbols.getRelativeDateTimeSymbols()) :
        null;
  }
  if (!this.rdtfSymbols_) {
    this.nativeMode_ = true;
  }

  /**
   * Flag to force numeric mode in all cases. Normally true.
   * @private @type {boolean}
   */
  this.alwaysNumeric_ = true;
  if (opt_numeric) {
    asserts.assert(
        opt_numeric == RelativeDateTimeFormat.NumericOption.ALWAYS ||
            opt_numeric == RelativeDateTimeFormat.NumericOption.AUTO,
        'Invalid opt_numeric value');
    if (opt_numeric == RelativeDateTimeFormat.NumericOption.ALWAYS) {
      this.alwaysNumeric_ = true;
    } else if (opt_numeric == RelativeDateTimeFormat.NumericOption.AUTO) {
      this.alwaysNumeric_ = false;
    }
  }

  /** @private @type {!RelativeDateTimeFormat.Style} */
  this.style_ = RelativeDateTimeFormat.Style.LONG;
  if (opt_style) {
    asserts.assert(
        opt_style >= RelativeDateTimeFormat.Style.LONG &&
            opt_style <= RelativeDateTimeFormat.Style.NARROW,
        'Style must be LONG, SHORT, or NARROW');
    this.style_ = opt_style;
  }
};

/**
 * Values for setting the numeric mode in the constructor.
 * @enum {string}
 */
RelativeDateTimeFormat.NumericOption = {
  ALWAYS: 'always',
  AUTO: 'auto',
};

/**
 * Collection of public style symbols.
 * @enum {number}
 */
RelativeDateTimeFormat.Style = {
  LONG: 0,
  SHORT: 1,
  NARROW: 2
};

/**
 * Relative unit constants for public use.
 * @enum {number}
 */
RelativeDateTimeFormat.Unit = {
  YEAR: 0,
  QUARTER: 1,
  MONTH: 2,
  WEEK: 3,
  DAY: 4,
  HOUR: 5,
  MINUTE: 6,
  SECOND: 7
};

/**
 * Formats a string with the amount and relative unit. If data for the quantity
 * is not available in the requested style, then it falls back to next style. If
 * not available in any style, then it reverts to formatNumeric for the same
 * unit.
 * @param {number} quantity  A desired offset from current time, negative
 *     for past, 0 for now, positive for future.
 * @param {!RelativeDateTimeFormat.Unit} relativeUnit  Type such as HOUR, YEAR,
 *     QUARTER.
 * @return {string} The formatted result. May be empty string for an
 *   unsupported locale.
 */
RelativeDateTimeFormat.prototype.format = function(quantity, relativeUnit) {
  asserts.assertNumber(quantity, 'Quantity must be a number');
  asserts.assert(
      relativeUnit >= RelativeDateTimeFormat.Unit.YEAR &&
          relativeUnit <= RelativeDateTimeFormat.Unit.SECOND,
      'Unit must be one of the supported values');

  /**
   * Special cases to force numeric units, in order
   * to match ICU4J as described in
   * http://unicode.org/cldr/trac/ticket/9165
   * http://bugs.icu-project.org/trac/ticket/12171
   */

  if (LocaleFeature.USE_ECMASCRIPT_I18N_RDTF) {
    return this.formatNative_(quantity, relativeUnit, this.alwaysNumeric_);
  } else {
    return this.formatPolyfill_(quantity, relativeUnit, this.alwaysNumeric_);
  }
};

/**
 * Format using pure JavaScript
 * @param {number} quantity Desired offset from current date/time.
 * @param {!RelativeDateTimeFormat.Unit} relativeUnit  Type such as HOUR, YEAR,
 *     QUARTER.
 * @param {boolean} useNumeric True if numeric output is forced.
 * @return {string} The formatted result. May be empty string for an
 *   unsupported locale.
 * @private
 */
RelativeDateTimeFormat.prototype.formatPolyfill_ = function(
    quantity, relativeUnit, useNumeric) {
  /**
   * Find the right data based on Unit, quantity, and plural.
   */
  const rdtfUnitPattern = this.getUnitStylePattern_(relativeUnit);
  // Formats using Closure Javascript. Check for forcing numeric and having
  // relative value with the given quantity.
  if (!useNumeric && rdtfUnitPattern && rdtfUnitPattern.R &&
      rdtfUnitPattern.R['' + quantity]) {
    return rdtfUnitPattern.R['' + quantity];
  } else {
    // Direction data doesn't exist. Fallback to format numeric.
    return this.formatNumericInternal_(quantity, rdtfUnitPattern);
  }
};

/**
 * Format using ECMAScript Intl class RelativeTimeFormat
 * @param {number} quantity Desired offset from current date/time.
 * @param {!RelativeDateTimeFormat.Unit} relativeUnit  Type such as HOUR, YEAR,
 *     QUARTER.
 * @param {boolean} useNumeric True if numeric output is forced.
 * @return {string} The formatted result. May be empty string for an
 *   unsupported locale.
 * @private
 */
RelativeDateTimeFormat.prototype.formatNative_ = function(
    quantity, relativeUnit, useNumeric) {
  // Use built-in ECMAScript Intl object.
  let options = {
    'numeric': useNumeric ? 'always' : 'auto',
  };
  switch (this.style_) {
    case RelativeDateTimeFormat.Style.NARROW:
      options['style'] = 'narrow';
      break;
    case RelativeDateTimeFormat.Style.SHORT:
      options['style'] = 'short';
      break;
    case RelativeDateTimeFormat.Style.LONG:
      options['style'] = 'long';
    default:
      break;
  }

  // Use built-in ECMAScript Intl object.
  const intl = goog.global.Intl;
  let intlFormatter;
  try {
    // Fix "_" to "-" to correspond to BCP-47.
    intlFormatter =
        new intl.RelativeTimeFormat(goog.LOCALE.replace(/_/g, '-'), options);
  } catch (err) {
    // An empty string is returned for an unsupported LOCALE.
    return '';
  }

  let unit = 'year';
  switch (relativeUnit) {
    case RelativeDateTimeFormat.Unit.YEAR:
      unit = 'year';
      break;
    case RelativeDateTimeFormat.Unit.QUARTER:
      unit = 'quarter';
      break;
    case RelativeDateTimeFormat.Unit.MONTH:
      unit = 'month';
      break;
    case RelativeDateTimeFormat.Unit.WEEK:
      unit = 'week';
      break;
    case RelativeDateTimeFormat.Unit.DAY:
      unit = 'day';
      break;
    case RelativeDateTimeFormat.Unit.HOUR:
      unit = 'hour';
      break;
    case RelativeDateTimeFormat.Unit.MINUTE:
      unit = 'minute';
      break;
    case RelativeDateTimeFormat.Unit.SECOND:
      unit = 'second';
      break;
  }
  return intlFormatter.format(quantity, unit);
};

/**
 * Format with forced numeric value and relative unit.
 * @param {number} quantity  The number of units.
 *     Negative zero will use PAST, while unsiged or positive indicates FUTURE.
 * @param {!relativeDateTimeSymbols.StyleElement|undefined} unitStylePattern Has
 *     PAST and FUTURE fields.
 * @return {string}  The formatted result.
 * @private
 */
RelativeDateTimeFormat.prototype.formatNumericInternal_ = function(
    quantity, unitStylePattern) {
  if (!unitStylePattern) return '';

  /**
   * Stores the plural formatting string.
   * @type {string}
   */
  let relTimeString;
  const absQuantity = Math.abs(quantity);

  // Apply MessageFormat to the unit with FUTURE or PAST quantity, with test for
  // signed zero value.
  if (quantity > 0 || (quantity == 0 && (1 / quantity) == Infinity)) {
    relTimeString = unitStylePattern.F;
  } else {
    // Negative zero is interpreted as the past.
    relTimeString = unitStylePattern.P;
  }

  /**
   * Formatter for the messages requiring units. Plural formatting needed.
   * @type {?MessageFormat}
   */
  // Take basic message and wrap with plural message type.
  const msgFormatter = new MessageFormat('{N,plural,' + relTimeString + '}');
  return msgFormatter.format({'N': absQuantity});
};


/**
 * From the data, return the information for the given unit and style.
 * @param {number} relativeUnit
 * @return {!relativeDateTimeSymbols.StyleElement|undefined}  RelativeUnitStyle
 * @private
 */
RelativeDateTimeFormat.prototype.getUnitStylePattern_ = function(relativeUnit) {
  const unitInfo = this.getUnitPattern_(relativeUnit);
  asserts.assertObject(unitInfo);
  return this.getStylePattern_(unitInfo);
};


/**
 * Use public unit symbol to retrieve data for that unit, given the style.
 * @param{!relativeDateTimeSymbols.RelativeDateTimeFormatStyles} unit
 * @return {!relativeDateTimeSymbols.StyleElement|undefined}
 * @private
 */
RelativeDateTimeFormat.prototype.getStylePattern_ = function(unit) {
  // Fall back from NARROW to SHORT to LONG as needed.
  switch (this.style_) {
    case RelativeDateTimeFormat.Style.NARROW:
      if (unit.NARROW != undefined) {
        return unit.NARROW;
      }
    case RelativeDateTimeFormat.Style.SHORT:
      if (unit.SHORT != undefined) {
        return unit.SHORT;
      }
    case RelativeDateTimeFormat.Style.LONG:
    default:
      return unit.LONG;
  }
};

/**
 * Returns the style set for this formatter.
 * @return {number}  One of LONG, SHORT, NARROW,
 */
RelativeDateTimeFormat.prototype.getFormatStyle = function() {
  return this.style_;
};

/**
 * Returns the status of the alwaysNumeric field.
 * @return {!RelativeDateTimeFormat.NumericOption}
 */
RelativeDateTimeFormat.prototype.getNumericMode = function() {
  if (this.alwaysNumeric_) {
    return RelativeDateTimeFormat.NumericOption.ALWAYS;
  } else {
    return RelativeDateTimeFormat.NumericOption.AUTO;
  }
};

/**
 * Use public unit symbol to retrieve data for that unit.
 * @param {number|!relativeDateTimeSymbols.RelativeDateTimeFormatStyles} unit
 * @return {!relativeDateTimeSymbols.RelativeDateTimeFormatStyles}
 * @private
 */
RelativeDateTimeFormat.prototype.getUnitPattern_ = function(unit) {
  switch (unit) {
    default:
    case RelativeDateTimeFormat.Unit.YEAR:
      return this.rdtfSymbols_.YEAR;
    case RelativeDateTimeFormat.Unit.QUARTER:
      return this.rdtfSymbols_.QUARTER;
    case RelativeDateTimeFormat.Unit.MONTH:
      return this.rdtfSymbols_.MONTH;
    case RelativeDateTimeFormat.Unit.WEEK:
      return this.rdtfSymbols_.WEEK;
    case RelativeDateTimeFormat.Unit.DAY:
      return this.rdtfSymbols_.DAY;
    case RelativeDateTimeFormat.Unit.HOUR:
      return this.rdtfSymbols_.HOUR;
    case RelativeDateTimeFormat.Unit.MINUTE:
      return this.rdtfSymbols_.MINUTE;
    case RelativeDateTimeFormat.Unit.SECOND:
      return this.rdtfSymbols_.SECOND;
  }
};

/**
 * Returns relative field for an offset of a given value unit
 * if it is defined for the current style.
 * If the value does not exist, return undefined.
 * For example, is there a -2 offset for DAY in the current locale and style.
 * Note: This data is not available in an ECMAScript implementation.
 * @param{!RelativeDateTimeFormat.Unit} unit
 * @param{string|number} offset
 * @return{string|undefined}
 * @deprecated
 */
RelativeDateTimeFormat.prototype.isOffsetDefinedForUnit = function(
    unit, offset) {
  if (this.rdtfSymbols_ == undefined) {
    return undefined;
  }

  const rdtfUnitPattern = this.getUnitStylePattern_(unit);
  // Check for force numeric and requested unit and offset.
  if (typeof (offset) == 'string') {
    offset = Number(offset);
  }
  if (rdtfUnitPattern && rdtfUnitPattern.R && rdtfUnitPattern.R['' + offset]) {
    return rdtfUnitPattern.R['' + offset];
  } else {
    return undefined;
  }
};

/**
 * Returns the implementation used for this formatter.
 * @return {boolean}  True iff native mode. False if polyfill.
 * @package
 */
RelativeDateTimeFormat.prototype.isNativeMode = function() {
  return this.nativeMode_;
};

/**
 * Returns true if a ECMAScript formatter is available in the browser.
 * @return {boolean} Whether the ECMAScript implementation available.
 * @package
 */
RelativeDateTimeFormat.prototype.hasNativeRdtf = function() {
  const intl = goog.global.Intl;
  return (Boolean(intl && intl.RelativeTimeFormat));
};

exports = RelativeDateTimeFormat;
