import React from "react";
import { SortableContainer } from "react-sortable-hoc";
var ChevronDirection;
(function (ChevronDirection) {
    ChevronDirection["left"] = "left";
    ChevronDirection["right"] = "right";
})(ChevronDirection || (ChevronDirection = {}));
export var AllowableDnDAxis;
(function (AllowableDnDAxis) {
    AllowableDnDAxis["x"] = "x";
    AllowableDnDAxis["y"] = "y";
})(AllowableDnDAxis || (AllowableDnDAxis = {}));
const SORT_CONTAINER_DRAG_DISTANCE = 10;
const defaultOnDragEnd = () => {
    console.warn("No onDragEnd passed to FinsembleDnDContext");
};
const defaultOnDragBegin = Function.prototype;
const defaultOnDrag = Function.prototype;
const defaultOnDragOver = Function.prototype;
const defaultOnLayoutUpdate = Function.prototype;
const DEFAULT_CONTAINER_MEASUREMENTS = {
    clientLeft: 0,
    clientWidth: 0,
    scrollLeft: 0,
    scrollWidth: 0,
    offsetLeft: 0,
    offsetWidth: 0,
};
const DEFAULT_CHEVRON_SCROLL_DISTANCE = 60;
/**
 * Listens for resize events on the DOM and changes whether we should render the scrolling chevrons.
 * @param overflowStateSetter
 */
const addResizeHandler = (overflowStateSetter) => () => {
    window.addEventListener("resize", overflowStateSetter);
    return () => {
        window.removeEventListener("resize", overflowStateSetter);
    };
};
/**
 * Scrolls forward or back
 * @param containerRef
 * @param distance How far to move
 * @param state Current scroll state
 * @param stateSetters Scroll state setters
 * @param scrollDir Whether we're scrolling forward or backwards
 * @param deltaY The scroll amount that came from the `wheel` event.
 */
const scrollHorizontally = (distance, state, stateSetters, scrollDir, deltaY) => {
    const { setScrollLeft } = stateSetters;
    const { containerScrollLeft, containerMeasurements } = state;
    if (containerMeasurements.scrollWidth === 0)
        return;
    let scrollAmount = 0;
    let delta = 0;
    if (deltaY) {
        delta = Math.max(-1, Math.min(1, -deltaY));
    }
    else if (scrollDir) {
        delta = Math.max(-1, Math.min(1, scrollDir));
    }
    let fakeItemWidth = 0;
    // advance the scroll position 40 px forward or back, depending on the mouse wheel direction.
    // eslint-disable-next-line no-mixed-operators
    let newScrollLeft = containerScrollLeft - delta * distance;
    if (scrollAmount >= distance) {
        return fakeItemWidth;
    }
    // See the links inside of the main body of FinsembleDnDContext if offsetLeft/Width are unfamiliar to you
    const rightEdgeOfScrollingViewport = containerMeasurements.offsetLeft + containerMeasurements.offsetWidth + newScrollLeft;
    const rightMostEdgeOfScrollingArea = containerMeasurements.offsetLeft + containerMeasurements.scrollWidth;
    // if we've scrolled beyond the right-most edge of the area, calculate the fake item width
    if (rightEdgeOfScrollingViewport > rightMostEdgeOfScrollingArea) {
        fakeItemWidth = newScrollLeft - (containerMeasurements.scrollWidth - containerMeasurements.clientWidth);
    }
    // if we've scrolled past the left-most area, set the scrollLeft to the leftMost edge of the region.
    if (newScrollLeft < 0)
        newScrollLeft = 0;
    if (newScrollLeft !== containerScrollLeft) {
        setScrollLeft(newScrollLeft);
    }
    return fakeItemWidth;
};
/**
 * Simple component that renders the left and right chevrons.
 * @param props
 */
const ScrollingChevron = (props) => {
    const { isDisabled, chevronDirection, showToolTip, title, scrollFunction } = props;
    const classNames = `scroll-button ff-chevron-${chevronDirection} ${isDisabled ? "greyedOut" : null}`;
    const toolTip = showToolTip ? title : "";
    return (React.createElement("div", { className: "chevron-container", title: toolTip },
        React.createElement("i", { className: classNames, onClick: scrollFunction })));
};
/**
 * The FinsembleDnDContext allows for all of the Draggable and Droppable components to work.
 * It wraps its children and allows them to do draggy and droppy things.
 * It also allows for horizontal containers to provide scrolling functionality.
 * @param props
 * @param props.id ID for the container that wraps the sortable container
 * @param props.allowHorizontalScroll whether the DND context should allow for horizontal scrolling
 * @param props.showScrollButtonsTooltip
 * @param props.leftButtonTooltip Tooltip for left scroll button
 * @param props.rightButtonTooltip Tooltip for right scroll button
 * @param props.chevronScrollDistance how far clicking the chevrons should push the content forward or backwards
 * @param props.onDragEnd handler for sorting
 * @param props.onDragBegin handler for sorting
 * @param props.onDragOver
 * @param props.onLayoutUpdate handler for rendering. Invokes on useLayoutEffect
 * @param props.distance how far the user has to drag an element to trigger sorting behavior
 * @param props.axis whether to sort vertically or horizontally.
 * @param props.className optional styles to pass the the sortable container
 */
export const FinsembleDnDContext = (props) => {
    var _a, _b, _c, _d, _e, _f, _g, _h;
    const { allowHorizontalScroll } = props;
    const onDragEnd = (_a = props.onDragEnd) !== null && _a !== void 0 ? _a : defaultOnDragEnd;
    const onDragBegin = (_b = props.onDragStart) !== null && _b !== void 0 ? _b : defaultOnDragBegin;
    const onDragMove = (_c = props.onDragMove) !== null && _c !== void 0 ? _c : defaultOnDrag;
    const onDragOver = (_d = props.onDragOver) !== null && _d !== void 0 ? _d : defaultOnDragOver;
    // This is the numberr of pixels that a draggable element has to be dragged before the sorting kicks in.
    const distance = (_e = props.distance) !== null && _e !== void 0 ? _e : SORT_CONTAINER_DRAG_DISTANCE;
    const onLayoutUpdate = (_f = props.onLayoutUpdate) !== null && _f !== void 0 ? _f : defaultOnLayoutUpdate;
    // This is the stateful variable that holds the active 'scrollLeft' property for the DOM node
    // that's rendered inside of the sort container.
    const [containerScrollLeft, setContainerScrollLeft] = React.useState(0);
    // read the comment in doForceUpdate to understand this.
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const [forceUpdate, setForceUpdate] = React.useState(0);
    // Index of current visible left item
    const [leftItemIndex, setLeftItemIndex] = React.useState(0);
    // Width of fake right (last) item to align the position of the left item
    const [fakeItemWidth, setFakeItemWidth] = React.useState(0);
    const containerRef = React.useRef(null);
    // An object with all of the measurements that we'll need to set chevron visibility.
    // If you don't understand client vs offset vs scrollWidth/Left,
    // go check out this image https://i.stack.imgur.com/Cl1IA.png
    // Here's an interactive version too: https://jsfiddle.net/y8Y32/25/
    const containerMeasurements = containerRef.current
        ? {
            clientLeft: containerRef.current.clientLeft,
            clientWidth: containerRef.current.clientWidth,
            offsetLeft: containerRef.current.offsetLeft,
            offsetWidth: containerRef.current.offsetWidth,
            // not using the actual container's scrollLeft because it may not have been set yet.
            // this uses the scrollLeft we have set in state.
            scrollLeft: containerScrollLeft,
            scrollWidth: containerRef.current.scrollWidth,
        }
        : DEFAULT_CONTAINER_MEASUREMENTS;
    const chevronScrollDistance = (_g = props.chevronScrollDistance) !== null && _g !== void 0 ? _g : DEFAULT_CHEVRON_SCROLL_DISTANCE;
    // Whether the elements inside of the DND container have overflowed the clientWidth.
    const isOverflowPresent = 
    // Explicitly made this !== 0 instead of just checking whether clientWidth was truthy
    // so it was clear why this check was here. If it's not clear,
    // if the clientWidth of the containerRef is 0, we should not have any chevrons. Ever.
    // A client width of 0 indicates that the Ref's children haven't fully rendered.
    containerMeasurements.clientWidth !== 0 &&
        // On a monitor that's scaled at 100%, when clientWidth and scrollWidth are the same
        // there is no overflow present. When monitors are NOT scaled at 100%,
        // there's a chance that these two numbers are off.
        Math.abs(containerMeasurements.clientWidth - containerMeasurements.scrollWidth) > 0;
    const showChevrons = allowHorizontalScroll && isOverflowPresent;
    // disabled === 'grayed out' and onClick won't fire. When you've scrolled all the way to the left,
    // the left chevron is disabled. If you've scrolled all the way to the right, the right chevron is disabled
    const leftChevronDisabled = !!(showChevrons && containerScrollLeft === 0);
    const rightChevronDisabled = !!(showChevrons &&
        /**
         * This is a calculation that figures out where the right edge of the current viewport is.
         * It's a little tricky to imagine. So look at this diagram.
            +-------------------------------------------scrollWidth-------------------------------------------+
                                 |----------------------------------------------------|
                                 |													  |
            +-------------------------------------------------------------------------------------------------+
            |(0)                 |(200)                                               |(200)                  |(300)
            |                    |                                                    |                       |
            |                    |                                                    |                       |
            |                    |                                                    |                       |
            |                    |                                                    |                       |
            |                    |                                                    |                       |
            |                    |                                                    |                       |
            |                    |                                                    |                       |
            |                    |                                                    |                       |
            |                    |                                                    |                       |
            |                    |                                                    |                       |
            |                    |                                                    | (A)                   |
            +-------------------------------------------------------------------------------------------------+
                                 | 													  |
                                 +----------------------clientWidth-------------------+
                                 ^                                                    ^
                                 |                                                    |
                                 |                                                    |
                                 |                                                    |
                                 +                                                    +
                                 scrollLeft = 100                                    scrollRight = 200

         * The numbers in parens are the numbers for the 'scroll box'. We're trying to figure out where the point labeled (A) is located.
         *
         * Walking through the calculation, we take the scrollWidth and subtract the scrollLeft. This gives us the right edge of the scroll box.
         * offsetWidth is effectively the right edge of the element. If the right edge of the scroll box and the right edge of the element are the same number,
         * then we cannot scroll any further, and the chevron should be disabled.
         */
        containerMeasurements.scrollWidth - containerScrollLeft - containerMeasurements.offsetWidth === 0);
    const scrollFunctionState = {
        containerScrollLeft,
        containerMeasurements,
    };
    const scrollFunctionStateSetters = {
        setScrollLeft: setContainerScrollLeft,
    };
    /**
     * Left Chevron's onClickHandler
     */
    const scrollToLeft = () => {
        var _a, _b;
        if (leftChevronDisabled)
            return;
        let visibleLeftItem = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.childNodes[leftItemIndex - 1];
        if (visibleLeftItem) {
            let calculatedFakeItemWidth = scrollHorizontally((_b = visibleLeftItem === null || visibleLeftItem === void 0 ? void 0 : visibleLeftItem.clientWidth) !== null && _b !== void 0 ? _b : chevronScrollDistance, scrollFunctionState, scrollFunctionStateSetters, 1);
            setFakeItemWidth(calculatedFakeItemWidth);
            setLeftItemIndex(leftItemIndex - 1);
        }
    };
    /**
     * Right Chevron's onClickHandler
     */
    const scrollToRight = () => {
        var _a, _b;
        if (rightChevronDisabled)
            return;
        let visibleLeftItem = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.childNodes[leftItemIndex];
        if (visibleLeftItem) {
            let calculatedFakeItemWidth = scrollHorizontally((_b = visibleLeftItem === null || visibleLeftItem === void 0 ? void 0 : visibleLeftItem.clientWidth) !== null && _b !== void 0 ? _b : chevronScrollDistance, scrollFunctionState, scrollFunctionStateSetters, -1);
            setFakeItemWidth(calculatedFakeItemWidth);
            setLeftItemIndex(leftItemIndex + 1);
        }
    };
    /**
     * "wheel" handler. scrolls forward or backwards
     * @param event
     */
    const scrollme = (event) => {
        if (!event.nativeEvent.deltaY)
            return;
        // do scroll only if the container is overflowed
        if (isOverflowPresent) {
            event.nativeEvent.deltaY < 0 ? scrollToLeft() : scrollToRight();
        }
    };
    const doDOMManipulation = () => {
        if (props.allowHorizontalScroll && containerRef.current)
            containerRef.current.scrollLeft = containerScrollLeft;
    };
    // This function increments `forceUpdate`, which is a piece of state that's irrelevant to the
    // rendering of this component. After a resize, we need to re-measure the DOM so we can figure out if
    // the chevrons are necessary. Because no state changes on a resize. the component doesn't rerender.
    // This function will change state, which will cause a re-rendering, and the component will be re-measured.
    const doForceUpdate = () => {
        setForceUpdate((currentValueOfForceUpdate) => currentValueOfForceUpdate + 1);
    };
    // This function is needed for correct positioning after resizing the window.
    const scrollToInitialState = () => {
        const calculatedFakeItemWidth = scrollHorizontally(containerScrollLeft, scrollFunctionState, scrollFunctionStateSetters, 1);
        setFakeItemWidth(calculatedFakeItemWidth);
        setLeftItemIndex(0);
        setContainerScrollLeft(0);
    };
    // After every render, set the actual DOM element's scrollLeft to be whatever we calculated during the previous render.
    React.useLayoutEffect(doDOMManipulation);
    // after the children change, we need to force an update so that we can re-measure the DOM after the render happens
    // without this, adding and removing elements from the DNDContext won't recalculate whether the chevrons should render
    React.useEffect(doForceUpdate, [props.children]);
    // Whenever the window resizes, force an update so that the DOM is re-measured.
    React.useEffect(addResizeHandler(doForceUpdate), []);
    // Whenever the window resizes, scroll widgets to the left.
    React.useEffect(addResizeHandler(scrollToInitialState), []);
    /**
     * Actual container that provides drag and drop functionality
     */
    const SortContainer = SortableContainer((sortableContainerProps) => (React.createElement("div", { onWheel: scrollme, ref: containerRef, className: props.className },
        sortableContainerProps.children,
        React.createElement("div", { style: { width: `${fakeItemWidth}px`, flexShrink: 0 } }))));
    // After every render, call onLayoutUpdate function
    React.useLayoutEffect(() => {
        onLayoutUpdate();
    }, [SortContainer]);
    // The external lib `react-sortable-hoc` which we use to animate Drag&Drop is not compatible with React 18.
    // It adds the copy of dragging item to the `document.body` and sometimes (KB52318) doesn't remove it after drop.
    // The function below is a workaround. It removes extra items if there are any.
    const removeExtraItemsAfterDnD = (elementClassName) => {
        // We are looking for extra elements that are directly in the body
        const elements = document.body.querySelectorAll(`:scope > .${elementClassName}`);
        elements === null || elements === void 0 ? void 0 : elements.forEach((element) => {
            var _a;
            (_a = element.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(element);
        });
    };
    return (React.createElement("div", { id: props.id },
        showChevrons && (React.createElement(ScrollingChevron, { isDisabled: leftChevronDisabled, chevronDirection: ChevronDirection.left, showToolTip: props.showScrollButtonsTooltip, title: props.leftButtonTooltip, scrollFunction: scrollToLeft })),
        React.createElement(SortContainer, { onSortEnd: (e) => {
                var _a, _b;
                onDragEnd(e);
                // Since the component is used in several places, the search and removal of elements is carried out by the class of the first child.
                removeExtraItemsAfterDnD((_b = (_a = e === null || e === void 0 ? void 0 : e.nodes[0]) === null || _a === void 0 ? void 0 : _a.node) === null || _b === void 0 ? void 0 : _b.getAttribute("class"));
            }, onSortStart: onDragBegin, onSortMove: onDragMove, onSortOver: onDragOver, distance: distance, axis: (_h = props.axis) !== null && _h !== void 0 ? _h : AllowableDnDAxis.y, lockAxis: props.lockAxis }, props.children),
        showChevrons && (React.createElement(ScrollingChevron, { isDisabled: rightChevronDisabled, chevronDirection: ChevronDirection.right, showToolTip: props.showScrollButtonsTooltip, title: props.rightButtonTooltip, scrollFunction: scrollToRight }))));
};
//# sourceMappingURL=FinsembleDnDContext.js.map