import React from 'react'
import { useGridContext, useGridSelector } from '../../context/grid-context'
import type { DragStartEvent, DragMoveEvent } from '@dnd-kit/core'
import {
    DndContext,
    DragOverlay,
    useSensor,
    useSensors,
    defaultDropAnimation,
} from '@dnd-kit/core'
import { TouchSensor, MouseSensor, KeyboardSensor } from './sensors'
import type { CollisionDetails } from './collision'
import { collisionFactory } from './collision'
import { useGridComponents } from '../../context/grid-components-context'
import { modifiersFactory } from './modifiers'
import isEqual from 'lodash.isequal'
import { coordinatesFactory } from './coordinates'
import { measuringFactory } from './measuring'
import { useAnnouncements } from './announcements'
import styled from 'styled-components'
import { useColumnMove } from './column'
import { DRAG_TYPE_HEADER_CELL, DRAG_TYPE_HEADER_GROUP_CELL } from './constants'
import { Portal } from '@planview/pv-uikit'

export type GridDragControllerProps = {
    children: React.ReactNode
}

const dropAnimationWithoutHiding = {
    ...defaultDropAnimation,
    duration: 0,
    sideEffects: null,
}

const HOVER_TO_EXPAND_DELAY = 700

const NO_COLLISION: CollisionDetails = {
    parentId: null,
    prevId: null,
    prevInsertId: null,
    level: 0,
    operation: 'between',
}

const StyledDragOverlay = styled(DragOverlay)<{
    theme: { zindex: number }
}>`
    z-index: ${(props) => props.theme.zindex} !important;
`

const Backdrop = styled.div`
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: all;
`

export const GridDragController = ({ children }: GridDragControllerProps) => {
    const grid = useGridContext()
    const { GridDragOverlay, GridHeaderDragOverlay } = useGridComponents()
    const rowHeight = useGridSelector(grid.selectors.selectRowHeight)

    const { onMoveStart, onMove, onMoveEnd, onMoveCancel, minLeft, maxLeft } =
        useColumnMove()

    const { parentId } = useGridSelector(
        grid.selectors.selectDragCollisionDetails
    )

    React.useEffect(() => {
        if (parentId) {
            const rowId = parentId
            const state = grid.getState()
            const expandable = grid.selectors.selectIsRowExpandable(
                state,
                rowId
            )
            const expanded = grid.selectors.selectIsRowExpanded(state, rowId)

            if (expandable && !expanded) {
                const timer = window.setTimeout(() => {
                    grid.api.tree.expand(rowId, true)
                }, HOVER_TO_EXPAND_DELAY)
                return () => window.clearTimeout(timer)
            }
        }
    }, [grid, parentId])

    const mouseSensor = useSensor(MouseSensor, {
        activationConstraint: {
            distance: 5,
        },
    })
    const touchSensor = useSensor(TouchSensor, {
        activationConstraint: {
            delay: 250,
            tolerance: 10,
        },
    })

    const keyboardSensor = useSensor(KeyboardSensor, {
        coordinateGetter: coordinatesFactory(rowHeight),
    })

    const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor)

    const gridRowIntersection = React.useMemo(
        () => collisionFactory(grid),
        [grid]
    )
    const { snapCenterToCursor, restrictToHorizontalAxis, rubberBand } =
        React.useMemo(() => modifiersFactory(grid), [grid])
    const measuringConfig = React.useMemo(() => measuringFactory(grid), [grid])

    const [draggingHeader, setDraggingHeader] = React.useState(false)

    const onDragStart = React.useCallback(
        (e: DragStartEvent) => {
            const type = e.active.data.current ? e.active.data.current.type : ''
            if (
                type === DRAG_TYPE_HEADER_CELL ||
                type === DRAG_TYPE_HEADER_GROUP_CELL
            ) {
                onMoveStart(e.active.id as string, e)
                setDraggingHeader(true)
            } else {
                grid.api.navigation.disable()
                grid.api.drag.start(e.active.id)
            }
        },
        [grid.api.drag, grid.api.navigation, onMoveStart]
    )

    const onDragMove = React.useCallback(
        (e: DragMoveEvent) => {
            const state = grid.getState()
            const dragDetails = grid.selectors.selectDragCollisionDetails(state)
            const collisionData = e.collisions?.[0]?.data as CollisionDetails

            if (!isEqual(dragDetails, collisionData)) {
                const details: CollisionDetails = collisionData ?? NO_COLLISION
                grid.api.drag.move(details)
            }
        },
        [grid]
    )

    const onDragEndHeader = React.useCallback(() => {
        onMoveEnd()
        setDraggingHeader(false)
    }, [onMoveEnd])

    const onDragCancelHeader = React.useCallback(() => {
        onMoveCancel()
        setDraggingHeader(false)
    }, [onMoveCancel])

    const onDragEnd = React.useCallback(() => {
        grid.api.drag.complete()
        grid.api.navigation.enable()
    }, [grid.api.drag, grid.api.navigation])

    const onDragCancel = React.useCallback(() => {
        grid.api.drag.cancel()
        grid.api.navigation.enable()
    }, [grid.api.drag, grid.api.navigation])

    const { announcements, screenReaderInstructions } = useAnnouncements()

    return (
        <DndContext
            sensors={sensors}
            collisionDetection={gridRowIntersection}
            measuring={draggingHeader ? undefined : measuringConfig}
            onDragStart={onDragStart}
            onDragMove={draggingHeader ? onMove : onDragMove}
            onDragEnd={draggingHeader ? onDragEndHeader : onDragEnd}
            onDragCancel={draggingHeader ? onDragCancelHeader : onDragCancel}
            modifiers={
                draggingHeader
                    ? [restrictToHorizontalAxis, rubberBand(minLeft, maxLeft)]
                    : [snapCenterToCursor]
            }
            accessibility={{
                announcements,
                screenReaderInstructions,
            }}
        >
            {children}
            <Portal>
                {draggingHeader && <Backdrop />}
                <StyledDragOverlay dropAnimation={dropAnimationWithoutHiding}>
                    <GridDragOverlay />
                    <GridHeaderDragOverlay />
                </StyledDragOverlay>
            </Portal>
        </DndContext>
    )
}
