import { defineComponent, h, onScopeDispose, PropType, ref, watch } from 'vue'

import { CButton } from '../button'
import { CFormControlWrapper } from '../form/CFormControlWrapper'
import { CPicker } from '../picker'

import { CTimePickerRollCol } from './CTimePickerRollCol'

import {
  convert12hTo24h,
  convertTimeToDate,
  getAmPm,
  getLocalizedTimePartials,
  getSelectedHour,
  getSelectedMinutes,
  getSelectedSeconds,
  isValidTime,
} from './utils'

import type { LocalizedTimePartials } from './types'

import { useDebouncedCallback } from '../../composables'
import { Color } from '../props'

const CTimePicker = defineComponent({
  name: 'CTimePicker',
  props: {
    /**
     * Accessible label for the hours selection element.
     */
    ariaSelectHoursLabel: String,
    /**
     * Accessible label for the AM/PM selection element.
     *
     * @since 5.16.0
     */
    ariaSelectMeridiemLabel: String,
    /**
     * Accessible label for the minutes selection element.
     *
     * @since 5.16.0
     */
    ariaSelectMinutesLabel: String,
    /**
     * Accessible label for the seconds selection element.
     *
     * @since 5.16.0
     */
    ariaSelectSecondsLabel: String,
    /**
     * Set if the component should use the 12/24 hour format. If `true` forces the interface to a 12-hour format. If `false` forces the interface into a 24-hour format. If `auto` the current locale will determine the 12 or 24-hour interface by default locales.
     *
     * @since 4.7.0
     */
    ampm: {
      type: [Boolean, String] as PropType<boolean | 'auto'>,
      default: 'auto',
      validator: (value: boolean | 'auto') => {
        if (typeof value == 'string') {
          return ['auto'].includes(value)
        }
        if (typeof value == 'boolean') {
          return true
        }
        return false
      },
    },
    /**
     * Toggle visibility or set the content of cancel button.
     */
    cancelButton: {
      type: [Boolean, String],
      default: 'Cancel',
    },
    /**
     * Sets the color context of the cancel button to one of CoreUI’s themed colors.
     *
     * @values 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light'
     */
    cancelButtonColor: {
      ...Color,
      default: 'primary',
    },
    /**
     * Size the cancel button small or large.
     *
     * @values 'sm', 'lg'
     */
    cancelButtonSize: {
      type: String,
      default: 'sm',
      validator: (value: string) => {
        return ['sm', 'lg'].includes(value)
      },
    },
    /**
     * Set the cancel button variant to an outlined button or a ghost button.
     *
     * @values 'ghost', 'outline'
     */
    cancelButtonVariant: {
      type: String,
      default: 'ghost',
      validator: (value: string) => {
        return ['ghost', 'outline'].includes(value)
      },
    },
    /**
     * Toggle visibility of the cleaner button.
     */
    cleaner: {
      type: Boolean,
      default: true,
    },
    /**
     * Toggle visibility or set the content of confirm button.
     */
    confirmButton: {
      type: [Boolean, String],
      default: 'OK',
    },
    /**
     * Sets the color context of the confirm button to one of CoreUI’s themed colors.
     *
     * @values 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light'
     */
    confirmButtonColor: {
      ...Color,
      default: 'primary',
    },
    /**
     * Size the confirm button small or large.
     *
     * @values 'sm', 'lg'
     */
    confirmButtonSize: {
      type: String,
      default: 'sm',
      validator: (value: string) => {
        return ['sm', 'lg'].includes(value)
      },
    },
    /**
     * Set the confirm button variant to an outlined button or a ghost button.
     *
     * @values 'ghost', 'outline'
     */
    confirmButtonVariant: {
      type: String,
      validator: (value: string) => {
        return ['ghost', 'outline'].includes(value)
      },
    },
    /**
     * Set container type for the component, or the element the dropdown is rendered into.
     *
     * - `'dropdown'` - the component is rendered as a dropdown (default),
     * - `'inline'` - the component is rendered inline, without a dropdown,
     * - an HTML element, a CSS selector, or a function that returns a single element - the
     *   dropdown is rendered inside that element. Implies `teleport`. **[Added in v5.21.0]**
     */
    container: {
      type: [String, Object, Function] as PropType<
        'dropdown' | 'inline' | string | HTMLElement | (() => HTMLElement) | null
      >,
      default: 'dropdown',
    },
    /**
     * Toggle the disabled state for the component.
     */
    disabled: Boolean,
    /**
     * Provide valuable, actionable feedback.
     *
     * @since 4.6.0
     */
    feedback: String,
    /**
     * Provide valuable, actionable feedback.
     *
     * @since 4.6.0
     */
    feedbackInvalid: String,
    /**
     * Provide valuable, actionable invalid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`.
     *
     * @since 4.6.0
     */
    feedbackValid: String,
    /**
     * Specify a list of available hours using an array, or customize the filtering of hours through a function.
     *
     * @since 5.0.0
     */
    hours: {
      type: [Array, Function] as PropType<number[] | ((hour: number) => number[])>,
    },
    /**
     * The id global attribute defines an identifier (ID) that must be unique in the whole document.
     */
    id: String,
    /**
     * Toggle visibility or set the content of the input indicator.
     */
    indicator: {
      type: Boolean,
      default: true,
    },
    /**
     * Defines the delay (in milliseconds) for the input field's onChange event.
     *
     * @since 5.0.0
     */
    inputOnChangeDelay: {
      type: Number,
      default: 750,
    },
    /**
     * Toggle the readonly state for the component.
     */
    inputReadOnly: Boolean,
    /**
     * Set component validation state to invalid.
     *
     * @since 4.6.0
     */
    invalid: {
      type: Boolean,
      default: undefined,
    },
    /**
     * Add a caption for a component.
     *
     * @since 4.6.0
     */
    label: String,
    /**
     * Sets the default locale for components. If not set, it is inherited from the navigator.language.
     */
    locale: {
      type: String,
      default: 'default',
    },
    /**
     * Toggle the display of minutes, specify a list of available minutes using an array, or customize the filtering of minutes through a function.
     *
     * @since 5.0.0
     */
    minutes: {
      type: [Array, Boolean, Function] as PropType<
        number[] | ((hour: number) => number[]) | boolean
      >,
      default: true,
    },
    /**
     * Set the name attribute for the input element.
     *
     * @since 5.3.0
     */
    name: String,
    /**
     * Specifies a short hint that is visible in the input.
     */
    placeholder: {
      type: String,
      default: 'Select time',
    },
    /**
     * When present, it specifies that must be filled out before submitting the form.
     *
     * @since 4.9.0
     */
    required: Boolean,
    /**
     * Toggle the display of seconds, specify a list of available seconds using an array, or customize the filtering of seconds through a function.
     *
     * @since 4.7.0
     */
    seconds: {
      type: [Array, Boolean, Function] as PropType<
        number[] | ((hour: number) => number[]) | boolean
      >,
      default: true,
    },
    /**
     * Size the component small or large.
     *
     * @values 'sm', 'lg'
     */
    size: {
      type: String,
      default: undefined,
      validator: (value: string) => {
        return ['sm', 'lg'].includes(value)
      },
    },
    /**
     * Generates dropdown menu using Teleport.
     *
     * @since 5.8.0
     */
    teleport: {
      type: [Boolean],
      default: false,
    },
    /**
     * Add helper text to the component.
     *
     * @since 4.6.0
     */
    text: String,
    /**
     * Initial selected time.
     */
    time: [Date, String],
    /**
     * Display validation feedback in a styled tooltip.
     *
     * @since 4.6.0
     */
    tooltipFeedback: Boolean,
    /**
     * Set component validation state to valid.
     *
     * @since 4.6.0
     */
    valid: {
      type: Boolean,
      default: undefined,
    },
    /**
     * Set the time picker variant to a roll or select.
     *
     * @values 'roll', 'select'
     */
    variant: {
      type: String,
      default: 'roll',
      validator: (value: string) => {
        return ['roll', 'select'].includes(value)
      },
    },
    /**
     * Toggle the visibility of the component.
     */
    visible: Boolean,
  },
  emits: [
    /**
     * Callback fired when the time changed.
     */
    'change',
    /**
     * Callback fired when the component requests to be hidden.
     */
    'hide',
    /**
     * Callback fired when the component requests to be shown.
     */
    'show',
    /**
     * Callback fired when the time changed.
     *
     * @since 4.7.0
     */
    'update:time',
  ],
  setup(props, { emit, attrs, slots }) {
    const formRef = ref()
    const inputRef = ref()
    const columnRefs = ref<(HTMLElement | null)[]>([])
    const date = ref<Date | null>(convertTimeToDate(props.time))
    const ampm = ref<'am' | 'pm' | null>(
      date.value ? getAmPm(new Date(date.value), props.locale) : null
    )
    const initialDate = ref<Date | null>(null)
    const visible = ref(props.visible)
    const localizedTimePartials = ref<LocalizedTimePartials>({
      listOfHours: [],
      listOfMinutes: [],
      listOfSeconds: [],
      hour12: false,
    })
    const isValid = ref<boolean | undefined>(
      props.valid ?? (props.invalid === true ? false : undefined)
    )

    const setColumnRef = (index: number, el: HTMLElement | null) => {
      columnRefs.value[index] = el
    }

    watch(
      () => props.time,
      () => {
        date.value = convertTimeToDate(props.time)
      }
    )

    watch(
      () => [props.valid, props.invalid],
      () => {
        isValid.value = props.valid ?? (props.invalid === true ? false : undefined)
      }
    )

    watch(
      date,
      () => {
        localizedTimePartials.value = getLocalizedTimePartials(
          props.locale,
          props.ampm,
          props.hours,
          props.minutes,
          props.seconds
        )

        if (date.value) {
          ampm.value = getAmPm(new Date(date.value), props.locale)
        }
      },
      { immediate: true }
    )

    watch(inputRef, () => {
      if (inputRef.value && inputRef.value.form) {
        formRef.value = inputRef.value.form
      }
    })

    let submitForm: HTMLFormElement | null = null
    const handleSubmit = (event: Event) => {
      setTimeout(() => handleFormValidation(event.target as HTMLFormElement))
    }

    watch([formRef, date], () => {
      if (submitForm) {
        submitForm.removeEventListener('submit', handleSubmit)
        submitForm = null
      }

      if (formRef.value) {
        submitForm = formRef.value
        submitForm.addEventListener('submit', handleSubmit)
        handleFormValidation(formRef.value)
      }
    })

    onScopeDispose(() => {
      if (submitForm) {
        submitForm.removeEventListener('submit', handleSubmit)
        submitForm = null
      }
    })

    const handleClear = (event: Event) => {
      event.stopPropagation()
      date.value = null
      emit('change', null)
      emit('update:time', null)
    }

    const handleFormValidation = (form: HTMLFormElement) => {
      if (!form.classList.contains('was-validated')) {
        return
      }

      if (date.value) {
        isValid.value = true
        return
      }
      isValid.value = false
    }

    const handleTimeChange = (set: 'hours' | 'minutes' | 'seconds' | 'meridiem', value: string) => {
      const _date = date.value || new Date('1970-01-01')

      if (set === 'meridiem') {
        const currentHours = _date.getHours()

        if (value === 'am') {
          ampm.value = 'am'
          // Convert PM hours (12-23) to AM hours (0-11)
          if (currentHours >= 12) {
            _date.setHours(currentHours - 12)
          }
        }

        if (value === 'pm') {
          ampm.value = 'pm'
          // Convert AM hours (0-11) to PM hours (12-23)
          if (currentHours < 12) {
            _date.setHours(currentHours + 12)
          }
        }
      }

      if (set === 'hours') {
        if (localizedTimePartials.value && localizedTimePartials.value.hour12) {
          _date.setHours(convert12hTo24h(ampm.value ?? 'am', Number.parseInt(value)))
        } else {
          _date.setHours(Number.parseInt(value))
        }
      }

      if (set === 'minutes') {
        _date.setMinutes(Number.parseInt(value))
      }

      if (set === 'seconds') {
        _date.setSeconds(Number.parseInt(value))
      }

      date.value = new Date(_date)
      emit('change', _date.toTimeString(), _date.toLocaleTimeString(props.locale), _date)
      emit('update:time', _date.toLocaleTimeString(props.locale))
    }

    const handleInputChange = useDebouncedCallback((event: Event) => {
      if (isValidTime((event.target as HTMLInputElement).value)) {
        const _date = convertTimeToDate((event.target as HTMLInputElement).value)
        date.value = _date

        if (_date) {
          emit('change', _date.toTimeString(), _date.toLocaleTimeString(props.locale), _date)
          emit('update:time', _date.toLocaleTimeString(props.locale))
        } else {
          emit('change', null)
          emit('update:time', null)
        }
      } else {
        emit('change', null)
        emit('update:time', null)
      }
    }, props.inputOnChangeDelay)

    const InputGroup = () =>
      h('div', { class: 'time-picker-input-group' }, [
        h('input', {
          autocomplete: 'off',
          class: 'time-picker-input',
          disabled: props.disabled,
          id: props.id,
          name: props.name,
          onInput: handleInputChange,
          placeholder: props.placeholder,
          readonly: props.inputReadOnly,
          ref: inputRef,
          required: props.required,
          value: date.value
            ? date.value.toLocaleTimeString(props.locale, {
                hour12: localizedTimePartials.value && localizedTimePartials.value.hour12,
                hour: 'numeric',
                ...(props.minutes && { minute: 'numeric' }),
                ...(props.seconds && { second: 'numeric' }),
              })
            : '',
        }),
        props.indicator &&
          h('div', {
            class: 'time-picker-indicator',
            ...(!props.disabled && {
              onClick: (event: Event) => {
                event.stopPropagation()
                visible.value = !visible.value
              },
              onKeydown: (event: KeyboardEvent) => {
                if (event.key === 'Enter') {
                  visible.value = !visible.value
                }
              },
              tabIndex: 0,
            }),
          }),
        props.cleaner &&
          date.value &&
          h('div', {
            class: 'time-picker-cleaner',
            onClick: (event: Event) => handleClear(event),
          }),
      ])

    const TimePickerSelect = () => [
      h('span', { class: 'time-picker-inline-icon' }),
      h(
        'select',
        {
          class: 'time-picker-inline-select',
          disabled: props.disabled,
          onChange: (event: Event) =>
            handleTimeChange('hours', (event.target as HTMLSelectElement).value),
          ...(date.value && { value: getSelectedHour(date.value, props.locale) }),
          'aria-label': props.ariaSelectHoursLabel,
        },
        localizedTimePartials.value &&
          localizedTimePartials.value.listOfHours.map((option) =>
            h(
              'option',
              {
                value: option.value.toString(),
              },
              option.label
            )
          )
      ),
      props.minutes && ':',
      props.minutes &&
        h(
          'select',
          {
            class: 'time-picker-inline-select',
            disabled: props.disabled,
            onChange: (event: Event) =>
              handleTimeChange('minutes', (event.target as HTMLSelectElement).value),
            ...(date.value && { value: getSelectedMinutes(date.value) }),
            'aria-label': props.ariaSelectMinutesLabel,
          },
          localizedTimePartials.value &&
            localizedTimePartials.value.listOfMinutes?.map((option) =>
              h(
                'option',
                {
                  value: option.value.toString(),
                },
                option.label
              )
            )
        ),
      props.seconds && ':',
      props.seconds &&
        h(
          'select',
          {
            class: 'time-picker-inline-select',
            disabled: props.disabled,
            onChange: (event: Event) =>
              handleTimeChange('seconds', (event.target as HTMLSelectElement).value),
            ...(date.value && { value: getSelectedSeconds(date.value) }),
            'aria-label': props.ariaSelectSecondsLabel,
          },
          localizedTimePartials.value &&
            localizedTimePartials.value.listOfSeconds?.map((option) =>
              h(
                'option',
                {
                  value: option.value.toString(),
                },
                option.label
              )
            )
        ),
      localizedTimePartials.value &&
        localizedTimePartials.value.hour12 &&
        h(
          'select',
          {
            class: 'time-picker-inline-select',
            disabled: props.disabled,
            onChange: (event: Event) =>
              handleTimeChange('meridiem', (event.target as HTMLSelectElement).value),
            value: ampm.value,
            'aria-label': props.ariaSelectMeridiemLabel,
          },
          [
            h(
              'option',
              {
                value: 'am',
              },
              'AM'
            ),
            h(
              'option',
              {
                value: 'pm',
              },
              'PM'
            ),
          ]
        ),
    ]

    const TimePickerRoll = () => [
      h(CTimePickerRollCol, {
        ariaLabel: props.ariaSelectHoursLabel,
        columnIndex: 0,
        columnRefs: columnRefs.value,
        elements: localizedTimePartials.value && localizedTimePartials.value.listOfHours,
        onClick: (index: number) => handleTimeChange('hours', index.toString()),
        selected: getSelectedHour(date.value, props.locale, props.ampm),
        setColumnRef,
      }),
      props.minutes &&
        h(CTimePickerRollCol, {
          ariaLabel: props.ariaSelectMinutesLabel,
          columnIndex: 1,
          columnRefs: columnRefs.value,
          elements: localizedTimePartials.value && localizedTimePartials.value.listOfMinutes,
          onClick: (index: number) => handleTimeChange('minutes', index.toString()),
          selected: getSelectedMinutes(date.value),
          setColumnRef,
        }),
      props.seconds &&
        h(CTimePickerRollCol, {
          ariaLabel: props.ariaSelectSecondsLabel,
          columnIndex: props.minutes ? 2 : 1,
          columnRefs: columnRefs.value,
          elements: localizedTimePartials.value && localizedTimePartials.value.listOfSeconds,
          onClick: (index: number) => handleTimeChange('seconds', index.toString()),
          selected: getSelectedSeconds(date.value),
          setColumnRef,
        }),
      localizedTimePartials.value &&
        localizedTimePartials.value.hour12 &&
        h(CTimePickerRollCol, {
          ariaLabel: props.ariaSelectMeridiemLabel,
          columnIndex: (props.minutes ? 1 : 0) + (props.seconds ? 1 : 0) + 1,
          columnRefs: columnRefs.value,
          elements: [
            { value: 'am', label: 'AM' },
            { value: 'pm', label: 'PM' },
          ],
          onClick: (value: string) => handleTimeChange('meridiem', value),
          selected: ampm.value,
          setColumnRef,
        }),
    ]

    return () =>
      h(
        CFormControlWrapper,
        {
          ...(typeof attrs['aria-describedby'] === 'string' && {
            describedby: attrs['aria-describedby'],
          }),
          feedback: props.feedback,
          feedbackInvalid: props.feedbackInvalid,
          feedbackValid: props.feedbackValid,
          id: props.id,
          invalid: isValid.value === false ? true : false,
          label: props.label,
          text: props.text,
          tooltipFeedback: props.tooltipFeedback,
          valid: isValid.value,
        },
        {
          default: () =>
            h(
              CPicker,
              {
                class: [
                  'time-picker',
                  {
                    [`time-picker-${props.size}`]: props.size,
                    disabled: props.disabled,
                    'is-invalid': isValid.value === false ? true : false,
                    'is-valid': isValid.value,
                  },
                ],
                container: props.container,
                disabled: props.disabled,
                dropdownClassNames: 'time-picker-dropdown',
                footer: true,
                onHide: () => {
                  visible.value = false
                  emit('hide')
                },
                onShow: () => {
                  if (date.value) {
                    initialDate.value = new Date(date.value)
                  }

                  visible.value = true
                  emit('show')
                },
                teleport: props.teleport,
                visible: visible.value,
              },
              {
                ...(slots.cancelButton && {
                  cancelButton: () => slots.cancelButton && slots.cancelButton(),
                }),
                ...(slots.confirmButton && {
                  confirmButton: () => slots.confirmButton && slots.confirmButton(),
                }),
                toggler: () => InputGroup(),
                default: () =>
                  h(
                    'div',
                    {
                      class: [
                        'time-picker-body',
                        {
                          ['time-picker-roll']: props.variant === 'roll',
                        },
                      ],
                      ...(props.variant !== 'select' && { role: 'group' }),
                    },
                    props.variant === 'select' ? TimePickerSelect() : TimePickerRoll()
                  ),
                footer: () =>
                  h('div', { class: 'time-picker-footer' }, [
                    props.cancelButton &&
                      h(
                        CButton,
                        {
                          color: props.cancelButtonColor,
                          onClick: () => {
                            if (initialDate.value) {
                              date.value = new Date(initialDate.value)
                            }
                            visible.value = false
                          },
                          size: props.cancelButtonSize,
                          variant: props.cancelButtonVariant,
                        },
                        () => props.cancelButton
                      ),
                    props.confirmButton &&
                      h(
                        CButton,
                        {
                          color: props.confirmButtonColor,
                          onClick: () => {
                            visible.value = false
                          },
                          size: props.confirmButtonSize,
                          variant: props.confirmButtonVariant,
                        },
                        () => props.confirmButton
                      ),
                  ]),
              }
            ),
        }
      )
  },
})
export { CTimePicker }
