import {
    Component,
    ContextType,
    createContext,
    SyntheticEvent,
    cloneElement,
    Ref,
    ReactElement,
    Children,
    ReactComponentElement,
    ReactInstance,
    isValidElement,
    MouseEvent,
    FocusEvent,
    Fragment,
} from 'react'
// import * as PropTypes from 'prop-types'
// import {default as c} from 'classnames'

import {
    codeWarningOnce,
    safeInvoke,
    addEventListener,
    EventListenerHandler,
    ifNodeContains,
    refNode,
    pickDerivedStateFromProps,
    isPromiseLike, isDev,
} from '@befe/brick-utils'

import {isFunction, isUndefined} from 'lodash-es'
import {isPopper, Popper, PopperProps} from './popper'
import {PopperContext} from './popper-context'

// 不同于 PopperTarget，不接受模拟的 PopperJS.ReferenceObject
// 因为一个模拟的 ReferenceObject 无所谓 trigger 触发方式，没有意义
type TriggerTarget = (() => HTMLElement | null | void) | HTMLElement | null


export interface PopperTriggerProps {
    /**
     * 触发方式
     */
    type?: 'click' | 'hover' | 'focus'

    /**
     * @todo 是否禁用
     * @private
     *
     * 暂不提供
     * - trigger 是个无实体的 wrap，按理说应没有所谓的 disabled
     * - 考虑 `<PopperTrigger><Button disabled /></PopperTrigger>` 这种情况（目标 disabled, 但可以 hover 出 tips 的情景），
     * 多一层次 disabled 实际有点混淆
     * - `beforeChange` / `onChange` 可以满足 “（根据需要）不触发” 的情况，且api 使用复杂度没有比 `disabled` 大
     */
    disabled?: boolean

    /**
     * popper 是否显示控制值
     */
    visible?: boolean

    /**
     * 指定 popper 的目标元素
     * 注意
     * - 不接受 ReactNode
     * - 不接受模拟的 PopperJS.ReferenceObject，因为一个模拟的 ReferenceObject 无所谓 trigger 触发方式，没有意义
     * @type (() => HTMLElement | null | void) | HTMLElement | null
     */
    target?: TriggerTarget

    /**
     * popper 是否显示默认值
     */
    defaultVisible?: boolean

    /**
     * visible 变化回调
     */
    onChange?: (visible: boolean, e?: Event | SyntheticEvent) => void

    /**
     * visible 变化前的回调，根据返回值进行 visible 的变化
     * - promise，当 promise resolve 后进行 visible change, promise reject 则不进行
     * - false，visible 不进行 visible change
     * - true | void，立即进行 visible change
     */
    beforeChange?: (visible: boolean) => Promise<unknown> | boolean | void

    /**
     * mouseEnter 延迟触发，单位：毫秒
     */
    mouseEnterDelay?: number

    /**
     * mouseLeave 延迟触发，单位：毫秒
     */
    mouseLeaveDelay?: number

    /**
     * focus 延迟出发，单位：毫秒
     */
    focusDelay?: number

    /**
     * blur 延迟触发，单位：毫秒
     */
    blurDelay?: number

    /**
     * 是否隐藏时隐藏 parent
     */
    shouldHideParent?: boolean

    /**
     * 对于 type click，
     * document 会绑定一个 mousedown 事件，实现 "点击 popper 外部时隐藏 popper" 行为
     *
     * - 设成 `false` 可去掉此行为
     * - 也接受一个回调，以应付额外的自定义判断情况，比如，但如果点击 outside 的是某特定元素，则不关闭
     */
    shouldHideOnMousedownDocument?: boolean | ((e: Event) => boolean)
}

type TriggerEventType = 'onClick' | 'onMouseEnter' | 'onMouseLeave' | 'onFocus' | 'onBlur'
type PopperEventType = 'onMouseEnter' | 'onMouseLeave'

export const PopperTriggerContext = createContext({})
export const PopperTriggerConsumer = PopperTriggerContext

export const POPPER_TRIGGER_MINIMUM_MOUSE_LEAVE_DELAY = 60

export const POPPER_TRIGGER_INVALID_CHILDREN_WARNING = 'PopperTrigger 只允许 ReactElement 作为 children'
export const POPPER_TRIGGER_CONTROL_VISIBLE_INIT_WARNING = [
    '[PopperTrigger] it won\'t show properly if not provide `props.target`',
    'while setting visible `true` by `props.visible` / `props.defaultVisible` at initialization',
].join(' ')

interface PopperTriggerState {
    visible: boolean
    target?: TriggerTarget
}

interface ReactElementWithRef extends ReactElement {
    ref?: Ref<ReactInstance>
}

export class PopperTrigger extends Component<PopperTriggerProps, PopperTriggerState> {
    static displayName = 'PopperTrigger'
    // static propTypes = {}
    static defaultProps = {
        type: 'click',
        defaultVisible: false,
        disabled: false,
        mouseEnterDelay: 150,
        mouseLeaveDelay: POPPER_TRIGGER_MINIMUM_MOUSE_LEAVE_DELAY,
        focusDelay: 150,
        blurDelay: 0,
        shouldHideOnMousedownDocument: true,
    }

    static contextType = PopperContext

    static getDerivedStateFromProps(nextProps: PopperTriggerProps) {
        return pickDerivedStateFromProps(nextProps, ['visible', 'target'])
    }

    context!: ContextType<typeof PopperContext>

    // hasChildShow = false
    handleClickOutside: EventListenerHandler | null = null

    nodeTarget?: ReactInstance | null = null
    elemPopperWrap?: HTMLDivElement | null = null
    nodePopper?: Popper | null = null
    // elemPopperWrap?: ComponentType<Popper> | null = null

    visibleTimeoutId: number | null = null

    scrollElement: HTMLElement | null = null

    constructor(props: PopperTriggerProps) {
        super(props)
        const {target, visible} = props
        const initialState: PopperTriggerState = {
            visible: isUndefined(visible) ? !!props.defaultVisible : visible,
            target,
        }

        codeWarningOnce(
            !initialState.visible || !!initialState.target,
            POPPER_TRIGGER_CONTROL_VISIBLE_INIT_WARNING
        )
        this.state = initialState
    }

    refTarget = (node: ReactInstance) => {
        this.nodeTarget = node
        const childTarget = (this.childTarget as ReactElementWithRef)
        childTarget && refNode(childTarget.ref, node)
    }

    refPopper = (node: Popper) => {
        this.nodePopper = node
        const childPopper = (this.childPopper as ReactElementWithRef)
        childPopper && refNode(childPopper.ref, node)
    }

    refPopperWrap = (node: HTMLDivElement) => {
        this.elemPopperWrap = node
        const childPopper = this.childPopper!
        refNode(childPopper.props.refPopperWrap, node)
    }

    get targetElement() {
        const {target} = this.state
        if (isFunction(target)) {
            return target()
        }

        return target
    }

    get contextValue() {
        return {
            ...this.context,
            hideByChild: this.hideByChild,
        }
    }

    get childrenArray() {
        const {children} = this.props
        if (!children) {
            return []
        }

        return Children.toArray(this.props.children).filter(child => {
            codeWarningOnce(
                isValidElement(child),
                POPPER_TRIGGER_INVALID_CHILDREN_WARNING
            )
            return isValidElement(child)
        }) as ReactElement[]
    }

    get childTarget() {
        return this.childrenArray.find(child => !isPopper(child)) as ReactComponentElement<any>
    }

    get childPopper() {
        return this.childrenArray.find(child => isPopper(child)) as ReactElement<PopperProps> | null
    }

    get targetInjectProps() {
        const eventHandlers = this.props.type && {
            'click': {
                onClick: this.handleClickTarget,
            },
            'hover': {
                onMouseEnter: this.handleMouseEnterTarget,
                onMouseLeave: this.handleMouseLeaveTarget,
            },
            'focus': {
                onFocus: this.handleFocusTarget,
                onBlur: this.handleBlurTarget,
            },
        }[this.props.type]
        return {
            ref: this.refTarget,
            ...eventHandlers,
        }
    }

    get popperInjectProps() {
        const eventHandlers = this.props.type === 'hover' && {
            onMouseEnter: this.handleMouseEnterPopper,
            onMouseLeave: this.handleMouseLeavePopper,
        }
        return {
            ref: this.refPopper,
            refPopperWrap: this.refPopperWrap,
            visible: this.state.visible,
            target: this.state.target,
            ...eventHandlers,
        }
    }

    handleClickTarget = (e: MouseEvent) => {
        return this.invokeTargetEventHandler('onClick', e)
    }

    handleMouseEnterTarget = (e: MouseEvent) => {
        this.invokeTargetEventHandler('onMouseEnter', e, true)
    }

    handleMouseLeaveTarget = (e: MouseEvent) => {
        this.invokeTargetEventHandler('onMouseLeave', e, false)
    }

    handleFocusTarget = (e: FocusEvent) => {
        this.invokeTargetEventHandler('onFocus', e, true)
    }

    handleBlurTarget = (e: FocusEvent) => {
        this.invokeTargetEventHandler('onBlur', e, false)
    }

    handleMouseEnterPopper = (e: MouseEvent<HTMLDivElement>) => {
        this.invokePopperEventHandler('onMouseEnter', e)
        this.clearVisibleDelayTimeout()
    }

    handleMouseLeavePopper = (e: MouseEvent<HTMLDivElement>) => {
        this.invokePopperEventHandler('onMouseLeave', e)
        this.delaySetVisible(false, this.getEventDelay('onMouseLeave'), e)
    }

    handleClickDoc = (e: Event) => {
        // don't use `findDOMNode` which has been deprecated in StrictMode.
        // https://reactjs.org/docs/react-dom.html#finddomnode
        // https://reactjs.org/docs/strict-mode.html#warning-about-deprecated-finddomnode-usage
        const {shouldHideOnMousedownDocument} = this.props
        const shouldClose = typeof shouldHideOnMousedownDocument === 'function'
            ? shouldHideOnMousedownDocument(e)
            : shouldHideOnMousedownDocument

        if (
            shouldClose
            && !ifNodeContains(this.targetElement, e.target as Node)
            && !this.nodePopper?.isDescendant(e.target as Node)
        ) {
            this.setVisible(false, e)
            this.hideParent()
        }
    }

    updateClickOutsideHandler() {
        if (!this.state.visible) {
            this.clearClickOutsideHandler()
        } else if (
            !this.handleClickOutside && this.props.type === 'click' && this.props.shouldHideOnMousedownDocument
        ) {
            this.handleClickOutside = addEventListener(window.document, 'mousedown', this.handleClickDoc)
        }
    }

    clearClickOutsideHandler() {
        if (this.handleClickOutside) {
            this.handleClickOutside.remove()
            this.handleClickOutside = null
        }
    }

    delaySetVisible(visible: boolean, delay: number, e: SyntheticEvent) {
        this.clearVisibleDelayTimeout()
        if (delay > 0) {
            e.persist()
            this.visibleTimeoutId = window.setTimeout(() => {
                this.setVisible(visible, e)
                this.clearVisibleDelayTimeout()
            }, delay)
        } else {
            this.setVisible(visible, e)
        }
    }

    clearVisibleDelayTimeout() {
        if (this.visibleTimeoutId) {
            clearTimeout(this.visibleTimeoutId)
            this.visibleTimeoutId = null
        }
    }

    hideByChild = () => {
        // @todo to-remove untouchable
        // 对于子孙节点是应该跳过的判断，在调用 hideByChild 之前已经进行过，且按职责考虑，也不应该在此进行
        // if (e && ifNodeContains(this.elemPopperWrap as Element, e.target as Node)) {
        //     return
        // }
        this.setVisible(false)
        this.hideParent()
    }

    hideParent() {
        if (this.props.shouldHideParent) {
            safeInvoke(this.context.hideByChild)
        }
    }

    /**
     * @public
     */
    setVisible(visible: boolean, e?: Event | SyntheticEvent) {
        this.clearVisibleDelayTimeout()

        const beforeChangeResult = safeInvoke(this.props.beforeChange, visible)

        if (beforeChangeResult === false) {
            return
        }

        const change = () => {
            if (isUndefined(this.props.visible)) {
                this.setState({
                    visible,
                })
            }
            safeInvoke(this.props.onChange, visible, e)
        }


        if (beforeChangeResult === true || beforeChangeResult === undefined) {
            change()
            // 同步的 beforeChange 情况下保持同步，以保证执行顺序符合预期
            // 不应转成异步
            // beforeChangeResult = Promise.resolve()
        }

        if (isPromiseLike(beforeChangeResult)) {
            const syntheticEvent = (e as SyntheticEvent)
            if (syntheticEvent && typeof syntheticEvent.persist === 'function') {
                syntheticEvent.persist()
            }

            beforeChangeResult.then(change).catch(error => {
                isDev() && console.log(`[Target] beforeChange rejected: ${String(error)}`)
            })
        }

    }

    setTarget(target: HTMLElement) {
        this.setState({
            target,
        })
    }

    getEventDelay(eventType: TriggerEventType | PopperEventType) {
        const props = this.props
        const delay = eventType && {
            'onBlur': props.blurDelay,
            'onFocus': props.focusDelay,
            'onMouseLeave': Math.max(
                props.mouseLeaveDelay || 0,
                POPPER_TRIGGER_MINIMUM_MOUSE_LEAVE_DELAY
            ),
            'onMouseEnter': props.mouseEnterDelay,
            'onClick': 0,
        }[eventType] || 0

        return Math.max(0, delay)
    }

    invokeTargetEventHandler(
        type: TriggerEventType,
        e: MouseEvent | FocusEvent,
        nextVisible = !this.state.visible
    ) {
        const childTarget = this.childTarget
        const result = childTarget
            && safeInvoke(childTarget.props[type] as unknown as (e: MouseEvent | FocusEvent) => void, e)
        this.delayToggleVisible(e, this.getEventDelay(type), nextVisible)
        return result
    }

    invokePopperEventHandler(type: PopperEventType, e: MouseEvent<HTMLDivElement>) {
        const childPopper = this.childPopper
        childPopper && safeInvoke(childPopper.props[type], e)
    }

    delayToggleVisible(
        e: MouseEvent | FocusEvent,
        delay: number,
        nextVisible: boolean
    ) {
        this.clearVisibleDelayTimeout()
        if (isUndefined(this.props.target)) {
            this.setTarget(e.currentTarget as HTMLElement)
        }

        if (this.state.visible !== nextVisible) {
            this.delaySetVisible(nextVisible, delay, e)
        }

    }

    componentWillUnmount(): void {
        this.clearClickOutsideHandler()
    }

    componentDidMount(): void {
        if (this.nodeTarget instanceof HTMLElement) {
            this.setTarget(this.nodeTarget)
        }
        this.componentDidUpdate()
    }

    componentDidUpdate(): void {
        this.updateClickOutsideHandler()
    }

    render() {
        const children = this.childrenArray.map(child => {
            return child && isPopper(child)
                ? cloneElement(child, this.popperInjectProps)
                : cloneElement(child, this.targetInjectProps)
        })

        return (
            <PopperContext.Provider value={this.contextValue}>
                <Fragment>{children}</Fragment>
            </PopperContext.Provider>
        )
    }
}
