import React, { useEffect, useImperativeHandle } from 'react'
import type {
    Column,
    GridRowData,
    GridProps,
    GridRowMeta,
    GridRowId,
    ColumnGroup,
} from '../../types'
import { isGridServerData } from '../../type-predicates'
import { size } from '@planview/pv-utilities'
import { GridBase } from '../grid-base'
import { useGridComponents } from '../../context/grid-components-context'

import { useGrid } from '../../hooks'
import { GridProvider } from '../../context/grid-context'
import type { GridApi } from '../../api'
import { usePreferencesAdapter } from '../../hooks/use-preferences-adapter'
import { ACTIONS_COLUMN_ID, SELECTION_COLUMN_ID } from '../../constants'
import { useIntl } from 'react-intl'
import { GridActionsMenuProvider } from '../../context/grid-actions-menu-context'
import { GridLiveRegionProvider } from '../../context/grid-live-region-context'
import { setColumnDefaults, setColumnGroupDefaults } from '../../utils/columns'
import { useGridPrivateSettings } from '../../context/grid-private-settings-context'
import { emptyIds } from '../../state/compositeSelectors/range-selection'
import { getIdsFromRange } from '../../utils/range'

const AUTO_SELECTION_DELAY = 500

const gridRowHeights = {
    small: size.small,
    medium: size.medium,
}

const GridImpl = <
    TDataModel extends GridRowData,
    TMetaModel extends GridRowMeta<TDataModel>,
>(
    {
        label,
        columns,
        columnGroups = [],
        enableColumnVisibilityMenu = false,
        rows,
        rowHeight = 'medium',
        onRowClick,
        selection: selectionIncoming,
        selectionMode = 'multi',
        onSelectionChange,
        expandedRows,
        onExpandedRowsChange,
        loading,
        sort,
        sortMode,
        defaultSort,
        onSortChange,
        multiColumnSort = true,
        filteredIds,
        filter,
        filterMode,
        preferencesAdapter,
        onCellChange,
        emptyContent,
        actionsMenu,
        rowDrag,
    }: GridProps<TDataModel>,
    ref: React.ForwardedRef<GridApi>
) => {
    const {
        hideHeaders,
        initialFocusMode,
        loopHorizontally,
        spreadsheet,
        rangeSelection,
        onRangeSelectionChange,
        disableHorizontalScroll,
        onScrollbarVisibilityChange,
    } = useGridPrivateSettings()

    const {
        GridHeaderCellCheckbox,
        GridCellCheckbox,
        GridCellGrabber,
        GridCellActionsMenu,
        GridDragController,
    } = useGridComponents()
    const grid = useGrid()

    const { locale } = useIntl()

    useImperativeHandle(ref, () => grid.api)

    const { ids: selectionIds, autoScroll = false } =
        selectionIncoming instanceof Set
            ? { ids: selectionIncoming }
            : (selectionIncoming ?? {})

    const lastSelection = React.useRef<{
        ids: Set<GridRowId>
        time: number
    } | null>(null)

    useEffect(
        () =>
            grid.events.on('onSelectionChange', (selection) => {
                const { controlled } = grid.getState().selection
                if (!controlled) {
                    grid.dispatch({
                        type: 'updateSelection',
                        payload: selection,
                    })
                }
                if (selection.size <= 1) {
                    grid.dispatch({
                        type: 'setSelectionAnchor',
                        payload:
                            selection.size === 1
                                ? selection.entries().next().value![0]
                                : null,
                    })
                }
                lastSelection.current = { ids: selection, time: Date.now() }
                onSelectionChange?.(selection)
            }),
        [grid, onSelectionChange]
    )

    useEffect(
        () =>
            grid.events.on('onRangeSelectionChange', (selection) => {
                const state = grid.getState()
                const controlled =
                    grid.selectors.selectIsRangeSelectionControlled(state)

                if (!controlled) {
                    grid.dispatch({
                        type: 'updateRangeSelection',
                        payload: selection,
                    })
                }

                const columnIds = grid.selectors.selectColumnIds(state)
                const rowIds = grid.selectors.selectRowIds(state)

                const rangeIds = selection
                    ? getIdsFromRange(selection, rowIds, columnIds)
                    : emptyIds

                onRangeSelectionChange?.(selection, {
                    rowIds: [...rangeIds.rowIds],
                    columnIds: [...rangeIds.columnIds],
                })
            }),
        [grid, onRangeSelectionChange]
    )

    useEffect(
        () =>
            grid.events.on('onExpandedRowsChange', (expandedRows, context) => {
                const expandedControlled =
                    grid.selectors.selectAreExpandedRowsControlled(
                        grid.getState()
                    )
                if (!expandedControlled) {
                    grid.dispatch({
                        type: 'updateExpandedRows',
                        payload: expandedRows,
                    })
                }
                onExpandedRowsChange?.(expandedRows, context)
            }),
        [grid, onExpandedRowsChange]
    )

    useEffect(
        () =>
            grid.events.on('onSortChange', (sort) => {
                const { controlled } = grid.getState().sort
                if (!controlled) {
                    grid.dispatch({
                        type: 'updateSort',
                        payload: sort,
                    })
                }
                onSortChange?.(sort)
            }),
        [grid, onSortChange]
    )

    useEffect(() => {
        grid.dispatch({
            type: 'updateSortLocale',
            payload: locale,
        })
    }, [grid, locale])

    useEffect(
        () =>
            grid.events.on('onCellChange', (payload) => {
                onCellChange?.(payload)
            }),
        [grid, onCellChange]
    )

    useEffect(
        () =>
            grid.events.on('onRowClick', (rowId) => {
                onRowClick?.(rowId)
            }),
        [grid, onRowClick]
    )

    useEffect(
        () =>
            grid.events.on('onScrollbarVisibilityChange', (args) => {
                onScrollbarVisibilityChange?.(args)
            }),
        [grid, onScrollbarVisibilityChange]
    )

    const actionsMenuPresent = !!actionsMenu

    const groupsWithDefaults: ColumnGroup<TDataModel>[] = React.useMemo(
        () => columnGroups.map(setColumnGroupDefaults),
        [columnGroups]
    )

    const rowDragEnabled = !!rowDrag

    const columnsWithDefaults: Column<TDataModel>[] = React.useMemo(() => {
        const columnDefs: Column<TDataModel>[] = columns.map(setColumnDefaults)

        if (spreadsheet && rowDragEnabled && selectionMode !== 'multi') {
            columnDefs.unshift(
                setColumnDefaults({
                    id: SELECTION_COLUMN_ID,
                    label: '',
                    width: size.smallCompact,
                    resizable: false,
                    sortable: false,
                    movable: false,
                    hideable: false,
                    sticky: columnDefs[0]?.sticky,
                    lockedLocation: 'left',
                    cell: {
                        Renderer: GridCellGrabber,
                    },
                } as Column<TDataModel>)
            )
        }

        if (selectionMode === 'multi') {
            columnDefs.unshift({
                id: SELECTION_COLUMN_ID,
                label: '',
                width:
                    spreadsheet && rowDragEnabled
                        ? size.small + size.smallCompact
                        : size.small,
                resizable: false,
                sortable: false,
                movable: false,
                hideable: false,
                sticky: columnDefs[0]?.sticky,
                lockedLocation: 'left',
                header: {
                    Renderer: GridHeaderCellCheckbox,
                },
                cell: {
                    Renderer: GridCellCheckbox,
                },
            } as Column<TDataModel>)
        }

        if (actionsMenuPresent) {
            columnDefs.push({
                id: ACTIONS_COLUMN_ID,
                label: '',
                width: size.small,
                resizable: false,
                sortable: false,
                movable: false,
                hideable: false,
                sticky: 'right',
                lockedLocation: 'right',
                cell: {
                    Renderer: GridCellActionsMenu,
                },
            } as Column<TDataModel>)
        }

        return columnDefs
    }, [
        spreadsheet,
        columns,
        selectionMode,
        actionsMenuPresent,
        rowDragEnabled,
        GridHeaderCellCheckbox,
        GridCellCheckbox,
        GridCellGrabber,
        GridCellActionsMenu,
    ])

    const footerEnabled = React.useMemo(
        () =>
            columnsWithDefaults.some(
                (c) => c.footer?.aggregation || c.footer?.label
            ),
        [columnsWithDefaults]
    )

    const filterFn = React.useMemo(() => {
        if (filter) {
            return filter
        }
        if (filteredIds) {
            const filteredIdSet = new Set(filteredIds)
            return (row: any) => filteredIdSet.has(row.id)
        }
    }, [filteredIds, filter])

    const rowDragSettings = React.useMemo(
        () => ({
            enabled: rowDragEnabled,
            mode: rowDrag?.mode ?? 'default',
            multiple: rowDrag?.multiple === true,
            enableLeafConversion: rowDrag?.enableLeafConversion,
            canDrop: rowDrag?.canDrop,
            previewColumnId: rowDrag?.previewColumnId ?? undefined,
        }),
        [
            rowDragEnabled,
            rowDrag?.mode,
            rowDrag?.multiple,
            rowDrag?.enableLeafConversion,
            rowDrag?.canDrop,
            rowDrag?.previewColumnId,
        ]
    )

    useEffect(() => {
        const onDrop = rowDrag?.onDrop
        return grid.events.on('onRowDrop', (payload) => {
            onDrop?.(payload)
        })
    }, [grid, rowDrag?.onDrop])

    useEffect(() => {
        grid.dispatch({
            type: 'applyProps',
            payload: {
                loading,
                actionsMenuPresent,
                rows,
                expandedRows,
                selection: selectionIds,
                selectionMode:
                    selectionMode === 'multi-hidden' ? 'multi' : selectionMode,
                filter: filterFn,
                filterMode,
                sort,
                sortMode,
                multiColumnSort,
                defaultSort,
                columns: columnsWithDefaults,
                columnGroups: groupsWithDefaults,
                enableColumnVisibilityMenu:
                    enableColumnVisibilityMenu &&
                    columnsWithDefaults.some((c) => c.hideable !== false),
                footerEnabled,
                rowDrag: rowDragSettings,
                hideHeaders,
                initialFocusMode,
                loopHorizontally,
                spreadsheet,
                rangeSelection,
            },
        })
    }, [
        grid,
        rows,
        expandedRows,
        selectionIds,
        sort,
        sortMode,
        multiColumnSort,
        defaultSort,
        filterFn,
        filterMode,
        selectionMode,
        columnsWithDefaults,
        actionsMenuPresent,
        footerEnabled,
        rowDragSettings,
        groupsWithDefaults,
        enableColumnVisibilityMenu,
        loading,
        hideHeaders,
        initialFocusMode,
        loopHorizontally,
        spreadsheet,
        rangeSelection,
    ])

    useEffect(() => {
        /* Don't apply change while loading */
        if (loading) {
            return
        }

        if (autoScroll && selectionIds && selectionIds.size > 0) {
            if (lastSelection.current) {
                const { ids, time } = lastSelection.current
                const now = Date.now()

                if (
                    time - now < AUTO_SELECTION_DELAY &&
                    ids.size === selectionIds.size &&
                    [...selectionIds].every((id) => ids.has(id))
                ) {
                    // This is the same as the last update,
                    // we don't need to auto scroll this
                    return
                }

                lastSelection.current = null
            }

            const rowId = [...selectionIds][selectionIds.size - 1]

            /* Give time for render of rows and rowVirtualizer to update */
            const raf = window.requestAnimationFrame(() => {
                const columnIds = grid.selectors.selectColumnIds(
                    grid.getState()
                )
                grid.api.scroll.scrollTo(rowId, columnIds[0], 'auto')
                grid.api.focus.set(rowId, columnIds[0], 'body', 'first')
            })
            return () => {
                window.cancelAnimationFrame(raf)
            }
        } else {
            lastSelection.current = null
        }
    }, [grid, loading, selectionIds, autoScroll])

    const rowsFetch = isGridServerData<TDataModel, TMetaModel>(rows)
        ? rows.fetch
        : null

    useEffect(() => {
        if (rowsFetch) {
            return grid.events.on('onVisibleRowsChange', rowsFetch)
        }
    }, [grid, rowsFetch])

    const preferencesInitialized = usePreferencesAdapter({
        grid,
        preferencesAdapter,
    })

    if (!preferencesInitialized) {
        return null
    }

    return (
        <GridProvider {...grid}>
            <GridActionsMenuProvider actionsMenu={actionsMenu}>
                <GridLiveRegionProvider>
                    <GridDragController>
                        <GridBase
                            disableHorizontalScroll={disableHorizontalScroll}
                            label={label}
                            rowHeight={gridRowHeights[rowHeight] || size.medium}
                            loading={loading}
                            emptyContent={emptyContent}
                        />
                    </GridDragController>
                </GridLiveRegionProvider>
            </GridActionsMenuProvider>
        </GridProvider>
    )
}
/**
 * **Important:** The Grid relies heavily on receiving immutable data
 * that changes only when actual changes occur. Even though the examples
 * will show arrays and objects being passed directly into `columns`
 * and `rows` props, in your actual application you'll want to ensure
 * you are passing stable objects into these properties.
 */
export const Grid = React.forwardRef(GridImpl) as <
    TDataModel extends GridRowData,
    TMetaModel extends GridRowMeta<TDataModel>,
>(
    props: GridProps<TDataModel, TMetaModel> & {
        ref?: React.ForwardedRef<GridApi>
    }
) => ReturnType<typeof GridImpl>
