/* global _defaultLang, _collator */
/**
 * Dates and Numbers formatting and string comparison using Intl
 * On client this module exposed as `UB.formatter` and `Vue.prototype.$UB.formatter`
 *
 * - for available date format options see https://tc39.es/ecma402/#datetimeformat-objects
 * - for available number format options see https://tc39.es/ecma402/#numberformat-objects
 *
 * @module formatByPattern
 * @author xmax
 * @memberOf module:@unitybase/cs-shared
 */
// {month:  '2-digit', day: 'numeric', year: 'numeric',  hour: '2-digit', minute: '2-digit', second: '2-digit'})
const datePatterns = {
  date: { month: '2-digit', day: '2-digit', year: 'numeric' },
  dateFull: { month: '2-digit', day: '2-digit', year: '2-digit' },
  dateShort: { month: '2-digit', year: '2-digit' },
  dateFullLong: { month: 'long', day: '2-digit', year: 'numeric' },
  dateMYY: { month: '2-digit', year: 'numeric' },
  dateMYLong: { month: 'long', year: 'numeric' },
  time: { hour: '2-digit', minute: '2-digit' },
  timeFull: { hour: '2-digit', minute: '2-digit', second: '2-digit' },
  dateTime: { month: '2-digit', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' },
  dateTimeFull: { month: '2-digit', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' },
  dateTimeFullWithTz: { month: '2-digit', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', timeZoneName: 'short' }
}
// {style: 'decimal', useGrouping: true, minimumIntegerDigits: 10, maximumFractionDigits: 2, minimumFractionDigits: 2, minimumSignificantDigits: 5}
const numberPatterns = {
  sum: { style: 'decimal', useGrouping: true, maximumFractionDigits: 2, minimumFractionDigits: 2 },
  numberGroup: { style: 'decimal', useGrouping: true, maximumFractionDigits: 0 },
  sumDelim: { style: 'decimal', useGrouping: true, maximumFractionDigits: 2, minimumFractionDigits: 2 },
  number: { style: 'decimal', useGrouping: false, maximumFractionDigits: 0 },
  decimal1: { style: 'decimal', useGrouping: true, maximumFractionDigits: 1, minimumFractionDigits: 1 },
  decimal2: { style: 'decimal', useGrouping: true, maximumFractionDigits: 2, minimumFractionDigits: 2 },
  decimal3: { style: 'decimal', useGrouping: true, maximumFractionDigits: 3, minimumFractionDigits: 3 },
  decimal4: { style: 'decimal', useGrouping: true, maximumFractionDigits: 4, minimumFractionDigits: 4 },
  decimal5: { style: 'decimal', useGrouping: true, maximumFractionDigits: 5, minimumFractionDigits: 5 },
  decimal6: { style: 'decimal', useGrouping: true, maximumFractionDigits: 6, minimumFractionDigits: 6 }
}

const datePatternNames = Object.keys(datePatterns)
const numberPatternNames = Object.keys(numberPatterns)

/**
 * lang to ICU locale hook (if defined by setLang2LocaleHook)
 *
 * @private
 * @type {null|Function}
 */
let l2lHook = null
/**
 * default language to ICU locale transformation. Can be extended/overridden by formatByPattern.addIrregularLangToLocales
 */
let langToICUDefaults = {
  en: 'en-GB',
  ru: 'ru-RU',
  uk: 'uk-UA',
  az: 'az'
}

// TODO - FIX ME by prevent `@unitybase/cs-shared` package includes into every compiled module
//  (adminui-pub, adminui-vue, vendor packages etc.).
if (typeof _defaultLang === 'undefined') {
  // eslint-disable-next-line no-global-assign
  _defaultLang = null
  setDefaultLang('en')
}

let lang2localeCache = {}
/**
 * Return a ICU locale based on UB language
 *
 * @param {string} lang
 * @returns {string}
 */
function lang2locale (lang) {
  lang = lang || _defaultLang
  if (lang2localeCache[lang]) {
    return lang2localeCache[lang]
  } else {
    let locale
    if (l2lHook) {
      locale = l2lHook(lang)
    } else {
      if ((lang.length < 3) && langToICUDefaults[lang]) {
        locale = langToICUDefaults[lang]
      } else {
        locale = lang + '-' + lang.toUpperCase()
      }
    }
    lang2localeCache[lang] = locale
    return locale
  }
}

module.exports.lang2locale = lang2locale

/**
 * Intl number formatters cache.
 *
 * Keys is a language, values is an object with keys is date pattern, value is Intl.NumberFormat for this pattern
 * {en: {sum: new Intl.NumberFormat('en-US', numberPatterns.sum)}
 *
 * @private
 */
let numberFormatters = {}

/**
 * Intl Date formatters cache.
 *
 * Keys is a language, values is an object with keys is date pattern, value is Intl.DateTimeFormat for this pattern
 * {en: {date: new Intl.DateTimeFormat('en-US', datePatterns.date)}
 *
 * @private
 */
let dateTimeFormatters = {}

/**
 * Format date by pattern
 *
 * @example
const formatByPattern = require('@unitybase/cs-shared').formatByPattern
const d = new Date(2020, 04, 23, 13, 14)
formatByPattern.formatDate(d, 'date') // on client can be called without 3rd lang parameter - will be formatted for user default lang (for uk - 23.05.2020)
formatByPattern.formatDate('2020-05-23', 'date', 'uk') // 23.05.2020
formatByPattern.formatDate(d, 'date', 'en') // 05/23/2020
formatByPattern.formatDate(d, 'dateTime', 'uk') // 23.05.2020 13:14
formatByPattern.formatDate(d, 'dateTimeFull', 'uk') // 23.05.2020 13:14:00
 * @param {*} dateVal Date object or Number/String what will be converted to Date using new Date();
 *   null, undefined and empty string will be converted to empty string
 * @param {string} patternName One of `formatByPattern.datePatterns`
 * @param {string} [lang=defaultLang] UB language code. If not specified value defined by setDefaultLang is used
 * @returns {string}
 */
module.exports.formatDate = function (dateVal, patternName, lang = _defaultLang) {
  if (!dateVal) return ''
  if (!(dateVal instanceof Date)) dateVal = new Date(dateVal)

  // lazy create Intl object
  if (!dateTimeFormatters[lang]) dateTimeFormatters[lang] = {}
  if (!dateTimeFormatters[lang][patternName]) {
    const pattern = datePatterns[patternName]
    if (!pattern) throw new Error(`Unknown date pattern ${patternName}`)
    const locale = lang2locale(lang)
    dateTimeFormatters[lang][patternName] = new Intl.DateTimeFormat(locale, pattern)
  }
  return dateTimeFormatters[lang][patternName].format(dateVal)
}

/**
 * Format number by pattern. Use parseFloat to convert non-number numVal argument into Number. Returns empty string for `!numVal` and `NaN`
 *
 * @example
const formatByPattern = require('@unitybase/cs-shared').formatByPattern
const n = 2305.1
formatByPattern.formatNumber(n, 'sum', 'en') // 2,305.10
formatByPattern.formatNumber('2305.1', 'sum', 'en') // 2,305.10
formatByPattern.formatNumber(n, 'sum') // on client can be called without 3rd lang parameter - will be formatted for user default lang (for uk "2 305,10")
 * @param {*} numVal
 * @param {string} patternName One of `formatByPattern.datePatterns`
 * @param {string} [lang=defaultLang] UB language code. If not specified value defined by `setDefaultLang` is used
 * @returns {string}
 */
module.exports.formatNumber = function (numVal, patternName, lang = _defaultLang) {
  if (!numVal && (numVal !== 0)) return ''
  const v = (typeof numVal === 'number') ? numVal : parseFloat(numVal)
  if (Number.isNaN(v)) return ''
  // lazy create Intl object
  if (!numberFormatters[lang]) numberFormatters[lang] = {}
  if (!numberFormatters[lang][patternName]) {
    const pattern = numberPatterns[patternName]
    if (!pattern) throw new Error(`Unknown number pattern ${patternName}`)
    const locale = lang2locale(lang)
    numberFormatters[lang][patternName] = new Intl.NumberFormat(locale, pattern)
  }
  return numberFormatters[lang][patternName].format(numVal)
}

/**
 * Set application-specific UB lang to ICU locale transformation hook.
 * Default hook uses `{en: 'en-US', ru: 'ru-RU', uk: 'uk-UA', az: 'az'}` translation, any other language `ln` translated into `ln-LN`.
 *
 * Application can redefine this rule by sets his own hook, for example to translate `en -> 'en-GB'` etc.
 *
 * @param {Function} newL2lHook function what takes a UB language string and returns a ICU locale string
 */
module.exports.setLang2LocaleHook = function (newL2lHook) {
  l2lHook = newL2lHook
  // reset cache
  numberFormatters = {}
  dateTimeFormatters = {}
}

/**
 * Register custom (instead of Intl) Date/DateTime formatter for specified language and pattern.
 *
 * @param {string} lang
 * @param {string} patternName
 * @param {Function} formatFunction function what accept Date as parameter and returns string formatted according to `lang` and `patternName`
 */
module.exports.registerCustomDateTimeFormatter = function (lang, patternName, formatFunction) {
  if (!dateTimeFormatters[lang]) dateTimeFormatters[lang] = {}
  if (!dateTimeFormatters[lang][patternName]) dateTimeFormatters[lang][patternName] = {}
  dateTimeFormatters[lang][patternName].format = formatFunction
}

/**
 * Register custom (instead of Intl) Number formatter for specified language and pattern.
 *
 * @param {string} lang
 * @param {string} patternName
 * @param {Function} formatFunction function what accept number as parameter and returns string formatted according to `lang` and `patternName`
 */
module.exports.registerCustomNumberFormatter = function (lang, patternName, formatFunction) {
  if (!numberFormatters[lang]) numberFormatters[lang] = {}
  if (!numberFormatters[lang][patternName]) numberFormatters[lang][patternName] = {}
  numberFormatters[lang][patternName].format = formatFunction
}

/**
 * Available date patterns
 *
 * @type {string[]}
 */
module.exports.datePatterns = datePatternNames
/**
 * Available Number patterns
 *
 * @type {string[]}
 */
module.exports.numberPatterns = numberPatternNames

/**
 * Registers custom date pattern (should be called once)
 *
 * @example
// format Date for New_York time zone
$UB.formatter.registerDatePattern('dateTimeInNewYork', {
  month: '2-digit', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit',
  timeZone: 'America/New_York'
})
 * @param {string} patternName Pattern name
 * @param {object} intlOptions Intl.DateFormat constructor options - see https://tc39.es/ecma402/#datetimeformat-objects
 */
module.exports.registerDatePattern = function (patternName, intlOptions) {
  if (!patternName || typeof patternName !== 'string') {
    throw new Error('Invalid date pattern name')
  }
  if (datePatterns[patternName]) {
    throw new Error(`Date pattern ${patternName} already registered`)
  }
  if (!intlOptions || typeof intlOptions !== 'object') {
    throw new Error('Invalid intlOptions - should be object')
  }
  datePatterns[patternName] = intlOptions
  datePatternNames.push(patternName)
}

/**
 * Registers custom number pattern
 *
 * @param {string} patternName Pattern name
 * @param {object} intlOptions Intl.NumberFormat constructor options - see https://tc39.es/ecma402/#numberformat-objects
 */
module.exports.registerNumberPattern = function (patternName, intlOptions) {
  if (!patternName || typeof patternName !== 'string') {
    throw new Error('Invalid number pattern name')
  }
  if (numberPatterns[patternName]) {
    throw new Error(`Number pattern ${patternName} already registered`)
  }
  if (!intlOptions || typeof intlOptions !== 'object') {
    throw new Error('Invalid intlOptions - should be object')
  }
  numberPatterns[patternName] = intlOptions
  numberPatternNames.push(patternName)
}

/**
 * Gets date pattern by name
 *
 * @param {string} patternName Pattern name
 * @returns {object} Pattern description for Intl
 */
module.exports.getDatePattern = function (patternName) {
  return datePatterns[patternName] && Object.assign({}, datePatterns[patternName])
}

/**
 * Gets number pattern by name
 *
 * @param {string} patternName Pattern name
 * @returns {object} Pattern description for Intl
 */
module.exports.getNumberPattern = function (patternName) {
  return numberPatterns[patternName] && Object.assign({}, numberPatterns[patternName])
}

/**
 * Set a default language to use with `strCmp`, `formatNumber` and `formatDate`.
 * For UI this is usually a logged-in user language
 *
 * @param {string} lang
 */
function setDefaultLang (lang) {
  if (_defaultLang === lang) return
  // eslint-disable-next-line no-global-assign
  _defaultLang = lang
  // eslint-disable-next-line no-global-assign
  _collator = null
  if ((typeof Intl === 'object') && Intl.Collator) {
    // eslint-disable-next-line no-global-assign
    _collator = new Intl.Collator(lang, { numeric: true })
  }
}

module.exports.setDefaultLang = setDefaultLang

/**
 * Add language to locales map, what overrides defaults
 *
 * @param {object} additionLang2locales
 */
function addIrregularLangToLocales (additionLang2locales) {
  langToICUDefaults = Object.assign(langToICUDefaults, additionLang2locales)
  lang2localeCache = {}
}

module.exports.addIrregularLangToLocales = addIrregularLangToLocales

/**
 * Compare two value:
 *  - if one of value is string takes into account current client locale (see setDefaultLang)
 *  - `null' and `undefined' are always smaller than any other value
 *  - `Date` objects compared correctly (using getTime())
 * Returns 0 if values are equal, otherwise 1 or -1
 *
 * @param {*} v1
 * @param {*} v2
 * @returns {number}
 */
module.exports.collationCompare = function (v1, v2) {
  if (_collator && ((typeof v1 === 'string') || (typeof v2 === 'string'))) { // Use collator for strings
    return _collator.compare(v1, v2)
  } else {
    // place null and undefined first (== is used instead of === to null\undefined compare)
    if ((v1 == null) && (v2 != null)) {
      return -1
    } else if ((v2 == null) && (v1 != null)) {
      return 1
    }
    // compare date using seconds since epoch
    if (v1 instanceof Date) {
      v1 = v1.getTime()
    }
    if (v2 instanceof Date) {
      v2 = v2.getTime()
    }

    if (v1 === v2) return 0
    return v1 > v2 ? 1 : -1
  }
}