/* eslint-disable camelcase */
import {FC, ReactNode, FocusEvent, MouseEvent} from 'react'
import {Popper} from '@befe/brick-comp-popper'
import {
    createDelayActionQueue,
    createListenerGroup,
    getClosestMappedSize,
    getDefault,
    isDev,
    pickDerivedStateFromProps,
    StringIndexedObject,
} from '@befe/brick-utils'
import dayjs, {Dayjs} from 'dayjs'
import c from 'classnames'
import {InputProps} from '@befe/brick-comp-input'
import {ConfiguredComponent} from '@befe/brick-core'
import {UiTimePanel, COL_NAMES} from '@befe/brick-comp-time-picker'
import {Link} from '@befe/brick-comp-link'
import {Button} from '@befe/brick-comp-button'
import {createDatePanel} from '../modules/create-date-panel'
import {createMonthPanel, TypeModuleMonthPanel} from '../modules/create-month-panel'
import {createQuarterPanel} from '../modules/create-quarter-panel'
import {getPickerMode} from '../utils/picker-utils'
import {getValueExtractor} from '../utils/get-value-extractor'
import {extendDayjsPlugins} from '../utils/extend-dayjs-plugins'
import {localeDictDatePicker} from '../locale'
import {TypeActionLink, TypeDatePickerMode, TypeDateValue, TypePickerSize, TypePickerStatus} from './prop-types'
import {DEFAULT_ITEM_FORMAT} from './const-default-format'
import {CalendarIcon} from './partial-calender-icon'
import {QuickActions} from './create-quick-actions'
import {createDatePickerUserInput} from './create-user-input'

type InnerDatePickerPropsFromInput = Pick<InputProps, 'placeholder' | 'onFocus' | 'onBlur' | 'autoFocus'>

export interface DatePickerProps extends InnerDatePickerPropsFromInput {
    /**
     * 自定义 class
     */
    className?: string

    /**
     * 选中的日期值 (控制属性)
     *
     * @type Date | DayJS | null
     */
    value?: TypeDateValue

    /**
     * 选中的日期值 (非控制属性)
     *
     * @type Date | DayJS | null
     */
    defaultValue?: TypeDateValue

    disabled?: boolean

    /**
     * 整体大小控制 : xs, sm, md, lg, xl
     * 会影响 "输入框" & "面板" 的尺寸
     */
    size?: TypePickerSize

    /**
     * 是否当前出于出错状态
     */
    status?: TypePickerStatus

    /**
     * 选择器的取值类型
     */
    mode?: TypeDatePickerMode

    /**
     * 用来将 value 格式化给 input 框, 以及从 input 框中还原 value
     *
     * 默认:
     * - date:     'YYYY-MM-DD'
     * - month:    'YYYY-MM'
     * - quarter:  'YYYY-\QQ'
     *
     * 可参考:
     * [基础 format 格式](https://github.com/iamkun/dayjs/blob/HEAD/docs/en/API-reference.md#format-formatstringwithtokens-string)
     * & [高级 format 格式](https://github.com/iamkun/dayjs/blob/66ce23f2e18c5506e8f1a7ef20d3483a4df80087/docs/en/Plugin.md#advancedformat)
     */
    itemFormat?: string

    renderItem?: (
        currentDate: Dayjs,
        today: Dayjs
    ) => (JSX.Element | string | number)

    getDisabledItem?: (currentDate: Dayjs, today: Dayjs) => boolean

    /**
     * 当日期面板打开时, 设置其 "显示的面板位置"
     *
     * 这个可能主要在当没有选择日期时, 用于确定面板月份的显示策略 ( 缺省方案是显示 "当月" 或 "当年" )
     */
    getDefaultDisplayDate?: (today: Dayjs) => Dayjs

    /**
     * 只在 date 模式有意义, 是否允许选择时间
     *
     * @default false
     */
    showTime?: boolean

    /**
     * 是否显示此刻
     *
     * @default false
     */
    hideNow?: boolean

    /**
     * 是否显示时间
     *
     * @default false
     */
    showSecond?: boolean

    /**
     * 某天中, 可选的时间范围
     *
     * start, end 0 ~ 24 x 60 x 60 (一天的秒数), 如果为 null, 视为 无限制
     */
    getDisabledTime?: (currentDate: Dayjs) => ({
        start: number | null
        end: number | null
    })

    /**
     * 面板的显隐 (控制属性)
     */
    open?: boolean | undefined

    /**
     * 当选择器的面板显隐状态发生更改时, 会触发该回调
     * 这个是提供给外部组件, 进行完全的 date picker 面板显隐控制用
     *
     * @note: 如果你要接管 open 属性, 请务必确保这个 API 也实现了
     *
     * @param openStatus
     */
    onOpenChange?: (
        openStatus: boolean,
        value: Dayjs | null,
    ) => void

    /**
     * 日期选择器 input 框的 placeholder
     */
    placeholder?: string

    style?: CSSStyleDeclaration

    /**
     * 面板的附加 className
     */
    popupClassName?: string

    /**
     * @todo
     *
     * 面板的附加 style 控制
     */
    popupStyle?: CSSStyleDeclaration

    /**
     * 当选择中一个日期时, 会触发该回调
     *
     * @param selected
     */
    onChange?: (selected: Dayjs | null) => void

    /**
     * 自定义绘制 "面板的脚区域"
     */
    renderFooter?: () => JSX.Element | TypeActionLink[]

    /**
     * @todo
     * 自定义绘制面板的侧边快捷操作
     */
    renderQuickActions?: () => TypeActionLink[]

    /**
     * 提供给日期 (date) 选择中, 快速切换月份时, 年份的起始范围
     *
     * 默认: 已选中年或当前年的往前 12年
     */
    yearStart?: number

    /**
     * 参见 yearStart,
     *
     * 默认: 已选中年或当前年的往后 12年
     */
    yearEnd?: number

    /**
     * 是否支持清除已选值
     */
    clearable?: boolean

    onFocus?: (e: FocusEvent) => void
    onBlur?: (e: FocusEvent) => void

    autoFocus?: boolean

    onMouseEnter?: (e: MouseEvent) => void
    onMouseLeave?: (e: MouseEvent) => void

    iconSvg?: FC
    iconNode?: ReactNode
}

export interface DatePickerState {
    open: boolean
    isZooming: boolean

    inputValue: string

    value: Dayjs | null

    timeState: {
        hour: number | undefined
        minute: number | undefined
        second: number | undefined
    }

    displayedDate: Dayjs | null
    zoomedDisplayedDate: Dayjs | null

    isUserInput: boolean
    userInputText: string

    isHoverOverCalendar: boolean
}

export interface Type_InnerDatePicker {
    changeValue: (
        selectedDate: (dayjs.Dayjs | null),
        opts?: {
            timeState?: DatePickerState['timeState']
            onStateChanged?: () => void
        }
    ) => void
    processOpenStatus: (opts: { open?: boolean; isUserInput?: boolean }) => void
}

const mode2PanelProp: StringIndexedObject = {
    month: 'monthPanel',
    quarter: 'quarterPanel',
}

export const preventPickerBlurred = (e: MouseEvent) => {
    e.preventDefault()
}

function getModeForTitle(
    mode: 'date' | 'month' | 'quarter',
    showTime = false,
    showSecond = false
) {
    if (mode === 'date') {
        if (!showTime) {
            return 'date'
        }

        if (!showSecond) {
            return 'date-time-without-second'
        }

        return 'date-time';
    }

    return mode;
}

/**
 * @todo. @review.
 * 这个是之前为了 "luffy" (如流那边的使用场景) 预留的一种写法, 目前已经不应该这么写了...
 * 太过费劲...
 * 包括 => create input 的部分
 *
 * @param customIconDefaultOption
 */
export function createInnerDatePicker(
    customIconDefaultOption: CalendarIcon['opts'] = {}
) {

    return class InnerDatePicker extends ConfiguredComponent<DatePickerProps, DatePickerState>
        implements Type_InnerDatePicker {
        // === 对外 static API ===
        static dayjs = dayjs

        // === locale ===
        componentLocale = localeDictDatePicker

        // === 内部 static API ===
        static displayName = 'DatePicker'

        static defaultProps: DatePickerProps = {
            mode: 'date',
            className: '',
            disabled: false,
            status: 'normal',

            showTime: false,
            hideNow: false,
            showSecond: false,

            yearStart: 12,
            yearEnd: 12,

            clearable: true,
        }


        // === 内部 props ===
        state: DatePickerState = {
            open: typeof this.props.open === 'undefined' ? false : this.props.open,
            isZooming: false,

            inputValue: '',

            // @todo:check
            value: null,

            // @note: showTime = false 的场景下, 我们不记录或维护 timeState, 默认为 00:00:00
            timeState: {
                hour: undefined,
                minute: undefined,
                second: undefined,
            },

            displayedDate: null,
            zoomedDisplayedDate: null,

            isUserInput: false,
            userInputText: '',

            isHoverOverCalendar: false,
        }

        userInput = createDatePickerUserInput(this, customIconDefaultOption)

        get inputElem() {
            return this.userInput.inputElem
        }

        get size(): DatePickerProps['size'] {
            return this.getDefaultValueUsingTheme('size', 'baseSize')
        }

        getWeekDayText = (day: number) => {
            return this.getLocaleText('get_week_day_text', day)
        }

        datePanel = createDatePanel(this, {
            valueProp: 'value',
            displayProp: 'displayedDate',
            onMouseDown: preventPickerBlurred,
            getTitle: (date: Dayjs) => this.getLocaleText('get_year_month_title', date as any),
            getWeekDayText: this.getWeekDayText,
            renderPanelSideContent: () => {
                return !this.props.showTime
                    ? null
                    : this.renderTimeArea()
            },
        })

        getPanelTitle_year = (date: Dayjs) => {
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
            return this.getLocaleText('get_year_title', date as any)
        }

        getMonthText = (month: number) => this.getLocaleText('get_month_text', month)

        zoomedDatePanel: TypeModuleMonthPanel = createMonthPanel(this, {
            isForDateZooming: true,

            valueProp: 'displayedDate',
            displayProp: 'zoomedDisplayedDate',
            onMouseDown: preventPickerBlurred,
            getTitle: this.getPanelTitle_year,
            getMonthText: this.getMonthText,
        })

        monthPanel = createMonthPanel(this, {
            isForDateZooming: false,

            valueProp: 'value',
            displayProp: 'displayedDate',
            onMouseDown: preventPickerBlurred,
            getTitle: this.getPanelTitle_year,
            getMonthText: this.getMonthText,
        })

        quarterPanel = createQuarterPanel(this, {
            valueProp: 'value',
            displayProp: 'displayedDate',
            onMouseDown: preventPickerBlurred,
            getTitle: this.getPanelTitle_year,
        })

        inputWrapperElem: HTMLDivElement | null = null

        panelWrapperElem: Popper | null = null

        actionQueue = createDelayActionQueue()
        listenerGroup = createListenerGroup()

        previousOpen: boolean | null = null

        quickActions = new QuickActions()

        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        constructor(props: any) {
            super(props)

            // 确保 dayjs 插件扩展成功
            extendDayjsPlugins()

            const initValue = typeof this.props.defaultValue === 'undefined'
                ? this.props.value : this.props.defaultValue

            this.state.value = initValue ? dayjs(initValue) : null

            // 如果有 state.value 值, 需要对齐该显示月份
            this.state.displayedDate = dayjs(this.state.value || undefined)

            const onItemSelected = (selectedDate: Dayjs) => {
                const showTime = this.props.showTime
                if (showTime) {
                    const timeState = this.state.timeState
                    selectedDate = selectedDate.set('hour', timeState.hour || 0)
                        .set('minute', timeState.minute || 0)
                        .set('second', timeState.second || 0)
                }

                this.changeValue(selectedDate, {
                    onStateChanged: () => {
                        this.setState({
                            isUserInput: Boolean(showTime),
                            userInputText: this.userInput.formatInputValue(selectedDate),
                        })
                        this.changeDatePickerOpenStatus(false, selectedDate)
                    },
                })
            }

            this.datePanel.init({
                onZoomOut: () => {
                    this.setState({
                        isZooming: true,
                        zoomedDisplayedDate: this.state.displayedDate,
                    })

                    this.zoomedDatePanel.setYearRangeAnchor(this.state.displayedDate!.year())
                    this.zoomedDatePanel.syncAsideScroll()
                },
                onSelected: onItemSelected,
            })

            this.zoomedDatePanel.init({
                onZoomIn: () => {
                    this.setState({
                        isZooming: false,
                    })
                },
                onSelected: () => {
                    this.setState({
                        isZooming: false,
                    })
                },
            })

            this.monthPanel.init({
                onSelected: onItemSelected,
            })

            this.quarterPanel.init({
                onSelected: onItemSelected,
            })

        }

        static getDerivedStateFromProps(nextProps: DatePickerProps, prevState: DatePickerState) {
            const partialState: Partial<DatePickerState> | null
                = pickDerivedStateFromProps(nextProps, ['open', 'value']) as (Partial<DatePickerState> | null)

            if (partialState && partialState.value) {
                const value = dayjs(partialState.value)
                partialState.value = value
                const hour = value.hour()
                const minute = value.minute()
                const second = value.second()
                if (
                    prevState.timeState.hour !== hour
                    || prevState.timeState.minute !== minute
                    || prevState.timeState.second !== second
                ) {
                    partialState.timeState = {
                        hour,
                        minute,
                        second,
                    }
                }
            }

            return partialState || null
        }

        refInputWrapper = (node: HTMLDivElement | null): void => {
            this.inputWrapperElem = node
        }
        refPanelWrapper = (node: Popper | null): void => {
            this.panelWrapperElem = node
        }

        // === 公共对内控制 API ===
        syncDisplayDate = () => {
            const mode = getPickerMode(this.props.mode)
            const value = this.state.value
            this.setState({
                displayedDate: value ? value.startOf(mode) : dayjs(),
                isZooming: false,
            })
        }

        /**
         * 用于 `非控型组件` 进行取值设置
         *
         * @param value
         */
        setValue = (value: TypeDateValue) => {
            if (!value) {
                this.setState({
                    value: null,
                })
            } else if (value) {
                const mode = getPickerMode(this.props.mode)
                value = dayjs(value).startOf(mode)

                this.setState({
                    value,
                    displayedDate: value,
                    isZooming: false,
                })
            }
        }

        /**
         * 用于提供给 `非控型组件` 的值获取
         */
        getValue = () => {
            return this.state.value
        }

        focus() {
            this.inputElem?.focus()
        }

        componentDidUpdate(): void {
            this.actionQueue.execute()
        }

        componentDidMount(): void {
            this.listenerGroup.add(document, {
                'mousedown': (e: MouseEvent) => {
                    if (
                        this.state.open

                        && this.panelWrapperElem
                        && this.panelWrapperElem.elemPopperWrap
                        && !this.panelWrapperElem.elemPopperWrap.contains(e.target as Node)

                        && this.inputWrapperElem
                        && !this.inputWrapperElem.contains(e.target as Node)
                    ) {
                        this.setOpen(false)
                        this.processOpenStatus({open: false})
                    }
                },
            })
            this.actionQueue.execute()

            /**
             * @important: 这里需要确保 props.open = true 时, 在初次显示之后,
             * 能把面板显示出来
             *
             * @todo: 如果 popper 有统一解决方案, 需要根据新的方案进行调整!! @郑亮亮, @吴俊
             *
             */
            if (this.props.open === true) {
                this.forceUpdate()
            }
        }

        componentWillUnmount(): void {
            this.listenerGroup.removeAll()
        }

        // === getter ===
        get className() {
            const {className} = this.props
            const size = this.size;
            return c(
                'brick-date-picker',
                {
                    [`brick-date-picker-size-${size || ''}`]: size,
                },
                className
            )
        }

        get popupClassName() {
            const {popupClassName, showTime} = this.props;
            const size = getClosestMappedSize(this.size);
            return c(
                'brick-date-picker-popper',
                {
                    [`brick-date-picker-popper-size-${size || ''}`]: size,
                    'brick-date-picker-popper-show-time': showTime,
                },
                popupClassName
            )
        }

        get computedItemFormat() {
            if (this.props.itemFormat) {
                return this.props.itemFormat
            }
            const {mode, showTime, showSecond} = this.props;
            const modeForTitle = getModeForTitle(getPickerMode(mode), showTime, showSecond)
            return DEFAULT_ITEM_FORMAT[modeForTitle];
        }

        get computedItemValueExtractor() {
            return getValueExtractor(this)
        }

        // === inner methods ===
        changeValue = (
            selectedDate: Dayjs | null,
            opts: {
                timeState?: DatePickerState['timeState']
                onStateChanged?: () => void
            } = {}
        ) => {
            const {
                onChange,
                showSecond,
            } = this.props

            const {
                onStateChanged,
            } = opts

            const timeState = opts.timeState || {
                hour: selectedDate ? selectedDate.get('hour') : undefined,
                minute: selectedDate ? selectedDate.get('minute') : undefined,
                second: selectedDate ? selectedDate.get('second') : undefined,
            }

            if (!showSecond && selectedDate) {
                selectedDate = selectedDate.set('second', 0);
            }

            if (onChange) {
                onChange(selectedDate)
            }

            /**
             * @note: 对应的 setState 应该是不同的 ( props.value 如无, 则应为 uncontrolled 心态)
             *
             * - undefined => uncontrolled
             * - null => 对应 "未选中 date 的时刻, time 理应也能被选择"
             */
            if (!this.props.value) {
                this.setState({
                    value: selectedDate,
                    timeState,
                }, onStateChanged)
            } else {
                onStateChanged?.()
            }
        }

        /**
         * - 确保打开时, scroll 到特定的年份
         * - displayDate 被重置
         */
        processOpenStatus = (opts: {
            open?: boolean
            isUserInput?: boolean
        }) => {
            const open = getDefault(opts.open, this.state.open)
            const userInput = getDefault(opts.isUserInput, this.state.isUserInput)
            const {getDefaultDisplayDate} = this.props

            const computedOpen = open || userInput || null

            if (computedOpen !== this.previousOpen && computedOpen) {
                const value = this.state.value
                let displayedDate: Dayjs

                if (value || !getDefaultDisplayDate) {
                    displayedDate = value ? value.clone() : dayjs()
                } else {
                    displayedDate = getDefaultDisplayDate(dayjs())
                }

                this.setState({
                    displayedDate,
                })

                const panelProp = mode2PanelProp[this.props.mode!] as 'monthPanel' | 'quarterPanel' | undefined
                if (panelProp) {
                    this[panelProp].setYearRangeAnchor(displayedDate.year())
                    this[panelProp].syncAsideScroll()
                }
            }
            this.previousOpen = computedOpen
        }

        /**
         * 如果涉及 value 更改, 务必将 value 传递过来
         * @param open
         * @param value
         */
        setOpen = (open: boolean, value?: Dayjs | null) => {
            // open 为非控型属性
            if (typeof this.props.open === 'undefined') {
                this.setState({open})
                return
            }

            if (!this.props.onOpenChange) {
                if (isDev()) {
                    throw new Error('当 `open` 为控制型属性时, 你应该接管这个属性的主动处理')
                }
                return
            }

            /**
             * @note: 为什么需要把 value 也回传 ?
             *
             * 因为在某些业务场景下, 我们可能会存在 :
             *
             * # 需求
             *
             * - 当 value 被清空时, 把整个 date picker 都卸载掉 (即最终 unmount)
             *
             * # 问题
             *
             * - 这时会存在外部需要用 value 来控制 "shouldShowDatePicker" 这个状态
             *  - 注意, 和 shouldOpenDatePickerPanel (即 `open` 属性) 有区别, 后者是控制 "是否打开面板"
             * - 而存在一种情况, 即清除掉 date picker input 内容之后, date picker 仍然获得焦点, 内部最终的 `open` 属性为 true,
             * - 所以, 这种场景下, 需要在 `onOpenChange` 事件下, 把 value & open 传递给上层调用方
             *  - 由它来决定是否某些 `open=true` 的情况下, 也等同于 `open=false` 并借由控制 `shouldShowDatePicker`
             *      - 最终, 卸载掉整个组件.
             */
            this.props.onOpenChange(
                open,
                typeof value === 'undefined' ? this.state.value : value
            )
        }

        // === handlers ===
        handleCalendarIconClick = () => {
            if (!this.props.disabled) {
                this.changeDatePickerOpenStatus(true)
            }
        }

        // ==== 选择器面板的显隐控制 ====
        changeDatePickerOpenStatus = (visible: boolean, value?: Dayjs | null) => {
            this.setState({
                isZooming: false,
            })
            this.setOpen(visible, value)
            this.processOpenStatus({open: visible})
        }

        refUITimePanel: UiTimePanel | null = null
        tryScrollToSelectedTime = () => {
            this.refUITimePanel?.scrollToSelected()
        }

        changeTime = (
            propName: typeof COL_NAMES[number],
            propValue: number
        ) => {
            let value = this.state.value

            value = value && value.set(propName, propValue)
            const userInputText = this.userInput.formatInputValue(value)

            const timeState = this.state.timeState
            timeState[propName] = propValue

            this.changeValue(value, {
                timeState,
                onStateChanged: () => {
                    this.setState({
                        userInputText: userInputText,
                    })
                },
            })
        }

        setCurrentDateTime = () => {
            const currentDateTime = dayjs().startOf('second')
            this.changeValue(currentDateTime, {
                onStateChanged: () => {
                    this.syncDisplayDate()
                    this.tryScrollToSelectedTime()
                    this.setState({
                        userInputText: this.userInput.formatInputValue(currentDateTime),
                    })
                },
            })
        }

        confirmDateTime = () => {
            this.setOpen(
                false
            )
            this.setState({
                isUserInput: false,
            })
            this.processOpenStatus({
                open: false,
                isUserInput: false,
            })
        }

        // === renderers ===
        renderTimeArea = () => {
            const {
                hour,
                minute,
                second,
            } = this.state.timeState

            const {showSecond} = this.props;

            return (
                <UiTimePanel
                    ref={panel => this.refUITimePanel = panel}
                    hour={hour}
                    minute={minute}
                    second={second}
                    showSecond={showSecond}
                    onChange={this.changeTime}
                />
            )
        }

        renderPanel = () => {
            const {mode, hideNow} = this.props

            const quickActionsElem = this.quickActions.renderQuickActions({
                getValue: () => this.state.value,
                getActionQueue: () => this.actionQueue,
                syncDisplayDate: () => this.syncDisplayDate(),
                onMouseDown: preventPickerBlurred,
                renderQuickActions: this.props.renderQuickActions,
            })

            let pickerElem = null

            if (mode === 'date') {
                if (!this.state.isZooming) {
                    pickerElem = this.datePanel.renderPanel()
                } else {
                    pickerElem = this.zoomedDatePanel.renderPanel()
                }
            } else if (mode === 'month') {
                pickerElem = this.monthPanel.renderPanel()
            } else if (mode === 'quarter') {
                pickerElem = this.quarterPanel.renderPanel()
            }

            let pickerMainContent
            if (quickActionsElem) {
                pickerMainContent = (
                    <div
                        className={'brick-date-picker-panel-with-quick-actions'}
                    >
                        {quickActionsElem}
                        {pickerElem}
                    </div>
                )
            } else {
                pickerMainContent = pickerElem
            }

            if (!this.props.showTime) {
                return pickerMainContent
            }

            const linkNow = hideNow ? null : (
                <Link onClick={this.setCurrentDateTime}>
                    此刻
                </Link>
            )

            return (
                <div className="brick-date-picker-panel-with-footer">
                    {pickerMainContent}
                    <div
                        className="brick-date-picker-panel-footer"
                        onMouseDown={preventPickerBlurred}
                    >
                        <div className="left-section">
                            {linkNow}
                        </div>
                        <div className="right-section">
                            <Button
                                type={'important'}
                                size={'sm'}
                                onClick={this.confirmDateTime}
                            >
                                确定
                            </Button>
                        </div>
                    </div>
                </div>
            )
        }

        render() {
            const shouldShowPanel = this.state.isUserInput || this.state.open

            return (
                <div
                    className={this.className}
                    ref={this.refInputWrapper}
                >
                    {this.userInput.renderInput()}
                    <Popper
                        className={this.popupClassName}
                        visible={shouldShowPanel}
                        placement={'bottom-start'}
                        target={this.userInput.inputElem}
                        ref={this.refPanelWrapper}
                    >
                        {this.renderPanel()}
                    </Popper>
                </div>
            )
        }
    }
}

export type TypeInnerDatePicker = InstanceType<ReturnType<typeof createInnerDatePicker>>
