<template>
  <VMenu v-model="menuIsOpen" v-bind="{ ...menuAttrs, ...menuProps }" eager :disabled="disabled">
    <template #activator="{ on }">
      <VTextField
        ref="input"
        v-bind="[textFieldAttrs, textFieldProps]"
        :value="formattedValue"
        readonly
        :disabled="disabled"
        :rules="textFieldAttrs['error-messages'].map((e) => () => e)"
        v-on="on"
      >
        <template #label>
          <slot name="label">{{ label }}</slot>
        </template>
      </VTextField>
    </template>

    <VCard class="pitcher-date-time-picker">
      <VCardText class="pa-0">
        <VRow noGutters>
          <VCol class="text-center">
            <VDatePicker
              ref="datepicker"
              v-model="internalDate"
              v-bind="[datePickerAttrs, $attrs]"
              class="rounded-t-0 pt-1"
              @change="onDateSelected"
            />
          </VCol>
          <VCol v-if="hasTimePicker" class="timepicker-container px-4 pt-4">
            <div class="heading mb-2">{{ textProps.timeHeading || $t('Time') }}</div>
            <TimePicker
              ref="timepicker"
              v-model="internalTime"
              class="mb-4"
              v-bind="timePickerAttrs"
              @on-press-enter="saveHandler"
              @input="onTimeSelected"
              @focus="handleTimePickerFocus"
              @blur="handleTimePickerBlur"
            />
          </VCol>
        </VRow>
      </VCardText>
      <VCardActions class="actions-container">
        <slot name="actions">
          <div class="actions-container__default pa-4">
            <VBtn v-if="!disableClear" @click="clearHandler">{{ textProps.clear || $t('Clear') }}</VBtn>
            <VSpacer />
            <VBtn v-if="lazy" :disabled="emitDisabled" :dark="!emitDisabled" :color="color" @click="saveHandler">
              {{ textProps.save || $t('Save') }}
            </VBtn>
          </div>
        </slot>
      </VCardActions>
    </VCard>
  </VMenu>
</template>

<script>
/* eslint-disable vue/no-deprecated-props-default-this, consistent-return */

import TimePicker, { convertTo12Hour, padLeftZero } from '../PTimePicker/PTimePicker'
import { computed, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from '@vue/composition-api'
import '../../utils/i18n-checker'

/**
 |--------------------------------------------------
 |	p-datetime-picker props
 |--------------------------------------------------
 accepts by default all props from date-picker in vuetify:
 https://vuetifyjs.com/en/api/v-date-picker/#props

 NOTE: Some of the props might not work properly as this is a custom component

 props: {
  value: accepts date/undefined/ISO or SF formats
  type: date | datetime, depends if it includes a timepicker,
  locale: possibly most important. This is from vuetify date picker. If you want to format date/time depending on users locale, specify this!
  withInitialValue: true | false, if you want picker to have an initial value, starts with new Date()
  initialView: date | month | year, if you want picker to start selection with year/month/date
  valueFormatter: format function, formats the default JavaScript ISO string. If date only, returns YYYY-MM-DD or YYYY-MM-DD HH:MM
  returnDate: true | false, false by default, this makes the picker to emit date object instead of ISO format
  textProps: { save: String | to change save button text, clear: String | to change clear button text, timeHeading: String | Text to appear above time selection (available only in `datetime` type) } by default translated with pitcher translation plugin
  menuProps: v-menu props for the container menu. Recommended not to manipulate unless it is small adjustments
  textFieldProps: v-text-field props for the input. Recommended not to manipulate unless it is small adjustments
  // Works only when type is datetime
  enableAmPm: to force enable 12h format
  minuteInterval: if you want to specify minute intervals, force select minutes to 5/10/15/30, default is 5.
  useISOType: a boolean determining that the date or dateTime input and output values won't be modifed by user timezone. If date only, returns YYYY-MM-DD or YYYY-MM-DD HH:MM. Cannot be used with valueFormatter or returnDate
  nudgeTopWhenKeyboardOpened: a number field that nudges the datetime picker menu to top by given pixels when time picker gets focus (when keyboard opens) - nullable
  disableClear: makes the picker unclearable by removing the clear button
}
 **/

const getDateString = (date) => {
  const days = date.getDate()
  const months = date.getMonth() + 1
  const years = date.getFullYear()

  const monthsPadded = months >= 10 ? months : `0${months}`

  return `${years}-${monthsPadded}-${days}`
}
const getTime = (date) => `${padLeftZero(date.getHours())}:${padLeftZero(date.getMinutes())}`
const is12Hour = (locale) => {
  return Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hourCycle === 'h12'
}

const getTimeZoneOffsetMS = (date) => {
  return date.getTimezoneOffset() * 60000
}

const adjustTimeZoneOffset = (date) => {
  if (date.constructor.name !== 'Date') {
    throw Error('Date value sent to adjustTimeZoneOffset function is not a valid date object!')
  }

  const offsetMS = getTimeZoneOffsetMS(date)

  // return date object with timezone calculated
  return new Date(Date.parse(date) - offsetMS)
}

const removeTimeZoneIfExists = (dateUTCStr) => {
  if (dateUTCStr.includes('Z')) {
    const date = new Date(dateUTCStr)

    const offsetMS = getTimeZoneOffsetMS(date)

    return new Date(Date.parse(date) + offsetMS).toISOString()
  }

  return dateUTCStr
}

export function getZeroHourFromTimeZone() {
  const date = new Date()
  const offset = date.getTimezoneOffset()
  let remainingMinutes = Math.abs(offset % 60)
  let offsetHours = Math.floor((offset / 60) * -1)

  if (offsetHours < 0) {
    offsetHours = 24 + offsetHours
    remainingMinutes = remainingMinutes ? 60 - remainingMinutes : 0
  }

  return `${Math.abs(offsetHours) < 10 ? '0' : ''}${offsetHours}:${remainingMinutes < 10 ? '0' : ''}${remainingMinutes}`
}

/**
 * Pitcher Datetime Picker component
 * @inherits v-date-picker
 */

export default {
  name: 'PDatetimePicker',
  components: {
    TimePicker,
  },
  inheritAttrs: false,
  props: {
    value: {
      type: [Date, String],
      default: null,
    },
    type: {
      type: String,
      default: 'date',
      validator: (v) => {
        if (!['datetime', 'date'].includes(v)) {
          console.error(`[Vue warn]: Validation error date-time-picker!`)
          console.error(`[Vue warn]: accepted type values: date | datetime`)

          return false
        }

        return true
      },
    },
    label: {
      type: String,
      default() {
        return this.type.includes('time') ? $t('Date/Time') : $t('Date')
      },
    },
    initialView: {
      type: String,
      default: 'date',
      validator: (v) => {
        if (!['date', 'month', 'year'].includes(v)) {
          console.error(`[Vue warn]: Validation error date-time-picker!`)
          console.error(`[Vue warn]: accepted initial-view values: date | month | year`)

          return false
        }

        return true
      },
    },
    lazy: {
      type: Boolean,
      default: false,
    },
    closeOnSelect: {
      type: Boolean,
      default: true,
    },
    withInitialValue: {
      type: Boolean,
      default: false,
    },
    enableAmPm: {
      type: Boolean,
      default: undefined,
    },
    useISOType: {
      type: Boolean,
      default: false,
    },
    returnDate: {
      type: Boolean,
      default: false,
    },
    valueFormatter: {
      type: Function,
      required: false,
      // date sent as ISO string
      default(date) {
        if (!date) return ''

        if (this.type.includes('time')) return date.substring(0, 19)

        return date.substring(0, 10)
      },
    },
    minuteInterval: {
      type: Number,
      default: 5,
    },
    textProps: {
      type: Object,
      default: () => ({
        save: '',
        clear: '',
        timeHeading: '',
      }),
      validator: (val) =>
        Object.keys(val).every(
          (key) =>
            ({
              save: true,
              clear: true,
              timeHeading: true,
            }[key] && typeof val[key] === 'string')
        ),
    },
    menuProps: {
      type: Object,
      default: () => ({}),
    },
    textFieldProps: {
      type: Object,
      default: () => ({}),
    },
    rules: {
      type: Array,
      default: () => [],
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    nudgeTopWhenKeyboardOpened: {
      type: Number,
      default: null,
    },
    disableClear: {
      type: Boolean,
      default: false,
    }
  },
  emits: ['input', 'timepicker-focus', 'timepicker-blur'],
  setup(props, ctx) {
    const menuNudgeTop = ref(props.menuProps.nudgeTop || 0)
    const state = reactive({
      menuIsOpen: false,
      // Internal date & time
      internalDate: '',
      internalTime: '',
      // Main date object to work on
      dateTime: '',
      color: computed(() => ctx.attrs.color || 'primary'),
      hasTimePicker: computed(() => props.type.includes('time')),
    })
    const showMenu = () => (state.menuIsOpen = true)
    const inputSlot = computed(() => (ctx.refs.input ? ctx.refs.input.$el.querySelector('.v-input__slot') : undefined))

    // DatePicker attrs, merged with $attrs in template
    const datePickerAttrs = computed(() => ({
      min: '1950-01-01',
      showCurrent: true,
      noTitle: true,
      color: state.color,
      activePicker: props.initialView?.toUpperCase(),
    }))

    const timePickerAttrs = computed(() => ({
      enableAmPm: typeof props.enableAmPm !== 'undefined' ? props.enableAmPm : is12Hour(ctx.attrs.locale),
      color: state.color,
      minuteInterval: props.minuteInterval,
      disabled: !state.internalDate,
      readonly: ctx.attrs.readonly,
    }))

    const validate = function() {
      const value = selectedDateTime.value
      const errors = []

      for (const rule of props.rules) {
        const error = rule(value)

        if (error !== true) {
          errors.push(error)
        }
      }

      return errors
    }

    // TextField attrs, merged with textFieldProps in template
    const textFieldAttrs = computed(() => {
      const errors = validate()

      return {
        color: state.color,
        label: props.label,
        outlined: true,
        dense: true,
        prependInnerIcon: 'fa fa-calendar',
        disabled: ctx.attrs.disabled,
        'error-count': errors.length,
        error: errors.length > 0,
        'error-messages': errors,
      }
    })

    // Menu attrs, merged with menuProps in template
    const menuAttrs = computed(() => ({
      closeOnContentClick: false,
      offsetY: true,
      nudgeTop: menuNudgeTop.value,
      minWidth: state.hasTimePicker ? '535px' : '290px',
    }))

    const saveHandler = () => {
      if (emitDisabled.value) return

      ctx.emit('input', selectedDateTime.value)
      state.menuIsOpen = false
    }

    const clearHandler = () => {
      state.internalDate = ''
      state.internalTime = ''

      ctx.emit('input', '')
    }

    // disable emitting if not filling required conditions
    const emitDisabled = computed(() => {
      if (
        state.hasTimePicker &&
        (!state.internalDate || !state.internalTime || ctx.refs.timepicker?.inputValue.length !== 4)
      ) {
        return true
      } else if (!state.internalDate) {
        return true
      }

      return false
    })

    // when user selects a date, update date of dateTime object
    const onDateSelected = () => {
      if (state.hasTimePicker) {
        ctx.refs.timepicker.focus()
      }

      emit()

      if (!state.hasTimePicker && props.closeOnSelect && !props.lazy) {
        state.menuIsOpen = false
      }
    }

    // emit immediately if not lazy when time is selected
    const onTimeSelected = () => {
      if (!dateTime.value) return

      emit()
    }

    function emit() {
      if (!props.lazy) {
        ctx.emit('input', selectedDateTime.value)
      }
    }

    const dateTime = computed(() => {
      if (!state.internalDate) {
        return null
      }

      return new Date(`${state.internalDate.replace(/-/g, '/')} ${state.internalTime || getZeroHourFromTimeZone()}`)
    })

    // emit value, always emits in ISO format
    const selectedDateTime = computed(() => {
      if (!dateTime.value) return

      return props.returnDate
        ? dateTime.value
        : props.useISOType
        ? state.hasTimePicker
          ? `${state.internalDate} ${state.internalTime}`
          : state.internalDate
        : props.valueFormatter(dateTime.value.toISOString())
    })

    // value to show in input field
    const formattedValue = computed(() => {
      if (!dateTime.value) return

      // Format date based on user locale
      let formattedDateTime = dateTime.value.toLocaleString(ctx.attrs.locale, {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
      })

      // needs to format time as well if it has timepicker
      if (state.hasTimePicker && state.internalTime) {
        // parse time, done in this way to disable timezone
        // eslint-disable-next-line prefer-const
        let [hour, minute] = state.internalTime.split(':')
        let end = ''

        // convert if user has 12h format
        if (is12Hour(ctx.attrs.locale) && props.enableAmPm) {
          const { convertedHour, ampm } = convertTo12Hour(hour)

          hour = convertedHour
          end = ampm
        }

        const newTime = `${hour}:${minute}${end ? ` ${end}` : ''}`

        // append to formattedDateTime
        formattedDateTime = `${formattedDateTime} ${newTime}`
      }

      return `${formattedDateTime}`
    })

    // parsing datetime when value is updated
    const parseAsDate = (val, adjustTimeZone = false) => {
      if (!val) return
      if (val.constructor.name === 'Date') return val

      // Parse date from string, regex to match SalesForce format
      const regex = /\+\d{4}/g
      let newDate = val

      if (newDate.match(regex)) {
        // convert to plain ISO String
        newDate = newDate.replace(regex, '')
      }

      // if string includes Z, remove time zone to parse properly
      newDate = removeTimeZoneIfExists(newDate)

      // if no time picker, remove time before parsin as a date
      // this because of the date changes because of timezone difference
      if (!state.hasTimePicker) {
        newDate = newDate.substring(0, 10)
      }

      // parse as date
      const parsedDate = new Date(newDate)

      if (isNaN(parsedDate)) {
        throw new Error(`[Vue warn]: datetime-picker could not parse the date! Received data: ${parsedDate}`)
      }

      // returns a date that is adjusted to map as local date
      return adjustTimeZone ? adjustTimeZoneOffset(parsedDate) : parsedDate
    }

    const adjustTimePicker = () => {
      ctx.root.$nextTick(() => {
        ctx.refs.timepicker?.adjustMinute()
        ctx.refs.timepicker?.emit()
      })
    }

    function initializeValuesWith(date) {
      state.internalDate = getDateString(date)
      state.internalTime = getTime(date)
      adjustTimePicker()
    }

    watch(
      () => props.value,
      (newVal, oldVal) => {
        // we need to handle case if we need to clear value from outside
        if (!newVal && oldVal) {
          clearHandler()

          return
        }

        // if null or undefined is used in value and withInitialValue is false
        // or if picker is cleared
        if (!newVal && !oldVal && !props.withInitialValue) return

        if (props.useISOType) {
          if (newVal && !oldVal && !state.internalDate) {
            initializeValuesWith(new Date(newVal.replace(/-/g, '/')))
          }
          if (!newVal && !oldVal && props.withInitialValue) {
            initializeValuesWith(new Date(newVal.replace(/-/g, '/')))
            emit()
          }
        } else {
          if (newVal && !oldVal && !state.internalDate) {
            const newDateTime = newVal.constructor.name === 'Date' ? newVal : parseAsDate(newVal, true)

            initializeValuesWith(newDateTime)
          }

          if (!newVal && !oldVal && props.withInitialValue) {
            const newDate = new Date()

            initializeValuesWith(newDate)
            emit()
          }
        }
      },
      { immediate: true }
    )

    onMounted(() => {
      const validateProps = () => {
        if (props.returnDate && props.useISOType) {
          console.warn(`PDatetimePicker: Cannot use 'returnDate' with 'useISOType'`)
        }
        if (
          props.useISOType &&
          !(props.valueFormatter.name === 'default' || props.valueFormatter.name === '_default') //'default' is used on unit testing
        ) {
          console.warn(`PDatetimePicker: Cannot use 'valueFormatter' with 'useISOType'`)
        }
        if (
          props.returnDate &&
          !(props.valueFormatter.name === 'default' || props.valueFormatter.name === '_default') //'default' is used on unit testing
        ) {
          console.warn(`PDatetimePicker: Cannot use 'valueFormatter' with 'returnDate'`)
        }
      }

      if (inputSlot.value) {
        inputSlot.value.addEventListener('click', showMenu)
      }

      validateProps()
    })

    onBeforeUnmount(() => {
      if (inputSlot.value) {
        inputSlot.value?.removeEventListener?.('click', showMenu)
      }
    })

    function moveMenuToTopBy(nugdePixels) {
      if (typeof nugdePixels === 'number' || typeof nugdePixels === 'string') {
        menuNudgeTop.value = nugdePixels
      }
    }

    function handleTimePickerFocus(e) {
      moveMenuToTopBy(props.nudgeTopWhenKeyboardOpened)
      ctx.emit('timepicker-focus', e)
    }

    function handleTimePickerBlur(e) {
      moveMenuToTopBy(props.menuProps.nudgeTop || 0)
      ctx.emit('timepicker-blur', e)
    }

    return {
      ...toRefs(state),
      datePickerAttrs,
      timePickerAttrs,
      textFieldAttrs,
      menuAttrs,
      formattedValue,
      emitDisabled,
      saveHandler,
      clearHandler,
      onDateSelected,
      onTimeSelected,
      handleTimePickerFocus,
      handleTimePickerBlur,
    }
  },
}
</script>

<style lang="scss" scoped>
.pitcher-date-time-picker {
  ::v-deep {
    .v-input .v-input__control .v-input__slot .v-text-field__slot input::-webkit-outer-spin-button,
    .v-input .v-input__control .v-input__slot .v-text-field__slot input::-webkit-inner-spin-button {
      -webkit-appearance: none;
      margin: 0;
    }
  }

  .timepicker-container {
    border-left: 1px solid $p-border-color;

    .heading {
      color: map-deep-get($material-light, 'text', 'primary');
      font-weight: 700;
      font-size: 16px;
      text-align: center;
    }

    ::v-deep .pitcher-timepicker__wrapper {
      justify-content: center;
    }
  }

  .actions-container {
    border-top: 1px solid $p-border-color;
    padding: 0;

    &__default {
      flex: 1;
      display: flex;
      align-items: center;
    }
  }
}
</style>
