import React from 'react';
import { DragSource, DropTarget } from 'react-dnd';
import itemTypes from './itemTypes';
import { getValuesByKey, hoverAboveItself } from './utils';
import { NestableListBaseContext } from './NestableListBaseContext';
import { getEmptyImage } from '../DragAndDrop/Draggable/DragUtils';
import classNames from 'classnames';
import { dataAttributes } from '../DragAndDrop/Draggable/constants';
// keep track of horizontal mouse movement
const mouse = {
    lastX: 0,
};
function increaseHorizontalLevel(prevPosition, prevIndex) {
    const nextPosition = prevPosition.slice(0, -1);
    // append to prevSibling's children
    nextPosition.push(prevIndex - 1, -1);
    return nextPosition;
}
function decreaseHorizontalLevel(prevPosition) {
    const nextPosition = prevPosition.slice(0, -1);
    nextPosition[nextPosition.length - 1] += 1;
    return nextPosition;
}
function calculateHandleOffset(handleRect, containerRect) {
    return {
        x: handleRect.x - containerRect.x,
        y: handleRect.y - containerRect.y,
    };
}
const cardSource = {
    isDragging(props, monitor) {
        const ids = getValuesByKey(monitor.getItem().data, 'id', 'children');
        return ids.indexOf(props.id) > -1;
    },
    beginDrag(props, monitor, component) {
        props.onDragStart && props.onDragStart(props);
        const node = component.rootRef.current;
        if (!node) {
            return null;
        }
        const clientRect = node.getBoundingClientRect();
        let handleOffset = { x: 0, y: 0 };
        // needed to fix dnd drag offset data
        if (component.handleNode) {
            handleOffset = calculateHandleOffset(component.handleNode.getBoundingClientRect(), clientRect);
        }
        return {
            id: props.id,
            dragged: false, // needed for workaround of immediately fired dragend event after dragstart
            index: props.index,
            position: props.position,
            data: props.item,
            groupName: props.groupName,
            depth: props.depth,
            // rect for entire component including children
            clientRect,
            handleOffset,
        };
    },
    endDrag: (props, monitor) => {
        mouse.lastX = 0;
        props.dropItem(monitor.getItem());
        props.onDragEnd && props.onDragEnd(props);
    },
};
const determineHorizontalPosition = ({ monitor, props, hoverNode }) => {
    const item = monitor.getItem();
    // the item being dragged
    const { position: prevPosition, depth: dragDepth, index: prevIndex } = item;
    // props for component underneath drag
    const { position: hoverPosition, siblings: hoverSiblings, maxDepth, threshold, } = props;
    // determine mouse position
    const clientOffset = monitor.getClientOffset() || { x: 0, y: 0 };
    const initialClientOffset = monitor.getInitialClientOffset() || {
        x: 0,
        y: 0,
    };
    // rect for entire component including children
    const hoverClientRect = hoverNode.getBoundingClientRect();
    const isOverSelf = hoverAboveItself(prevPosition, hoverPosition);
    // set mouse.lastX if it isn't set yet (first hover event)
    mouse.lastX = mouse.lastX || initialClientOffset.x;
    const currMouseX = clientOffset.x;
    const mouseDistanceX = currMouseX - mouse.lastX;
    const nearLeftEdge = currMouseX < hoverClientRect.left + 10;
    const currentIndex = props.index;
    // nextPosition will be overwritten when moving horizontally
    let nextPosition = hoverPosition;
    // disable horizontal movement is previous sibling has lockDropArea=true prop
    if (isOverSelf &&
        currentIndex !== 0 &&
        !!hoverSiblings[currentIndex - 1].lockDropArea &&
        mouseDistanceX > 0) {
        return nextPosition;
    }
    // disable horizontal movement is next sibling has lockDropArea=true prop
    if (isOverSelf &&
        currentIndex < hoverSiblings.length - 2 &&
        !!hoverSiblings[currentIndex + 1].lockDropArea &&
        mouseDistanceX < 0) {
        return nextPosition;
    }
    // moving horizontally
    if (isOverSelf && (nearLeftEdge || Math.abs(mouseDistanceX) >= threshold)) {
        // reset lastX for new phase
        mouse.lastX = currMouseX;
        // increase horizontal level
        if (mouseDistanceX > 0 &&
            // has previous sibling
            prevIndex - 1 >= 0 &&
            // isn't at max depth
            prevPosition.length + dragDepth - 1 !== maxDepth) {
            nextPosition = increaseHorizontalLevel(prevPosition, prevIndex);
        }
        // decrease horizontal level
        if (mouseDistanceX < 0 &&
            // is nested
            prevPosition.length > 1 &&
            // is last item in array
            prevIndex === hoverSiblings.length - 1) {
            nextPosition = decreaseHorizontalLevel(prevPosition);
        }
    }
    if (props.preventChangeDepth &&
        nextPosition.length - prevPosition.length === -1 // means that new parent is suitable for current dragged item
    ) {
        const isSameParent = nextPosition.every((position, depth) => {
            return prevPosition[depth] === position;
        });
        if (!isSameParent) {
            nextPosition = prevPosition.map((position, depth) => {
                return nextPosition[depth] !== undefined ? nextPosition[depth] : 0;
            });
        }
    }
    return nextPosition;
};
const allowItemMove = ({ prevPosition, nextPosition, monitor, hoverNode, props, }) => {
    // don't replace items with themselves
    if (hoverAboveItself(prevPosition, nextPosition)) {
        return;
    }
    // prevent drop if preventChangeDepth and depth is changed
    if (props.preventChangeDepth && nextPosition.length !== prevPosition.length) {
        return;
    }
    // Prevent drag drop, if parent has been changed
    if (props.preventChangeParent) {
        const prevParentPath = prevPosition.slice(0, -1);
        const nextParentPath = nextPosition.slice(0, -1);
        if (prevParentPath.length !== nextParentPath.length ||
            !prevParentPath.every((val, i) => Object.is(val, nextParentPath?.[i]))) {
            return;
        }
    }
    // Prevent drag drop, if element move outside pinned elements
    if (props.enforcePinnedOrder) {
        const prevIndex = prevPosition[prevPosition.length - 1];
        const targetIndex = nextPosition[nextPosition.length - 1];
        const prev = props.siblings[prevIndex];
        const next = props.siblings[targetIndex];
        if ((!!prev.isPinned && !next.isPinned) ||
            (!prev.isPinned && !!next.isPinned)) {
            return;
        }
    }
    const { position: hoverPosition } = props;
    const isOverSelf = hoverAboveItself(prevPosition, hoverPosition);
    const clientOffset = monitor.getClientOffset() || { x: 0, y: 0 };
    // rect for entire component including children
    const hoverClientRect = hoverNode.getBoundingClientRect();
    // rect for item without children
    const hoverItemClientRect = hoverNode.children[0].getBoundingClientRect();
    // get vertical middle
    const hoverMiddleY = (hoverClientRect.bottom - hoverClientRect.top) / 2;
    // get pixels to the top
    const hoverClientY = clientOffset.y - hoverClientRect.top;
    // dragging child item to another position with same parent
    if (nextPosition.length === prevPosition.length) {
        const last = nextPosition.length - 1;
        const previousIndex = prevPosition[last];
        const nextIndex = nextPosition[last];
        // only perform the move when the mouse has crossed half of the items height
        // when dragging downwards, only move when the cursor is below 50%
        // when dragging upwards, only move when the cursor is above 50%
        // dragging downwards
        if (previousIndex < nextIndex && hoverClientY < hoverMiddleY) {
            return;
        }
        // dragging upwards
        if (previousIndex > nextIndex && hoverClientY > hoverMiddleY) {
            return;
        }
    }
    else if (
    // dragging child item over parent item
    nextPosition.length < prevPosition.length &&
        nextPosition[nextPosition.length - 1] ===
            prevPosition[prevPosition.length - 2]) {
        const hoverItemMiddleY = (hoverItemClientRect.bottom - hoverItemClientRect.top) / 2;
        // cancel if hovering in lower half of parent item
        if (hoverClientY > hoverItemMiddleY) {
            return;
        }
    }
    else if (!isOverSelf && clientOffset.y > hoverItemClientRect.bottom) {
        // cancel if over a nested target that isn't its own child
        return;
    }
    return true;
};
const cardTarget = {
    hover(props, monitor, component) {
        // prevent drag and drop between different groups.
        // currently drag and drop between multiple nestable lists is not supported
        if (monitor.getItem().groupName !== props.groupName) {
            return;
        }
        const item = monitor.getItem();
        // the item being dragged
        const { position: prevPosition, data: dragItem, depth: dragDepth } = item;
        // props for component underneath drag
        const { position: hoverPosition, maxDepth } = props;
        const hoverDepth = hoverPosition.length - 1;
        const totalDepth = hoverDepth + dragDepth;
        // don't exceed max depth
        if (totalDepth > maxDepth) {
            return;
        }
        const hoverNode = component.rootRef.current;
        if (!hoverNode) {
            return;
        }
        const nextPosition = determineHorizontalPosition({
            monitor,
            props,
            hoverNode,
        });
        if (!allowItemMove({ prevPosition, nextPosition, monitor, hoverNode, props })) {
            return;
        }
        // this is where the actual move happens
        const nextPos = props.moveItem({
            dragItem,
            prevPosition,
            nextPosition,
        });
        item.prevPosition = prevPosition;
        item.prevIndex = item.index;
        item.dragged = true;
        // note: we're mutating the monitor item here!
        // generally it's better to avoid mutations,
        // but it's good here for the sake of performance
        // to avoid expensive index searches
        item.position = nextPos;
        item.index = nextPos[nextPos.length - 1];
    },
};
class Item extends React.PureComponent {
    constructor() {
        super(...arguments);
        this.state = {
            shouldRenderChildren: true,
        };
        this.unmounted = false;
        this.rootRef = React.createRef();
        this.handleNode = null;
    }
    componentWillUnmount() {
        this.unmounted = true;
    }
    componentDidMount() {
        // use empty image as a drag preview so browsers don't draw it
        // and we can draw whatever we want on the custom drag layer instead.
        this.props.connectDragPreview(getEmptyImage(), {
            // IE fallback: specify that we'd rather screenshot the node
            // when it already knows it's being dragged so we can hide it with CSS.
            captureDraggingState: true,
        });
        this.updateShouldRenderChildren();
    }
    componentDidUpdate() {
        this.updateShouldRenderChildren();
    }
    updateShouldRenderChildren() {
        const { isPlaceholder, isRenderDraggingChildren } = this.props;
        const shouldRenderChildren = !isPlaceholder || isRenderDraggingChildren;
        // start workaround of immediately fired dragend event after dragstart
        // https://github.com/react-dnd/react-dnd/issues/766#issuecomment-748255082
        if (shouldRenderChildren !== this.state.shouldRenderChildren) {
            if (!this.props.dragged &&
                this.state.shouldRenderChildren !== shouldRenderChildren) {
                setTimeout(() => {
                    if (!this.unmounted) {
                        this.setState({
                            shouldRenderChildren,
                        });
                    }
                }, 0);
            }
            else {
                this.setState({
                    shouldRenderChildren,
                });
            }
        }
        // end workaround of immediately fired dragend event after dragstart
    }
    render() {
        const { item, position, children, isPlaceholder, connectDragSource, connectDropTarget, useDragHandle, renderItem, renderPrefix, theme, readOnly, isVeryLastItem, siblings, } = this.props;
        // params passed to renderItem callback
        const renderParams = {
            item,
            siblings,
            isVeryLastItem,
            isPlaceholder,
            isPreview: false,
            connectDragSource: () => { },
            depth: position.length,
        };
        const draggableTargetDataProps = {
            [dataAttributes.draggableTarget]: true,
            'data-hook': 'nestable-item',
        };
        const renderItemWithDataAttributes = params => React.cloneElement(renderItem(params), {
            [dataAttributes.draggableSource]: true,
            [dataAttributes.depth]: position.length - 1,
            [dataAttributes.id]: item.id,
        });
        const classes = classNames('nestable-item', theme && theme.item);
        const { draggable = true, lockDropArea = false, isParentLocked = false, dragDisabled, } = item;
        if (!draggable || readOnly || dragDisabled) {
            if (lockDropArea) {
                return (React.createElement("div", { ref: this.rootRef, className: classes, ...draggableTargetDataProps },
                    renderPrefix(renderParams),
                    renderItemWithDataAttributes(renderParams),
                    this.state.shouldRenderChildren && children));
            }
            if (isParentLocked && draggable && !readOnly) {
                return connectDragSource(React.createElement("div", { ref: this.rootRef, className: classes, ...draggableTargetDataProps },
                    renderPrefix(renderParams),
                    renderItemWithDataAttributes(renderParams),
                    this.state.shouldRenderChildren && children));
            }
            return (React.createElement("div", { ref: this.rootRef, className: classes, ...draggableTargetDataProps },
                renderPrefix(renderParams),
                renderItemWithDataAttributes(renderParams),
                this.state.shouldRenderChildren && children));
        }
        if (useDragHandle) {
            renderParams.connectDragSource = handle => {
                const handleWithRef = React.cloneElement(handle, {
                    ref: node => (this.handleNode = node),
                });
                return connectDragSource(handleWithRef);
            };
            if (lockDropArea) {
                return connectDropTarget(React.createElement("div", { ref: this.rootRef, className: classes, ...draggableTargetDataProps },
                    renderPrefix(renderParams),
                    renderItemWithDataAttributes(renderParams),
                    this.state.shouldRenderChildren && children));
            }
            if (isParentLocked) {
                return connectDragSource(React.createElement("div", { ref: this.rootRef, className: classes, ...draggableTargetDataProps },
                    renderPrefix(renderParams),
                    renderItemWithDataAttributes(renderParams),
                    this.state.shouldRenderChildren && children));
            }
            return connectDropTarget(React.createElement("div", { ref: this.rootRef, className: classes, ...draggableTargetDataProps },
                renderPrefix(renderParams),
                renderItemWithDataAttributes(renderParams),
                this.state.shouldRenderChildren && children));
        }
        if (isParentLocked) {
            return connectDragSource(React.createElement("div", { ref: this.rootRef, className: classes, ...draggableTargetDataProps },
                renderPrefix(renderParams),
                renderItemWithDataAttributes(renderParams),
                this.state.shouldRenderChildren && children));
        }
        return connectDropTarget(connectDragSource(React.createElement("div", { ref: this.rootRef, className: classes, ...draggableTargetDataProps },
            renderPrefix(renderParams),
            renderItemWithDataAttributes(renderParams),
            this.state.shouldRenderChildren && children)));
    }
}
export const DragItemSource = DragSource(itemTypes.nestedItem, cardSource, (connect, monitor) => ({
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isPlaceholder: monitor.isDragging(),
    dragged: monitor.getItem() && monitor.getItem().dragged,
}))(Item);
export const DropItemTarget = DropTarget(itemTypes.nestedItem, cardTarget, connect => ({
    connectDropTarget: connect.dropTarget(),
}))(DragItemSource);
class ItemWithContext extends React.PureComponent {
    render() {
        return (React.createElement(NestableListBaseContext.Consumer, null, context => React.createElement(DropItemTarget, { ...this.props, ...context })));
    }
}
export default ItemWithContext;
//# sourceMappingURL=Item.js.map