import React, { useMemo } from 'react'
import { GridSyncContext } from './context'
import type {
    GridHoverCallback,
    GridScrollCallback,
    GridScrollHeightCallback,
    GridScrollHeightRecord,
    GridScrollPosition,
    GridSyncState,
    SyncContext,
} from './types'
import type { GridRowId } from '../../types'

export type GridSyncProviderProps = {
    orientation?: 'vertical' | 'horizontal'
    children: React.ReactNode
}
function useScrollState() {
    const ref = React.useRef<GridSyncState>(undefined)

    if (!ref.current) {
        ref.current = {
            scroll: {
                listeners: new Map(),
            },
            height: {
                listeners: new Map(),
            },
            hover: {
                listeners: new Map(),
            },
        }
    }
    return ref.current
}

export const GridSyncProvider = ({
    orientation = 'vertical',
    children,
}: GridSyncProviderProps) => {
    const provider = useScrollState()

    const lastScrollTop = React.useRef<number>(-1)
    const lastScrollLeft = React.useRef<number>(-1)

    const updateScroll = React.useCallback(
        (id: string, position: GridScrollPosition) => {
            const { scrollTop, scrollLeft } = position

            /* Prevent reporting the same scroll multiple times */
            if (
                (orientation === 'vertical' &&
                    lastScrollTop.current === scrollTop) ||
                (orientation === 'horizontal' &&
                    lastScrollLeft.current === scrollLeft)
            ) {
                return
            }

            lastScrollTop.current = scrollTop
            lastScrollLeft.current = scrollLeft

            const val =
                orientation === 'vertical' ? { scrollTop } : { scrollLeft }
            for (const [listenerId, listener] of provider.scroll.listeners) {
                if (listenerId !== id) {
                    listener(val)
                }
            }
        },
        [provider, orientation]
    )

    const scrollSubscribe = React.useCallback(
        (id: string, callback: GridScrollCallback) => {
            provider.scroll.listeners.set(id, callback)

            return () => {
                provider.scroll.listeners.delete(id)
            }
        },
        [provider]
    )

    const heights = React.useRef<GridScrollHeightRecord[]>([])

    const updateHeight = React.useCallback(
        (id: string, scrollbar: number, head: number, tail: number) => {
            heights.current = [
                ...heights.current.filter((record) => record.id !== id),
                { id, scrollbar, head, tail },
            ]

            for (const [listenerId, listener] of provider.height.listeners) {
                if (listenerId !== id) {
                    listener(
                        heights.current.filter(
                            (record) => record.id !== listenerId
                        )
                    )
                }
            }

            return heights.current.filter((record) => record.id !== id)
        },
        [provider]
    )

    const heightSubscribe = React.useCallback(
        (id: string, callback: GridScrollHeightCallback) => {
            provider.height.listeners.set(id, callback)

            return () => {
                /**
                 * Clean up listeners
                 */
                provider.height.listeners.delete(id)

                /**
                 * Clean up state
                 */
                heights.current = heights.current.filter(
                    (record) => record.id !== id
                )
            }
        },
        [provider]
    )

    const lastHoverId = React.useRef<GridRowId | null>(null)

    const updateHover = React.useCallback(
        (id: string, rowId: GridRowId | null) => {
            if (lastHoverId.current === rowId) {
                return
            }

            lastHoverId.current = rowId

            for (const [listenerId, listener] of provider.hover.listeners) {
                if (listenerId !== id) {
                    listener(rowId)
                }
            }
        },
        [provider]
    )

    const hoverSubscribe = React.useCallback(
        (id: string, callback: GridHoverCallback) => {
            provider.hover.listeners.set(id, callback)

            return () => {
                provider.hover.listeners.delete(id)
            }
        },
        [provider]
    )

    const state = useMemo<SyncContext>(
        () => ({
            enabled: true,
            scroll: {
                subscribe: scrollSubscribe,
                update: updateScroll,
            },
            height: {
                subscribe: heightSubscribe,
                update: updateHeight,
            },
            hover: {
                subscribe: hoverSubscribe,
                update: updateHover,
            },
        }),
        [
            scrollSubscribe,
            updateScroll,
            heightSubscribe,
            updateHeight,
            hoverSubscribe,
            updateHover,
        ]
    )
    return (
        <GridSyncContext.Provider value={state}>
            {children}
        </GridSyncContext.Provider>
    )
}
