/* eslint-disable prefer-rest-params */
/**
 * 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 { isDate, isNumber, isNumeric } from '../type'
import { fillChar } from '../string'

const daysInMonths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
const yyyymmddReg = new RegExp(
  '^(\\d{4})(/|-)(((0)?[1-9])|(1[0-2]))((/|-)(((0)?[1-9])|([1-2][0-9])|(3[0-1])))' +
    '?( ((0)?[0-9]|1[0-9]|20|21|22|23):([0-5]?[0-9])((:([0-5]?[0-9]))?(.([0-9]{1,6}))?)?)?$'
)
const mmddyyyyReg = new RegExp(
  '^(((0)?[1-9])|(1[0-2]))(/|-)(((0)?[1-9])|([1-2][0-9])|(3[0-1]))?(/|-)?(\\d{4})' +
    '( ((0)?[0-9]|1[0-9]|20|21|22|23):([0-5]?[0-9])((:([0-5]?[0-9]))?(.([0-9]{1,6}))?)?)?$'
)
const iso8601Reg = new RegExp(
  '^(\\d{4})-(((0)?[1-9])|(1[0-2]))-(((0)?[1-9])|([1-2][0-9])|(3[0-1]))T' +
    '(((0)?[0-9]|1[0-9]|20|21|22|23):([0-5]?[0-9])((:([0-5]?[0-9]))?(.([0-9]{1,6}))?)?)?(Z|([+-])' +
    '((0)?[0-9]|1[0-9]|20|21|22|23):?([0-5]?[0-9]))$'
)

const dateFormatRegs = {
  'y{1,4}': /y{1,4}/,
  'M{1,2}': /M{1,2}/,
  'd{1,2}': /d{1,2}/,
  'h{1,2}': /h{1,2}/,
  'H{1,2}': /H{1,2}/,
  'm{1,2}': /m{1,2}/,
  's{1,2}': /s{1,2}/,
  'S{1,3}': /S{1,3}/,
  'Z{1,1}': /Z{1,1}/
}

const maxDateValues = {
  YEAR: 9999,
  MONTH: 11,
  DATE: 31,
  HOUR: 23,
  MINUTE: 59,
  SECOND: 59,
  MILLISECOND: 999
}

const timezone1 = '-12:00,-11:00,-10:00,-09:30,-08:00,-07:00,-06:00,-05:00,-04:30,-04:00,-03:30,-02:00,-01:00'
const timezone2 = '-00:00,+00:00,+01:00,+02:00,+03:00,+03:30,+04:00,+04:30,+05:00,+05:30,+05:45,+06:00'
const timezone3 = '+06:30,+07:00,+08:00,+09:00,+10:00,+10:30,+11:00,+11:30,+12:00,+12:45,+13:00,+14:00'
const timezones: string[] = [...timezone1.split(','), ...timezone2.split(','), ...timezone3.split(',')]

export const parseIso8601 = (str: string) => {
  const m = iso8601Reg.exec(str)
  if (m && m.length === 25) {
    const year = Number(m[1])
    const month = Number(m[2]) - 1
    const day = Number(m[6])
    const hours = m[12] || 0
    const minutes = m[14] || 0
    const seconds = m[17] || 0
    return { year, month, day, hours, minutes, seconds }
  }
  return undefined
}

const getTimezone = (date) => {
  const timezoneoffset = 0 - date.getTimezoneOffset() / 60
  let timezone

  if (timezoneoffset === 0) {
    timezone = 'Z'
  } else if (timezoneoffset > 0) {
    timezone = '+' + (timezoneoffset > 10 ? timezoneoffset : '0' + timezoneoffset) + '00'
  } else {
    timezone = (timezoneoffset < -10 ? timezoneoffset : '-0' + -timezoneoffset) + '00'
  }

  return timezone
}

/**
 * 判断年份是否为闰年。
 *
 *     isLeapYear(2017)  // false
 *     isLeapYear(2000)  // true
 *
 * @param {Number} year 年份
 * @returns {Boolean}
 */
export const isLeapYear = (year) => year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)

const getMilliseconds = (milliseconds) =>
  milliseconds > maxDateValues.MILLISECOND ? Number(String(milliseconds).substring(0, 3)) : milliseconds

const getDateFromData = ({ year, month, date, hours, minutes, seconds, milliseconds }) => {
  let daysInMonth = daysInMonths[month]

  if (isLeapYear(year) && month === 1) {
    daysInMonth += 1
  }

  if (date <= daysInMonth) {
    return new Date(year, month, date, hours, minutes, seconds, getMilliseconds(milliseconds))
  }
}

const yyyymmddDateParser = (m) => {
  /* istanbul ignore else */
  if (m.length === 23) {
    const year = Number(m[1])
    const month = m[3] - 1
    const date = Number(m[9] || 1)
    const hours = m[15] || 0
    const minutes = m[17] || 0
    const seconds = m[20] || 0
    const milliseconds = m[22] || 0

    return getDateFromData({
      date,
      year,
      hours,
      month,
      seconds,
      minutes,
      milliseconds
    })
  }
}

const mmddyyyyDateParser = (m) => {
  /* istanbul ignore else */
  if (m.length === 22) {
    const year = Number(m[12])
    const month = m[1] - 1
    const date = Number(m[6] || 1)
    const hours = m[14] || 0
    const minutes = m[16] || 0
    const seconds = m[19] || 0
    const milliseconds = m[21] || 0

    return getDateFromData({
      year,
      month,
      date,
      hours,
      minutes,
      seconds,
      milliseconds
    })
  }
}

const iso8601DateParser = (m) => {
  if (m.length !== 25) {
    return
  }
  const year = Number(m[1])
  const month = m[2] - 1
  const date = Number(m[6])
  const offset = new Date(year, month, date).getTimezoneOffset()
  const hours = m[12] || 0
  const minutes = m[14] || 0
  const seconds = m[17] || 0
  const milliseconds = m[19] || 0
  let timeZone = m[20]
  const sign = m[21]
  const offsetHours = m[22] || 0
  const offsetMinutes = m[24] || 0
  let daysInMonth = daysInMonths[month]
  let actHours
  let actMinutes
  if (isLeapYear(year) && month === 1) {
    daysInMonth += 1
  }
  if (date <= daysInMonth) {
    if (timeZone === 'Z') {
      actHours = hours - offset / 60
      actMinutes = minutes
    } else {
      if (!timeZone.includes(':')) {
        timeZone = timeZone.substr(0, 3) + ':' + timeZone.substr(3)
      }
      if (!timezones.includes(timeZone)) {
        return
      }

      actHours = sign === '+' ? hours - offsetHours - offset / 60 : Number(hours) + Number(offsetHours) - offset / 60
      actMinutes = sign === '+' ? minutes - offsetMinutes : Number(minutes) + Number(offsetMinutes)
    }

    return new Date(year, month, date, actHours, actMinutes, seconds, getMilliseconds(milliseconds))
  }
}

// 添加适当的类型定义
type DateParser = (m: any) => Date | undefined
type DateParserTuple = [RegExp, DateParser]

const dateParsers: DateParserTuple[] = [
  [yyyymmddReg, yyyymmddDateParser],
  [mmddyyyyReg, mmddyyyyDateParser],
  [iso8601Reg, iso8601DateParser]
]

const parseDate = (str: string) => {
  for (let i = 0, len = dateParsers.length; i < len; i++) {
    const m = dateParsers[i][0].exec(str)

    if (m && m.length > 0) {
      return dateParsers[i][1](m)
    }
  }
}

const matchDateArray = (arr, value, text) => {
  if (text) {
    switch (text) {
      case 'yyyy':
      case 'yy':
        arr[0] = value
        break
      case 'M':
      case 'MM':
        arr[1] = value - 1
        break
      case 'd':
      case 'dd':
        arr[2] = value
        break
      case 'h':
      case 'hh':
        arr[3] = value
        break
      case 'm':
      case 'mm':
        arr[4] = value
        break
      case 's':
      case 'ss':
        arr[5] = value
        break
      case 'S':
      case 'SS':
      case 'SSS':
        arr[6] = value
        break
      default:
        break
    }
  }
}

const getDateArray = (str, dateFormat) => {
  const arr = [0, -1, 0, 0, 0, 0]

  if (str.length !== dateFormat.length) {
    return arr
  }

  let valuePos = 0
  let textPos = 0

  for (let i = 0, len = str.length; i < len; i++) {
    const charValue = str.substr(i, 1)
    const notNum = isNaN(Number(charValue)) || charValue.trim() === ''

    if ((notNum && charValue === dateFormat.substr(i, 1)) || i === len - 1) {
      let value
      let text

      if (notNum) {
        value = str.substring(valuePos, i)
        valuePos = i + 1
        const end = dateFormat.indexOf(charValue, textPos)

        text = dateFormat.substring(textPos, end === -1 ? dateFormat.length : end)

        textPos = end + 1
      } else {
        value = str.substring(valuePos, len)
        text = dateFormat.substring(textPos, len)
      }

      if (value.length === text.length || value) {
        matchDateArray(arr, value, text)
      }
    }
  }

  return arr
}

const invalideTime = (time: number, min: number, max: number): boolean => isNaN(time) || time < min || time > max

const invalideValue = ({ year, month, date, hours, minutes, seconds, milliseconds }) =>
  invalideTime(year, 0, maxDateValues.YEAR) ||
  invalideTime(month, 0, maxDateValues.MONTH) ||
  invalideTime(date, 0, maxDateValues.DATE) ||
  invalideTime(hours, 0, maxDateValues.HOUR) ||
  invalideTime(minutes, 0, maxDateValues.MINUTE) ||
  invalideTime(seconds, 0, maxDateValues.SECOND) ||
  invalideTime(milliseconds, 0, maxDateValues.MILLISECOND)

const innerParse = (value, dateFormat) => {
  if (typeof dateFormat === 'string') {
    const arr = getDateArray(value, dateFormat)
    const year = Number(arr[0])
    const month = Number(arr[1])
    const date = Number(arr[2] || 1)
    const hours = Number(arr[3] || 0)
    const minutes = Number(arr[4] || 0)
    const seconds = Number(arr[5] || 0)
    const milliseconds = Number(arr[6] || 0)

    if (
      invalideValue({
        year,
        month,
        date,
        hours,
        minutes,
        seconds,
        milliseconds
      })
    ) {
      return
    }

    return getDateFromData({
      year,
      date,
      month,
      minutes,
      hours,
      milliseconds,
      seconds
    })
  } else {
    return parseDate(value)
  }
}

/**
 * 将字符串或数字转换成 Date 类型。
 *
 *     toDate('2008/02/02')             // new Date(2008, 1, 2)
 *     toDate(Date.UTC(2008, 1, 2))     // new Date(Date.UTC(2008, 1, 2))
 *     toDate('2008/2/2', 'yyyy/M/d')   // new Date(2008, 1, 2)
 *     toDate('2008/02')                // new Date(2008, 1, 1)
 *     toDate('02/2008')                // new Date(2008, 1, 1)
 *     toDate('2008-02-01T20:08+08:00') // new Date(Date.UTC(2008, 0, 31, 16))
 *     toDate('2008-02-01T04:08-08:00') // new Date(Date.UTC(2008, 1, 1, 8))
 *
 * @param {String|Number} value 日期类型字符串或数字
 * @param {String} [dateFormat] 转换格式
 *
 * 当 value 为字符串类型时,如果不提供,则尽可能按常见格式去解析。
 * 常见格式为 yyyy[/-]MM[/-]dd hh:mm:ss.SSS, MM[/-]dd [/-]yyyy hh:mm:ss.SSS 及 ISO8601 时间格式。
 *
 * 如果提供,则按具体格式严格匹配解析,并且年份必须为4位。
 *  - yyyy 代表年份
 *  - M 或 MM 代表1位或2位的月份
 *  - d 或 dd 代表1位或2位的天数
 *  - h 或 hh 代表24小时的1位或2位的小时
 *  - m 或 mm 代表1位或2位的分钟,
 *  - s 或 ss 代表1位或2位的秒
 *  - S 或 SS 或 SSS 代表1位或2位或3位的毫秒
 *
 * @param {String} [minDate] 最小时间,默认为 0001-01-01 00:00:00.000
 * @returns {Date}
 */
export const toDate = (value, dateFormat, minDate) => {
  let date

  if (isNumber(value)) {
    date = new Date(value)
  } else if (typeof value === 'string') {
    date = innerParse(value, dateFormat)
  }

  if (minDate) {
    const min = (minDate && toDate(minDate)) || new Date(1, 1, 1, 0, 0, 0)
    return date && date < min ? min : date
  }

  return date
}

/**
 * 日期数据接口
 */
interface DateData {
  year: number
  month: number
  date: number
  hours: number
  minutes: number
  seconds: number
  milliseconds: number
}

/**
 * 日期格式化的正则表达式映射
 */
interface DateFormatRegs {
  [key: string]: RegExp
}

/**
 * 日期各部分的最大值
 */
interface MaxDateValues {
  YEAR: number
  MONTH: number
  DATE: number
  HOUR: number
  MINUTE: number
  SECOND: number
  MILLISECOND: number
}

/**
 * 将 Date 实例转换成日期字符串
 * @param date - Date 实例或日期字符串
 * @param dateFormat - 转换格式,默认为 'yyyy/MM/dd hh:mm:ss'
 * @param afterFormat - 转换后的格式(仅当date为字符串且有3个参数时有效)
 * @returns 格式化后的日期字符串
 */
export const format = function (date: Date | string, dateFormat = 'yyyy/MM/dd hh:mm:ss'): any {
  if (isDate(date)) {
    if (typeof dateFormat === 'string') {
      const o: Record<string, number | string> = {
        'y{1,4}': (date as Date).getFullYear(),
        'M{1,2}': (date as Date).getMonth() + 1,
        'd{1,2}': (date as Date).getDate(),
        'h{1,2}': (date as Date).getHours(),
        'H{1,2}': (date as Date).getHours(),
        'm{1,2}': (date as Date).getMinutes(),
        's{1,2}': (date as Date).getSeconds(),
        'S{1,3}': (date as Date).getMilliseconds(),
        'Z{1,1}': getTimezone(date as Date)
      }

      Object.keys(o).forEach((k) => {
        const m = dateFormat.match(dateFormatRegs[k])

        if (k && m && m.length) {
          dateFormat = dateFormat.replace(m[0], k === 'Z{1,1}' ? o[k] : fillChar(o[k].toString(), m[0].length))
        }
      })

      return dateFormat
    }
  } else if (typeof date === 'string' && arguments.length >= 2) {
    let afterFormat = dateFormat

    if (arguments.length === 2) {
      dateFormat = undefined
    } else {
      afterFormat = arguments[2]
    }

    const dateValue = toDate(date, dateFormat)
    return dateValue ? format(dateValue, afterFormat) : ''
  }
}

/**
 * 将当前操作的时间变更时区,主要用于转换一个其他时区的时间。
 *
 *     var date = new Date(2017, 0, 1)
 *     getDateWithNewTimezone(date, 0, -2)
 *
 * @param {Date} date Date 实例或日期字符串
 * @param {Number} otz 原时区 -12~13
 * @param {Number} ntz 目标时区 -12~13 默认为当前时区
 * @param {Boolean} TimezoneOffset 时区偏移量
 * @returns {Date}
 */
export const getDateWithNewTimezone = (date, otz, ntz, timezoneOffset = 0) => {
  if (!isDate(date) || !isNumeric(otz) || !isNumeric(ntz) || !isNumeric(timezoneOffset)) {
    return
  }

  const otzOffset = -otz * 60
  const ntzOffset = -ntz * 60
  const dstOffeset = timezoneOffset * 60
  const utc = date.getTime() + otzOffset * 60000

  return new Date(utc - (ntzOffset - dstOffeset) * 60000)
}

/**
 * 按时区将 Date 实例转换成字符串
 * @param date - Date 实例或日期字符串
 * @param dateFormat - 转换格式
 * @param timezone - 时区
 * @returns 格式化的日期字符串
 */
export const toDateStr = (date: Date | string, dateFormat: string, timezone?: number): string => {
  if (date && isNumeric(timezone)) {
    const parsedTimezone = parseFloat(timezone.toString())
    timezone = parseFloat(parsedTimezone.toFixed(2))

    const dateObj = isDate(date) ? (date as Date) : new Date(toDate(date as string) as Date)
    date = getDateWithNewTimezone(dateObj, 0, timezone) as Date
  }

  return format(date, dateFormat)
}

/**
 * 获取日期所在周的第一天,默认周一为第一天(可扩展周日为第一天)。
 *
 *     getWeekOfFirstDay()                            // 返回当前日期所在周的周一同一时间
 *     getWeekOfFirstDay(true)                        // 返回当前日期所在周的周日同一时间
 *     getWeekOfFirstDay(new Date(2019, 8, 5))        // new Date(2019, 8, 2)
 *     getWeekOfFirstDay(new Date(2019, 8, 5)), true) // new Date(2019, 8, 1)
 *
 * @param {Date} [date=new Date()] date 日期实例,默认当天
 * @param {Boolean} [isSunFirst] 是否设置周日为第一天,非必填
 * @returns {Date}
 */
export const getWeekOfFirstDay = (date, isSunFirst) => {
  typeof date === 'boolean' && (isSunFirst = date)
  isDate(date) || (date = new Date())

  const day = date.getDay()
  let dayOfMonth = date.getDate()

  if (day === 0) {
    !isSunFirst && (dayOfMonth -= 6)
  } else {
    dayOfMonth = dayOfMonth - day + (!isSunFirst && 1)
  }

  return new Date(date.getFullYear(), date.getMonth(), dayOfMonth)
}

const TZRE = /(-|\+)(\d{2}):?(\d{2})$/

export const getLocalTimezone = () => 0 - new Date().getTimezoneOffset() / 60

export const getStrTimezone = (value) => {
  const localTimeZone = getLocalTimezone()
  const match = typeof value === 'string' && value.match(TZRE)

  if (match) {
    const minoffset = Number(match[2]) + Number(match[3]) / 60
    value = minoffset * `${match[1]}1`
  }

  if (isNumber(value) && value >= -12 && value <= 12) {
    return value
  }

  return localTimeZone
}