import { useCallback } from 'react'
import { useGridContext, useGridSelector } from '../context/grid-context'
import type { SpreadsheetGridPastedCell } from '../grids/spreadsheet-grid/types'
import type { GridConfirmPayload, GridRowId } from '../types'
import { healSpannedRange, isRangeEqual } from '../utils/range'

function parsePlainTable(plain: string) {
    return plain.split('\n').map((r) => r.split('\t'))
}

function parseCustomTable(
    custom: string
): undefined | SpreadsheetGridPastedCell[][] {
    try {
        const parsed: {
            rowId: GridRowId
            values: Record<
                string,
                { label: string; value: unknown; colSpan?: number }
            >
        }[] = JSON.parse(custom)
        if (!Array.isArray(parsed)) {
            return undefined
        }
        const rows: SpreadsheetGridPastedCell[][] = []
        for (const row of parsed) {
            const cells: SpreadsheetGridPastedCell[] = []
            for (const key in row.values) {
                const cellValue = row.values[key]
                cells.push({
                    value: cellValue.value,
                    colSpan: cellValue.colSpan ?? 1,
                })
            }
            rows.push(cells)
        }
        return rows
    } catch {
        return undefined
    }
}

function parseHtmlTable(
    html: string
): undefined | SpreadsheetGridPastedCell[][] {
    const doc = new DOMParser().parseFromString(html, 'text/html')
    const table = doc.querySelector('table')
    if (!table) {
        return undefined
    }

    const rows: SpreadsheetGridPastedCell[][] = []
    for (const rowElement of table.querySelectorAll('tr')) {
        if (rowElement.closest('thead')) {
            continue
        }
        const cells: SpreadsheetGridPastedCell[] = []
        for (const cellElement of rowElement.children) {
            const headerCell = cellElement.closest('th') != null
            const colSpan = parseInt(
                cellElement.getAttribute('colspan') ?? '1',
                10
            )
            if (!headerCell) {
                const cell: SpreadsheetGridPastedCell = {
                    value: cellElement.textContent ?? '',
                    colSpan,
                }
                cells.push(cell)
            }
        }
        rows.push(cells)
    }

    return rows
}

const RANGE_PASTE_ANIMATION_DURATION = 300

export const useRangePaste = (
    onBulkCellChange?: (
        payload: Map<GridRowId, GridConfirmPayload<any>[]>
    ) => void,
    container: HTMLElement | null = null
) => {
    const grid = useGridContext()

    const isSpreadsheet = useGridSelector(grid.selectors.selectIsSpreadsheet)

    const handlePaste = useCallback(
        (e: React.ClipboardEvent) => {
            const state = grid.getState()
            const range = grid.selectors.selectRangeSelection(state)
            const isEditing = grid.selectors.selectIsEditing(state)
            if (!range || !onBulkCellChange || isEditing) {
                return
            }

            e.preventDefault()

            const plainSource = e.clipboardData?.getData('text/plain')
            const plain = plainSource ? parsePlainTable(plainSource) : undefined

            const htmlSource = e.clipboardData?.getData('text/html')
            const html = htmlSource ? parseHtmlTable(htmlSource) : undefined

            const customSource = e.clipboardData?.getData(
                'application/x-pvds-grid'
            )
            const custom = customSource
                ? parseCustomTable(customSource)
                : undefined

            if (!custom && !html && !plain) {
                /* No data found to process */
                return
            }

            const gridRowIds = grid.selectors.selectRowIds(state)
            const gridColumnIds = grid.selectors.selectColumnIds(state)
            const rangeData = grid.selectors.selectRangeIds(state)
            const topRowId = rangeData.rowIds[0]
            const leftColumnId = rangeData.columnIds[0]

            const rows = (custom ?? html ?? plain)!
            const rowCount = rows.length
            const columnCount = Math.max(
                ...rows.map((r) =>
                    r.reduce(
                        (a, c) =>
                            a + (typeof c === 'string' ? 1 : (c.colSpan ?? 1)),
                        0
                    )
                )
            )

            const topRowIndex = gridRowIds.indexOf(topRowId)
            const endRowId =
                gridRowIds[
                    Math.min(topRowIndex + rowCount - 1, gridRowIds.length - 1)
                ]
            const endRowIndex = gridRowIds.indexOf(endRowId)

            const leftColumnIndex = gridColumnIds.indexOf(leftColumnId)
            const endColumnId =
                gridColumnIds[
                    Math.min(
                        leftColumnIndex + columnCount - 1,
                        gridColumnIds.length - 1
                    )
                ]

            const payloads: GridConfirmPayload<any>[] = []

            for (
                let rowIndex = topRowIndex, i = 0;
                rowIndex <= endRowIndex;
                rowIndex++, i++
            ) {
                const rowId = gridRowIds[rowIndex]
                const htmlRow = rows[i]
                let htmlColumnIndex = 0
                let columnIndex = leftColumnIndex

                let columnsToProcess: { columnId: string; value: unknown }[] =
                    []

                /* Take a first pass to map each pasted cell to a column, honoring pasted column spans, but ignoring column spanning in the grid */
                while (htmlColumnIndex < columnCount) {
                    const cell = htmlRow[htmlColumnIndex]
                    const value =
                        typeof cell === 'string' ? cell : (cell?.value ?? '')
                    const colSpan =
                        typeof cell === 'string' ? 1 : (cell?.colSpan ?? 1)

                    for (let k = 0; k < colSpan; k++) {
                        const currentColumnId = gridColumnIds[columnIndex + k]
                        if (currentColumnId == null) {
                            break
                        }
                        if (k === 0) {
                            columnsToProcess.push({
                                columnId: currentColumnId,
                                value,
                            })
                        } else {
                            columnsToProcess.push({
                                columnId: currentColumnId,
                                value: '',
                            })
                        }
                    }

                    htmlColumnIndex++
                    columnIndex += colSpan
                }

                /* Now process each column, taking into account column spanning in the grid */
                while (columnsToProcess.length > 0) {
                    const toProcess = columnsToProcess.shift()!
                    let { columnId } = toProcess
                    const nextValue = toProcess.value

                    const colSpanConfig =
                        grid.selectors.selectColumnSpanByRowId(state, rowId)

                    const relatedSpanConfig =
                        colSpanConfig == null
                            ? null
                            : [...colSpanConfig.values()].find((c) =>
                                  c.positionColumnIds?.includes(columnId)
                              )
                    if (relatedSpanConfig) {
                        columnId = relatedSpanConfig.as ?? relatedSpanConfig.id
                        const groupedColumnIds =
                            relatedSpanConfig.positionColumnIds!.filter((id) =>
                                gridColumnIds.includes(id)
                            )

                        columnsToProcess = columnsToProcess.filter(
                            (c) => !groupedColumnIds.includes(c.columnId)
                        )
                    }

                    const editable = grid.api.content.isEditable({
                        rowId,
                        columnId,
                    })

                    if (editable) {
                        const value = grid.api.content.getValue({
                            rowId,
                            columnId,
                        })

                        payloads.push({
                            rowId,
                            columnId,
                            nextValue,
                            previousValue: value,
                        })
                    }
                }
            }

            if (!payloads.length) {
                return
            }
            const groupedPayloads = new Map<
                GridRowId,
                GridConfirmPayload<any>[]
            >()
            payloads.forEach((payload) => {
                if (!groupedPayloads.has(payload.rowId)) {
                    groupedPayloads.set(payload.rowId, [])
                }
                groupedPayloads.get(payload.rowId)?.push(payload)
            })
            const result = onBulkCellChange(groupedPayloads)

            if (result == null || result === true) {
                const newRange = healSpannedRange(
                    {
                        from: { rowId: topRowId, columnId: leftColumnId },
                        to: { rowId: endRowId, columnId: endColumnId },
                    },
                    grid
                )
                if (!isRangeEqual(newRange, range)) {
                    grid.api.rangeSelection.select(newRange)
                }

                const currentFocus = grid.selectors.selectCurrentFocus(state)

                if (
                    currentFocus.area !== 'body' ||
                    currentFocus.rowId !== topRowId ||
                    currentFocus.columnId !== leftColumnId
                ) {
                    grid.api.focus.set(topRowId, leftColumnId, 'body', 'first')
                }

                container?.setAttribute('data-pvds-grid-range-pasted', 'true')
                setTimeout(() => {
                    container?.removeAttribute('data-pvds-grid-range-pasted')
                }, RANGE_PASTE_ANIMATION_DURATION)
            } else {
                container?.setAttribute('data-pvds-grid-range-pasted', 'error')
                setTimeout(() => {
                    container?.removeAttribute('data-pvds-grid-range-pasted')
                }, RANGE_PASTE_ANIMATION_DURATION)
            }
        },
        [grid, onBulkCellChange, container]
    )

    if (!isSpreadsheet || !onBulkCellChange) {
        return undefined
    }

    return handlePaste
}
