import {
    Component,
    ContextType,
    Ref,
    MouseEventHandler,
    CSSProperties,
    createRef,
    PropsWithChildren,
    ReactNode,
    isValidElement,
} from 'react'
// import * as PropTypes from 'prop-types'
import c from 'classnames'
import {
    createPopper,
    Instance as PopperInstance,
    Options as PopperOptions,
    Placement,
    Rect as PopperRect,
    ModifierArguments,
    VirtualElement,
} from '@popperjs/core'
import {Popup, PopupProps} from '@befe/brick-comp-popup'
import {
    deprecatedProp,
    ifNodeContains,
    pick,
    refNode,
    safeInvoke,
} from '@befe/brick-utils'
import {isEqual, isFunction, isUndefined, isNumber} from 'lodash-es'
import {ChildNodePopperGetter, PopperContext} from './popper-context'

export type PopperTargetObject = Element | VirtualElement
export type PopperTarget = PopperTargetObject | (() => PopperTargetObject | void | null) | null

type PopperInstanceState = PopperInstance['state']
type ModifierData = PopperInstanceState['modifiersData']
type PropsFromPopup = Omit<PopupProps, 'onChange'>

export interface PopperProps extends PropsFromPopup {
    /**
     * 自定义 class
     */
    className?: string

    /**
     * 是否显示
     */
    visible?: boolean

    /**
     * 目标元素
     *
     * @type Element | VirtualElement | (() => Element | VirtualElement | void | null)
     */
    target?: PopperTarget

    /**
     * 浮层相对于 target 的位置
     * | 'top-start'   | 'top'    | 'top-end'
     * | 'right-start' | 'right'  | 'right-end'
     * | 'bottom-end'  | 'bottom' | 'bottom-start'
     * | 'left-end'    | 'left'   | 'left-start'
     */
    placement?: Placement

    /**
     * 位置策略
     * - 默认为 `'absolute'` 可以满足绝大多数场景，
     * - 如若果 target element 位于一个 fixed 容器，设置为 `'fixed'`
     */
    strategy?: PopperOptions['strategy']

    /**
     * use `strategy` instead
     * @deprecated since 0.2.44
     */
    positionFixed?: boolean

    /**
     * modifier 设置
     */
    modifiers?: PopperOptions['modifiers']

    /**
     * popperWrap 的 element ref 函数
     */
    refPopperWrap?: Ref<HTMLDivElement>

    /**
     * popperWrap 最小宽度匹配到 target 的宽度
     */
    matchMinWidthToTarget?: boolean

    /**
     * popperWrap 宽度匹配到 target 的宽度
     */
    matchWidthToTarget?: boolean

    /**
     * 鼠标进入 popper wrap 回调
     */
    onMouseEnter?: MouseEventHandler<HTMLDivElement>

    /**
     * 鼠标离开 popper wrap 回调
     */
    onMouseLeave?: MouseEventHandler<HTMLDivElement>

    /**
     * popper 总状态更新时的回调（预留）
     */
    onChange?: (popperState: PopperState) => void

    /**
     * 是否带箭头
     */
    withArrow?: boolean

    /**
     * 箭头垂直于 placement 的偏移，只在 placement 为 \*-end, \*-start 时有效
     * - `'auto'`: 当 popper 宽/高尺寸小于 target 时按默认值偏移
     * - `number`: 按指定量偏移
     */
    arrowOffset?: 'auto' | number
}

interface PopperState {
    placement: Placement
    popperStyles: CSSProperties
    arrowStyles: CSSProperties
    modifiersData: ModifierData
    popperRect: PopperRect
    targetRect: PopperRect
    isHidden?: boolean
}

function getTargetObject(target?: PopperTarget) {
    if (isFunction(target)) {
        return target()
    }

    return target
}

function normalizeNaNProperties(style: CSSProperties) {
    const normalizedStyle: Record<string, unknown> = {}
    Object.keys(style).forEach(key => {
        const k = key as keyof CSSProperties
        const value = style[k]
        normalizedStyle[k] = typeof value === 'string' || !isNaN(value as number)
            ? value
            : ''
    })

    return normalizedStyle as CSSProperties
}

function getNumberCssProp(styles: CSSProperties, propName: keyof CSSProperties) {
    const value = styles[propName]

    if (isNumber(value)) {
        return value
    }

    if (isUndefined(value)) {
        return NaN
    }

    return Math.round(parseFloat(value)) || NaN
}

const ARROW_INSET_NAME: Record<string, string> = {
    'top-start': 'left',
    'bottom-start': 'left',
    'top-end': 'right',
    'bottom-end': 'right',
    'left-start': 'top',
    'right-start': 'top',
    'left-end': 'bottom',
    'right-end': 'bottom',
}

// arrow 设计宽度
// const ARROW_SIZE = 8  // 8
// const ARROW_BOX_SIZE = Math.round(Math.sqrt(Math.pow(ARROW_SIZE, 2) / 2))  // 6 同 popper.$popper-arrow-box-size

// 小尺寸 target 情景下 arrow 中心的固定偏移量 (设计值)
const DEFAULT_ARROW_OFFSET_HORIZONTAL = 20
const DEFAULT_ARROW_OFFSET_VERTICAL = 12

// 小尺寸 target 的 临界宽高，如果 target 元素的宽高小于这个临界值，就切换为固定偏移量的对齐方式
const TERMINATE_WIDTH = DEFAULT_ARROW_OFFSET_HORIZONTAL * 2
const TERMINATE_HEIGHT = DEFAULT_ARROW_OFFSET_VERTICAL * 2

const initialPopperStyles: CSSProperties = {
    position: 'absolute',
    opacity: 0,
    pointerEvents: 'none',
}

const initialArrowStyles: CSSProperties = {}
const initialPopperRect = {
    x: 0,
    y: 0,
    width: 0,
    height: 0,
}
const initialTargetRect = {
    x: 0,
    y: 0,
    width: 0,
    height: 0,
}

// type PopperPropsWithDefaults = PopperProps & Required<Pick<PopperProps, keyof typeof Popper.defaultProps>>

const DOM_RECT_KEYS = ['height', 'width', 'top', 'left'] as const
// type RectKey = (typeof DOM_RECT_KEYS)[number]


// 判断 target / popper 尺寸是否有变化
// 用 "差异小于某个很小的百分比" 来判断，
// 因为存在 "浏览器可能在前后两次绘制存在细微差别" 的差别（理论上应该一致的）, 比如 chrome 88 缩小为 80% 情况下
// 细微差别容差 RECT_ALLOWANCE 没有坚实根据，取的经验值
const RECT_ALLOWANCE = 0.01
const isRectChanged = (rectLeft: DOMRect | null | undefined, rectRight: DOMRect | null | undefined) => {
    if (rectLeft === rectRight || !rectLeft || !rectRight) {
        return false
    }

    return DOM_RECT_KEYS.some(key => {
        const diff = Math.abs(rectLeft[key] - rectRight[key]) / ((rectLeft[key] + rectRight[key]) / 2)
        return diff > RECT_ALLOWANCE
    })
}

export class Popper extends Component<PopperProps, PopperState> {
    static displayName = 'Popper'
    static propTypes = {
        positionFixed: deprecatedProp('^0.2.42', 'use `strategy` instead'),
    }
    static defaultProps = {
        className: '',
        destroyOnHide: true,
        placement: 'bottom',
        strategy: 'absolute',
        matchMinWidthToTarget: false,
        withArrow: false,
        arrowOffset: 'auto',
    }

    static contextType = PopperContext

    context!: ContextType<typeof PopperContext>

    state: PopperState = {
        placement: Popper.defaultProps.placement as Placement,
        popperStyles: initialPopperStyles,
        arrowStyles: initialArrowStyles,
        modifiersData: {},
        popperRect: initialPopperRect,
        targetRect: initialTargetRect,
        isHidden: undefined,
    }

    elemPopperWrap: HTMLDivElement | null = null
    popperInstance: PopperInstance | null = null

    targetObject: Element | null = null

    targetLastRect: DOMRect | null = null
    popperLastRect: DOMRect | null = null

    refArrow = createRef<HTMLDivElement>()
    // getChildNodePopper?: ChildNodePopperGetter

    descendantPoppersGetters: ChildNodePopperGetter[] = []

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

    nodePopperGetter = () => {
        return this
    }

    setChildPopperGetter = (childNodePopperGetter: ChildNodePopperGetter) => {
        /* istanbul ignore else */
        if (!this.descendantPoppersGetters.includes(childNodePopperGetter)) {
            this.descendantPoppersGetters.push(childNodePopperGetter)
        }
    }

    removeChildPopperGetter = (childNodePopperGetter: ChildNodePopperGetter) => {
        const idx = this.descendantPoppersGetters.indexOf(childNodePopperGetter)
        idx > -1 && this.descendantPoppersGetters.splice(idx, 1)
    }

    get strategy() {
        const {strategy, positionFixed} = this.props
        return isUndefined(strategy) && positionFixed ? 'fixed' : strategy
    }

    get options(): Partial<PopperOptions> {
        const modifiers: PopperOptions['modifiers'] = [
            {
                name: 'applyStyles',
                enabled: false,
            },
            {
                name: 'arrow',
                enabled: this.props.withArrow,
                options: {
                    element: this.refArrow.current || undefined,
                },
            },
            {
                name: 'computeStyles',
                options: {
                    // firefox 下 translate3d 会导致层叠上层元素拖拽发生层叠下层元素的异常选中
                    // 详见 http://minerva.weiyun.baidu.com/pages/minerva.html#/main?item=f30qi3e2kh3cylahrp8
                    // @todo 待重新确认
                    gpuAcceleration: false,
                },
            },
            {
                name: 'updateState',
                enabled: true,
                phase: 'write',
                fn: this.popperUpdateStateModifier,
                requires: ['computeStyles'],
            },
            ...(this.props.modifiers || []),
        ]

        return {
            placement: this.props.placement,
            strategy: this.strategy,
            modifiers,
        }
    }

    get wrapWidthStyle() {
        const {matchMinWidthToTarget, matchWidthToTarget} = this.props
        const p = this.state.placement
        const style: CSSProperties = {}

        if (!['bottom', 'top'].includes(p.split('-')[0])) {
            return style
        }

        const targetWidth = (this.target as Element)?.getBoundingClientRect().width
        if (matchWidthToTarget) {
            style.width = targetWidth
        }

        if (matchMinWidthToTarget) {
            style.minWidth = targetWidth
        }

        return style
    }

    get wrapStyle(): CSSProperties {
        if (!this.elemPopperWrap || !this.popperInstance) {
            return initialPopperStyles
        }
        const {popperStyles, targetRect, popperRect, placement, modifiersData} = this.state
        const arrowStyle = this.arrowStyle
        const [pMain, pAlign] = placement.split('-')

        const smallTargetStyle: CSSProperties = {}
        const isCenter = !pAlign

        // 对于带箭头的非 center 对齐，在 “小尺寸 target” 情况下，
        // 需要对 popper 进行偏移，保证 arrow 在对正 target 中心前提下，同时 arrow 离边缘 offset 为设计的最小值
        if (
            this.refArrow.current && targetRect && popperRect && !isCenter && (
                (['top', 'bottom'].includes(pMain) && targetRect.width < TERMINATE_WIDTH)
                || (['left', 'right'].includes(pMain) && targetRect.height < TERMINATE_HEIGHT)
            )
        ) {
            // arrow 里只有分别基于 top, left 的 x, y
            const {arrow} = modifiersData
            if (['top', 'bottom'].includes(pMain)) {
                const popperLeft = getNumberCssProp(popperStyles, 'left')
                const popperRight = getNumberCssProp(popperStyles, 'right')
                const arrowLeft = getNumberCssProp(arrowStyle, 'left')
                const arrowRight = getNumberCssProp(arrowStyle, 'right')

                // top-end, bottom-end 情况下，popper 使用 right
                smallTargetStyle[pAlign !== 'end' ? 'left' : 'right'] = pAlign !== 'end'
                    ? popperLeft - arrowLeft + (arrow?.x || 0)
                    : popperRight - arrowRight + (Math.round(popperRect.width) - (arrow?.x || 0))

            } else if (['left', 'right'].includes(pMain)) {
                const popperTop = getNumberCssProp(popperStyles, 'top')
                const popperBottom = getNumberCssProp(popperStyles, 'bottom')
                const arrowTop = getNumberCssProp(arrowStyle, 'top')
                const arrowBottom = getNumberCssProp(arrowStyle, 'bottom')

                // right-end, left-end 情况下，popper 使用 bottom
                smallTargetStyle[pAlign !== 'end' ? 'top' : 'bottom'] = pAlign !== 'end'
                    ? popperTop - arrowTop + (arrow?.y || 0)
                    : popperBottom - arrowBottom + (popperRect.height - (arrow?.y || 0))

            }
        }

        return normalizeNaNProperties({
            position: initialPopperStyles.position,
            ...popperStyles,
            ...smallTargetStyle,
            ...this.wrapWidthStyle,
        })
    }

    get arrowStyle(): CSSProperties {
        const {arrowOffset} = this.props
        const {targetRect, popperRect, placement = '', arrowStyles} = this.state

        const [pMain, pAlign] = placement.split('-')
        if (!this.refArrow.current || !targetRect || !popperRect) {
            return initialArrowStyles
        }
        const validArrowStyles = normalizeNaNProperties(arrowStyles)
        const isCenter = !pAlign

        // arrowOffset cases

        // 中心位置，arrowOffset 无效
        if (isCenter) {
            return validArrowStyles
        }

        // custom fixed offset
        if (typeof arrowOffset === 'number') {
            return {
                [ARROW_INSET_NAME[placement]]: arrowOffset,
            }
        }

        if (arrowOffset === 'auto') {
            // 如果 target 元素的 3/4 宽度大于气泡的宽度，则采用边缘对齐&箭头处于气泡的中心
            if (
                (['top', 'bottom'].includes(pMain) && targetRect.width * 3 / 4 > popperRect.width)
                || (['left', 'right'].includes(pMain) && targetRect.height * 3 / 4 > popperRect.height)
            ) {
                return {
                    [ARROW_INSET_NAME[placement]]: '50%',
                }
            }

            // 如果 target元素的宽高小于临界宽高，即为 “小尺寸 target” 则箭头偏移量给固定的设计值
            if (['top', 'bottom'].includes(pMain) && targetRect.width < TERMINATE_WIDTH) {
                return {
                    [ARROW_INSET_NAME[placement]]: DEFAULT_ARROW_OFFSET_HORIZONTAL,
                }
            }
            if (['left', 'right'].includes(pMain) && targetRect.height < TERMINATE_HEIGHT) {
                return {
                    [ARROW_INSET_NAME[placement]]: DEFAULT_ARROW_OFFSET_VERTICAL,
                }
            }

        }

        return validArrowStyles
    }

    get target() {
        return getTargetObject(this.props.target)
    }

    isDescendant = (elem: Node): boolean => {
        return ifNodeContains(this.elemPopperWrap, elem)
            || this.descendantPoppersGetters.some(popperGetter => !!(popperGetter?.()?.isDescendant(elem)))
    }

    updateLastRect() {
        // target 和 popper 虽然实例不变，但都有可能动态改变内容
        // 需要通过对比前后尺寸、位置确定是否要更新 popper 位置
        this.targetLastRect = this.target?.getBoundingClientRect() || null
        this.popperLastRect = this.props.visible && this.elemPopperWrap
            ? this.elemPopperWrap.getBoundingClientRect()
            : null
    }

    popperUpdateStateModifier = (data: ModifierArguments<Record<string, unknown>>) => {
        const {state} = data
        const popperState: PopperState = {
            ...pick(state, [
                'placement',
            ]),
            // isHidden 的判断不能加入 state.modifiersData.hide?.hasPopperEscaped，否则在极矮条件下将总是看不到 popper
            isHidden: state.modifiersData.hide?.isReferenceHidden,
            modifiersData: state.modifiersData,
            popperStyles: state.styles.popper as CSSProperties,
            arrowStyles: state.styles.arrow as CSSProperties,
            popperRect: state.rects.popper,
            targetRect: state.rects.reference,
        }

        // console.log({popperState: state})
        this.setState(popperState)
        safeInvoke(this.props.onChange, popperState)
    }

    destroyPopperInstance() {
        if (this.popperInstance) {
            this.popperInstance.destroy()
            this.popperInstance = null
        }
    }

    reCreatePopperInstance() {
        this.destroyPopperInstance()
        const elemTarget = this.target as Element
        const elemPopperWrap = this.elemPopperWrap

        if (elemTarget && elemPopperWrap) {
            this.popperInstance = createPopper(elemTarget, elemPopperWrap, this.options)
        }

        this.targetObject = elemTarget;
    }

    updatePopperInstance() {
        return this.popperInstance?.update()
    }

    updatePopperInstanceOptions() {
        return this.popperInstance?.setOptions(this.options)
    }

    renderPopperArrow = () => {
        if (!this.props.withArrow) {
            return null
        }
        const {placement} = this.state
        const direction = placement
            ? {
                'top': 'down',
                'bottom': 'up',
                'left': 'right',
                'right': 'left',
            }[placement.split('-')[0]]
            : ''
        const className = c(
            'brick-popper-arrow',
            direction && `direction-${direction}`
        )
        return (
            <div
                className={className}
                style={normalizeNaNProperties(this.arrowStyle)}
                ref={this.refArrow}
            />
        )
    }

    renderPopper = () => {
        const popperProps = {
            ...pick(this.props, ['onMouseEnter', 'onMouseLeave']),
            className: 'brick-popper-wrap',
            style: this.wrapStyle,
            ref: this.refPopperWrap,
        }
        const {children} = this.props

        return children && (
            <div {...popperProps}>
                <div className={'brick-popper-inner'}>
                    {children}
                </div>
                {this.renderPopperArrow()}
            </div>
        )
    }

    renderPopup = () => {
        const popupProps = {
            className: c(
                'brick-popper',
                `brick-popper-placement-${this.state.placement}`,
                {
                    'brick-popper-hidden': this.state.isHidden,
                    'brick-popper-with-arrow': this.props.withArrow,
                },
                this.props.className
            ),
            ...pick(this.props, ['visible', 'destroyOnHide', 'disablePortal', 'portalContainer']),
        }
        return (
            <Popup {...popupProps}>{this.renderPopper()}</Popup>
        )
    }

    componentWillUnmount(): void {
        this.destroyPopperInstance()
        safeInvoke(this.context.removeChildPopperFromParent, this.nodePopperGetter)
    }

    componentDidMount() {
        if (this.props.visible) {
            this.reCreatePopperInstance()
        }
        safeInvoke(this.context.setChildPopperForParent, this.nodePopperGetter)
    }

    componentDidUpdate(prevProps: Readonly<PropsWithChildren<PopperProps>>): void {
        const {
            visible, children, placement, target, positionFixed, strategy, modifiers,
        } = this.props

        const shouldReCreate = (visible && !this.popperInstance)
            || (children && !prevProps.children)
            || !isEqual(this.targetObject, this.target)
            || getTargetObject(target) !== getTargetObject(prevProps.target)


        if (shouldReCreate) {
            this.reCreatePopperInstance()
            this.updateLastRect()
            return
        }

        if (!visible) {
            this.destroyPopperInstance()
            this.updateLastRect()
            return
        }


        const shouldUpdateOptions = placement !== prevProps.placement
            || strategy !== prevProps.strategy
            || positionFixed !== prevProps.positionFixed
            || !isEqual(modifiers, prevProps.modifiers)

        if (shouldUpdateOptions) {
            void this.updatePopperInstanceOptions()
        }
        else if (
            isRectChanged(this.targetLastRect, (this.target as HTMLElement)?.getBoundingClientRect())
            || isRectChanged(this.popperLastRect, this.elemPopperWrap?.getBoundingClientRect())
        ) {
            void this.updatePopperInstance()
        }

        this.updateLastRect()
    }

    render() {
        const contextValue = {
            ...this.context,
            // 逐级传递，
            setChildPopperForParent: this.setChildPopperGetter,
            removeChildPopperFromParent: this.removeChildPopperGetter,
        }

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

export const isPopper = (node: ReactNode) => {
    return isValidElement(node) && node.type === Popper
}
