import {
  MILLISECONDS_A_DAY,
  MILLISECONDS_A_HOUR,
  MILLISECONDS_A_MINUTE,
  MILLISECONDS_A_SECOND,
  MILLISECONDS_A_WEEK
} from '../../constant'

const MILLISECONDS_A_YEAR = MILLISECONDS_A_DAY * 365
const MILLISECONDS_A_MONTH = MILLISECONDS_A_YEAR / 12

const DURATION_REGEX_PARSE = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/
const DURATION_REGEX_FORMAT = /\[([^\]]+)]|YYYY|YY|Y|M{1,2}|D{1,2}|H{1,2}|m{1,2}|s{1,2}|SSS/g

const unitToMS = {
  years: MILLISECONDS_A_YEAR,
  months: MILLISECONDS_A_MONTH,
  days: MILLISECONDS_A_DAY,
  hours: MILLISECONDS_A_HOUR,
  minutes: MILLISECONDS_A_MINUTE,
  seconds: MILLISECONDS_A_SECOND,
  milliseconds: 1,
  weeks: MILLISECONDS_A_WEEK
}

const isDuration = d => d instanceof Duration // eslint-disable-line no-use-before-define

let $d
let $u

const wrapper = (input, instance, unit) =>
  new Duration(input, unit, instance.$l) // eslint-disable-line no-use-before-define

const prettyUnit = unit => `${$u.p(unit)}s`
const isNegative = number => number < 0
const roundNumber = number =>
  (isNegative(number) ? Math.ceil(number) : Math.floor(number))
const absolute = number => Math.abs(number)
const getNumberUnitFormat = (number, unit) => {
  if (!number) {
    return {
      negative: false,
      format: ''
    }
  }

  if (isNegative(number)) {
    return {
      negative: true,
      format: `${absolute(number)}${unit}`
    }
  }

  return {
    negative: false,
    format: `${number}${unit}`
  }
}

class Duration {
  constructor(input, unit, locale) {
    this.$d = {}
    this.$l = locale
    if (input === undefined) {
      this.$ms = 0
      this.parseFromMilliseconds()
    }
    if (unit) {
      return wrapper(input * unitToMS[prettyUnit(unit)], this)
    }
    if (typeof input === 'number') {
      this.$ms = input
      this.parseFromMilliseconds()
      return this
    }
    if (typeof input === 'object') {
      Object.keys(input).forEach((k) => {
        this.$d[prettyUnit(k)] = input[k]
      })
      this.calMilliseconds()
      return this
    }
    if (typeof input === 'string') {
      const d = input.match(DURATION_REGEX_PARSE)
      if (d) {
        const properties = d.slice(2)
        const numberD = properties.map(value => (value != null ? Number(value) : 0));
        [
          this.$d.years,
          this.$d.months,
          this.$d.weeks,
          this.$d.days,
          this.$d.hours,
          this.$d.minutes,
          this.$d.seconds
        ] = numberD
        this.calMilliseconds()
        return this
      }
    }
    return this
  }

  calMilliseconds() {
    this.$ms = Object.keys(this.$d).reduce((total, unit) => (
      total + ((this.$d[unit] || 0) * (unitToMS[unit]))
    ), 0)
  }

  parseFromMilliseconds() {
    let { $ms } = this
    this.$d.years = roundNumber($ms / MILLISECONDS_A_YEAR)
    $ms %= MILLISECONDS_A_YEAR
    this.$d.months = roundNumber($ms / MILLISECONDS_A_MONTH)
    $ms %= MILLISECONDS_A_MONTH
    this.$d.days = roundNumber($ms / MILLISECONDS_A_DAY)
    $ms %= MILLISECONDS_A_DAY
    this.$d.hours = roundNumber($ms / MILLISECONDS_A_HOUR)
    $ms %= MILLISECONDS_A_HOUR
    this.$d.minutes = roundNumber($ms / MILLISECONDS_A_MINUTE)
    $ms %= MILLISECONDS_A_MINUTE
    this.$d.seconds = roundNumber($ms / MILLISECONDS_A_SECOND)
    $ms %= MILLISECONDS_A_SECOND
    this.$d.milliseconds = $ms
  }

  toISOString() {
    const Y = getNumberUnitFormat(this.$d.years, 'Y')
    const M = getNumberUnitFormat(this.$d.months, 'M')

    let days = +this.$d.days || 0
    if (this.$d.weeks) {
      days += this.$d.weeks * 7
    }

    const D = getNumberUnitFormat(days, 'D')
    const H = getNumberUnitFormat(this.$d.hours, 'H')
    const m = getNumberUnitFormat(this.$d.minutes, 'M')

    let seconds = this.$d.seconds || 0
    if (this.$d.milliseconds) {
      seconds += this.$d.milliseconds / 1000
      seconds = Math.round(seconds * 1000) / 1000
    }

    const S = getNumberUnitFormat(seconds, 'S')

    const negativeMode =
      Y.negative ||
      M.negative ||
      D.negative ||
      H.negative ||
      m.negative ||
      S.negative

    const T = H.format || m.format || S.format ? 'T' : ''
    const P = negativeMode ? '-' : ''

    const result = `${P}P${Y.format}${M.format}${D.format}${T}${H.format}${m.format}${S.format}`
    return result === 'P' || result === '-P' ? 'P0D' : result
  }

  toJSON() {
    return this.toISOString()
  }

  format(formatStr) {
    const str = formatStr || 'YYYY-MM-DDTHH:mm:ss'
    const matches = {
      Y: this.$d.years,
      YY: $u.s(this.$d.years, 2, '0'),
      YYYY: $u.s(this.$d.years, 4, '0'),
      M: this.$d.months,
      MM: $u.s(this.$d.months, 2, '0'),
      D: this.$d.days,
      DD: $u.s(this.$d.days, 2, '0'),
      H: this.$d.hours,
      HH: $u.s(this.$d.hours, 2, '0'),
      m: this.$d.minutes,
      mm: $u.s(this.$d.minutes, 2, '0'),
      s: this.$d.seconds,
      ss: $u.s(this.$d.seconds, 2, '0'),
      SSS: $u.s(this.$d.milliseconds, 3, '0')
    }
    return str.replace(DURATION_REGEX_FORMAT, (match, $1) => $1 || String(matches[match]))
  }

  as(unit) {
    return this.$ms / (unitToMS[prettyUnit(unit)])
  }

  get(unit) {
    let base = this.$ms
    const pUnit = prettyUnit(unit)
    if (pUnit === 'milliseconds') {
      base %= 1000
    } else if (pUnit === 'weeks') {
      base = roundNumber(base / unitToMS[pUnit])
    } else {
      base = this.$d[pUnit]
    }
    return base || 0 // a === 0 will be true on both 0 and -0
  }

  add(input, unit, isSubtract) {
    let another
    if (unit) {
      another = input * unitToMS[prettyUnit(unit)]
    } else if (isDuration(input)) {
      another = input.$ms
    } else {
      another = wrapper(input, this).$ms
    }

    return wrapper(this.$ms + (another * (isSubtract ? -1 : 1)), this)
  }

  subtract(input, unit) {
    return this.add(input, unit, true)
  }

  locale(l) {
    const that = this.clone()
    that.$l = l
    return that
  }

  clone() {
    return wrapper(this.$ms, this)
  }

  humanize(withSuffix) {
    return $d()
      .add(this.$ms, 'ms')
      .locale(this.$l)
      .fromNow(!withSuffix)
  }

  valueOf() {
    return this.asMilliseconds()
  }

  milliseconds() { return this.get('milliseconds') }
  asMilliseconds() { return this.as('milliseconds') }
  seconds() { return this.get('seconds') }
  asSeconds() { return this.as('seconds') }
  minutes() { return this.get('minutes') }
  asMinutes() { return this.as('minutes') }
  hours() { return this.get('hours') }
  asHours() { return this.as('hours') }
  days() { return this.get('days') }
  asDays() { return this.as('days') }
  weeks() { return this.get('weeks') }
  asWeeks() { return this.as('weeks') }
  months() { return this.get('months') }
  asMonths() { return this.as('months') }
  years() { return this.get('years') }
  asYears() { return this.as('years') }
}

const manipulateDuration = (date, duration, k) =>
  date.add(duration.years() * k, 'y')
    .add(duration.months() * k, 'M')
    .add(duration.days() * k, 'd')
    .add(duration.hours() * k, 'h')
    .add(duration.minutes() * k, 'm')
    .add(duration.seconds() * k, 's')
    .add(duration.milliseconds() * k, 'ms')

export default (option, Dayjs, dayjs) => {
  $d = dayjs
  $u = dayjs().$utils()
  dayjs.duration = function (input, unit) {
    const $l = dayjs.locale()
    return wrapper(input, { $l }, unit)
  }
  dayjs.isDuration = isDuration

  const oldAdd = Dayjs.prototype.add
  const oldSubtract = Dayjs.prototype.subtract

  Dayjs.prototype.add = function (value, unit) {
    if (isDuration(value)) {
      return manipulateDuration(this, value, 1)
    }

    return oldAdd.bind(this)(value, unit)
  }

  Dayjs.prototype.subtract = function (value, unit) {
    if (isDuration(value)) {
      return manipulateDuration(this, value, -1)
    }

    return oldSubtract.bind(this)(value, unit)
  }
}
