/**
 * Copyright (c) 2022 - present TinyVue Authors.
 * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
 *
 * Use of this source code is governed by an MIT-style license.
 *
 * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
 * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
 * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
 *
 */

import { DATEPICKER } from '../index'
import { isNull, isDate } from '../type'

const fecha = {}
const digitsReg = ['\\d\\d?', '\\d{3}', '\\d{4}']
const twoDigits = digitsReg[0]
const threeDigits = digitsReg[1]
const fourDigits = digitsReg[2]
const word = '[^\\s]+'
const literal = /\[([^]*?)\]/gm
const noop = () => undefined
const formats = {
  shortDate: 'M/D/yy',
  mediumDate: 'MMM d, yyyy',
  longDate: 'MMMM d, yyyy',
  fullDate: 'dddd, MMMM d, yyyy',
  default: 'ddd MMM dd yyyy HH:mm:ss',
  shortTime: 'HH:mm',
  mediumTime: 'HH:mm:ss',
  longTime: 'HH:mm:ss.SSS'
}

const shorten = (arr, sLen) => {
  let newArr = []

  for (let i = 0, len = arr.length; i < len; i++) {
    newArr.push(arr[i].substr(0, sLen))
  }

  return newArr
}

const monthUpdate = (arrName) => (date, value, i18n) => {
  const index = i18n[arrName].indexOf(value.charAt(0).toUpperCase() + value.substr(1).toLowerCase())

  if (~index) {
    date.month = index
  }
}

const pad = (val, len) => {
  val = String(val)
  len = len || 2

  while (val.length < len) {
    val = '0' + val
  }

  return val
}

const regexEscape = (str) => str.replace(/[|\\{()[^$+*?.-]/g, '\\$&')

const fullTimeReg = /d{1,4}|M{1,4}|yy(?:yy)?|S{1,3}|Do|ZZ|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g
const dayNames = DATEPICKER.fullWeeks
const monthNames = DATEPICKER.fullMonths
const monthNamesShort = shorten(monthNames, 3)
const dayNamesShort = shorten(dayNames, 3)
const parts = ['th', 'st', 'nd', 'rd']

fecha.i18n = {
  dayNames,
  monthNames,
  dayNamesShort,
  monthNamesShort,
  amPm: ['am', 'pm'],
  doFn: (D) => D + parts[D % 10 > 3 ? 0 : ((D - (D % 10) !== 10) * D) % 10]
}

const formatFlags = {
  D: (dateObj) => dateObj.getDay(),
  DD: (dateObj) => pad(dateObj.getDay()),
  Do: (dateObj, i18n) => i18n.doFn(dateObj.getDate()),
  d: (dateObj) => dateObj.getDate(),
  dd: (dateObj) => pad(dateObj.getDate()),
  ddd: (dateObj, i18n) => i18n.dayNamesShort[dateObj.getDay()],
  dddd: (dateObj, i18n) => i18n.dayNames[dateObj.getDay()],
  M: (dateObj) => dateObj.getMonth() + 1,
  MM: (dateObj) => pad(dateObj.getMonth() + 1),
  MMM: (dateObj, i18n) => i18n.monthNamesShort[dateObj.getMonth()],
  MMMM: (dateObj, i18n) => i18n.monthNames[dateObj.getMonth()],
  yy: (dateObj) => pad(String(dateObj.getFullYear()), 4).substr(2),
  yyyy: (dateObj) => pad(dateObj.getFullYear(), 4),
  h: (dateObj) => dateObj.getHours() % 12 || 12,
  hh: (dateObj) => pad(dateObj.getHours() % 12 || 12),
  H: (dateObj) => dateObj.getHours(),
  HH: (dateObj) => pad(dateObj.getHours()),
  m: (dateObj) => dateObj.getMinutes(),
  mm: (dateObj) => pad(dateObj.getMinutes()),
  s: (dateObj) => dateObj.getSeconds(),
  ss: (dateObj) => pad(dateObj.getSeconds()),
  S: (dateObj) => Math.round(dateObj.getMilliseconds() / 100),
  SS: (dateObj) => pad(Math.round(dateObj.getMilliseconds() / 10), 2),
  SSS: (dateObj) => pad(dateObj.getMilliseconds(), 3),
  a: (dateObj, i18n) => (dateObj.getHours() < 12 ? i18n.amPm[0] : i18n.amPm[1]),
  A: (dateObj, i18n) => (dateObj.getHours() < 12 ? i18n.amPm[0].toUpperCase() : i18n.amPm[1].toUpperCase()),
  ZZ: (dateObj) => {
    const offset = dateObj.getTimezoneOffset()
    return (offset > 0 ? '-' : '+') + pad(Math.floor(Math.abs(offset) / 60) * 100 + (Math.abs(offset) % 60), 4)
  }
}

const parseFlags = {
  d: [
    twoDigits,
    (date, value) => {
      date.day = value
    }
  ],
  Do: [
    twoDigits + word,
    (date, value) => {
      date.day = parseInt(value, 10)
    }
  ],
  M: [
    twoDigits,
    (date, value) => {
      date.month = value - 1
    }
  ],
  yy: [
    twoDigits,
    (date, value) => {
      const now = new Date()
      const cent = Number(String(now.getFullYear()).substr(0, 2))
      date.year = String(value > 68 ? cent - 1 : cent) + value
    }
  ],
  h: [
    twoDigits,
    (date, value) => {
      date.hour = value
    }
  ],
  m: [
    twoDigits,
    (date, value) => {
      date.minute = value
    }
  ],
  s: [
    twoDigits,
    (date, value) => {
      date.second = value
    }
  ],
  yyyy: [
    fourDigits,
    (date, value) => {
      date.year = value
    }
  ],
  S: [
    '\\d',
    (date, value) => {
      date.millisecond = value * 100
    }
  ],
  SS: [
    '\\d{2}',
    (date, value) => {
      date.millisecond = value * 10
    }
  ],
  SSS: [
    threeDigits,
    (date, value) => {
      date.millisecond = value
    }
  ],
  D: [twoDigits, noop],
  ddd: [word, noop],
  MMM: [word, monthUpdate('monthNamesShort')],
  MMMM: [word, monthUpdate('monthNames')],
  a: [
    word,
    (date, value, i18n) => {
      const val = value.toLowerCase()
      if (val === i18n.amPm[0]) {
        date.isPm = false
      } else if (val === i18n.amPm[1]) {
        date.isPm = true
      }
    }
  ],
  ZZ: [
    '[^\\s]*?[\\+\\-]\\d\\d:?\\d\\d|[^\\s]*?Z',
    (date, value) => {
      let parts = String(value).match(/([+-]|\d\d)/gi)
      let minutes

      if (parts) {
        minutes = Number(parts[1] * 60) + parseInt(parts[2], 10)
        date.timezoneOffset = parts[0] === '+' ? minutes : -minutes
      }
    }
  ]
}

const fmts = ['A', 'DD', 'dd', 'mm', 'hh', 'MM', 'ss', 'hh', 'H', 'HH']

fecha.masks = formats
parseFlags.dddd = parseFlags.ddd

fmts.forEach((name) => {
  if (name === 'MM') {
    parseFlags[name] = parseFlags[name.substr(0, 1)]
  } else {
    parseFlags[name] = parseFlags[name.substr(0, 1).toLowerCase()]
  }
})

fecha.format = (dateObj, mask, i18nSettings) => {
  const i18n = i18nSettings || fecha.i18n

  if (typeof dateObj === 'number') {
    dateObj = new Date(dateObj)
  }

  if (!isDate(dateObj) || isNaN(dateObj.getTime())) {
    throw new Error('Invalid Date in fecha.format')
  }

  mask = fecha.masks[mask] || mask || fecha.masks.default

  let literals = []

  mask = mask.replace(literal, ($0, $1) => {
    literals.push($1)
    return '@@@'
  })

  mask = mask.replace(fullTimeReg, ($0) =>
    $0 in formatFlags ? formatFlags[$0](dateObj, i18n) : $0.slice(1, $0.length - 1)
  )

  return mask.replace(/@@@/g, () => literals.shift())
}

const getNewFormat = (format, parseInfo) => {
  let literals = []

  let newFormat = regexEscape(format).replace(fullTimeReg, ($0) => {
    if (parseFlags[$0]) {
      const info = parseFlags[$0]
      parseInfo.push(info[1])

      return '(' + info[0] + ')'
    }

    return $0
  })

  newFormat = newFormat.replace(/@@@/g, () => literals.shift())

  return newFormat
}

const getDate = (dateInfo) => {
  let date
  const today = new Date()

  if (!isNull(dateInfo.timezoneOffset)) {
    dateInfo.minute = Number(dateInfo.minute || 0) - Number(dateInfo.timezoneOffset)

    const { year, month, day, hour, minute, second, millisecond } = dateInfo

    date = new Date(
      Date.UTC(year || today.getFullYear(), month || 0, day || 1, hour || 0, minute || 0, second || 0, millisecond || 0)
    )
  } else {
    const { year, month, day, hour, minute, second, millisecond } = dateInfo

    date = new Date(
      year || today.getFullYear(),
      month || 0,
      day || 1,
      hour || 0,
      minute || 0,
      second || 0,
      millisecond || 0
    )
  }
  return date
}

fecha.parse = (dateStr, format, i18nSettings) => {
  const i18n = i18nSettings || fecha.i18n

  if (typeof format !== 'string') {
    throw new TypeError('Invalid format in fecha.parse')
  }

  format = fecha.masks[format] || format

  if (dateStr.length > 1000) {
    return null
  }

  let dateInfo = {}
  let parseInfo = []
  let literals = []

  format = format.replace(literal, ($0, $1) => {
    literals.push($1)
    return '@@@'
  })

  const newFormat = getNewFormat(format, parseInfo)
  const matches = dateStr.match(new RegExp(newFormat, 'i'))

  if (!matches) {
    return null
  }

  for (let i = 1, len = matches.length; i < len; i++) {
    parseInfo[i - 1](dateInfo, matches[i], i18n)
  }

  if (dateInfo.isPm === true && !isNull(dateInfo.hour) && Number(dateInfo.hour) !== 12) {
    dateInfo.hour = Number(dateInfo.hour) + 12
  } else if (dateInfo.isPm === false && Number(dateInfo.hour) === 12) {
    dateInfo.hour = 0
  }

  return getDate(dateInfo)
}

export { fecha }
