import React, { useEffect, useMemo } from 'react'
import { useGridContext, useGridSelector } from '../../context/grid-context'
import { createStructuredSelector } from 'reselect'
import { createAutoScroller } from './auto-scroller'
import { positionFactory } from './position'
import { healSpannedRange, isRangeEqual } from '../../utils/range'

export type GridRangeSelectionProps = {
    scroller: HTMLDivElement | null
}

const GridRangeSelectionImpl = ({ scroller }: GridRangeSelectionProps) => {
    const grid = useGridContext()

    const combinedSelector = useMemo(
        () =>
            createStructuredSelector({
                flatIds: grid.selectors.selectRowIds,
                rowHeight: grid.selectors.selectRowHeight,
                columnsWidths: grid.selectors.selectColumnWidths,
                columnIds: grid.selectors.selectColumnIds,
                headerHeight: grid.selectors.selectHeaderHeight,
            }),
        [grid]
    )

    const positioningState = useGridSelector(combinedSelector)

    const { getColumnFromX, getRowFromY } = useMemo(
        () => positionFactory(positioningState),
        [positioningState]
    )

    const stickyPosition = useGridSelector(
        grid.selectors.selectStickyColumnDetails
    )

    const totalWidth = useMemo(
        () =>
            positioningState.columnsWidths.reduce(
                (acc, width) => acc + width,
                0
            ),
        [positioningState.columnsWidths]
    )

    const supportsRangeSelection = useGridSelector(
        grid.selectors.selectIsSpreadsheet
    )

    useEffect(() => {
        if (!scroller || !supportsRangeSelection) {
            return
        }

        const updateRangeSelection = ({
            x1,
            y1,
            x2,
            y2,
        }: {
            x1: number
            x2: number
            y1: number
            y2: number
        }) => {
            const startingColumn = getColumnFromX(x1).columnId
            const startingRow = getRowFromY(y1).rowId
            const endingColumn = getColumnFromX(x2).columnId
            const endingRow = getRowFromY(y2).rowId

            const state = grid.getState()
            const existingRange =
                grid.selectors.selectRangeSelectionPreview(state)

            if (
                startingColumn != null &&
                startingRow != null &&
                endingColumn != null &&
                endingRow != null
            ) {
                const newRange = healSpannedRange(
                    {
                        from: {
                            columnId: startingColumn,
                            rowId: startingRow,
                        },
                        to: {
                            columnId: endingColumn,
                            rowId: endingRow,
                        },
                    },
                    grid
                )

                if (isRangeEqual(existingRange, newRange)) {
                    return
                }
                grid.dispatch({
                    type: 'updateRangeSelectionPreview',
                    payload: newRange,
                })
            } else {
                if (!existingRange) {
                    return
                }
                grid.dispatch({
                    type: 'updateRangeSelectionPreview',
                    payload: null,
                })
            }
        }

        const scrollContainer = scroller

        function handlePointerDown(e: PointerEvent) {
            if (!(e.target instanceof Element) || !supportsRangeSelection) {
                return
            }

            if (e.target instanceof HTMLInputElement) {
                grid.dispatch({
                    type: 'updateRangeSelectionPreview',
                    payload: null,
                })
                // Ignore clicks on input elements
                return
            }

            const scrollLeft = scrollContainer.scrollLeft
            const scrollTop = scrollContainer.scrollTop
            const scrollBounds = scrollContainer.getBoundingClientRect()

            const x = e.clientX
            const y = e.clientY

            let gridX = x - scrollBounds.left
            const gridY = y - scrollBounds.top + scrollTop
            const dragStartedOnLeftStickyColumn =
                stickyPosition.left.width && gridX < stickyPosition.left.width
            let autoScrollComplete = false

            let dragStartedOnRightStickyColumn = false

            if (totalWidth > scrollBounds.width && stickyPosition.right.width) {
                if (gridX > scrollBounds.width - stickyPosition.right.width) {
                    dragStartedOnRightStickyColumn = true
                }
            }

            if (dragStartedOnRightStickyColumn) {
                gridX += totalWidth - scrollBounds.width
            } else if (!dragStartedOnLeftStickyColumn) {
                gridX += scrollLeft
            }

            const column = getColumnFromX(gridX)
            const row = getRowFromY(gridY)

            if (column.outOfBounds || row.outOfBounds) {
                grid.dispatch({
                    type: 'updateRangeSelectionPreview',
                    payload: null,
                })
                return
            }

            e.stopPropagation()

            const state = grid.getState()
            const footerEnabled = grid.selectors.selectAggregationEnabled(state)
            const rowHeight = grid.selectors.selectRowHeight(state)
            // TODO how does this work when synchronizing with another grid?
            const autoScroller = createAutoScroller({
                scrollContainer,
                scrollThreshold: {
                    top: 0,
                    left: stickyPosition.left.width || 0,
                    right: stickyPosition.right.width || 0,
                    bottom: footerEnabled ? rowHeight : 0, // TODO: footer?
                },
                scrollSpeed: 20,
                initialX: x,
            })

            updateRangeSelection({
                x1: gridX,
                x2: gridX,
                y1: gridY,
                y2: gridY,
            })

            function handlePointerMove(moveEvent: PointerEvent) {
                const newScrollLeft = scrollContainer.scrollLeft
                const newScrollTop = scrollContainer.scrollTop
                const newScrollBounds = scrollContainer.getBoundingClientRect()
                const newX = moveEvent.clientX
                const newY = moveEvent.clientY

                let newGridX = newX - newScrollBounds.left
                const newGridY = newY - newScrollBounds.top + newScrollTop

                if (dragStartedOnLeftStickyColumn) {
                    if (
                        !autoScrollComplete &&
                        newGridX > stickyPosition.left.width
                    ) {
                        /* Fix scroll one time when dragging from left sticky toward right side of grid */
                        scrollContainer.scrollLeft = 0
                        autoScrollComplete = true
                    }
                    if (autoScrollComplete) {
                        newGridX += newScrollLeft
                    }
                } else if (dragStartedOnRightStickyColumn) {
                    if (
                        !autoScrollComplete &&
                        newGridX <
                            newScrollBounds.width - stickyPosition.right.width
                    ) {
                        /* Fix scroll one time when dragging from right sticky toward left side of grid */
                        scrollContainer.scrollLeft = totalWidth
                        autoScrollComplete = true
                    }
                    if (autoScrollComplete) {
                        newGridX += newScrollLeft
                    } else {
                        newGridX += Math.max(
                            totalWidth - newScrollBounds.width,
                            0
                        )
                    }
                } else {
                    newGridX += newScrollLeft
                }

                updateRangeSelection({
                    x1: gridX,
                    x2: newGridX,
                    y1: gridY,
                    y2: newGridY,
                })

                autoScroller.updateScroll(moveEvent.clientX, moveEvent.clientY)
            }

            function cleanup() {
                window.removeEventListener('pointermove', handlePointerMove)
                window.removeEventListener('pointerup', handlePointerUp)
                window.removeEventListener('pointercancel', handlePointerCancel)
                autoScroller.stop()
            }

            function handlePointerCancel() {
                cleanup()
            }

            function handlePointerUp() {
                const rangeSelection =
                    grid.selectors.selectRangeSelectionPreview(grid.getState())
                if (rangeSelection) {
                    grid.api.rangeSelection.select(rangeSelection)
                }
                grid.dispatch({
                    type: 'updateRangeSelectionPreview',
                    payload: null,
                })
                cleanup()
            }

            window.addEventListener('pointermove', handlePointerMove)
            window.addEventListener('pointerup', handlePointerUp)
            window.addEventListener('pointercancel', handlePointerCancel)
        }

        scrollContainer.addEventListener('pointerdown', handlePointerDown)

        const handlePreventSelection = (e: Event) => {
            e.preventDefault()
        }
        scrollContainer.addEventListener('selectstart', handlePreventSelection)

        return () => {
            scrollContainer.removeEventListener(
                'pointerdown',
                handlePointerDown
            )
            scrollContainer.removeEventListener(
                'selectstart',
                handlePreventSelection
            )
        }
    }, [
        grid,
        supportsRangeSelection,
        scroller,
        getColumnFromX,
        getRowFromY,
        positioningState.columnIds,
        stickyPosition,
        totalWidth,
    ])

    return null
}

export const GridRangeSelection = React.memo(GridRangeSelectionImpl)
GridRangeSelection.displayName = 'GridRangeSelection'
