"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.YfmTableDnDHandler = void 0;
const model_1 = require("../../../../../../pm/model.js");
const state_1 = require("../../../../../../pm/state.js");
const utils_1 = require("../../../../../../pm/utils.js");
const lodash_1 = require("../../../../../../lodash.js");
const table_utils_1 = require("../../../../../../table-utils/index.js");
const table_desc_1 = require("../../../../../../table-utils/table-desc.js");
const YfmTableSpecs_1 = require("../../../YfmTableSpecs/index.js");
const dnd_plugin_1 = require("../plugins/dnd-plugin.js");
const focus_plugin_1 = require("../plugins/focus-plugin.js");
const utils_2 = require("../utils.js");
const dnd_drop_cursor_1 = require("./dnd-drop-cursor.js");
const dnd_ghost_1 = require("./dnd-ghost.js");
require("./dnd.css");
const MOUSE_MOVE_DEBOUNCE = 100; // ms
const DRAG_START_THRESHOLD = 4; // px
class YfmTableDnDHandler {
    _rowHandler;
    _columnHandler;
    get row() {
        return this._rowHandler;
    }
    get column() {
        return this._columnHandler;
    }
    constructor(view, params) {
        this._rowHandler = new YfmTableRowDnDHandler(view, params);
        this._columnHandler = new YfmTableColumnDnDHandler(view, params);
    }
    update(node) {
        this._rowHandler.update(node);
        this._columnHandler.update(node);
    }
    destroy() {
        this._rowHandler.destroy();
        this._columnHandler.destroy();
    }
}
exports.YfmTableDnDHandler = YfmTableDnDHandler;
class YfmTableDnDAbstractHandler {
    _cellGetPos;
    _editorView;
    _logger;
    _dropCursor;
    __cellNode;
    __dragging = false;
    __destroyed = false;
    __dragMouseDown = false;
    constructor(view, params) {
        this._editorView = view;
        this.__cellNode = params.cellNode;
        this._cellGetPos = params.cellGetPos;
        this._logger = params.logger;
        this._dropCursor = params.dropCursor;
    }
    update(cellnode) {
        this.__cellNode = cellnode;
    }
    destroy() {
        this.__destroyed = true;
        this._clearDragging(false);
    }
    control_handleMouseDown = (event) => {
        this.__dragMouseDown = { pageX: event.pageX, pageY: event.pageY };
        this._dropCursor.clear();
    };
    control_handleMouseUp = () => {
        this.__dragMouseDown = false;
    };
    control_handleMouseLeave = () => {
        this.__dragMouseDown = false;
    };
    control_handleMouseMove = (event) => {
        if (!this._dragMouseDown || !isDragThresholdPassed(this._dragMouseDown, event))
            return;
        if (this._editorView.dragging || this._dragging)
            return;
        this._startDragging(event);
    };
    get _cellNode() {
        return this.__cellNode;
    }
    get _dragging() {
        return this.__dragging;
    }
    set _dragging(val) {
        this.__dragging = val;
    }
    get _destroyed() {
        return this.__destroyed;
    }
    get _dragMouseDown() {
        return this.__dragMouseDown;
    }
    _getTableDescAndCellInfo() {
        const tcellPos = this._cellGetPos();
        const tableNode = tcellPos === undefined
            ? undefined
            : (0, utils_1.findParentNodeClosestToPos)(this._editorView.state.doc.resolve(tcellPos), table_utils_1.isTableNode);
        const tableDesc = tableNode && table_desc_1.TableDesc.create(tableNode.node)?.bind(tableNode.pos);
        const cellInfo = tableDesc?.base.getCellInfo(this._cellNode);
        return cellInfo
            ? {
                cellPos: tcellPos,
                table: tableNode,
                tableDesc: tableDesc,
                cellInfo,
            }
            : null;
    }
    _clearDragging(clearDecorations) {
        this.__dragging = false;
        this.__dragMouseDown = false;
        this._dropCursor.clear();
        this._editorView.dragging = null;
        if (clearDecorations !== false)
            this._editorView.dispatch((0, dnd_plugin_1.clearAllSelections)(this._editorView.state.tr));
    }
}
class YfmTableRowDnDHandler extends YfmTableDnDAbstractHandler {
    constructor(view, params) {
        super(view, {
            ...params,
            logger: params.logger.nested({ component: 'row-dnd-handler' }),
            dropCursor: new dnd_drop_cursor_1.DropCursor(view, params.dropCursor),
        });
    }
    canDrag() {
        const res = this._getTableDescAndCellInfo();
        if (!res)
            return false;
        const rowRange = res.tableDesc.base.getRowRangeByRowIdx(res.cellInfo.row);
        return rowRange.safeTopBoundary && rowRange.safeBottomBoundary;
    }
    _startDragging = (event) => {
        const info = this._getTableDescAndCellInfo();
        if (!info)
            return;
        const { tableDesc, cellInfo } = info;
        const rowRanges = tableDesc.base.getRowRanges();
        const currRowRange = tableDesc.base.getRowRangeByRowIdx(cellInfo.row);
        if (!currRowRange.safeTopBoundary || !currRowRange.safeBottomBoundary)
            return;
        this._dragging = true;
        this._logger.event({ event: 'row-drag-start' });
        {
            const { tr } = this._editorView.state;
            (0, focus_plugin_1.hideHoverDecos)(tr);
            (0, dnd_plugin_1.selectDraggedRow)(tr, (0, utils_2.getSelectedCellsForRows)(info.tableDesc, currRowRange));
            this._editorView.dispatch(tr);
        }
        {
            const from = tableDesc.getPosForRow(currRowRange.startIdx).from;
            const to = tableDesc.getPosForRow(currRowRange.endIdx).to;
            this._editorView.dragging = {
                move: true,
                slice: this._editorView.state.doc.slice(from, to, false),
            };
        }
        const draggedRangeIdx = rowRanges.indexOf(currRowRange);
        const ghost = new dnd_ghost_1.YfmTableDnDGhost(this._editorView, {
            type: 'row',
            initial: event,
            rangeIdx: draggedRangeIdx,
            tableDesc,
        });
        const onMoveDebounced = (0, lodash_1.debounce)((event) => {
            this._moveDragging(event, {
                rangeIdx: draggedRangeIdx,
                tableDesc,
            });
        }, MOUSE_MOVE_DEBOUNCE, { maxWait: MOUSE_MOVE_DEBOUNCE });
        const onMove = (event) => {
            ghost.move(event);
            onMoveDebounced(event);
        };
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', () => {
            onMoveDebounced.flush();
            ghost.destroy();
            document.removeEventListener('mousemove', onMove);
            this._endDragging(currRowRange, tableDesc);
        }, { once: true });
    };
    _moveDragging(event, { rangeIdx, tableDesc: initialTableDesc, }) {
        if (this._destroyed || !this._dragging)
            return;
        const info = this._getTableDescAndCellInfo();
        if (!info || info.tableDesc.base !== initialTableDesc.base) {
            this._clearDragging();
            return;
        }
        const { tableDesc } = info;
        const ranges = tableDesc.base.getRowRanges();
        const boxes = ranges.map((range) => {
            const firstRowStartPos = tableDesc.getPosForRow(range.startIdx).from;
            const lastRowPos = tableDesc.getPosForRow(range.endIdx);
            const lastRowStartPos = lastRowPos.from;
            const lastRowEndPos = lastRowPos.to;
            const firstRowElem = this._editorView.domAtPos(firstRowStartPos + 1).node;
            const firstRowBox = firstRowElem.getBoundingClientRect();
            const lastRowElem = this._editorView.domAtPos(lastRowStartPos + 1).node;
            const lastRowBox = lastRowElem.getBoundingClientRect();
            return {
                pos: { from: firstRowStartPos, to: lastRowEndPos },
                rect: {
                    top: firstRowBox.top,
                    bottom: lastRowBox.bottom,
                },
                safeTopBoundary: range.safeTopBoundary,
                safeBottomBoundary: range.safeBottomBoundary,
            };
        });
        const pos = (() => {
            const { clientY } = event;
            const draggedRangeBox = boxes[rangeIdx];
            const isDraggedFirst = rangeIdx === 0;
            const isDraggedLast = rangeIdx === boxes.length - 1;
            if (!isDraggedFirst && clientY < draggedRangeBox.rect.top) {
                for (const box of boxes.slice(0, rangeIdx).reverse()) {
                    if (clientY < box.rect.top)
                        continue;
                    if (!box.safeTopBoundary)
                        continue;
                    return box.pos.from;
                }
                return boxes[0].pos.from;
            }
            if (!isDraggedLast && clientY > draggedRangeBox.rect.bottom) {
                for (const box of boxes.slice(rangeIdx + 1)) {
                    if (clientY > box.rect.bottom)
                        continue;
                    if (!box.safeBottomBoundary)
                        continue;
                    return box.pos.to;
                }
                return boxes.at(-1).pos.to;
            }
            return null;
        })();
        if (typeof pos === 'number')
            this._dropCursor.setPos(pos);
        else
            this._dropCursor.clear();
    }
    _endDragging(draggedRange, initialTableDesc) {
        if (this._destroyed || !this._dragging)
            return;
        const point = this._dropCursor.getPos();
        this._clearDragging();
        this._logger.event({ event: 'row-drag-end' });
        if (point === null) {
            this._editorView.focus();
            return;
        }
        const info = this._getTableDescAndCellInfo();
        if (!info || info.tableDesc.base !== initialTableDesc.base) {
            this._editorView.focus();
            return;
        }
        const rangeFrom = info.tableDesc.getPosForRow(draggedRange.startIdx).from;
        const rangeTo = info.tableDesc.getPosForRow(draggedRange.endIdx).to;
        if (point === rangeFrom || point === rangeTo) {
            this._editorView.focus();
            return;
        }
        const { tr } = this._editorView.state;
        const fragment = tr.doc.slice(rangeFrom, rangeTo, false).content;
        if (point > rangeFrom) {
            tr.insert(point, fragment);
            tr.delete(rangeFrom, rangeTo);
        }
        else {
            tr.delete(rangeFrom, rangeTo);
            tr.insert(point, fragment);
        }
        tr.setSelection(state_1.TextSelection.near(tr.doc.resolve(tr.mapping.map(point + 1)), 1));
        tr.scrollIntoView();
        this._editorView.dispatch(tr);
        this._editorView.focus();
    }
}
class YfmTableColumnDnDHandler extends YfmTableDnDAbstractHandler {
    constructor(view, params) {
        super(view, {
            ...params,
            logger: params.logger.nested({ component: 'column-dnd-handler' }),
            dropCursor: new dnd_drop_cursor_1.TableColumnDropCursor(view, params.dropCursor),
        });
    }
    canDrag() {
        const res = this._getTableDescAndCellInfo();
        if (!res)
            return false;
        const rowRange = res.tableDesc.base.getColumnRangeByColumnIdx(res.cellInfo.column);
        return rowRange.safeLeftBoundary && rowRange.safeRightBoundary;
    }
    _startDragging(event) {
        const info = this._getTableDescAndCellInfo();
        if (!info)
            return;
        const { tableDesc, cellInfo } = info;
        const columnRanges = tableDesc.base.getColumnRanges();
        const currColumnRange = tableDesc.base.getColumnRangeByColumnIdx(cellInfo.column);
        if (!currColumnRange.safeLeftBoundary || !currColumnRange.safeRightBoundary)
            return;
        this._dragging = true;
        this._logger.event({ event: 'column-drag-start' });
        {
            const { tr } = this._editorView.state;
            (0, focus_plugin_1.hideHoverDecos)(tr);
            (0, dnd_plugin_1.selectDraggedColumn)(tr, (0, utils_2.getSelectedCellsForColumns)(tableDesc, currColumnRange));
            this._editorView.dispatch(tr);
        }
        {
            this._editorView.dragging = {
                move: true,
                slice: model_1.Slice.empty,
            };
        }
        const draggedRangeIdx = columnRanges.indexOf(currColumnRange);
        const ghost = new dnd_ghost_1.YfmTableDnDGhost(this._editorView, {
            type: 'column',
            initial: event,
            rangeIdx: draggedRangeIdx,
            tableDesc,
        });
        const onMoveDebounced = (0, lodash_1.debounce)((event) => {
            this._moveDragging(event, {
                rangeIdx: draggedRangeIdx,
                tableDesc,
            });
        }, MOUSE_MOVE_DEBOUNCE, { maxWait: MOUSE_MOVE_DEBOUNCE });
        const onMove = (event) => {
            ghost.move(event);
            onMoveDebounced(event);
        };
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', () => {
            onMoveDebounced.flush();
            ghost.destroy();
            document.removeEventListener('mousemove', onMove);
            this._endDragging(currColumnRange, tableDesc);
        }, { once: true });
    }
    _moveDragging(event, { rangeIdx, tableDesc: initialTableDesc, }) {
        if (this._destroyed || !this._dragging)
            return;
        const info = this._getTableDescAndCellInfo();
        if (!info || info.tableDesc.base !== initialTableDesc.base) {
            this._clearDragging();
            return;
        }
        const { tableDesc } = info;
        const ranges = tableDesc.base.getColumnRanges();
        const boxes = ranges.map((range) => {
            const firstColStartPos = getCellPos(tableDesc.getPosForCell(0, range.startIdx), 'start');
            const lastColPos = tableDesc.getPosForCell(0, range.startIdx);
            const lastColStartPos = getCellPos(lastColPos, 'start');
            const lastColEndPos = getCellPos(lastColPos, 'end');
            const firstColElem = this._editorView.domAtPos(firstColStartPos + 1).node;
            const firstColBox = firstColElem.getBoundingClientRect();
            const lastColElem = this._editorView.domAtPos(lastColStartPos + 1).node;
            const lastColBox = lastColElem.getBoundingClientRect();
            return {
                pos: { from: firstColStartPos, to: lastColEndPos },
                rect: {
                    left: firstColBox.left,
                    right: lastColBox.right,
                },
                safeLeftBoundary: range.safeLeftBoundary,
                safeRightBoundary: range.safeRightBoundary,
            };
        });
        const pos = (() => {
            const { clientX } = event;
            const draggedRangeBox = boxes[rangeIdx];
            const isDraggedFirst = rangeIdx === 0;
            const isDraggedLast = rangeIdx === boxes.length - 1;
            if (!isDraggedFirst && clientX < draggedRangeBox.rect.left) {
                for (const box of boxes.slice(0, rangeIdx).reverse()) {
                    if (clientX < box.rect.left)
                        continue;
                    if (!box.safeLeftBoundary)
                        continue;
                    return box.pos.from;
                }
                return boxes[0].pos.from;
            }
            if (!isDraggedLast && clientX > draggedRangeBox.rect.right) {
                for (const box of boxes.slice(rangeIdx + 1)) {
                    if (clientX > box.rect.right)
                        continue;
                    if (!box.safeRightBoundary)
                        continue;
                    return box.pos.to;
                }
                return boxes.at(-1).pos.to;
            }
            return null;
        })();
        if (typeof pos === 'number')
            this._dropCursor.setPos(pos);
        else
            this._dropCursor.clear();
    }
    _endDragging(draggedRange, initialTableDesc) {
        if (this._destroyed || !this._dragging)
            return;
        const point = this._dropCursor.getPos();
        this._clearDragging();
        this._logger.event({ event: 'column-drag-end' });
        if (point === null) {
            this._editorView.focus();
            return;
        }
        const info = this._getTableDescAndCellInfo();
        if (!info || info.tableDesc.base !== initialTableDesc.base) {
            this._editorView.focus();
            return;
        }
        const currRangeFrom = info.tableDesc.getPosForCell(0, draggedRange.startIdx).from;
        const currRangeTo = info.tableDesc.getPosForCell(0, draggedRange.endIdx)
            .to;
        if (point === currRangeFrom || point === currRangeTo)
            return;
        const targetColumnIndex = (() => {
            if (point === info.tableDesc.getPosForRow(0).to - 1) {
                return info.tableDesc.cols;
            }
            const cellNode = this._editorView.state.doc.nodeAt(point);
            if (!cellNode || cellNode.type.name !== YfmTableSpecs_1.YfmTableNode.Cell)
                return null;
            const cellInfo = info.tableDesc.base.getCellInfo(cellNode);
            if (!cellInfo)
                return null;
            return cellInfo.column;
        })();
        if (targetColumnIndex === null ||
            targetColumnIndex === info.cellInfo.column ||
            targetColumnIndex === info.cellInfo.column + 1) {
            this._editorView.focus();
            return;
        }
        const { tr } = this._editorView.state;
        for (let rowIdx = 0; rowIdx < info.tableDesc.rows; rowIdx++) {
            const from = getCellPos(info.tableDesc.getPosForCell(rowIdx, draggedRange.startIdx), 'start');
            const to = getCellPos(info.tableDesc.getPosForCell(rowIdx, draggedRange.endIdx), 'end');
            if (from === to)
                continue;
            let targetPos;
            if (targetColumnIndex === info.tableDesc.cols) {
                targetPos = info.tableDesc.getPosForRow(rowIdx).to - 1;
            }
            else if (targetColumnIndex === 0) {
                targetPos = info.tableDesc.getPosForRow(rowIdx).from + 1;
            }
            else {
                targetPos = getCellPos(info.tableDesc.getPosForCell(rowIdx, targetColumnIndex), 'start');
            }
            if (targetPos !== from && targetPos !== to) {
                const fragment = tr.doc.slice(from, to, false).content;
                tr.insert(tr.mapping.map(targetPos), fragment);
                tr.delete(tr.mapping.map(from), tr.mapping.map(to));
            }
        }
        this._editorView.dispatch(tr.scrollIntoView());
        this._editorView.focus();
    }
}
function getCellPos(pos, dir) {
    if (pos.type === 'virtual')
        return pos.closestPos;
    return dir === 'start' ? pos.from : pos.to;
}
function isDragThresholdPassed(init, curr) {
    return (Math.abs(init.pageX - curr.pageX) >= DRAG_START_THRESHOLD ||
        Math.abs(init.pageY - curr.pageY) >= DRAG_START_THRESHOLD);
}
//# sourceMappingURL=dnd.js.map
