import getUTCWeekYear from '../../../_lib/getUTCWeekYear/index'
import setUTCDay from '../../../_lib/setUTCDay/index'
import setUTCISODay from '../../../_lib/setUTCISODay/index'
import setUTCISOWeek from '../../../_lib/setUTCISOWeek/index'
import setUTCWeek from '../../../_lib/setUTCWeek/index'
import startOfUTCISOWeek from '../../../_lib/startOfUTCISOWeek/index'
import startOfUTCWeek from '../../../_lib/startOfUTCWeek/index'

var MILLISECONDS_IN_HOUR = 3600000
var MILLISECONDS_IN_MINUTE = 60000
var MILLISECONDS_IN_SECOND = 1000

var numericPatterns = {
  month: /^(1[0-2]|0?\d)/, // 0 to 12
  date: /^(3[0-1]|[0-2]?\d)/, // 0 to 31
  dayOfYear: /^(36[0-6]|3[0-5]\d|[0-2]?\d?\d)/, // 0 to 366
  week: /^(5[0-3]|[0-4]?\d)/, // 0 to 53
  hour23h: /^(2[0-3]|[0-1]?\d)/, // 0 to 23
  hour24h: /^(2[0-4]|[0-1]?\d)/, // 0 to 24
  hour11h: /^(1[0-1]|0?\d)/, // 0 to 11
  hour12h: /^(1[0-2]|0?\d)/, // 0 to 12
  minute: /^[0-5]?\d/, // 0 to 59
  second: /^[0-5]?\d/, // 0 to 59

  singleDigit: /^\d/, // 0 to 9
  twoDigits: /^\d{1,2}/, // 0 to 99
  threeDigits: /^\d{1,3}/, // 0 to 999
  fourDigits: /^\d{1,4}/, // 0 to 9999

  anyDigitsSigned: /^-?\d+/,
  singleDigitSigned: /^-?\d/, // 0 to 9, -0 to -9
  twoDigitsSigned: /^-?\d{1,2}/, // 0 to 99, -0 to -99
  threeDigitsSigned: /^-?\d{1,3}/, // 0 to 999, -0 to -999
  fourDigitsSigned: /^-?\d{1,4}/ // 0 to 9999, -0 to -9999
}

var timezonePatterns = {
  basicOptionalMinutes: /^([+-])(\d{2})(\d{2})?|Z/,
  basic: /^([+-])(\d{2})(\d{2})|Z/,
  basicOptionalSeconds: /^([+-])(\d{2})(\d{2})((\d{2}))?|Z/,
  extended: /^([+-])(\d{2}):(\d{2})|Z/,
  extendedOptionalSeconds: /^([+-])(\d{2}):(\d{2})(:(\d{2}))?|Z/
}

function parseNumericPattern(pattern, string, valueCallback) {
  var matchResult = string.match(pattern)

  if (!matchResult) {
    return null
  }

  var value = parseInt(matchResult[0], 10)

  return {
    value: valueCallback ? valueCallback(value) : value,
    rest: string.slice(matchResult[0].length)
  }
}

function parseTimezonePattern(pattern, string) {
  var matchResult = string.match(pattern)

  if (!matchResult) {
    return null
  }

  // Input is 'Z'
  if (matchResult[0] === 'Z') {
    return {
      value: 0,
      rest: string.slice(1)
    }
  }

  var sign = matchResult[1] === '+' ? 1 : -1
  var hours = matchResult[2] ? parseInt(matchResult[2], 10) : 0
  var minutes = matchResult[3] ? parseInt(matchResult[3], 10) : 0
  var seconds = matchResult[5] ? parseInt(matchResult[5], 10) : 0

  return {
    value:
      sign *
      (hours * MILLISECONDS_IN_HOUR +
        minutes * MILLISECONDS_IN_MINUTE +
        seconds * MILLISECONDS_IN_SECOND),
    rest: string.slice(matchResult[0].length)
  }
}

function parseAnyDigitsSigned(string, valueCallback) {
  return parseNumericPattern(
    numericPatterns.anyDigitsSigned,
    string,
    valueCallback
  )
}

function parseNDigits(n, string, valueCallback) {
  switch (n) {
    case 1:
      return parseNumericPattern(
        numericPatterns.singleDigit,
        string,
        valueCallback
      )
    case 2:
      return parseNumericPattern(
        numericPatterns.twoDigits,
        string,
        valueCallback
      )
    case 3:
      return parseNumericPattern(
        numericPatterns.threeDigits,
        string,
        valueCallback
      )
    case 4:
      return parseNumericPattern(
        numericPatterns.fourDigits,
        string,
        valueCallback
      )
    default:
      return parseNumericPattern(
        new RegExp('^\\d{1,' + n + '}'),
        string,
        valueCallback
      )
  }
}

function parseNDigitsSigned(n, string, valueCallback) {
  switch (n) {
    case 1:
      return parseNumericPattern(
        numericPatterns.singleDigitSigned,
        string,
        valueCallback
      )
    case 2:
      return parseNumericPattern(
        numericPatterns.twoDigitsSigned,
        string,
        valueCallback
      )
    case 3:
      return parseNumericPattern(
        numericPatterns.threeDigitsSigned,
        string,
        valueCallback
      )
    case 4:
      return parseNumericPattern(
        numericPatterns.fourDigitsSigned,
        string,
        valueCallback
      )
    default:
      return parseNumericPattern(
        new RegExp('^-?\\d{1,' + n + '}'),
        string,
        valueCallback
      )
  }
}

function dayPeriodEnumToHours(enumValue) {
  switch (enumValue) {
    case 'morning':
      return 4
    case 'evening':
      return 17
    case 'pm':
    case 'noon':
    case 'afternoon':
      return 12
    case 'am':
    case 'midnight':
    case 'night':
    default:
      return 0
  }
}

function normalizeTwoDigitYear(twoDigitYear, currentYear) {
  var isCommonEra = currentYear > 0
  // Absolute number of the current year:
  // 1 -> 1 AC
  // 0 -> 1 BC
  // -1 -> 2 BC
  var absCurrentYear = isCommonEra ? currentYear : 1 - currentYear

  var result
  if (absCurrentYear <= 50) {
    result = twoDigitYear || 100
  } else {
    var rangeEnd = absCurrentYear + 50
    var rangeEndCentury = Math.floor(rangeEnd / 100) * 100
    var isPreviousCentury = twoDigitYear >= rangeEnd % 100
    result = twoDigitYear + rangeEndCentury - (isPreviousCentury ? 100 : 0)
  }

  return isCommonEra ? result : 1 - result
}

var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
var DAYS_IN_MONTH_LEAP_YEAR = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

// User for validation
function isLeapYearIndex(year) {
  return year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
}

/*
 * |     | Unit                           |     | Unit                           |
 * |-----|--------------------------------|-----|--------------------------------|
 * |  a  | AM, PM                         |  A* | Milliseconds in day            |
 * |  b  | AM, PM, noon, midnight         |  B  | Flexible day period            |
 * |  c  | Stand-alone local day of week  |  C* | Localized hour w/ day period   |
 * |  d  | Day of month                   |  D  | Day of year                    |
 * |  e  | Local day of week              |  E  | Day of week                    |
 * |  f  |                                |  F* | Day of week in month           |
 * |  g* | Modified Julian day            |  G  | Era                            |
 * |  h  | Hour [1-12]                    |  H  | Hour [0-23]                    |
 * |  i! | ISO day of week                |  I! | ISO week of year               |
 * |  j* | Localized hour w/ day period   |  J* | Localized hour w/o day period  |
 * |  k  | Hour [1-24]                    |  K  | Hour [0-11]                    |
 * |  l* | (deprecated)                   |  L  | Stand-alone month              |
 * |  m  | Minute                         |  M  | Month                          |
 * |  n  |                                |  N  |                                |
 * |  o! | Ordinal number modifier        |  O* | Timezone (GMT)                 |
 * |  p  |                                |  P  |                                |
 * |  q  | Stand-alone quarter            |  Q  | Quarter                        |
 * |  r* | Related Gregorian year         |  R! | ISO week-numbering year        |
 * |  s  | Second                         |  S  | Fraction of second             |
 * |  t! | Seconds timestamp              |  T! | Milliseconds timestamp         |
 * |  u  | Extended year                  |  U* | Cyclic year                    |
 * |  v* | Timezone (generic non-locat.)  |  V* | Timezone (location)            |
 * |  w  | Local week of year             |  W* | Week of month                  |
 * |  x  | Timezone (ISO-8601 w/o Z)      |  X  | Timezone (ISO-8601)            |
 * |  y  | Year (abs)                     |  Y  | Local week-numbering year      |
 * |  z* | Timezone (specific non-locat.) |  Z* | Timezone (aliases)             |
 *
 * Letters marked by * are not implemented but reserved by Unicode standard.
 *
 * Letters marked by ! are non-standard, but implemented by date-fns:
 * - `o` modifies the previous token to turn it into an ordinal (see `parse` docs)
 * - `i` is ISO day of week. For `i` and `ii` is returns numeric ISO week days,
 *   i.e. 7 for Sunday, 1 for Monday, etc.
 * - `I` is ISO week of year, as opposed to `w` which is local week of year.
 * - `R` is ISO week-numbering year, as opposed to `Y` which is local week-numbering year.
 *   `R` is supposed to be used in conjunction with `I` and `i`
 *   for universal ISO week-numbering date, whereas
 *   `Y` is supposed to be used in conjunction with `w` and `e`
 *   for week-numbering date specific to the locale.
 */
var parsers = {
  // Era
  G: {
    priority: 140,

    parse: function(string, token, match, _options) {
      switch (token) {
        // AD, BC
        case 'G':
        case 'GG':
        case 'GGG':
          return (
            match.era(string, { width: 'abbreviated' }) ||
            match.era(string, { width: 'narrow' })
          )
        // A, B
        case 'GGGGG':
          return match.era(string, { width: 'narrow' })
        // Anno Domini, Before Christ
        case 'GGGG':
        default:
          return (
            match.era(string, { width: 'wide' }) ||
            match.era(string, { width: 'abbreviated' }) ||
            match.era(string, { width: 'narrow' })
          )
      }
    },

    set: function(date, flags, value, _options) {
      flags.era = value
      date.setUTCFullYear(value, 0, 1)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: ['R', 'u', 't', 'T']
  },

  // Year
  y: {
    // From http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns
    // | Year     |     y | yy |   yyy |  yyyy | yyyyy |
    // |----------|-------|----|-------|-------|-------|
    // | AD 1     |     1 | 01 |   001 |  0001 | 00001 |
    // | AD 12    |    12 | 12 |   012 |  0012 | 00012 |
    // | AD 123   |   123 | 23 |   123 |  0123 | 00123 |
    // | AD 1234  |  1234 | 34 |  1234 |  1234 | 01234 |
    // | AD 12345 | 12345 | 45 | 12345 | 12345 | 12345 |

    priority: 130,

    parse: function(string, token, match, _options) {
      var valueCallback = function(year) {
        return {
          year: year,
          isTwoDigitYear: token === 'yy'
        }
      }

      switch (token) {
        case 'y':
          return parseNDigits(4, string, valueCallback)
        case 'yo':
          return match.ordinalNumber(string, {
            unit: 'year',
            valueCallback: valueCallback
          })
        default:
          return parseNDigits(token.length, string, valueCallback)
      }
    },

    validate: function(_date, value, _options) {
      return value.isTwoDigitYear || value.year > 0
    },

    set: function(date, flags, value, _options) {
      var currentYear = date.getUTCFullYear()

      if (value.isTwoDigitYear) {
        var normalizedTwoDigitYear = normalizeTwoDigitYear(
          value.year,
          currentYear
        )
        date.setUTCFullYear(normalizedTwoDigitYear, 0, 1)
        date.setUTCHours(0, 0, 0, 0)
        return date
      }

      var year =
        !('era' in flags) || flags.era === 1 ? value.year : 1 - value.year
      date.setUTCFullYear(year, 0, 1)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: ['Y', 'R', 'u', 'w', 'I', 'i', 'e', 'c', 't', 'T']
  },

  // Local week-numbering year
  Y: {
    priority: 130,

    parse: function(string, token, match, _options) {
      var valueCallback = function(year) {
        return {
          year: year,
          isTwoDigitYear: token === 'YY'
        }
      }

      switch (token) {
        case 'Y':
          return parseNDigits(4, string, valueCallback)
        case 'Yo':
          return match.ordinalNumber(string, {
            unit: 'year',
            valueCallback: valueCallback
          })
        default:
          return parseNDigits(token.length, string, valueCallback)
      }
    },

    validate: function(_date, value, _options) {
      return value.isTwoDigitYear || value.year > 0
    },

    set: function(date, flags, value, options) {
      var currentYear = getUTCWeekYear(date, options)

      if (value.isTwoDigitYear) {
        var normalizedTwoDigitYear = normalizeTwoDigitYear(
          value.year,
          currentYear
        )
        date.setUTCFullYear(
          normalizedTwoDigitYear,
          0,
          options.firstWeekContainsDate
        )
        date.setUTCHours(0, 0, 0, 0)
        return startOfUTCWeek(date, options)
      }

      var year =
        !('era' in flags) || flags.era === 1 ? value.year : 1 - value.year
      date.setUTCFullYear(year, 0, options.firstWeekContainsDate)
      date.setUTCHours(0, 0, 0, 0)
      return startOfUTCWeek(date, options)
    },

    incompatibleTokens: [
      'y',
      'R',
      'u',
      'Q',
      'q',
      'M',
      'L',
      'I',
      'd',
      'D',
      'i',
      't',
      'T'
    ]
  },

  // ISO week-numbering year
  R: {
    priority: 130,

    parse: function(string, token, _match, _options) {
      if (token === 'R') {
        return parseNDigitsSigned(4, string)
      }

      return parseNDigitsSigned(token.length, string)
    },

    set: function(_date, _flags, value, _options) {
      var firstWeekOfYear = new Date(0)
      firstWeekOfYear.setUTCFullYear(value, 0, 4)
      firstWeekOfYear.setUTCHours(0, 0, 0, 0)
      return startOfUTCISOWeek(firstWeekOfYear)
    },

    incompatibleTokens: [
      'G',
      'y',
      'Y',
      'u',
      'Q',
      'q',
      'M',
      'L',
      'w',
      'd',
      'D',
      'e',
      'c',
      't',
      'T'
    ]
  },

  // Extended year
  u: {
    priority: 130,

    parse: function(string, token, _match, _options) {
      if (token === 'u') {
        return parseNDigitsSigned(4, string)
      }

      return parseNDigitsSigned(token.length, string)
    },

    set: function(date, _flags, value, _options) {
      date.setUTCFullYear(value, 0, 1)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: ['G', 'y', 'Y', 'R', 'w', 'I', 'i', 'e', 'c', 't', 'T']
  },

  // Quarter
  Q: {
    priority: 120,

    parse: function(string, token, match, _options) {
      switch (token) {
        // 1, 2, 3, 4
        case 'Q':
        case 'QQ': // 01, 02, 03, 04
          return parseNDigits(token.length, string)
        // 1st, 2nd, 3rd, 4th
        case 'Qo':
          return match.ordinalNumber(string, { unit: 'quarter' })
        // Q1, Q2, Q3, Q4
        case 'QQQ':
          return (
            match.quarter(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.quarter(string, { width: 'narrow', context: 'formatting' })
          )
        // 1, 2, 3, 4 (narrow quarter; could be not numerical)
        case 'QQQQQ':
          return match.quarter(string, {
            width: 'narrow',
            context: 'formatting'
          })
        // 1st quarter, 2nd quarter, ...
        case 'QQQQ':
        default:
          return (
            match.quarter(string, { width: 'wide', context: 'formatting' }) ||
            match.quarter(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.quarter(string, { width: 'narrow', context: 'formatting' })
          )
      }
    },

    validate: function(_date, value, _options) {
      return value >= 1 && value <= 4
    },

    set: function(date, _flags, value, _options) {
      date.setUTCMonth((value - 1) * 3, 1)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: [
      'Y',
      'R',
      'q',
      'M',
      'L',
      'w',
      'I',
      'd',
      'D',
      'i',
      'e',
      'c',
      't',
      'T'
    ]
  },

  // Stand-alone quarter
  q: {
    priority: 120,

    parse: function(string, token, match, _options) {
      switch (token) {
        // 1, 2, 3, 4
        case 'q':
        case 'qq': // 01, 02, 03, 04
          return parseNDigits(token.length, string)
        // 1st, 2nd, 3rd, 4th
        case 'qo':
          return match.ordinalNumber(string, { unit: 'quarter' })
        // Q1, Q2, Q3, Q4
        case 'qqq':
          return (
            match.quarter(string, {
              width: 'abbreviated',
              context: 'standalone'
            }) ||
            match.quarter(string, { width: 'narrow', context: 'standalone' })
          )
        // 1, 2, 3, 4 (narrow quarter; could be not numerical)
        case 'qqqqq':
          return match.quarter(string, {
            width: 'narrow',
            context: 'standalone'
          })
        // 1st quarter, 2nd quarter, ...
        case 'qqqq':
        default:
          return (
            match.quarter(string, { width: 'wide', context: 'standalone' }) ||
            match.quarter(string, {
              width: 'abbreviated',
              context: 'standalone'
            }) ||
            match.quarter(string, { width: 'narrow', context: 'standalone' })
          )
      }
    },

    validate: function(_date, value, _options) {
      return value >= 1 && value <= 4
    },

    set: function(date, _flags, value, _options) {
      date.setUTCMonth((value - 1) * 3, 1)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: [
      'Y',
      'R',
      'Q',
      'M',
      'L',
      'w',
      'I',
      'd',
      'D',
      'i',
      'e',
      'c',
      't',
      'T'
    ]
  },

  // Month
  M: {
    priority: 110,

    parse: function(string, token, match, _options) {
      var valueCallback = function(value) {
        return value - 1
      }

      switch (token) {
        // 1, 2, ..., 12
        case 'M':
          return parseNumericPattern(
            numericPatterns.month,
            string,
            valueCallback
          )
        // 01, 02, ..., 12
        case 'MM':
          return parseNDigits(2, string, valueCallback)
        // 1st, 2nd, ..., 12th
        case 'Mo':
          return match.ordinalNumber(string, {
            unit: 'month',
            valueCallback: valueCallback
          })
        // Jan, Feb, ..., Dec
        case 'MMM':
          return (
            match.month(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.month(string, { width: 'narrow', context: 'formatting' })
          )
        // J, F, ..., D
        case 'MMMMM':
          return match.month(string, { width: 'narrow', context: 'formatting' })
        // January, February, ..., December
        case 'MMMM':
        default:
          return (
            match.month(string, { width: 'wide', context: 'formatting' }) ||
            match.month(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.month(string, { width: 'narrow', context: 'formatting' })
          )
      }
    },

    validate: function(_date, value, _options) {
      return value >= 0 && value <= 11
    },

    set: function(date, _flags, value, _options) {
      date.setUTCMonth(value, 1)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: [
      'Y',
      'R',
      'q',
      'Q',
      'L',
      'w',
      'I',
      'D',
      'i',
      'e',
      'c',
      't',
      'T'
    ]
  },

  // Stand-alone month
  L: {
    priority: 110,

    parse: function(string, token, match, _options) {
      var valueCallback = function(value) {
        return value - 1
      }

      switch (token) {
        // 1, 2, ..., 12
        case 'L':
          return parseNumericPattern(
            numericPatterns.month,
            string,
            valueCallback
          )
        // 01, 02, ..., 12
        case 'LL':
          return parseNDigits(2, string, valueCallback)
        // 1st, 2nd, ..., 12th
        case 'Lo':
          return match.ordinalNumber(string, {
            unit: 'month',
            valueCallback: valueCallback
          })
        // Jan, Feb, ..., Dec
        case 'LLL':
          return (
            match.month(string, {
              width: 'abbreviated',
              context: 'standalone'
            }) ||
            match.month(string, { width: 'narrow', context: 'standalone' })
          )
        // J, F, ..., D
        case 'LLLLL':
          return match.month(string, { width: 'narrow', context: 'standalone' })
        // January, February, ..., December
        case 'LLLL':
        default:
          return (
            match.month(string, { width: 'wide', context: 'standalone' }) ||
            match.month(string, {
              width: 'abbreviated',
              context: 'standalone'
            }) ||
            match.month(string, { width: 'narrow', context: 'standalone' })
          )
      }
    },

    validate: function(_date, value, _options) {
      return value >= 0 && value <= 11
    },

    set: function(date, _flags, value, _options) {
      date.setUTCMonth(value, 1)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: [
      'Y',
      'R',
      'q',
      'Q',
      'M',
      'w',
      'I',
      'D',
      'i',
      'e',
      'c',
      't',
      'T'
    ]
  },

  // Local week of year
  w: {
    priority: 100,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'w':
          return parseNumericPattern(numericPatterns.week, string)
        case 'wo':
          return match.ordinalNumber(string, { unit: 'week' })
        default:
          return parseNDigits(token.length, string)
      }
    },

    validate: function(_date, value, _options) {
      return value >= 1 && value <= 53
    },

    set: function(date, _flags, value, options) {
      return startOfUTCWeek(setUTCWeek(date, value, options), options)
    },

    incompatibleTokens: [
      'y',
      'R',
      'u',
      'q',
      'Q',
      'M',
      'L',
      'I',
      'd',
      'D',
      'i',
      't',
      'T'
    ]
  },

  // ISO week of year
  I: {
    priority: 100,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'I':
          return parseNumericPattern(numericPatterns.week, string)
        case 'Io':
          return match.ordinalNumber(string, { unit: 'week' })
        default:
          return parseNDigits(token.length, string)
      }
    },

    validate: function(_date, value, _options) {
      return value >= 1 && value <= 53
    },

    set: function(date, _flags, value, options) {
      return startOfUTCISOWeek(setUTCISOWeek(date, value, options), options)
    },

    incompatibleTokens: [
      'y',
      'Y',
      'u',
      'q',
      'Q',
      'M',
      'L',
      'w',
      'd',
      'D',
      'e',
      'c',
      't',
      'T'
    ]
  },

  // Day of the month
  d: {
    priority: 90,

    subPriority: 1,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'd':
          return parseNumericPattern(numericPatterns.date, string)
        case 'do':
          return match.ordinalNumber(string, { unit: 'date' })
        default:
          return parseNDigits(token.length, string)
      }
    },

    validate: function(date, value, _options) {
      var year = date.getUTCFullYear()
      var isLeapYear = isLeapYearIndex(year)
      var month = date.getUTCMonth()
      if (isLeapYear) {
        return value >= 1 && value <= DAYS_IN_MONTH_LEAP_YEAR[month]
      } else {
        return value >= 1 && value <= DAYS_IN_MONTH[month]
      }
    },

    set: function(date, _flags, value, _options) {
      date.setUTCDate(value)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: [
      'Y',
      'R',
      'q',
      'Q',
      'w',
      'I',
      'D',
      'i',
      'e',
      'c',
      't',
      'T'
    ]
  },

  // Day of year
  D: {
    priority: 90,

    subPriority: 1,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'D':
        case 'DD':
          return parseNumericPattern(numericPatterns.dayOfYear, string)
        case 'Do':
          return match.ordinalNumber(string, { unit: 'date' })
        default:
          return parseNDigits(token.length, string)
      }
    },

    validate: function(date, value, _options) {
      var year = date.getUTCFullYear()
      var isLeapYear = isLeapYearIndex(year)
      if (isLeapYear) {
        return value >= 1 && value <= 366
      } else {
        return value >= 1 && value <= 365
      }
    },

    set: function(date, _flags, value, _options) {
      date.setUTCMonth(0, value)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: [
      'Y',
      'R',
      'q',
      'Q',
      'M',
      'L',
      'w',
      'I',
      'd',
      'E',
      'i',
      'e',
      'c',
      't',
      'T'
    ]
  },

  // Day of week
  E: {
    priority: 90,

    parse: function(string, token, match, _options) {
      switch (token) {
        // Tue
        case 'E':
        case 'EE':
        case 'EEE':
          return (
            match.day(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.day(string, { width: 'short', context: 'formatting' }) ||
            match.day(string, { width: 'narrow', context: 'formatting' })
          )
        // T
        case 'EEEEE':
          return match.day(string, { width: 'narrow', context: 'formatting' })
        // Tu
        case 'EEEEEE':
          return (
            match.day(string, { width: 'short', context: 'formatting' }) ||
            match.day(string, { width: 'narrow', context: 'formatting' })
          )
        // Tuesday
        case 'EEEE':
        default:
          return (
            match.day(string, { width: 'wide', context: 'formatting' }) ||
            match.day(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.day(string, { width: 'short', context: 'formatting' }) ||
            match.day(string, { width: 'narrow', context: 'formatting' })
          )
      }
    },

    validate: function(_date, value, _options) {
      return value >= 0 && value <= 6
    },

    set: function(date, _flags, value, options) {
      date = setUTCDay(date, value, options)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: ['D', 'i', 'e', 'c', 't', 'T']
  },

  // Local day of week
  e: {
    priority: 90,
    parse: function(string, token, match, options) {
      var valueCallback = function(value) {
        var wholeWeekDays = Math.floor((value - 1) / 7) * 7
        return ((value + options.weekStartsOn + 6) % 7) + wholeWeekDays
      }

      switch (token) {
        // 3
        case 'e':
        case 'ee': // 03
          return parseNDigits(token.length, string, valueCallback)
        // 3rd
        case 'eo':
          return match.ordinalNumber(string, {
            unit: 'day',
            valueCallback: valueCallback
          })
        // Tue
        case 'eee':
          return (
            match.day(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.day(string, { width: 'short', context: 'formatting' }) ||
            match.day(string, { width: 'narrow', context: 'formatting' })
          )
        // T
        case 'eeeee':
          return match.day(string, { width: 'narrow', context: 'formatting' })
        // Tu
        case 'eeeeee':
          return (
            match.day(string, { width: 'short', context: 'formatting' }) ||
            match.day(string, { width: 'narrow', context: 'formatting' })
          )
        // Tuesday
        case 'eeee':
        default:
          return (
            match.day(string, { width: 'wide', context: 'formatting' }) ||
            match.day(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.day(string, { width: 'short', context: 'formatting' }) ||
            match.day(string, { width: 'narrow', context: 'formatting' })
          )
      }
    },
    validate: function(_date, value, _options) {
      return value >= 0 && value <= 6
    },
    set: function(date, _flags, value, options) {
      date = setUTCDay(date, value, options)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: [
      'y',
      'R',
      'u',
      'q',
      'Q',
      'M',
      'L',
      'I',
      'd',
      'D',
      'E',
      'i',
      'c',
      't',
      'T'
    ]
  },

  // Stand-alone local day of week
  c: {
    priority: 90,

    parse: function(string, token, match, options) {
      var valueCallback = function(value) {
        var wholeWeekDays = Math.floor((value - 1) / 7) * 7
        return ((value + options.weekStartsOn + 6) % 7) + wholeWeekDays
      }

      switch (token) {
        // 3
        case 'c':
        case 'cc': // 03
          return parseNDigits(token.length, string, valueCallback)
        // 3rd
        case 'co':
          return match.ordinalNumber(string, {
            unit: 'day',
            valueCallback: valueCallback
          })
        // Tue
        case 'ccc':
          return (
            match.day(string, {
              width: 'abbreviated',
              context: 'standalone'
            }) ||
            match.day(string, { width: 'short', context: 'standalone' }) ||
            match.day(string, { width: 'narrow', context: 'standalone' })
          )
        // T
        case 'ccccc':
          return match.day(string, { width: 'narrow', context: 'standalone' })
        // Tu
        case 'cccccc':
          return (
            match.day(string, { width: 'short', context: 'standalone' }) ||
            match.day(string, { width: 'narrow', context: 'standalone' })
          )
        // Tuesday
        case 'cccc':
        default:
          return (
            match.day(string, { width: 'wide', context: 'standalone' }) ||
            match.day(string, {
              width: 'abbreviated',
              context: 'standalone'
            }) ||
            match.day(string, { width: 'short', context: 'standalone' }) ||
            match.day(string, { width: 'narrow', context: 'standalone' })
          )
      }
    },

    validate: function(_date, value, _options) {
      return value >= 0 && value <= 6
    },

    set: function(date, _flags, value, options) {
      date = setUTCDay(date, value, options)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: [
      'y',
      'R',
      'u',
      'q',
      'Q',
      'M',
      'L',
      'I',
      'd',
      'D',
      'E',
      'i',
      'e',
      't',
      'T'
    ]
  },

  // ISO day of week
  i: {
    priority: 90,

    parse: function(string, token, match, _options) {
      var valueCallback = function(value) {
        if (value === 0) {
          return 7
        }
        return value
      }

      switch (token) {
        // 2
        case 'i':
        case 'ii': // 02
          return parseNDigits(token.length, string)
        // 2nd
        case 'io':
          return match.ordinalNumber(string, { unit: 'day' })
        // Tue
        case 'iii':
          return (
            match.day(string, {
              width: 'abbreviated',
              context: 'formatting',
              valueCallback: valueCallback
            }) ||
            match.day(string, {
              width: 'short',
              context: 'formatting',
              valueCallback: valueCallback
            }) ||
            match.day(string, {
              width: 'narrow',
              context: 'formatting',
              valueCallback: valueCallback
            })
          )
        // T
        case 'iiiii':
          return match.day(string, {
            width: 'narrow',
            context: 'formatting',
            valueCallback: valueCallback
          })
        // Tu
        case 'iiiiii':
          return (
            match.day(string, {
              width: 'short',
              context: 'formatting',
              valueCallback: valueCallback
            }) ||
            match.day(string, {
              width: 'narrow',
              context: 'formatting',
              valueCallback: valueCallback
            })
          )
        // Tuesday
        case 'iiii':
        default:
          return (
            match.day(string, {
              width: 'wide',
              context: 'formatting',
              valueCallback: valueCallback
            }) ||
            match.day(string, {
              width: 'abbreviated',
              context: 'formatting',
              valueCallback: valueCallback
            }) ||
            match.day(string, {
              width: 'short',
              context: 'formatting',
              valueCallback: valueCallback
            }) ||
            match.day(string, {
              width: 'narrow',
              context: 'formatting',
              valueCallback: valueCallback
            })
          )
      }
    },

    validate: function(_date, value, _options) {
      return value >= 1 && value <= 7
    },

    set: function(date, _flags, value, options) {
      date = setUTCISODay(date, value, options)
      date.setUTCHours(0, 0, 0, 0)
      return date
    },

    incompatibleTokens: [
      'y',
      'Y',
      'u',
      'q',
      'Q',
      'M',
      'L',
      'w',
      'd',
      'D',
      'E',
      'e',
      'c',
      't',
      'T'
    ]
  },

  // AM or PM
  a: {
    priority: 80,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'a':
        case 'aa':
        case 'aaa':
          return (
            match.dayPeriod(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.dayPeriod(string, { width: 'narrow', context: 'formatting' })
          )
        case 'aaaaa':
          return match.dayPeriod(string, {
            width: 'narrow',
            context: 'formatting'
          })
        case 'aaaa':
        default:
          return (
            match.dayPeriod(string, { width: 'wide', context: 'formatting' }) ||
            match.dayPeriod(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.dayPeriod(string, { width: 'narrow', context: 'formatting' })
          )
      }
    },

    set: function(date, _flags, value, _options) {
      date.setUTCHours(dayPeriodEnumToHours(value), 0, 0, 0)
      return date
    },

    incompatibleTokens: ['b', 'B', 'H', 'K', 'k', 't', 'T']
  },

  // AM, PM, midnight
  b: {
    priority: 80,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'b':
        case 'bb':
        case 'bbb':
          return (
            match.dayPeriod(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.dayPeriod(string, { width: 'narrow', context: 'formatting' })
          )
        case 'bbbbb':
          return match.dayPeriod(string, {
            width: 'narrow',
            context: 'formatting'
          })
        case 'bbbb':
        default:
          return (
            match.dayPeriod(string, { width: 'wide', context: 'formatting' }) ||
            match.dayPeriod(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.dayPeriod(string, { width: 'narrow', context: 'formatting' })
          )
      }
    },

    set: function(date, _flags, value, _options) {
      date.setUTCHours(dayPeriodEnumToHours(value), 0, 0, 0)
      return date
    },

    incompatibleTokens: ['a', 'B', 'H', 'K', 'k', 't', 'T']
  },

  // in the morning, in the afternoon, in the evening, at night
  B: {
    priority: 80,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'B':
        case 'BB':
        case 'BBB':
          return (
            match.dayPeriod(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.dayPeriod(string, { width: 'narrow', context: 'formatting' })
          )
        case 'BBBBB':
          return match.dayPeriod(string, {
            width: 'narrow',
            context: 'formatting'
          })
        case 'BBBB':
        default:
          return (
            match.dayPeriod(string, { width: 'wide', context: 'formatting' }) ||
            match.dayPeriod(string, {
              width: 'abbreviated',
              context: 'formatting'
            }) ||
            match.dayPeriod(string, { width: 'narrow', context: 'formatting' })
          )
      }
    },

    set: function(date, _flags, value, _options) {
      date.setUTCHours(dayPeriodEnumToHours(value), 0, 0, 0)
      return date
    },

    incompatibleTokens: ['a', 'b', 't', 'T']
  },

  // Hour [1-12]
  h: {
    priority: 70,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'h':
          return parseNumericPattern(numericPatterns.hour12h, string)
        case 'ho':
          return match.ordinalNumber(string, { unit: 'hour' })
        default:
          return parseNDigits(token.length, string)
      }
    },

    validate: function(_date, value, _options) {
      return value >= 1 && value <= 12
    },

    set: function(date, _flags, value, _options) {
      var isPM = date.getUTCHours() >= 12
      if (isPM && value < 12) {
        date.setUTCHours(value + 12, 0, 0, 0)
      } else if (!isPM && value === 12) {
        date.setUTCHours(0, 0, 0, 0)
      } else {
        date.setUTCHours(value, 0, 0, 0)
      }
      return date
    },

    incompatibleTokens: ['H', 'K', 'k', 't', 'T']
  },

  // Hour [0-23]
  H: {
    priority: 70,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'H':
          return parseNumericPattern(numericPatterns.hour23h, string)
        case 'Ho':
          return match.ordinalNumber(string, { unit: 'hour' })
        default:
          return parseNDigits(token.length, string)
      }
    },

    validate: function(_date, value, _options) {
      return value >= 0 && value <= 23
    },

    set: function(date, _flags, value, _options) {
      date.setUTCHours(value, 0, 0, 0)
      return date
    },

    incompatibleTokens: ['a', 'b', 'h', 'K', 'k', 't', 'T']
  },

  // Hour [0-11]
  K: {
    priority: 70,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'K':
          return parseNumericPattern(numericPatterns.hour11h, string)
        case 'Ko':
          return match.ordinalNumber(string, { unit: 'hour' })
        default:
          return parseNDigits(token.length, string)
      }
    },

    validate: function(_date, value, _options) {
      return value >= 0 && value <= 11
    },

    set: function(date, _flags, value, _options) {
      var isPM = date.getUTCHours() >= 12
      if (isPM && value < 12) {
        date.setUTCHours(value + 12, 0, 0, 0)
      } else {
        date.setUTCHours(value, 0, 0, 0)
      }
      return date
    },

    incompatibleTokens: ['a', 'b', 'h', 'H', 'k', 't', 'T']
  },

  // Hour [1-24]
  k: {
    priority: 70,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'k':
          return parseNumericPattern(numericPatterns.hour24h, string)
        case 'ko':
          return match.ordinalNumber(string, { unit: 'hour' })
        default:
          return parseNDigits(token.length, string)
      }
    },

    validate: function(_date, value, _options) {
      return value >= 1 && value <= 24
    },

    set: function(date, _flags, value, _options) {
      var hours = value <= 24 ? value % 24 : value
      date.setUTCHours(hours, 0, 0, 0)
      return date
    },

    incompatibleTokens: ['a', 'b', 'h', 'H', 'K', 't', 'T']
  },

  // Minute
  m: {
    priority: 60,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 'm':
          return parseNumericPattern(numericPatterns.minute, string)
        case 'mo':
          return match.ordinalNumber(string, { unit: 'minute' })
        default:
          return parseNDigits(token.length, string)
      }
    },

    validate: function(_date, value, _options) {
      return value >= 0 && value <= 59
    },

    set: function(date, _flags, value, _options) {
      date.setUTCMinutes(value, 0, 0)
      return date
    },

    incompatibleTokens: ['t', 'T']
  },

  // Second
  s: {
    priority: 50,

    parse: function(string, token, match, _options) {
      switch (token) {
        case 's':
          return parseNumericPattern(numericPatterns.second, string)
        case 'so':
          return match.ordinalNumber(string, { unit: 'second' })
        default:
          return parseNDigits(token.length, string)
      }
    },

    validate: function(_date, value, _options) {
      return value >= 0 && value <= 59
    },

    set: function(date, _flags, value, _options) {
      date.setUTCSeconds(value, 0)
      return date
    },

    incompatibleTokens: ['t', 'T']
  },

  // Fraction of second
  S: {
    priority: 30,

    parse: function(string, token, _match, _options) {
      var valueCallback = function(value) {
        return Math.floor(value * Math.pow(10, -token.length + 3))
      }
      return parseNDigits(token.length, string, valueCallback)
    },

    set: function(date, _flags, value, _options) {
      date.setUTCMilliseconds(value)
      return date
    },

    incompatibleTokens: ['t', 'T']
  },

  // Timezone (ISO-8601. +00:00 is `'Z'`)
  X: {
    priority: 10,

    parse: function(string, token, _match, _options) {
      switch (token) {
        case 'X':
          return parseTimezonePattern(
            timezonePatterns.basicOptionalMinutes,
            string
          )
        case 'XX':
          return parseTimezonePattern(timezonePatterns.basic, string)
        case 'XXXX':
          return parseTimezonePattern(
            timezonePatterns.basicOptionalSeconds,
            string
          )
        case 'XXXXX':
          return parseTimezonePattern(
            timezonePatterns.extendedOptionalSeconds,
            string
          )
        case 'XXX':
        default:
          return parseTimezonePattern(timezonePatterns.extended, string)
      }
    },

    set: function(date, flags, value, _options) {
      if (flags.timestampIsSet) {
        return date
      }
      return new Date(date.getTime() - value)
    },

    incompatibleTokens: ['t', 'T', 'x']
  },

  // Timezone (ISO-8601)
  x: {
    priority: 10,

    parse: function(string, token, _match, _options) {
      switch (token) {
        case 'x':
          return parseTimezonePattern(
            timezonePatterns.basicOptionalMinutes,
            string
          )
        case 'xx':
          return parseTimezonePattern(timezonePatterns.basic, string)
        case 'xxxx':
          return parseTimezonePattern(
            timezonePatterns.basicOptionalSeconds,
            string
          )
        case 'xxxxx':
          return parseTimezonePattern(
            timezonePatterns.extendedOptionalSeconds,
            string
          )
        case 'xxx':
        default:
          return parseTimezonePattern(timezonePatterns.extended, string)
      }
    },

    set: function(date, flags, value, _options) {
      if (flags.timestampIsSet) {
        return date
      }
      return new Date(date.getTime() - value)
    },

    incompatibleTokens: ['t', 'T', 'X']
  },

  // Seconds timestamp
  t: {
    priority: 40,

    parse: function(string, _token, _match, _options) {
      return parseAnyDigitsSigned(string)
    },

    set: function(_date, _flags, value, _options) {
      return [new Date(value * 1000), { timestampIsSet: true }]
    },

    incompatibleTokens: '*'
  },

  // Milliseconds timestamp
  T: {
    priority: 20,

    parse: function(string, _token, _match, _options) {
      return parseAnyDigitsSigned(string)
    },

    set: function(_date, _flags, value, _options) {
      return [new Date(value), { timestampIsSet: true }]
    },

    incompatibleTokens: '*'
  }
}

export default parsers
