/* eslint-disable camelcase */
import {ReactNode, MouseEvent, FocusEvent} from 'react'
import dayjs, {Dayjs} from 'dayjs'
import c from 'classnames'
import {Popper} from '@befe/brick-comp-popper'
import {
    createDelayActionQueue,
    createListenerGroup,
    createRef,
    getClosestMappedSize,
    pickDerivedStateFromProps,
} from '@befe/brick-utils'
import {isUndefined, pickBy} from 'lodash-es'
import {InputProps} from '@befe/brick-comp-input'
import {ConfiguredComponent} from '@befe/brick-core'
import {UiRangePanel} from '../ui-comps/ui-range-panel'
import {
    createMonthPanel,
    TypeModuleMonthPanel,
} from '../modules/create-month-panel'
import {getPickerMode} from '../utils/picker-utils'
import {createDatePanel} from '../modules/create-date-panel'
import {getValueExtractor} from '../utils/get-value-extractor'
import {extendDayjsPlugins} from '../utils/extend-dayjs-plugins'
import {localeDictDatePicker} from '../locale'
import {syncDisplayedDatePair} from '../utils/sync-range-displayed-date-pair'
import {QuickActions} from './create-quick-actions'
import {DEFAULT_ITEM_FORMAT} from './const-default-format'
import {createRangePickerUserInput} from './create-range-input'
import {
    TypeActionLink,
    TypeDatePickerMode,
    TypeDateValue,
    TypePickerSize,
    TypePickerStatus,
} from './prop-types'
import {preventPickerBlurred} from './create-inner-date-picker'

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

export interface RangePickerProps extends InnerRangePickerPropsFromInput {
    className?: string

    startValue?: TypeDateValue
    endValue?: TypeDateValue

    defaultStartValue?: TypeDateValue
    defaultEndValue?: TypeDateValue

    disabled?: boolean

    size?: TypePickerSize

    status?: TypePickerStatus

    mode?: TypeDatePickerMode

    itemFormat?: string

    renderItem?: (
        currentDate: Dayjs,
        today: Dayjs
    ) => (ReactNode | string | number)

    /**
     * @todo: 未实现 range picker 的 disabled 问题
     */
    getDisabledItem?: (currentDate: Dayjs, today: Dayjs) => boolean

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

    showTime?: boolean

    getDisabledTime?: (currentDate: Dayjs) => {
        start: number | null
        end: number | null
    }

    open?: boolean | undefined
    clearable?: boolean

    startPlaceholder?: string
    endPlaceholder?: string

    startStyle?: CSSStyleDeclaration
    endStyle?: CSSStyleDeclaration

    popupClassName?: string
    popupStyle?: CSSStyleDeclaration

    onChange?: (start: Dayjs | null, end: Dayjs | null) => void

    /**
     * @todo: 待实现
     *
     * 这个传递 start, end 的原因, 可参见 date picker 的 onOpenChange 的注释
     */
    onOpenChange?: (
        openStatus: boolean,
        start: Dayjs | null,
        end: Dayjs | null,
    ) => void

    renderFooter?: () => ReactNode | TypeActionLink[]
    renderQuickActions?: () => TypeActionLink[]

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

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

    yearStart?: number
    yearEnd?: number
}

export interface RangePickerState {
    open: boolean

    isStartZooming: boolean
    isEndZooming: boolean

    startValue: Dayjs | null
    endValue: Dayjs | null

    startDisplayedDate: Dayjs | null
    endDisplayedDate: Dayjs | null

    startZoomedDisplayedDate: Dayjs | null
    endZoomedDisplayedDate: Dayjs | null

    isStartUserInput: boolean
    startUserInputText: string

    isEndUserInput: boolean
    endUserInputText: string

    startFocus: boolean
    endFocus: boolean

    isSelecting: boolean
    firstSelectedDate: Dayjs | null
    secondHoveringDate: Dayjs | null
}

type TypeRangePickerValueProp = 'startValue' | 'endValue';
type TypeRangePickerDisplayedProp = 'startDisplayedDate' | 'endDisplayedDate';
type TypeRangePickerDefaultProp = 'defaultStartValue' | 'defaultEndValue';


function initPickerValue(
    comp: InnerRangePicker,
    {
        defaultValueProp,
        valueProp,
        displayDateProp,
        mode,
    }: {
        defaultValueProp: TypeRangePickerDefaultProp
        valueProp: TypeRangePickerValueProp
        displayDateProp: TypeRangePickerDisplayedProp
        mode: RangePickerProps['mode']
    }
) {
    mode = mode || 'date'

    const intialValue = typeof comp.props[defaultValueProp] === 'undefined'
        ? comp.props[valueProp]
        : comp.props[defaultValueProp]

    comp.state[valueProp] = intialValue
        ? dayjs(intialValue).startOf(mode)
        : null

    comp.state[displayDateProp] = dayjs(intialValue || undefined)
        .startOf(mode === 'date' ? 'month' : 'year')
}

/**
 */
export class InnerRangePicker extends ConfiguredComponent<RangePickerProps, RangePickerState> {

    static dayjs = dayjs

    static displayName = 'RangePicker'

    static defaultProps: RangePickerProps = {
        clearable: true,
    }

    defaultItemFormat = DEFAULT_ITEM_FORMAT[this.props.mode || 'date']
    componentLocale = localeDictDatePicker

    state: RangePickerState = {
        open: false,

        startValue: null,
        endValue: null,

        startDisplayedDate: null,
        endDisplayedDate: null,

        isStartZooming: false,
        isEndZooming: false,

        startZoomedDisplayedDate: null,
        endZoomedDisplayedDate: null,

        isStartUserInput: false,
        startUserInputText: '',

        isEndUserInput: false,
        endUserInputText: '',

        startFocus: false,
        endFocus: false,

        isSelecting: false,

        firstSelectedDate: null,
        secondHoveringDate: null,
    }

    refPanelWrapper = createRef<Popper>()

    userInput = createRangePickerUserInput(this)

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

    /**
     * @deprecated @0427: 为了避免歧义, 改为 inputWrapper (因此处并非 input element)
     */
    get inputElem() {
        return this.userInput.inputWrapperElem
    }

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

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

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

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

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

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

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

    startDatePanel = createDatePanel(this, {
        isRange: true,
        isStartPanel: true,
        getTitle: this.getPanelTitle_yearMonth,
        getWeekDayText: this.getWeekDayText,
    })

    endDatePanel = createDatePanel(this, {
        isRange: true,
        isStartPanel: false,
        getTitle: this.getPanelTitle_yearMonth,
        getWeekDayText: this.getWeekDayText,
    })

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

    zoomedStartDatePanel: TypeModuleMonthPanel = createMonthPanel(this, {
        isRange: true,
        isForDateZooming: true,
        isStartPanel: true,

        valueProp: 'startDisplayedDate',
        displayProp: 'startZoomedDisplayedDate',
        getTitle: this.getPanelTitle_year,
        getMonthText: this.getMonthText,
    })

    zoomedEndDatePanel: TypeModuleMonthPanel = createMonthPanel(this, {
        isRange: true,
        isForDateZooming: true,
        isStartPanel: false,

        valueProp: 'endDisplayedDate',
        displayProp: 'endZoomedDisplayedDate',
        getTitle: this.getPanelTitle_year,
        getMonthText: this.getMonthText,
    })

    /**
     * @todo: month range picker
     */
    startMonthPanel = createMonthPanel(this, {
        isRange: true,
        isStartPanel: true,
        getTitle: this.getPanelTitle_year,
        getMonthText: this.getMonthText,
        syncRangePickersYearAside: () => {
            const year = this.state.startDisplayedDate?.year()
            if (!year) {
                return
            }

            const endMonthPanel = this.endMonthPanel
            endMonthPanel.setYearRangeAnchor(year)
            endMonthPanel.syncAsideScroll()
        },
    })

    endMonthPanel = createMonthPanel(this, {
        isRange: true,
        isStartPanel: false,
        getTitle: this.getPanelTitle_year,
        getMonthText: this.getMonthText,
        syncRangePickersYearAside: () => {
            const year = this.state.endDisplayedDate?.year()
            if (!year) {
                return
            }

            const startMonthPanel = this.startMonthPanel
            startMonthPanel.setYearRangeAnchor(year)
            startMonthPanel.syncAsideScroll()
        },
    })

    actionQueue = createDelayActionQueue()
    listenerGroup = createListenerGroup()

    get className() {
        const {className} = this.props
        const size = this.size

        return c(
            'brick-range-picker',
            {
                [`brick-range-picker-size-${size || ''}`]: size,
            },
            className
        )
    }

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

    get computedItemFormat() {
        const itemFormat = this.props.itemFormat
        if (itemFormat) {
            return itemFormat
        }

        const mode = this.props.mode || 'date'
        return DEFAULT_ITEM_FORMAT[mode]
    }

    get computedItemValueExtractor() {
        return getValueExtractor(this)
    }

    get shouldShowPanel() {
        return this.state.open
            || this.state.isStartUserInput
            || this.state.isEndUserInput
    }

    triggerChange = (
        {
            startValue = this.state.startValue,
            endValue = this.state.endValue,
        }: {
            startValue?: Dayjs | null
            endValue?: Dayjs | null
        }
    ) => {
        const {onChange} = this.props
        onChange && onChange(
            startValue,
            endValue
        )
    }

    handleCalendarIconClick = () => {
        if (!this.props.disabled) {
            this.setOpenStatus({
                open: true,
            })
            this.startInputElem?.focus()
        }
    }

    setOpenStatus = (
        {
            open = this.state.open,
            isStartUserInput = this.state.isStartUserInput,
            isEndUserInput = this.state.isEndUserInput,
        }: {
            open?: boolean
            isStartUserInput?: boolean
            isEndUserInput?: boolean
        }) => {
        const {getDefaultDisplayDateRange} = this.props
        const prevOpen = this.shouldShowPanel
        const currOpen = open || isStartUserInput || isEndUserInput

        this.setState({
            open,
            isStartUserInput,
            isEndUserInput,
        })

        if (currOpen !== prevOpen && currOpen) {
            const startValue = this.state.startValue
            const endValue = this.state.endValue

            const {
                startDisplayedDate,
                endDisplayedDate,
            } = syncDisplayedDatePair(
                this.props.mode,
                startValue,
                endValue,
                getDefaultDisplayDateRange
            )

            this.setState({
                isSelecting: false,
                startDisplayedDate,
                endDisplayedDate,
            })

            if (this.props.mode === 'month') {
                const startMonthPanel = this.startMonthPanel
                startMonthPanel.setYearRangeAnchor(
                    startDisplayedDate.year()
                )
                startMonthPanel.syncAsideScroll()

                const endMonthPanel = this.endMonthPanel
                endMonthPanel.setYearRangeAnchor(
                    endDisplayedDate.year()
                )
                endMonthPanel.syncAsideScroll()
            }
        }
    }

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

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

        if (this.props.mode === 'quarter') {
            console.error('目前 brick 范围选择器暂不支持季度模式')
        }

        initPickerValue(this, {
            defaultValueProp: 'defaultStartValue',
            valueProp: 'startValue',
            displayDateProp: 'startDisplayedDate',
            mode: this.props.mode,
        })

        initPickerValue(this, {
            defaultValueProp: 'defaultEndValue',
            valueProp: 'endValue',
            displayDateProp: 'endDisplayedDate',
            mode: this.props.mode,
        })

        // 确保前后不要出现同一个月
        if (
            this.state.endDisplayedDate!.isSame(this.state.startDisplayedDate)
        ) {
            const mode = this.props.mode || 'date'
            if (mode === 'date') {
                this.state.endDisplayedDate = this.state.endDisplayedDate!.add(1, 'month')
            } else if (mode === 'month') {
                this.state.endDisplayedDate = this.state.endDisplayedDate!.add(1, 'year')
            }
        }

        const onRangeSelected = (startValue: Dayjs, endValue: Dayjs) => {
            this.triggerChange({
                startValue,
                endValue,
            })

            this.setState({
                startUserInputText: this.userInput.formatStartInputValue(startValue),
                endUserInputText: this.userInput.formatEndInputValue(endValue),
            })

            this.setOpenStatus({
                open: false,
                isStartUserInput: false,
                isEndUserInput: false,
            })
        }

        this.startDatePanel.init({
            onZoomOut: () => {
                this.setState({
                    isStartZooming: true,
                    startZoomedDisplayedDate: this.state.startDisplayedDate,
                })

                this.zoomedStartDatePanel.setYearRangeAnchor(
                    this.state.startDisplayedDate!.year()
                )
                this.zoomedStartDatePanel.syncAsideScroll()
            },
            onSelectedRange: onRangeSelected,
        })

        this.endDatePanel.init({
            onZoomOut: () => {
                this.setState({
                    isEndZooming: true,
                    endZoomedDisplayedDate: this.state.endDisplayedDate,
                })

                this.zoomedEndDatePanel.setYearRangeAnchor(
                    this.state.startDisplayedDate!.year()
                )
                this.zoomedEndDatePanel.syncAsideScroll()
            },
            onSelectedRange: onRangeSelected,
        })

        this.zoomedStartDatePanel.init({
            onZoomIn: () => {
                this.setState({isStartZooming: false})
            },
            onSelected: (value) => {
                const nextMonth = value.add(1, 'month')
                if (nextMonth.isAfter(this.state.endDisplayedDate)) {
                    this.setState({
                        endDisplayedDate: nextMonth,
                    })
                }

                this.setState({
                    isStartZooming: false,
                })
            },
        })


        this.zoomedEndDatePanel.init({
            onZoomIn: () => {
                this.setState({isEndZooming: false})
            },
            onSelected: (value) => {
                this.setState({isEndZooming: false})
                const prevMonth = value.subtract(1, 'month')
                if (prevMonth.isBefore(this.state.startDisplayedDate)) {
                    this.setState({
                        startDisplayedDate: prevMonth,
                    })
                }

                this.setState({
                    isEndZooming: false,
                })
            },
        })

        this.startMonthPanel.init({
            onSelectedRange: onRangeSelected,
        })

        this.endMonthPanel.init({
            onSelectedRange: onRangeSelected,
        })
    }

    static getDerivedStateFromProps(
        nextProps: RangePickerProps
    ) {
        const partialState = pickDerivedStateFromProps(
            nextProps,
            ['open', 'startValue', 'endValue']
        )

        const mode = nextProps.mode || 'date'

        if (partialState) {
            if (partialState.startValue) {
                partialState.startValue = dayjs(partialState.startValue).startOf(mode)
            }
            if (partialState.endValue) {
                partialState.endValue = dayjs(partialState.endValue).startOf(mode)
            }
        }

        return partialState || null
    }

    componentDidMount(): void {
        this.listenerGroup.add(document, {
            mousedown: (e: MouseEvent) => {
                const targetElem = e.target as Node
                if (
                    this.state.open

                    && this.refPanelWrapper.elem
                    && this.refPanelWrapper.elem.elemPopperWrap
                    && !this.refPanelWrapper.elem.elemPopperWrap.contains(targetElem)

                    && this.userInput.inputWrapperElem
                    && !this.userInput.inputWrapperElem.contains(targetElem)
                ) {
                    this.setOpenStatus({
                        open: false,
                    })
                }
            },
        })

        this.actionQueue.execute()
    }

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

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

    quickActions = new QuickActions()

    /**
     * @question. 这个是为了做 "测试用" 提供的内部 API?
     *
     * @param start
     * @param end
     */
    setValue = (start?: TypeDateValue, end?: TypeDateValue) => {
        let startValue: Dayjs | null | undefined
        let endValue: Dayjs | null | undefined
        const mode = getPickerMode(this.props.mode)

        if (start) {
            startValue = dayjs(start).startOf(mode)
        } else if (start === null) {
            startValue = null
        }

        if (end) {
            endValue = dayjs(end).startOf(mode)
            if (startValue && endValue.isBefore(startValue)) {
                ([startValue, endValue] = [
                    endValue,
                    startValue,
                ])
            }
        } else if (end === null) {
            endValue = null
        }

        const resultState = pickBy({
            startValue,
            endValue,
        }, (value) => !isUndefined(value)) as unknown as Pick<RangePickerState,
            'startValue' | 'endValue' | 'isStartZooming' | 'isEndZooming'>

        const isStartUndefined = isUndefined(startValue)
        if (isStartUndefined) {
            resultState.isStartZooming = false
        }

        const isEndUndefined = isUndefined(endValue)
        if (isEndUndefined) {
            resultState.isEndZooming = false
        }

        if (isStartUndefined && isEndUndefined) {
            return
        }

        this.setState(resultState)
        this.syncDisplayDate()
    }

    getValue = () => {
        return [
            this.state.startValue,
            this.state.endValue,
        ]
    }

    syncDisplayDate = () => {
        const startValue = this.state.startValue
        const endValue = this.state.endValue

        let endDisplayedDate = (
            endValue && endValue.startOf('month')
        ) || this.state.endDisplayedDate

        if (
            endDisplayedDate
            && startValue
        ) {
            const mode = this.props.mode || 'date'

            if (
                mode === 'date'
                && endDisplayedDate.isSame(startValue, 'month')
            ) {
                endDisplayedDate = startValue.add(1, 'month')
            } else if (
                mode === 'month'
                && endDisplayedDate.isSame(startValue, 'year')
            ) {
                endDisplayedDate = startValue.add(1, 'year')
            }
        }

        this.setState({
            startDisplayedDate: startValue && startValue.startOf('month'),
            endDisplayedDate,
            isStartZooming: false,
            isEndZooming: false,
            isSelecting: false,
        })
    }

    renderPanel() {
        const {
            titleElem: leftTitleElem,
            contentElem: leftContentElem,
            asideElem: leftAsideElem,
            contentType: leftContentType,
        } = this.state.isStartZooming
            ? this.zoomedStartDatePanel.getRenderElem()
            : this.props.mode === 'month' ? this.startMonthPanel.getRenderElem()
                : this.startDatePanel.getRenderElem()

        const {
            titleElem: rightTitleElem,
            contentElem: rightContentElem,
            asideElem: rightAsideElem,
            contentType: rightContentType,
        } = this.state.isEndZooming
            ? this.zoomedEndDatePanel.getRenderElem()
            : this.props.mode === 'month' ? this.endMonthPanel.getRenderElem()
                : this.endDatePanel.getRenderElem()

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

        return (
            <UiRangePanel
                actionElem={quickActionsElem}
                leftContentType={leftContentType}
                leftTitleElem={leftTitleElem}
                leftAsideElem={leftAsideElem}
                leftContentElem={leftContentElem}
                rightContentType={rightContentType}
                rightTitleElem={rightTitleElem}
                rightAsideElem={rightAsideElem}
                rightContentElem={rightContentElem}
                onMouseDown={preventPickerBlurred}
            />
        )

    }

    render() {
        const shouldShowPanel = this.shouldShowPanel

        const props = this.props
        return (
            <div className={this.className}>
                {this.userInput.renderInput({
                    status: props.status,
                    disabled: props.disabled,
                    size: this.size,
                })}
                <Popper
                    className={this.popupClassName}
                    visible={shouldShowPanel}
                    placement={'bottom-start'}
                    target={this.userInput.inputWrapperElem || undefined}
                    ref={this.refPanelWrapper.ref}
                >
                    {this.renderPanel()}
                </Popper>
            </div>
        )
    }
}
