import { isDate } from '../is/is.js'
import { pad, capitalize } from '../format/format.js'
import { jalaaliMonthLength } from './private.persian.js'
import Lang, { defaultLang } from '../../plugins/lang/Lang.js'

const MILLISECONDS_IN_DAY = 86400000,
  MILLISECONDS_IN_HOUR = 3600000,
  MILLISECONDS_IN_MINUTE = 60000,
  defaultMask = 'YYYY-MM-DDTHH:mm:ss.SSSZ',
  token =
    /\[((?:[^\]\\]|\\]|\\)*)\]|do|d{1,4}|Mo|M{1,4}|m{1,2}|wo|w{1,2}|Qo|Do|DDDo|D{1,4}|YY(?:YY)?|H{1,2}|h{1,2}|s{1,2}|S{1,3}|Z{1,2}|a{1,2}|[AQExX]/g,
  reverseToken =
    /(\[[^\]]*\])|do|d{1,4}|Mo|M{1,4}|m{1,2}|wo|w{1,2}|Qo|Do|DDDo|D{1,4}|YY(?:YY)?|H{1,2}|h{1,2}|s{1,2}|S{1,3}|Z{1,2}|a{1,2}|[AQExX]|([.*+:?^,\s${}()|\\]+)/g,
  regexStore = {}

function getRegexData(mask, dateLocale) {
  const days = '(' + dateLocale.days.join('|') + ')',
    key = mask + days

  if (regexStore[key] !== void 0) {
    return regexStore[key]
  }

  const daysShort = '(' + dateLocale.daysShort.join('|') + ')',
    months = '(' + dateLocale.months.join('|') + ')',
    monthsShort = '(' + dateLocale.monthsShort.join('|') + ')'

  const map = {}
  let index = 0

  const regexText = mask.replace(reverseToken, match => {
    index++
    switch (match) {
      case 'YY':
        map.YY = index
        return '(-?\\d{1,2})'
      case 'YYYY':
        map.YYYY = index
        return '(-?\\d{1,4})'
      case 'M':
        map.M = index
        return '(\\d{1,2})'
      case 'Mo':
        map.M = index++ // bumping to M
        return '(\\d{1,2}(st|nd|rd|th))'
      case 'MM':
        map.M = index // bumping to M
        return '(\\d{2})'
      case 'MMM':
        map.MMM = index
        return monthsShort
      case 'MMMM':
        map.MMMM = index
        return months
      case 'D':
        map.D = index
        return '(\\d{1,2})'
      case 'Do':
        map.D = index++ // bumping to D
        return '(\\d{1,2}(st|nd|rd|th))'
      case 'DD':
        map.D = index // bumping to D
        return '(\\d{2})'
      case 'H':
        map.H = index
        return '(\\d{1,2})'
      case 'HH':
        map.H = index // bumping to H
        return '(\\d{2})'
      case 'h':
        map.h = index
        return '(\\d{1,2})'
      case 'hh':
        map.h = index // bumping to h
        return '(\\d{2})'
      case 'm':
        map.m = index
        return '(\\d{1,2})'
      case 'mm':
        map.m = index // bumping to m
        return '(\\d{2})'
      case 's':
        map.s = index
        return '(\\d{1,2})'
      case 'ss':
        map.s = index // bumping to s
        return '(\\d{2})'
      case 'S':
        map.S = index
        return '(\\d{1})'
      case 'SS':
        map.S = index // bump to S
        return '(\\d{2})'
      case 'SSS':
        map.S = index // bump to S
        return '(\\d{3})'
      case 'A':
        map.A = index
        return '(AM|PM)'
      case 'a':
        map.a = index
        return '(am|pm)'
      case 'aa':
        map.aa = index
        return '(a\\.m\\.|p\\.m\\.)'

      case 'ddd':
        return daysShort
      case 'dddd':
        return days
      case 'Q':
      case 'd':
      case 'E':
        return '(\\d{1})'
      case 'do':
        index++
        return '(\\d{1}(st|nd|rd|th))'
      case 'Qo':
        return '(1st|2nd|3rd|4th)'
      case 'DDD':
      case 'DDDD':
        return '(\\d{1,3})'
      case 'DDDo':
        index++
        return '(\\d{1,3}(st|nd|rd|th))'
      case 'w':
        return '(\\d{1,2})'
      case 'wo':
        index++
        return '(\\d{1,2}(st|nd|rd|th))'
      case 'ww':
        return '(\\d{2})'

      case 'Z': // to split: (?:(Z)()()|([+-])?(\\d{2}):?(\\d{2}))
        map.Z = index
        return '(Z|[+-]\\d{2}:\\d{2})'
      case 'ZZ':
        map.ZZ = index
        return '(Z|[+-]\\d{2}\\d{2})'

      case 'X':
        map.X = index
        return '(-?\\d+)'
      case 'x':
        map.x = index
        return '(-?\\d{4,})'

      default:
        index--
        if (match[0] === '[') {
          match = match.substring(1, match.length - 1)
        }
        return match.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
    }
  })

  const res = { map, regex: new RegExp('^' + regexText) }
  regexStore[key] = res

  return res
}

function getDateLocale(paramDateLocale, langProps) {
  return paramDateLocale !== void 0
    ? paramDateLocale
    : langProps !== void 0
      ? langProps.date
      : defaultLang.date
}

function formatTimezone(offset, delimeter = '') {
  const sign = offset > 0 ? '-' : '+',
    absOffset = Math.abs(offset),
    hours = Math.floor(absOffset / 60),
    minutes = absOffset % 60

  return sign + pad(hours) + delimeter + pad(minutes)
}

function applyYearMonthDayChange(date, mod, sign) {
  let year = date.getFullYear(),
    month = date.getMonth()

  const day = date.getDate()

  if (mod.year !== void 0) {
    year += sign * mod.year
    delete mod.year
  }

  if (mod.month !== void 0) {
    month += sign * mod.month
    delete mod.month
  }

  date.setDate(1)
  date.setMonth(2)

  date.setFullYear(year)
  date.setMonth(month)
  date.setDate(Math.min(day, daysInMonth(date)))

  if (mod.date !== void 0) {
    date.setDate(date.getDate() + sign * mod.date)
    delete mod.date
  }

  return date
}

function applyYearMonthDay(date, mod, middle) {
  const year = mod.year !== void 0 ? mod.year : date[`get${middle}FullYear`](),
    month = mod.month !== void 0 ? mod.month - 1 : date[`get${middle}Month`](),
    maxDay = new Date(year, month + 1, 0).getDate(),
    day = Math.min(
      maxDay,
      mod.date !== void 0 ? mod.date : date[`get${middle}Date`]()
    )

  date[`set${middle}Date`](1)
  date[`set${middle}Month`](2)

  date[`set${middle}FullYear`](year)
  date[`set${middle}Month`](month)
  date[`set${middle}Date`](day)

  delete mod.year
  delete mod.month
  delete mod.date

  return date
}

function getChange(date, rawMod, sign) {
  const mod = normalizeMod(rawMod),
    d = new Date(date),
    t =
      mod.year !== void 0 || mod.month !== void 0 || mod.date !== void 0
        ? applyYearMonthDayChange(d, mod, sign) // removes year/month/day
        : d

  for (const key in mod) {
    const op = capitalize(key)
    t[`set${op}`](t[`get${op}`]() + sign * mod[key])
  }

  return t
}

function normalizeMod(mod) {
  const acc = { ...mod }

  if (mod.years !== void 0) {
    acc.year = mod.years
    delete acc.years
  }

  if (mod.months !== void 0) {
    acc.month = mod.months
    delete acc.months
  }

  if (mod.days !== void 0) {
    acc.date = mod.days
    delete acc.days
  }
  if (mod.day !== void 0) {
    acc.date = mod.day
    delete acc.day
  }

  if (mod.hour !== void 0) {
    acc.hours = mod.hour
    delete acc.hour
  }

  if (mod.minute !== void 0) {
    acc.minutes = mod.minute
    delete acc.minute
  }

  if (mod.second !== void 0) {
    acc.seconds = mod.second
    delete acc.second
  }

  if (mod.millisecond !== void 0) {
    acc.milliseconds = mod.millisecond
    delete acc.millisecond
  }

  return acc
}

export function adjustDate(date, rawMod, utc) {
  const mod = normalizeMod(rawMod),
    middle = utc === true ? 'UTC' : '',
    d = new Date(date),
    t =
      mod.year !== void 0 || mod.month !== void 0 || mod.date !== void 0
        ? applyYearMonthDay(d, mod, middle) // removes year/month/day
        : d

  for (const key in mod) {
    const op = key.charAt(0).toUpperCase() + key.slice(1)
    t[`set${middle}${op}`](mod[key])
  }

  return t
}

export function extractDate(str, mask, dateLocale) {
  const d = __splitDate(str, mask, dateLocale)

  const date = new Date(
    d.year,
    d.month === null ? null : d.month - 1,
    d.day === null ? 1 : d.day,
    d.hour,
    d.minute,
    d.second,
    d.millisecond
  )

  const tzOffset = date.getTimezoneOffset()

  return d.timezoneOffset === null || d.timezoneOffset === tzOffset
    ? date
    : getChange(date, { minutes: d.timezoneOffset - tzOffset }, 1)
}

export function __splitDate(str, mask, dateLocale, calendar, defaultModel) {
  const date = {
    year: null,
    month: null,
    day: null,
    hour: null,
    minute: null,
    second: null,
    millisecond: null,
    timezoneOffset: null,
    dateHash: null,
    timeHash: null
  }

  if (defaultModel !== void 0) Object.assign(date, defaultModel)

  if (str === void 0 || str === null || str === '' || typeof str !== 'string') {
    return date
  }

  if (mask === void 0) {
    mask = defaultMask
  }

  const langOpts = getDateLocale(dateLocale, Lang.props),
    months = langOpts.months,
    monthsShort = langOpts.monthsShort

  const { regex, map } = getRegexData(mask, langOpts)

  const match = str.match(regex)

  if (match === null) {
    return date
  }

  let tzString = ''

  if (map.X !== void 0 || map.x !== void 0) {
    const stamp = parseInt(match[map.X !== void 0 ? map.X : map.x], 10)

    if (isNaN(stamp) === true || stamp < 0) {
      return date
    }

    const d = new Date(stamp * (map.X !== void 0 ? 1000 : 1))

    date.year = d.getFullYear()
    date.month = d.getMonth() + 1
    date.day = d.getDate()
    date.hour = d.getHours()
    date.minute = d.getMinutes()
    date.second = d.getSeconds()
    date.millisecond = d.getMilliseconds()
  } else {
    if (map.YYYY !== void 0) {
      date.year = parseInt(match[map.YYYY], 10)
    } else if (map.YY !== void 0) {
      const y = parseInt(match[map.YY], 10)
      date.year = y < 0 ? y : 2000 + y
    }

    if (map.M !== void 0) {
      date.month = parseInt(match[map.M], 10)
      if (date.month < 1 || date.month > 12) {
        return date
      }
    } else if (map.MMM !== void 0) {
      date.month = monthsShort.indexOf(match[map.MMM]) + 1
    } else if (map.MMMM !== void 0) {
      date.month = months.indexOf(match[map.MMMM]) + 1
    }

    if (map.D !== void 0) {
      date.day = parseInt(match[map.D], 10)

      if (date.year === null || date.month === null || date.day < 1) {
        return date
      }

      const maxDay =
        calendar !== 'persian'
          ? new Date(date.year, date.month, 0).getDate()
          : jalaaliMonthLength(date.year, date.month)

      if (date.day > maxDay) {
        return date
      }
    }

    if (map.H !== void 0) {
      date.hour = parseInt(match[map.H], 10) % 24
    } else if (map.h !== void 0) {
      date.hour = parseInt(match[map.h], 10) % 12
      if (
        (map.A && match[map.A] === 'PM') ||
        (map.a && match[map.a] === 'pm') ||
        (map.aa && match[map.aa] === 'p.m.')
      ) {
        date.hour += 12
      }
      date.hour = date.hour % 24
    }

    if (map.m !== void 0) {
      date.minute = parseInt(match[map.m], 10) % 60
    }

    if (map.s !== void 0) {
      date.second = parseInt(match[map.s], 10) % 60
    }

    if (map.S !== void 0) {
      date.millisecond =
        parseInt(match[map.S], 10) * 10 ** (3 - match[map.S].length)
    }

    if (map.Z !== void 0 || map.ZZ !== void 0) {
      tzString =
        map.Z !== void 0 ? match[map.Z].replace(':', '') : match[map.ZZ]
      date.timezoneOffset =
        (tzString[0] === '+' ? -1 : 1) *
        (60 * tzString.slice(1, 3) + Number(tzString.slice(3, 5)))
    }
  }

  date.dateHash =
    pad(date.year, 4) + '/' + pad(date.month) + '/' + pad(date.day)
  date.timeHash =
    pad(date.hour) + ':' + pad(date.minute) + ':' + pad(date.second) + tzString

  return date
}

export function isValid(date) {
  return typeof date === 'number' ? true : isNaN(Date.parse(date)) === false
}

export function buildDate(mod, utc) {
  return adjustDate(new Date(), mod, utc)
}

export function getDayOfWeek(date) {
  const dow = new Date(date).getDay()
  return dow === 0 ? 7 : dow
}

export function getWeekOfYear(date) {
  // Remove time components of date
  const thursday = new Date(date.getFullYear(), date.getMonth(), date.getDate())

  // Change date to Thursday same week
  thursday.setDate(thursday.getDate() - ((thursday.getDay() + 6) % 7) + 3)

  // Take January 4th as it is always in week 1 (see ISO 8601)
  const firstThursday = new Date(thursday.getFullYear(), 0, 4)

  // Change date to Thursday same week
  firstThursday.setDate(
    firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7) + 3
  )

  // Check if daylight-saving-time-switch occurred and correct for it
  const ds = thursday.getTimezoneOffset() - firstThursday.getTimezoneOffset()
  thursday.setHours(thursday.getHours() - ds)

  // Number of weeks between target Thursday and first Thursday
  const weekDiff = (thursday - firstThursday) / (MILLISECONDS_IN_DAY * 7)
  return 1 + Math.floor(weekDiff)
}

function getDayIdentifier(date) {
  return date.getFullYear() * 10000 + date.getMonth() * 100 + date.getDate()
}

function getDateIdentifier(date, onlyDate /* = false */) {
  const d = new Date(date)
  return onlyDate === true ? getDayIdentifier(d) : d.getTime()
}

export function isBetweenDates(date, from, to, opts = {}) {
  const d1 = getDateIdentifier(from, opts.onlyDate),
    d2 = getDateIdentifier(to, opts.onlyDate),
    cur = getDateIdentifier(date, opts.onlyDate)

  return (
    (cur > d1 || (opts.inclusiveFrom === true && cur === d1)) &&
    (cur < d2 || (opts.inclusiveTo === true && cur === d2))
  )
}

export function addToDate(date, mod) {
  return getChange(date, mod, 1)
}
export function subtractFromDate(date, mod) {
  return getChange(date, mod, -1)
}

export function startOfDate(date, unit, utc) {
  const t = new Date(date),
    prefix = `set${utc === true ? 'UTC' : ''}`

  switch (unit) {
    case 'year':
    case 'years':
      t[`${prefix}Month`](0)
    case 'month':
    case 'months':
      t[`${prefix}Date`](1)
    case 'day':
    case 'days':
    case 'date':
      t[`${prefix}Hours`](0)
    case 'hour':
    case 'hours':
      t[`${prefix}Minutes`](0)
    case 'minute':
    case 'minutes':
      t[`${prefix}Seconds`](0)
    case 'second':
    case 'seconds':
      t[`${prefix}Milliseconds`](0)
  }
  return t
}

export function endOfDate(date, unit, utc) {
  const t = new Date(date),
    prefix = `set${utc === true ? 'UTC' : ''}`

  switch (unit) {
    case 'year':
    case 'years':
      t[`${prefix}Month`](11)
    case 'month':
    case 'months':
      t[`${prefix}Date`](daysInMonth(t))
    case 'day':
    case 'days':
    case 'date':
      t[`${prefix}Hours`](23)
    case 'hour':
    case 'hours':
      t[`${prefix}Minutes`](59)
    case 'minute':
    case 'minutes':
      t[`${prefix}Seconds`](59)
    case 'second':
    case 'seconds':
      t[`${prefix}Milliseconds`](999)
  }
  return t
}

export function getMaxDate(date /* , ...args */) {
  let t = new Date(date)
  Array.prototype.slice.call(arguments, 1).forEach(d => {
    t = Math.max(t, new Date(d))
  })
  return new Date(t)
}

export function getMinDate(date /*, ...args */) {
  let t = new Date(date)
  Array.prototype.slice.call(arguments, 1).forEach(d => {
    t = Math.min(t, new Date(d))
  })
  return new Date(t)
}

function getDiff(t, sub, interval) {
  return (
    (t.getTime() -
      t.getTimezoneOffset() * MILLISECONDS_IN_MINUTE -
      (sub.getTime() - sub.getTimezoneOffset() * MILLISECONDS_IN_MINUTE)) /
    interval
  )
}

export function getDateDiff(date, subtract, unit = 'days') {
  const t = new Date(date),
    sub = new Date(subtract)

  switch (unit) {
    case 'years':
    case 'year':
      return t.getFullYear() - sub.getFullYear()

    case 'months':
    case 'month':
      return (
        (t.getFullYear() - sub.getFullYear()) * 12 +
        t.getMonth() -
        sub.getMonth()
      )

    case 'days':
    case 'day':
    case 'date':
      return getDiff(
        startOfDate(t, 'day'),
        startOfDate(sub, 'day'),
        MILLISECONDS_IN_DAY
      )

    case 'hours':
    case 'hour':
      return getDiff(
        startOfDate(t, 'hour'),
        startOfDate(sub, 'hour'),
        MILLISECONDS_IN_HOUR
      )

    case 'minutes':
    case 'minute':
      return getDiff(
        startOfDate(t, 'minute'),
        startOfDate(sub, 'minute'),
        MILLISECONDS_IN_MINUTE
      )

    case 'seconds':
    case 'second':
      return getDiff(startOfDate(t, 'second'), startOfDate(sub, 'second'), 1000)
  }
}

export function getDayOfYear(date) {
  return getDateDiff(date, startOfDate(date, 'year'), 'days') + 1
}

export function inferDateFormat(date) {
  return isDate(date) === true
    ? 'date'
    : typeof date === 'number'
      ? 'number'
      : 'string'
}

export function getDateBetween(date, min, max) {
  const t = new Date(date)

  if (min) {
    const low = new Date(min)
    if (t < low) {
      return low
    }
  }

  if (max) {
    const high = new Date(max)
    if (t > high) {
      return high
    }
  }

  return t
}

export function isSameDate(date, date2, unit) {
  const t = new Date(date),
    d = new Date(date2)

  if (unit === void 0) {
    return t.getTime() === d.getTime()
  }

  switch (unit) {
    case 'second':
    case 'seconds':
      if (t.getSeconds() !== d.getSeconds()) {
        return false
      }
    case 'minute': // intentional fall-through
    case 'minutes':
      if (t.getMinutes() !== d.getMinutes()) {
        return false
      }
    case 'hour': // intentional fall-through
    case 'hours':
      if (t.getHours() !== d.getHours()) {
        return false
      }
    case 'day': // intentional fall-through
    case 'days':
    case 'date':
      if (t.getDate() !== d.getDate()) {
        return false
      }
    case 'month': // intentional fall-through
    case 'months':
      if (t.getMonth() !== d.getMonth()) {
        return false
      }
    case 'year': // intentional fall-through
    case 'years':
      if (t.getFullYear() !== d.getFullYear()) {
        return false
      }
      break
    default:
      throw new Error(`date isSameDate unknown unit ${unit}`)
  }

  return true
}

export function daysInMonth(date) {
  return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
}

function getOrdinal(n) {
  if (n >= 11 && n <= 13) {
    return `${n}th`
  }
  switch (n % 10) {
    case 1:
      return `${n}st`
    case 2:
      return `${n}nd`
    case 3:
      return `${n}rd`
  }
  return `${n}th`
}

const formatter = {
  // Year: 00, 01, ..., 99
  YY(date, dateLocale, forcedYear) {
    // workaround for < 1900 with new Date()
    const y = this.YYYY(date, dateLocale, forcedYear) % 100 // oxlint-disable-line new-cap
    return y >= 0 ? pad(y) : '-' + pad(Math.abs(y))
  },

  // Year: 1900, 1901, ..., 2099
  YYYY(date, _dateLocale, forcedYear) {
    // workaround for < 1900 with new Date()
    return forcedYear !== void 0 && forcedYear !== null
      ? forcedYear
      : date.getFullYear()
  },

  // Month: 1, 2, ..., 12
  M(date) {
    return date.getMonth() + 1
  },

  // Month: 1st, 2nd, ..., 12th
  Mo(date) {
    return getOrdinal(date.getMonth() + 1)
  },

  // Month: 01, 02, ..., 12
  MM(date) {
    return pad(date.getMonth() + 1)
  },

  // Month Short Name: Jan, Feb, ...
  MMM(date, dateLocale) {
    return dateLocale.monthsShort[date.getMonth()]
  },

  // Month Name: January, February, ...
  MMMM(date, dateLocale) {
    return dateLocale.months[date.getMonth()]
  },

  // Quarter: 1, 2, 3, 4
  Q(date) {
    return Math.ceil((date.getMonth() + 1) / 3)
  },

  // Quarter: 1st, 2nd, 3rd, 4th
  Qo(date) {
    return getOrdinal(this.Q(date)) // oxlint-disable-line new-cap
  },

  // Day of month: 1, 2, ..., 31
  D(date) {
    return date.getDate()
  },

  // Day of month: 1st, 2nd, ..., 31st
  Do(date) {
    return getOrdinal(date.getDate())
  },

  // Day of month: 01, 02, ..., 31
  DD(date) {
    return pad(date.getDate())
  },

  // Day of year: 1, 2, ..., 366
  DDD(date) {
    return getDayOfYear(date)
  },

  // Day of year: 1st, 2nd, ..., 366th
  DDDo(date) {
    return getOrdinal(getDayOfYear(date))
  },

  // Day of year: 001, 002, ..., 366
  DDDD(date) {
    return pad(getDayOfYear(date), 3)
  },

  // Day of week: 0, 1, ..., 6
  d(date) {
    return date.getDay()
  },

  // Day of week: 0th, 1st, ..., 6th
  do(date) {
    return getOrdinal(date.getDay())
  },

  // Day of week: Su, Mo, ...
  dd(date, dateLocale) {
    return dateLocale.days[date.getDay()].slice(0, 2)
  },

  // Day of week: Sun, Mon, ...
  ddd(date, dateLocale) {
    return dateLocale.daysShort[date.getDay()]
  },

  // Day of week: Sunday, Monday, ...
  dddd(date, dateLocale) {
    return dateLocale.days[date.getDay()]
  },

  // Day of ISO week: 1, 2, ..., 7
  E(date) {
    return date.getDay() || 7
  },

  // Week of Year: 1 2 ... 52 53
  w(date) {
    return getWeekOfYear(date)
  },

  // Week of Year: 1st 2nd ... 52nd 53rd
  wo(date) {
    return getOrdinal(getWeekOfYear(date))
  },

  // Week of Year: 01 02 ... 52 53
  ww(date) {
    return pad(getWeekOfYear(date))
  },

  // Hour: 0, 1, ... 23
  H(date) {
    return date.getHours()
  },

  // Hour: 00, 01, ..., 23
  HH(date) {
    return pad(date.getHours())
  },

  // Hour: 1, 2, ..., 12
  h(date) {
    const hours = date.getHours()
    return hours === 0 ? 12 : hours > 12 ? hours % 12 : hours
  },

  // Hour: 01, 02, ..., 12
  hh(date) {
    return pad(this.h(date))
  },

  // Minute: 0, 1, ..., 59
  m(date) {
    return date.getMinutes()
  },

  // Minute: 00, 01, ..., 59
  mm(date) {
    return pad(date.getMinutes())
  },

  // Second: 0, 1, ..., 59
  s(date) {
    return date.getSeconds()
  },

  // Second: 00, 01, ..., 59
  ss(date) {
    return pad(date.getSeconds())
  },

  // 1/10 of second: 0, 1, ..., 9
  S(date) {
    return Math.floor(date.getMilliseconds() / 100)
  },

  // 1/100 of second: 00, 01, ..., 99
  SS(date) {
    return pad(Math.floor(date.getMilliseconds() / 10))
  },

  // Millisecond: 000, 001, ..., 999
  SSS(date) {
    return pad(date.getMilliseconds(), 3)
  },

  // Meridiem: AM, PM
  A(date) {
    return date.getHours() < 12 ? 'AM' : 'PM'
  },

  // Meridiem: am, pm
  a(date) {
    return date.getHours() < 12 ? 'am' : 'pm'
  },

  // Meridiem: a.m., p.m.
  aa(date) {
    return date.getHours() < 12 ? 'a.m.' : 'p.m.'
  },

  // Timezone: -01:00, +00:00, ... +12:00
  Z(date, _dateLocale, _forcedYear, forcedTimezoneOffset) {
    const tzOffset =
      forcedTimezoneOffset === void 0 || forcedTimezoneOffset === null
        ? date.getTimezoneOffset()
        : forcedTimezoneOffset

    return formatTimezone(tzOffset, ':')
  },

  // Timezone: -0100, +0000, ... +1200
  ZZ(date, _dateLocale, _forcedYear, forcedTimezoneOffset) {
    const tzOffset =
      forcedTimezoneOffset === void 0 || forcedTimezoneOffset === null
        ? date.getTimezoneOffset()
        : forcedTimezoneOffset

    return formatTimezone(tzOffset)
  },

  // Seconds timestamp: 512969520
  X(date) {
    return Math.floor(date.getTime() / 1000)
  },

  // Milliseconds timestamp: 512969520900
  x(date) {
    return date.getTime()
  }
}

export function formatDate(
  val,
  mask,
  dateLocale,
  __forcedYear,
  __forcedTimezoneOffset
) {
  if ((val !== 0 && !val) || val === Infinity || val === -Infinity) return

  const date = new Date(val)

  if (isNaN(date)) return

  if (mask === void 0) {
    mask = defaultMask
  }

  const locale = getDateLocale(dateLocale, Lang.props)

  return mask.replace(token, (match, text) =>
    match in formatter
      ? formatter[match](date, locale, __forcedYear, __forcedTimezoneOffset)
      : text === void 0
        ? match
        : text.split('\\]').join(']')
  )
}

export function clone(date) {
  return isDate(date) === true ? new Date(date.getTime()) : date
}

export default {
  isValid,
  extractDate,
  buildDate,
  getDayOfWeek,
  getWeekOfYear,
  isBetweenDates,
  addToDate,
  subtractFromDate,
  adjustDate,
  startOfDate,
  endOfDate,
  getMaxDate,
  getMinDate,
  getDateDiff,
  getDayOfYear,
  inferDateFormat,
  getDateBetween,
  isSameDate,
  daysInMonth,
  formatDate,
  clone
}
