import ENGLISH, { Language } from './i18n'
import { RRule } from '../rrule'
import { Options, ByWeekday } from '../types'
import { Weekday } from '../weekday'
import { isArray, isNumber, isPresent } from '../helpers'

// =============================================================================
// Helper functions
// =============================================================================

/**
 * Return true if a value is in an array
 */
const contains = function (arr: string[], val: string) {
  return arr.indexOf(val) !== -1
}

// =============================================================================
// ToText
// =============================================================================

export type GetText = (id: string | number | Weekday) => string

const defaultGetText: GetText = (id) => id.toString()

export type DateFormatter = (year: number, month: string, day: number) => string

const defaultDateFormatter: DateFormatter = (
  year: number,
  month: string,
  day: number
) => `${month} ${day}, ${year}`

/**
 *
 * @param {RRule} rrule
 * Optional:
 * @param {Function} gettext function
 * @param {Object} language definition
 * @constructor
 */
export default class ToText {
  static IMPLEMENTED: string[][]
  private rrule: RRule
  private text: string[]
  private gettext: GetText
  private dateFormatter: DateFormatter
  private language: Language
  private options: Partial<Options>
  private origOptions: Partial<Options>
  private bymonthday: Options['bymonthday'] | null
  private byweekday: {
    allWeeks: ByWeekday[] | null
    someWeeks: ByWeekday[] | null
    isWeekdays: boolean
    isEveryDay: boolean
  } | null

  constructor(
    rrule: RRule,
    gettext: GetText = defaultGetText,
    language: Language = ENGLISH,
    dateFormatter: DateFormatter = defaultDateFormatter
  ) {
    this.text = []
    this.language = language || ENGLISH
    this.gettext = gettext
    this.dateFormatter = dateFormatter
    this.rrule = rrule
    this.options = rrule.options
    this.origOptions = rrule.origOptions

    if (this.origOptions.bymonthday) {
      const bymonthday = ([] as number[]).concat(this.options.bymonthday)
      const bynmonthday = ([] as number[]).concat(this.options.bynmonthday)

      bymonthday.sort((a, b) => a - b)
      bynmonthday.sort((a, b) => b - a)
      // 1, 2, 3, .., -5, -4, -3, ..
      this.bymonthday = bymonthday.concat(bynmonthday)
      if (!this.bymonthday.length) this.bymonthday = null
    }

    if (isPresent(this.origOptions.byweekday)) {
      const byweekday = !isArray(this.origOptions.byweekday)
        ? [this.origOptions.byweekday]
        : this.origOptions.byweekday
      const days = String(byweekday)

      this.byweekday = {
        allWeeks: byweekday.filter(function (weekday: Weekday) {
          return !weekday.n
        }),
        someWeeks: byweekday.filter(function (weekday: Weekday) {
          return Boolean(weekday.n)
        }),
        isWeekdays:
          days.indexOf('MO') !== -1 &&
          days.indexOf('TU') !== -1 &&
          days.indexOf('WE') !== -1 &&
          days.indexOf('TH') !== -1 &&
          days.indexOf('FR') !== -1 &&
          days.indexOf('SA') === -1 &&
          days.indexOf('SU') === -1,
        isEveryDay:
          days.indexOf('MO') !== -1 &&
          days.indexOf('TU') !== -1 &&
          days.indexOf('WE') !== -1 &&
          days.indexOf('TH') !== -1 &&
          days.indexOf('FR') !== -1 &&
          days.indexOf('SA') !== -1 &&
          days.indexOf('SU') !== -1,
      }

      const sortWeekDays = function (a: Weekday, b: Weekday) {
        return a.weekday - b.weekday
      }

      this.byweekday.allWeeks.sort(sortWeekDays)
      this.byweekday.someWeeks.sort(sortWeekDays)

      if (!this.byweekday.allWeeks.length) this.byweekday.allWeeks = null
      if (!this.byweekday.someWeeks.length) this.byweekday.someWeeks = null
    } else {
      this.byweekday = null
    }
  }

  /**
   * Test whether the rrule can be fully converted to text.
   *
   * @param {RRule} rrule
   * @return {Boolean}
   */
  static isFullyConvertible(rrule: RRule) {
    const canConvert = true

    if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false
    if (rrule.origOptions.until && rrule.origOptions.count) return false

    for (const key in rrule.origOptions) {
      if (contains(['dtstart', 'tzid', 'wkst', 'freq'], key)) return true
      if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false
    }

    return canConvert
  }

  isFullyConvertible() {
    return ToText.isFullyConvertible(this.rrule)
  }

  /**
   * Perform the conversion. Only some of the frequencies are supported.
   * If some of the rrule's options aren't supported, they'll
   * be omitted from the output an "(~ approximate)" will be appended.
   *
   * @return {*}
   */
  toString() {
    const gettext = this.gettext

    if (!(this.options.freq in ToText.IMPLEMENTED)) {
      return gettext('RRule error: Unable to fully convert this rrule to text')
    }

    this.text = [gettext('every')]
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    this[RRule.FREQUENCIES[this.options.freq]]()

    if (this.options.until) {
      this.add(gettext('until'))
      const until = this.options.until
      this.add(
        this.dateFormatter(
          until.getUTCFullYear(),
          this.language.monthNames[until.getUTCMonth()],
          until.getUTCDate()
        )
      )
    } else if (this.options.count) {
      this.add(gettext('for'))
        .add(this.options.count.toString())
        .add(
          this.plural(this.options.count) ? gettext('times') : gettext('time')
        )
    }

    if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)'))

    return this.text.join('')
  }

  HOURLY() {
    const gettext = this.gettext

    if (this.options.interval !== 1) this.add(this.options.interval.toString())

    this.add(
      this.plural(this.options.interval) ? gettext('hours') : gettext('hour')
    )
  }

  MINUTELY() {
    const gettext = this.gettext

    if (this.options.interval !== 1) this.add(this.options.interval.toString())

    this.add(
      this.plural(this.options.interval)
        ? gettext('minutes')
        : gettext('minute')
    )
  }

  DAILY() {
    const gettext = this.gettext

    if (this.options.interval !== 1) this.add(this.options.interval.toString())

    if (this.byweekday && this.byweekday.isWeekdays) {
      this.add(
        this.plural(this.options.interval)
          ? gettext('weekdays')
          : gettext('weekday')
      )
    } else {
      this.add(
        this.plural(this.options.interval) ? gettext('days') : gettext('day')
      )
    }

    if (this.origOptions.bymonth) {
      this.add(gettext('in'))
      this._bymonth()
    }

    if (this.bymonthday) {
      this._bymonthday()
    } else if (this.byweekday) {
      this._byweekday()
    } else if (this.origOptions.byhour) {
      this._byhour()
    }
  }

  WEEKLY() {
    const gettext = this.gettext

    if (this.options.interval !== 1) {
      this.add(this.options.interval.toString()).add(
        this.plural(this.options.interval) ? gettext('weeks') : gettext('week')
      )
    }

    if (this.byweekday && this.byweekday.isWeekdays) {
      if (this.options.interval === 1) {
        this.add(
          this.plural(this.options.interval)
            ? gettext('weekdays')
            : gettext('weekday')
        )
      } else {
        this.add(gettext('on')).add(gettext('weekdays'))
      }
    } else if (this.byweekday && this.byweekday.isEveryDay) {
      this.add(
        this.plural(this.options.interval) ? gettext('days') : gettext('day')
      )
    } else {
      if (this.options.interval === 1) this.add(gettext('week'))

      if (this.origOptions.bymonth) {
        this.add(gettext('in'))
        this._bymonth()
      }

      if (this.bymonthday) {
        this._bymonthday()
      } else if (this.byweekday) {
        this._byweekday()
      }

      if (this.origOptions.byhour) {
        this._byhour()
      }
    }
  }

  MONTHLY() {
    const gettext = this.gettext

    if (this.origOptions.bymonth) {
      if (this.options.interval !== 1) {
        this.add(this.options.interval.toString()).add(gettext('months'))
        if (this.plural(this.options.interval)) this.add(gettext('in'))
      } else {
        // this.add(gettext('MONTH'))
      }
      this._bymonth()
    } else {
      if (this.options.interval !== 1) {
        this.add(this.options.interval.toString())
      }
      this.add(
        this.plural(this.options.interval)
          ? gettext('months')
          : gettext('month')
      )
    }
    if (this.bymonthday) {
      this._bymonthday()
    } else if (this.byweekday && this.byweekday.isWeekdays) {
      this.add(gettext('on')).add(gettext('weekdays'))
    } else if (this.byweekday) {
      this._byweekday()
    }
  }

  YEARLY() {
    const gettext = this.gettext

    if (this.origOptions.bymonth) {
      if (this.options.interval !== 1) {
        this.add(this.options.interval.toString())
        this.add(gettext('years'))
      } else {
        // this.add(gettext('YEAR'))
      }
      this._bymonth()
    } else {
      if (this.options.interval !== 1) {
        this.add(this.options.interval.toString())
      }
      this.add(
        this.plural(this.options.interval) ? gettext('years') : gettext('year')
      )
    }

    if (this.bymonthday) {
      this._bymonthday()
    } else if (this.byweekday) {
      this._byweekday()
    }

    if (this.options.byyearday) {
      this.add(gettext('on the'))
        .add(this.list(this.options.byyearday, this.nth, gettext('and')))
        .add(gettext('day'))
    }

    if (this.options.byweekno) {
      this.add(gettext('in'))
        .add(
          this.plural((this.options.byweekno as number[]).length)
            ? gettext('weeks')
            : gettext('week')
        )
        .add(this.list(this.options.byweekno, undefined, gettext('and')))
    }
  }

  private _bymonthday() {
    const gettext = this.gettext
    if (this.byweekday && this.byweekday.allWeeks) {
      this.add(gettext('on'))
        .add(
          this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or'))
        )
        .add(gettext('the'))
        .add(this.list(this.bymonthday, this.nth, gettext('or')))
    } else {
      this.add(gettext('on the')).add(
        this.list(this.bymonthday, this.nth, gettext('and'))
      )
    }
    // this.add(gettext('DAY'))
  }

  private _byweekday() {
    const gettext = this.gettext
    if (this.byweekday.allWeeks && !this.byweekday.isWeekdays) {
      this.add(gettext('on')).add(
        this.list(this.byweekday.allWeeks, this.weekdaytext)
      )
    }

    if (this.byweekday.someWeeks) {
      if (this.byweekday.allWeeks) this.add(gettext('and'))

      this.add(gettext('on the')).add(
        this.list(this.byweekday.someWeeks, this.weekdaytext, gettext('and'))
      )
    }
  }

  private _byhour() {
    const gettext = this.gettext

    this.add(gettext('at')).add(
      this.list(this.origOptions.byhour, undefined, gettext('and'))
    )
  }

  private _bymonth() {
    this.add(
      this.list(this.options.bymonth, this.monthtext, this.gettext('and'))
    )
  }

  nth(n: number | string) {
    n = parseInt(n.toString(), 10)
    let nth: string
    const gettext = this.gettext

    if (n === -1) return gettext('last')

    const npos = Math.abs(n)
    switch (npos) {
      case 1:
      case 21:
      case 31:
        nth = npos + gettext('st')
        break
      case 2:
      case 22:
        nth = npos + gettext('nd')
        break
      case 3:
      case 23:
        nth = npos + gettext('rd')
        break
      default:
        nth = npos + gettext('th')
    }

    return n < 0 ? nth + ' ' + gettext('last') : nth
  }

  monthtext(m: number) {
    return this.language.monthNames[m - 1]
  }

  weekdaytext(wday: Weekday | number) {
    const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday()
    return (
      ((wday as Weekday).n ? this.nth((wday as Weekday).n) + ' ' : '') +
      this.language.dayNames[weekday]
    )
  }

  plural(n: number) {
    return n % 100 !== 1
  }

  add(s: string) {
    this.text.push(' ')
    this.text.push(s)
    return this
  }

  list(
    arr: ByWeekday | ByWeekday[],
    callback?: GetText,
    finalDelim?: string,
    delim = ','
  ) {
    if (!isArray(arr)) {
      arr = [arr]
    }
    const delimJoin = function (
      array: string[],
      delimiter: string,
      finalDelimiter: string
    ) {
      let list = ''

      for (let i = 0; i < array.length; i++) {
        if (i !== 0) {
          if (i === array.length - 1) {
            list += ' ' + finalDelimiter + ' '
          } else {
            list += delimiter + ' '
          }
        }
        list += array[i]
      }
      return list
    }

    callback =
      callback ||
      function (o) {
        return o.toString()
      }

    const realCallback = (arg: ByWeekday) => {
      return callback && callback.call(this, arg)
    }

    if (finalDelim) {
      return delimJoin(arr.map(realCallback), delim, finalDelim)
    } else {
      return arr.map(realCallback).join(delim + ' ')
    }
  }
}
