/**
 * Logic:
 *  When `mode` === `picker`,
 *  click will trigger `onSelect` (if value changed trigger `onChange` also).
 *  Panel change will not trigger `onSelect` but trigger `onPanelChange`
 */
import type { CSSProperties } from 'vue'
import { computed, createVNode, defineComponent, ref, toRef, watch, watchEffect } from 'vue'

import KeyCode from '../_util/KeyCode'
import classNames from '../_util/classNames'
import useMergedState from '../_util/hooks/useMergedState'
import type { VueNode } from '../_util/type'
import { warning } from '../vc-util/warning'
import { useInjectPanel, useProvidePanel } from './PanelContext'
import { useInjectRange } from './RangeContext'
import type { GenerateConfig } from './generate'
import type {
  Components,
  DisabledTime,
  Locale,
  OnPanelChange,
  PanelMode,
  PanelRefProps,
  PickerMode,
} from './interface'
import DatePanel from './panels/DatePanel'
import type { DateRender } from './panels/DatePanel/DateBody'
import DatetimePanel from './panels/DatetimePanel'
import DecadePanel from './panels/DecadePanel'
import MonthPanel from './panels/MonthPanel'
import type { MonthCellRender } from './panels/MonthPanel/MonthBody'
import QuarterPanel from './panels/QuarterPanel'
import type { SharedTimeProps } from './panels/TimePanel'
import TimePanel from './panels/TimePanel'
import WeekPanel from './panels/WeekPanel'
import YearPanel from './panels/YearPanel'
import { isEqual } from './utils/dateUtil'
import getExtraFooter from './utils/getExtraFooter'
import getRanges from './utils/getRanges'
import { getLowerBoundTime, setDateTime, setTime } from './utils/timeUtil'
import { PickerModeMap } from './utils/uiUtil'

export interface PickerPanelSharedProps<DateType> {
  prefixCls?: string
  // className?: string;
  // style?: React.CSSProperties;
  /** @deprecated Will be removed in next big version. Please use `mode` instead */
  mode?: PanelMode
  tabindex?: number

  // Locale
  locale: Locale
  generateConfig: GenerateConfig<DateType>

  // Value
  value?: DateType | null
  defaultValue?: DateType
  /** [Legacy] Set default display picker view date */
  pickerValue?: DateType
  /** [Legacy] Set default display picker view date */
  defaultPickerValue?: DateType

  // Date
  disabledDate?: (date: DateType) => boolean

  // Render
  dateRender?: DateRender<DateType>
  monthCellRender?: MonthCellRender<DateType>
  renderExtraFooter?: (mode: PanelMode) => VueNode

  // Event
  onSelect?: (value: DateType) => void
  onChange?: (value: DateType) => void
  onPanelChange?: OnPanelChange<DateType>
  onMousedown?: (e: MouseEvent) => void
  onOk?: (date: DateType) => void

  direction?: 'ltr' | 'rtl'

  hideHeader?: boolean
  onPickerValueChange?: (date: DateType) => void

  components?: Components
}

export type PickerPanelBaseProps<DateType> = {
  picker: Exclude<PickerMode, 'date' | 'time'>
} & PickerPanelSharedProps<DateType>

export type PickerPanelDateProps<DateType> = {
  picker?: 'date'
  showToday?: boolean
  showNow?: boolean

  // Time
  showTime?: boolean | SharedTimeProps<DateType>
  disabledTime?: DisabledTime<DateType>
} & PickerPanelSharedProps<DateType>

export type PickerPanelTimeProps<DateType> = {
  picker: 'time'
} & PickerPanelSharedProps<DateType> &
  SharedTimeProps<DateType>

export type PickerPanelProps<DateType> =
  | PickerPanelBaseProps<DateType>
  | PickerPanelDateProps<DateType>
  | PickerPanelTimeProps<DateType>

// TMP type to fit for ts 3.9.2
type OmitType<DateType> = Omit<PickerPanelBaseProps<DateType>, 'picker'> &
  Omit<PickerPanelDateProps<DateType>, 'picker'> &
  Omit<PickerPanelTimeProps<DateType>, 'picker'>
type MergedPickerPanelProps<DateType> = {
  picker?: PickerMode
} & OmitType<DateType>

function PickerPanel<DateType>() {
  return defineComponent<MergedPickerPanelProps<DateType>>({
    name: 'PickerPanel',
    inheritAttrs: false,
    props: {
      prefixCls: String,
      locale: Object,
      generateConfig: Object,
      value: Object,
      defaultValue: Object,
      pickerValue: Object,
      defaultPickerValue: Object,
      disabledDate: Function,
      mode: String,
      picker: { type: String, default: 'date' },
      tabindex: { type: [Number, String], default: 0 },
      showNow: { type: Boolean, default: undefined },
      showTime: [Boolean, Object],
      showToday: Boolean,
      renderExtraFooter: Function,
      dateRender: Function,
      hideHeader: { type: Boolean, default: undefined },
      onSelect: Function,
      onChange: Function,
      onPanelChange: Function,
      onMousedown: Function,
      onPickerValueChange: Function,
      onOk: Function,
      components: Object,
      direction: String,
      hourStep: { type: Number, default: 1 },
      minuteStep: { type: Number, default: 1 },
      secondStep: { type: Number, default: 1 },
    } as any,
    setup(props, { attrs }) {
      const needConfirmButton = computed(
        () => (props.picker === 'date' && !!props.showTime) || props.picker === 'time',
      )

      const isHourStepValid = computed(() => 24 % props.hourStep === 0)
      const isMinuteStepValid = computed(() => 60 % props.minuteStep === 0)
      const isSecondStepValid = computed(() => 60 % props.secondStep === 0)
      if (process.env.NODE_ENV !== 'production') {
        watchEffect(() => {
          const { generateConfig, value, hourStep = 1, minuteStep = 1, secondStep = 1 } = props
          warning(!value || generateConfig.isValidate(value), 'Invalidate date pass to `value`.')
          warning(
            !value || generateConfig.isValidate(value),
            'Invalidate date pass to `defaultValue`.',
          )
          warning(
            isHourStepValid.value,
            `\`hourStep\` ${hourStep} is invalid. It should be a factor of 24.`,
          )
          warning(
            isMinuteStepValid.value,
            `\`minuteStep\` ${minuteStep} is invalid. It should be a factor of 60.`,
          )
          warning(
            isSecondStepValid.value,
            `\`secondStep\` ${secondStep} is invalid. It should be a factor of 60.`,
          )
        })
      }

      const panelContext = useInjectPanel()
      const { operationRef, onSelect: onContextSelect, hideRanges, defaultOpenValue } = panelContext
      const { inRange, panelPosition, rangedValue, hoverRangedValue } = useInjectRange()
      const panelRef = ref<PanelRefProps>({})
      // Value
      const [mergedValue, setInnerValue] = useMergedState<DateType | null>(null, {
        value: toRef(props, 'value'),
        defaultValue: props.defaultValue,
        postState: (val) => {
          if (!val && defaultOpenValue?.value && props.picker === 'time') {
            return defaultOpenValue.value
          }
          return val
        },
      })

      // View date control
      const [viewDate, setInnerViewDate] = useMergedState<DateType | null>(null, {
        value: toRef(props, 'pickerValue'),
        defaultValue: props.defaultPickerValue || mergedValue.value,
        postState: (date) => {
          const { generateConfig, showTime, defaultValue } = props
          const now = generateConfig.getNow()
          if (!date) return now
          // When value is null and set showTime
          if (!mergedValue.value && props.showTime) {
            if (typeof showTime === 'object') {
              return setDateTime(
                generateConfig,
                Array.isArray(date) ? date[0] : date,
                showTime.defaultValue || now,
              )
            }
            if (defaultValue) {
              return setDateTime(generateConfig, Array.isArray(date) ? date[0] : date, defaultValue)
            }
            return setDateTime(generateConfig, Array.isArray(date) ? date[0] : date, now)
          }
          return date
        },
      })

      const setViewDate = (date: DateType) => {
        setInnerViewDate(date)
        if (props.onPickerValueChange) {
          props.onPickerValueChange(date)
        }
      }

      // Panel control
      const getInternalNextMode = (nextMode: PanelMode): PanelMode => {
        const getNextMode = PickerModeMap[props.picker!]
        if (getNextMode) {
          return getNextMode(nextMode)
        }

        return nextMode
      }

      // Save panel is changed from which panel
      const [mergedMode, setInnerMode] = useMergedState(
        () => {
          if (props.picker === 'time') {
            return 'time'
          }
          return getInternalNextMode('date')
        },
        {
          value: toRef(props, 'mode'),
        },
      )
      watch(
        () => props.picker,
        () => {
          setInnerMode(props.picker)
        },
      )

      const sourceMode = ref(mergedMode.value)
      const setSourceMode = (val: PanelMode) => {
        sourceMode.value = val
      }

      const onInternalPanelChange = (newMode: PanelMode | null, viewValue: DateType) => {
        const { onPanelChange, generateConfig } = props
        const nextMode = getInternalNextMode(newMode || mergedMode.value)
        setSourceMode(mergedMode.value)
        setInnerMode(nextMode)

        if (
          onPanelChange &&
          (mergedMode.value !== nextMode || isEqual(generateConfig, viewDate.value, viewDate.value))
        ) {
          onPanelChange(viewValue, nextMode)
        }
      }

      const triggerSelect = (
        date: DateType,
        type: 'key' | 'mouse' | 'submit',
        forceTriggerSelect = false,
      ) => {
        const { picker, generateConfig, onSelect, onChange, disabledDate } = props
        if (mergedMode.value === picker || forceTriggerSelect) {
          setInnerValue(date)

          if (onSelect) {
            onSelect(date)
          }

          if (onContextSelect) {
            onContextSelect(date, type)
          }

          if (
            onChange &&
            !isEqual(generateConfig, date, mergedValue.value) &&
            !disabledDate?.(date)
          ) {
            onChange(date)
          }
        }
      }

      // ========================= Interactive ==========================
      const onInternalKeydown = (e: KeyboardEvent) => {
        if (panelRef.value && panelRef.value.onKeydown) {
          if (
            [
              KeyCode.LEFT,
              KeyCode.RIGHT,
              KeyCode.UP,
              KeyCode.DOWN,
              KeyCode.PAGE_UP,
              KeyCode.PAGE_DOWN,
              KeyCode.ENTER,
            ].includes(e.which)
          ) {
            e.preventDefault()
          }
          return panelRef.value.onKeydown(e)
        }

        /* istanbul ignore next */
        /* eslint-disable no-lone-blocks */
        {
          warning(
            false,
            'Panel not correct handle keyDown event. Please help to fire issue about this.',
          )
          return false
        }
        /* eslint-enable no-lone-blocks */
      }

      const onInternalBlur = (e: FocusEvent) => {
        if (panelRef.value && panelRef.value.onBlur) {
          panelRef.value.onBlur(e)
        }
      }
      const onNow = () => {
        const { generateConfig, hourStep, minuteStep, secondStep } = props
        const now = generateConfig.getNow()
        const lowerBoundTime = getLowerBoundTime(
          generateConfig.getHour(now),
          generateConfig.getMinute(now),
          generateConfig.getSecond(now),
          isHourStepValid.value ? hourStep : 1,
          isMinuteStepValid.value ? minuteStep : 1,
          isSecondStepValid.value ? secondStep : 1,
        )
        const adjustedNow = setTime(
          generateConfig,
          now,
          lowerBoundTime[0], // hour
          lowerBoundTime[1], // minute
          lowerBoundTime[2], // second
        )
        triggerSelect(adjustedNow, 'submit')
      }

      const classString = computed(() => {
        const { prefixCls, direction } = props
        return classNames(`${prefixCls}-panel`, {
          [`${prefixCls}-panel-has-range`]:
            rangedValue && rangedValue.value && rangedValue.value[0] && rangedValue.value[1],
          [`${prefixCls}-panel-has-range-hover`]:
            hoverRangedValue &&
            hoverRangedValue.value &&
            hoverRangedValue.value[0] &&
            hoverRangedValue.value[1],
          [`${prefixCls}-panel-rtl`]: direction === 'rtl',
        })
      })
      useProvidePanel({
        ...panelContext,
        mode: mergedMode,
        hideHeader: computed(() =>
          props.hideHeader !== undefined ? props.hideHeader : panelContext.hideHeader?.value,
        ),
        hidePrevBtn: computed(() => inRange.value && panelPosition.value === 'right'),
        hideNextBtn: computed(() => inRange.value && panelPosition.value === 'left'),
      })

      watch(
        () => props.value,
        () => {
          if (props.value) {
            setInnerViewDate(props.value)
          }
        },
      )

      return () => {
        const {
          prefixCls = 'ant-picker',
          locale,
          generateConfig,
          disabledDate,
          picker = 'date',
          tabindex = 0,
          showNow,
          showTime,
          showToday,
          renderExtraFooter,
          onMousedown,
          onOk,
          components,
        } = props
        if (operationRef && panelPosition.value !== 'right') {
          operationRef.value = {
            onKeydown: onInternalKeydown,
            onClose: () => {
              if (panelRef.value && panelRef.value.onClose) {
                panelRef.value.onClose()
              }
            },
          }
        }

        // ============================ Panels ============================
        let panelNode: VueNode
        const pickerProps = {
          ...attrs,
          ...(props as MergedPickerPanelProps<DateType>),
          operationRef: panelRef,
          prefixCls,
          viewDate: viewDate.value,
          value: mergedValue.value,
          onViewDateChange: setViewDate,
          sourceMode: sourceMode.value,
          onPanelChange: onInternalPanelChange,
          disabledDate,
        }
        delete pickerProps.onChange
        delete pickerProps.onSelect
        switch (mergedMode.value) {
          case 'decade':
            panelNode = (
              <DecadePanel<DateType>
                {...pickerProps}
                onSelect={(date, type) => {
                  setViewDate(date)
                  triggerSelect(date, type)
                }}
              />
            )
            break

          case 'year':
            panelNode = (
              <YearPanel<DateType>
                {...pickerProps}
                onSelect={(date, type) => {
                  setViewDate(date)
                  triggerSelect(date, type)
                }}
              />
            )
            break

          case 'month':
            panelNode = (
              <MonthPanel<DateType>
                {...pickerProps}
                onSelect={(date, type) => {
                  setViewDate(date)
                  triggerSelect(date, type)
                }}
              />
            )
            break

          case 'quarter':
            panelNode = (
              <QuarterPanel<DateType>
                {...pickerProps}
                onSelect={(date, type) => {
                  setViewDate(date)
                  triggerSelect(date, type)
                }}
              />
            )
            break

          case 'week':
            panelNode = (
              <WeekPanel
                {...pickerProps}
                onSelect={(date, type) => {
                  setViewDate(date)
                  triggerSelect(date, type)
                }}
              />
            )
            break

          case 'time':
            delete pickerProps.showTime
            panelNode = (
              <TimePanel<DateType>
                {...pickerProps}
                {...(typeof showTime === 'object' ? showTime : null)}
                onSelect={(date, type) => {
                  setViewDate(date)
                  triggerSelect(date, type)
                }}
              />
            )
            break

          default:
            if (showTime) {
              panelNode = (
                <DatetimePanel
                  {...pickerProps}
                  onSelect={(date, type) => {
                    setViewDate(date)
                    triggerSelect(date, type)
                  }}
                />
              )
            } else {
              panelNode = (
                <DatePanel<DateType>
                  {...pickerProps}
                  onSelect={(date, type) => {
                    setViewDate(date)
                    triggerSelect(date, type)
                  }}
                />
              )
            }
        }

        // ============================ Footer ============================
        let extraFooter: VueNode
        let rangesNode: VueNode

        if (!hideRanges?.value) {
          extraFooter = getExtraFooter(prefixCls, mergedMode.value, renderExtraFooter)
          rangesNode = getRanges({
            prefixCls,
            components,
            needConfirmButton: needConfirmButton.value,
            okDisabled: !mergedValue.value || (disabledDate && disabledDate(mergedValue.value)),
            locale,
            showNow,
            onNow: needConfirmButton.value && onNow,
            onOk: () => {
              if (mergedValue.value) {
                triggerSelect(mergedValue.value, 'submit', true)
                if (onOk) {
                  onOk(mergedValue.value)
                }
              }
            },
          })
        }

        let todayNode: VueNode

        if (showToday && mergedMode.value === 'date' && picker === 'date' && !showTime) {
          const now = generateConfig.getNow()
          const todayCls = `${prefixCls}-today-btn`
          const disabled = disabledDate && disabledDate(now)
          todayNode = (
            <a
              class={classNames(todayCls, disabled && `${todayCls}-disabled`)}
              aria-disabled={disabled}
              onClick={() => {
                if (!disabled) {
                  triggerSelect(now, 'mouse', true)
                }
              }}
            >
              {locale.today}
            </a>
          )
        }
        return (
          <div
            tabindex={tabindex}
            class={classNames(classString.value, attrs.class)}
            style={attrs.style as CSSProperties}
            onKeydown={onInternalKeydown}
            onBlur={onInternalBlur}
            onMousedown={onMousedown}
          >
            {panelNode}
            {extraFooter || rangesNode || todayNode ? (
              <div class={`${prefixCls}-footer`}>
                {extraFooter}
                {rangesNode}
                {todayNode}
              </div>
            ) : null}
          </div>
        )
      }
    },
  })
}
const InterPickerPanel = PickerPanel<any>()
export default <DateType,>(props: MergedPickerPanelProps<DateType>) =>
  createVNode(InterPickerPanel, props)
