import React from 'react'
import { useGridComponents } from '../context/grid-components-context/hook'
import { size, useIsFocusVisible } from '@planview/pv-utilities'
import { useGridSelector, useGridContext } from '../context/grid-context/hook'
import { useHoverSync } from '../context/grid-sync-context'
import type { VirtualItem } from '@tanstack/react-virtual'
import { ROW_FOCUS_ID, SELECTION_COLUMN_ID } from '../constants'
import type { GridRowId } from '../types'
import type { StickyColumnDetails } from '../state'
import { useGridActionsMenu } from '../context/grid-actions-menu-context'
import { useDraggable } from '@dnd-kit/core'
import { DRAG_TYPE_ROW } from './grid-drag-controller'
import { isMac } from '../utils/is-mac'
import {
    getColumnSpanByRowId,
    healVirtualColumnsForRow,
} from '../utils/column-span'
import { createSelector } from 'reselect'

export type GridRowProps = {
    id: GridRowId
    virtualStart: number
    virtualHeight: number
    virtualColumns: VirtualItem[]
    stickyColumns: StickyColumnDetails
    width: number
}

const getDecoupledIndex = (virtualColumns: VirtualItem[]): number | null => {
    if (virtualColumns.length < 2) {
        return null
    }
    const lastDecoupled =
        virtualColumns[virtualColumns.length - 1].start -
            virtualColumns[virtualColumns.length - 2].end >
        0
    const firstDecoupled = virtualColumns[1].start - virtualColumns[0].end > 0

    return lastDecoupled ? virtualColumns.length - 1 : firstDecoupled ? 0 : null
}

//This empty label is added to prevent assistive technology from reading out the contents of the tr
const emptyLabel = ' '

export const GridRow = ({
    id,
    virtualHeight,
    virtualStart,
    virtualColumns: virtualColumnsIncoming,
    stickyColumns,
    width,
}: GridRowProps) => {
    const { GridCell, GridCellLoading, GridRowLayout } = useGridComponents()
    const grid = useGridContext()
    const { visible: actionsMenuVisible } = useGridActionsMenu()

    const { selectors } = useGridContext()

    const selectionMode = useGridSelector(selectors.selectSelectionMode)
    const spreadsheetMode = useGridSelector(selectors.selectIsSpreadsheet)
    const canSelect = selectionMode !== 'none' && !spreadsheetMode

    const isVisuallySelected = useGridSelector((state) =>
        selectors.selectIsRowVisuallySelected(state, id)
    )

    const isSelected = useGridSelector(
        (state) => selectors.selectRowSelectionState(state, id) === 'all'
    )
    const columnIds = useGridSelector(selectors.selectColumnIds)

    const columnWidths = useGridSelector(selectors.selectColumnWidths)

    const colSpanConfigSelector = React.useMemo(
        () =>
            /*
                A custom selector is created due to the frequency this will be called.
                With reselect < 5, we cannot cache per-row items at the compositeSelectors
                layer, so we need to create a custom selector for each row.
            */
            createSelector(
                [
                    (state) => selectors.selectRow(state, id),
                    (state) => selectors.selectRowMeta(state, id),
                    selectors.selectStickyColumnIds,
                    selectors.selectColSpanConfig,
                ],
                (
                    row,
                    rowMeta,
                    stickyColumnIds,
                    { colSpanFunctions, columnIds }
                ) => {
                    if (!row || !colSpanFunctions.length) {
                        return null
                    }

                    return getColumnSpanByRowId({
                        row,
                        rowMeta,
                        stickyColumnIds,
                        colSpanFunctions,
                        columnIds,
                    })
                }
            ),
        [id, selectors]
    )

    const colSpanConfig = useGridSelector(colSpanConfigSelector)

    const headerRowCount = useGridSelector((state) =>
        selectors.selectHeaderRowCount(state)
    )

    const rowIndex = useGridSelector((state) =>
        selectors.selectRowIndex(state, id)
    )

    const isLoaded = useGridSelector((state) =>
        selectors.selectIsRowLoaded(state, id)
    )

    const excluded = useGridSelector((state) =>
        isLoaded ? selectors.selectRowExcluded(state, id) : false
    )

    const isExpandable = useGridSelector((state) =>
        selectors.selectIsRowExpandable(state, id)
    )

    const isExpanded = useGridSelector((state) =>
        isExpandable ? selectors.selectIsRowExpanded(state, id) : false
    )

    const rowLevel = useGridSelector(
        (state) => grid.selectors.selectRowLevel(state, id) + 1
    )

    const ariaSetSize = useGridSelector((state) =>
        grid.selectors.selectAriaSetSize(state, id)
    )

    const ariaPosInset = useGridSelector((state) =>
        grid.selectors.selectAriaPosInset(state, id)
    )

    const hasFooter = useGridSelector((state) =>
        selectors.selectAggregationEnabled(state)
    )

    const hasFocus = useGridSelector((state) =>
        grid.selectors.selectHasFocus(state, ROW_FOCUS_ID, 'body', id)
    )

    const {
        innerRef: _innerRef,
        focusVisible,
        ...focusProps
    } = useIsFocusVisible()

    const { isHovered, onMouseEnter, onMouseLeave } = useHoverSync(id)

    const virtualColumns = React.useMemo(() => {
        if (!colSpanConfig) {
            return virtualColumnsIncoming
        }
        return healVirtualColumnsForRow({
            virtualColumns: virtualColumnsIncoming,
            colSpanConfig,
            columnIds,
            columnWidths,
        })
    }, [columnIds, columnWidths, colSpanConfig, virtualColumnsIncoming])

    const onClick = React.useCallback(
        (e: React.MouseEvent<HTMLElement>) => {
            if (isMac && e.ctrlKey && e.button === 0) {
                /* This is a Mac right click */
                return
            }
            if (selectors.selectIsEditing(grid.getState())) {
                return
            }
            if (canSelect) {
                const modifierPressed = e.metaKey || e.ctrlKey
                grid.api.selection.select(
                    id,
                    !(isSelected && modifierPressed),
                    !modifierPressed,
                    e.shiftKey
                )
                if (e.shiftKey) {
                    document.getSelection()?.removeAllRanges()
                }
            }
            grid.events.emit('onRowClick', id)
        },
        [selectors, grid, canSelect, id, isSelected]
    )

    const onContextMenu = React.useCallback(
        (e: React.MouseEvent<HTMLElement>) => {
            const target = e.target as HTMLElement
            /* Don't intercept links or form elements. Ensure event originated
               as a direct descendant and not via a react portal. */
            if (
                target.closest('a, input, textarea, select') ||
                !e.currentTarget.contains(target)
            ) {
                return
            }

            const state = grid.getState()
            const enabled = grid.selectors.selectActionsMenuEnabled(state)
            if (!enabled) {
                return
            }
            const row = grid.selectors.selectRow(state, id)
            const rowMeta = grid.selectors.selectRowMeta(state, id)

            if (actionsMenuVisible({ row, rowMeta })) {
                // We don't want to prevent default if the menu is not enabled or visible
                e.preventDefault()
                grid.api.actionsMenu.show(id, {
                    top: e.clientY,
                    left: e.clientX,
                })
            }
        },
        [id, grid, actionsMenuVisible]
    )

    function renderCell(columnId: string, virtualStart?: number) {
        const CellComponent =
            isLoaded || columnId === SELECTION_COLUMN_ID
                ? GridCell
                : GridCellLoading

        const config = colSpanConfig?.get(columnId)

        if (!columnId || config?.skip) {
            /* There is a risk that an extra column is rendered for focus, but
                    it has been removed before the range extractor is aware of it. */
            return null
        }
        return (
            <CellComponent
                virtualStart={virtualStart}
                key={columnId}
                rowId={id}
                columnId={columnId}
                renderColumnId={config?.as}
                spanColumns={config?.positionColumnIds}
            />
        )
    }

    const isDragging = useGridSelector((state) =>
        grid.selectors.selectIsRowDragging(state, id)
    )

    const isDraggingEnabled = useGridSelector(
        grid.selectors.selectIsDraggingEnabled
    )
    const canDragRow = useGridSelector((state) =>
        isDraggingEnabled ? grid.selectors.selectCanDragRow(state, id) : false
    )

    const dragDisabled = !canDragRow || !isLoaded
    const { attributes, listeners, setNodeRef } = useDraggable({
        id,
        data: {
            type: DRAG_TYPE_ROW,
            virtualStart,
        },
        disabled: dragDisabled,
    })

    const onPointerDown = React.useCallback(
        (e: React.PointerEvent) => {
            if (e.target === e.currentTarget && !dragDisabled) {
                grid.api.focus.set(id, ROW_FOCUS_ID, 'body', 'last')
            }
        },
        [grid.api, id, dragDisabled]
    )

    const adjustedListeners = React.useMemo(() => {
        if (spreadsheetMode) {
            const validateTarget = (
                target: EventTarget,
                currentTarget: EventTarget
            ) =>
                target === currentTarget ||
                (target instanceof Element &&
                    target.closest('[data-grid-row-drag-handle]'))
            return {
                onKeyDown: (e: React.KeyboardEvent) => {
                    if (validateTarget(e.target, e.currentTarget)) {
                        listeners?.onKeyDown?.(e)
                    }
                },
                onTouchStart: (e: React.TouchEvent) => {
                    if (validateTarget(e.target, e.currentTarget)) {
                        listeners?.onTouchStart?.(e)
                    }
                },
                onMouseDown: (e: React.MouseEvent) => {
                    if (validateTarget(e.target, e.currentTarget)) {
                        listeners?.onMouseDown?.(e)
                    }
                },
            }
        }
        return listeners
    }, [spreadsheetMode, listeners])

    /**
     * Figure out which column is decoupled from virtualized items: Meaning if is kept in the DOM because it has tab-index="0"
     * This is done to position the decoupled item correctly (as if all items were rendered)
     * so that the list can scroll the item into view if the item get's focus again.
     */
    const decoupledIndex = getDecoupledIndex(virtualColumns)
    const firstColumn =
        decoupledIndex === 0 ? virtualColumns[1] : virtualColumns[0]
    return (
        <GridRowLayout
            {...(canDragRow ? attributes : null)}
            {...focusProps}
            tabIndex={canDragRow ? (hasFocus ? 0 : -1) : undefined}
            data-current-row={hasFocus ? 'true' : undefined}
            role="row"
            ref={setNodeRef}
            aria-selected={selectionMode === 'none' ? undefined : isSelected}
            aria-rowindex={rowIndex + headerRowCount + 1}
            aria-expanded={isExpandable ? isExpanded : undefined}
            aria-level={rowLevel ? rowLevel : undefined}
            aria-setsize={ariaSetSize ? ariaSetSize : undefined}
            aria-posinset={ariaPosInset ? ariaPosInset : undefined}
            aria-label={canDragRow ? emptyLabel : undefined}
            $excluded={excluded}
            $selected={isVisuallySelected}
            $hovering={isHovered}
            $selectable={canSelect}
            $hasBorder={virtualHeight === size.medium || spreadsheetMode}
            $loading={!isLoaded}
            $noBorderOnLast={hasFooter}
            $draggable={canDragRow}
            $dragging={isDragging}
            $focusVisible={hasFocus && focusVisible}
            $spreadsheetMode={spreadsheetMode}
            style={{
                width,
                height: virtualHeight,
                transform: `translateY(${virtualStart}px)`,
                paddingLeft:
                    (firstColumn?.start || 0) - stickyColumns.left.width,
            }}
            onMouseEnter={onMouseEnter}
            onMouseLeave={onMouseLeave}
            onClick={onClick}
            onContextMenu={onContextMenu}
            onPointerDown={onPointerDown}
            {...adjustedListeners}
        >
            {stickyColumns.left.columns.map(({ id }) => renderCell(id))}
            {virtualColumns.map((virtualItem, ix) => {
                const { index } = virtualItem
                const columnId = columnIds[index]

                return renderCell(
                    columnId,
                    ix === decoupledIndex ? virtualItem.start : undefined
                )
            })}
            {stickyColumns.right.columns.map(({ id }) => renderCell(id))}
        </GridRowLayout>
    )
}

export const GridRowMemo = React.memo(GridRow) as typeof GridRow
