import _slicedToArray from '@babel/runtime/helpers/slicedToArray';
import 'core-js/modules/es.array.concat.js';
import 'core-js/modules/es.array.includes.js';
import 'core-js/modules/es.array.iterator.js';
import 'core-js/modules/es.object.assign.js';
import 'core-js/modules/es.object.to-string.js';
import 'core-js/modules/es.set.js';
import 'core-js/modules/es.string.includes.js';
import 'core-js/modules/es.string.iterator.js';
import 'core-js/modules/web.dom-collections.for-each.js';
import 'core-js/modules/web.dom-collections.iterator.js';
import React__default, { useState, useRef, useCallback, useEffect } from 'react';
import { settings } from '../../../constants/Settings.js';
import { tableTraverser } from '../tableUtilities.js';
import { TableDropRowOverlay } from './TableBodyRow/TableDropRowOverlay.js';
import { TableDragAvatar } from './TableDragAvatar.js';

var prefix = settings.prefix,
  iotPrefix = settings.iotPrefix;

/**
 * This is similar to `useState` but pairs the value with a ref to the value. The benefit of this is
 * that callback functions do not need to include refs in their dependency lists, but can access
 * their most recent values safely.
 *
 * This is important for some data used in manually added event listeners, like for "mousemove". If
 * listeners depend on normal state, they must be removed and added frequently to create a closure
 * around the new state. But by using a ref they only need to close around the mutable ref, then use
 * that to access the value they needs. For drag and drop, we need to ensure there are no
 * dependencies that would cause the `setupDnd` effect to tear down and set up repeatedly since they
 * will cause visual and performance problems.
 *
 * Note that this still returns a stateful value along with the ref. That normal state value should
 * be used for any traditional reactive updates.
 *
 * @param {any} initialValue Initial value to set. Omit to start with `undefined`.
 * @returns 3 item tuple of: reference to the value, the stateful value, and function to update both
 * of them. The setter does trigger a state update and potential rerender.
 */
function useRefAndState(initialValue) {
  var _useState = useState(initialValue),
    _useState2 = _slicedToArray(_useState, 2),
    value = _useState2[0],
    setValue = _useState2[1];
  var ref = useRef(initialValue);
  var setter = useCallback(function (newValue) {
    ref.current = newValue;
    setValue(newValue);
  }, []);
  return [ref, value, setter];
}
/**
 * If there is a vertical scroll bar on the document and in RTL mode then this returns its width.
 * Otherwise, returns 0.
 *
 * In RTL `getBoundingClientRect` is relative to the window edge, but pos:abs elements are relative
 * to the left-side, vertical scrollbar (if there is one). That means the positioned elements might
 * be shifted over by the vertical scrollbar width. This gets the width if relevant so it can be
 * subtracted out in some values.
 */

function getRtlVerticalScrollbarWidth() {
  /* istanbul ignore next */ // ignore rlt
  return document.dir === 'rtl' ? window.innerWidth - document.documentElement.clientWidth : 0;
}

/**
 * Event handlers to mix into the table and state variables about the current drag and drop
 * operation.
 *
 * @typedef {Object} UseTableDndResult
 * @property {boolean} isDragging If a drag is in progress.
 * @property {ReactNode} dragPreview A element that must be rendered as a child of the page body.
 * This is the image that's shown during a drag and follows the mouse.
 * @property {string} activeDropRowId The row a drag is hovering over and might drop on.
 * @property {Set<string>} canDropRowIds The row IDs that can be dropped on. These were given by the
 * caller from the `onDrag` callback.
 * @property {string[]} dragRowIds The rows that are being dragged.
 * @property {(MouseEvent) => void} handleEnterRow - Event handler that must be added as a
 * "mouseenter" listener to the table rows.
 * @property {(MouseEvent) => void} handleLeaveRow Event handler that must be added as a
 * "mouseleave" listener to the table rows.
 * @property {(MouseEvent) => void} handleStartPossibleDrag Event handler that must be added as a
 * "mousedown" listener on the all drag handles of the table rows.
 */

/**
 * Sets up a PAL table for drag and drop support.
 *
 * This is is hook purely to move this code into its own file. This is coupled with `TableBody` and
 * only makes sense using it there.
 *
 * @param {object[]} rows The rows of the table we're dragging in.
 * @param {string[]} selectedIds The currently selected row IDs or empty array.
 * @param {number | undefined} Optional minimum z-index for the drag image. Required if the table is
 * in a modal with a >0 z-index.
 * @param {(object[]) -> {preview: ReactNode, dropIds: string[]}} onDrag Callback fires when a drag
 * actually start. This passes the the table rows being dragged. It must return a tuple of a
 * ReactNode to show as the drag image and a list of row IDs that can accept a drop from the given
 * dragged row ID.
 * @param {(string[], string) => void} onDrop Callback when a drop succeeds. Passes the IDs of the
 * rows being dragged and the ID of the row being dropped on.
 * @returns {UseTableDndResult} Values to mix into the table.
 */
function useTableDnd(rows, selectedIds, zIndex, onDrag, onDrop, hasBreadcrumbDrop) {
  // These are related. When the user "mousedown"s on a drag handle, we consider it a "possible"
  // drag. At this point we add all the event listeners to track the drag. Put only after they move
  // past some threshold do we actually set `isDragging`. At that point the drag is "real" and we'll
  // fire the onDrag callback and draw the drag image.
  var _useState3 = useState(false),
    _useState4 = _slicedToArray(_useState3, 2),
    isPossibleDrag = _useState4[0],
    setIsPossibleDrag = _useState4[1];
  var _useRefAndState = useRefAndState(false),
    _useRefAndState2 = _slicedToArray(_useRefAndState, 3),
    isDraggingRef = _useRefAndState2[0],
    isDragging = _useRefAndState2[1],
    setIsDragging = _useRefAndState2[2];

  // The row being dragged
  var _useRefAndState3 = useRefAndState([]),
    _useRefAndState4 = _slicedToArray(_useRefAndState3, 3),
    dragRowIdsRef = _useRefAndState4[0],
    dragRowIds = _useRefAndState4[1],
    setDragRowIds = _useRefAndState4[2];

  // The rows that CAN be dropped on, but we're not necessarily over them right now. The caller
  // provides these from the onDrag callback.
  var _useState5 = useState(new Set()),
    _useState6 = _slicedToArray(_useState5, 2),
    canDropRowIds = _useState6[0],
    setCanDropRowIds = _useState6[1];

  // The drag preview we show under the cursors when dragging. The caller constructs this for use in
  // the onDrag callback.
  var _useState7 = useState(null),
    _useState8 = _slicedToArray(_useState7, 2),
    dragPreview = _useState8[0],
    setDragPreview = _useState8[1];
  var avatarRef = useRef(null);

  // The row ID we're hovering over during a drag. Only if it accepts a drop. Note there is a state
  // and ref for the same value. See setActiveDropRowId.
  var activeDropRowIdRef = useRef(null);

  //  The coords where a DnD starts from. Not a state change, just used later in response to mousemove.
  var dragStartCoordsRef = useRef();

  /**
   * After a failed drag (canceled or not over a good row) this animates the avatar back into its
   * original position to show that the drop did not succeed.
   */
  function snapBackAvatar() {
    if (avatarRef.current) {
      /* istanbul ignore next */
      avatarRef.current.ontransitionend = function () {
        return setDragPreview(null);
      };
      avatarRef.current.style.transition = 'transform 200ms';
      avatarRef.current.style.transform = "translate(".concat(document.documentElement.scrollLeft, "px, ").concat(document.documentElement.scrollTop, "px)");
    }
  }
  var handleDrop = useCallback(
  /**
   * Called when a drop occurs. Ends the drag operations and fires the `onDrop` callback if over a
   * row that can be dropped on.
   */
  function handleDrop(e) {
    e.stopPropagation();
    if (activeDropRowIdRef.current == null) {
      snapBackAvatar();
    } else {
      // Notify of success. Protect from caller errors.
      onDrop(dragRowIds, activeDropRowIdRef.current);
      setDragPreview(null);
    }
    setIsPossibleDrag(false);
  }, [activeDropRowIdRef, onDrop, dragRowIds]);
  var badMoveCountRef = useRef(0);
  var handleDragMove = useCallback(
  /**
   * Event handler when the mouse moves during a drag. This actually starts the real drag once the
   * mouse has moved past a threshold. Throughout the drag, updates the drag avatar position. This
   * does not track what row we're over--`handleEnterRow` know that from onmouseenter events.
   *
   * @param {React.MouseEvent} e
   */
  function handleDragMove(e) {
    if (e.buttons === 0) {
      badMoveCountRef.current += 1;
      if (badMoveCountRef.current >= 10) {
        // If the mouse goes outside the window and releases the button then the mouseup is missed
        // and the drag is stuck. If we see a few moves without a mouse button down then that's
        // probably the case. We don't do this on the first bad move since Safari and Firefox on
        // macOS can fire a mousemove without buttons BEFORE a mouse up sometimes, which would
        // cancel the drop too early. Waiting a few means this really is stuck, not just an
        // unexpected event order.
        setIsPossibleDrag(false);
      }
    }
    var _dragStartCoordsRef$c = dragStartCoordsRef.current,
      startX = _dragStartCoordsRef$c.startX,
      startY = _dragStartCoordsRef$c.startY;
    var diffX = e.clientX - startX + document.documentElement.scrollLeft;
    var diffY = e.clientY - startY + document.documentElement.scrollTop;
    if (!isDraggingRef.current && Math.abs(diffX) < 5 && Math.abs(diffY) < 5) {
      // Reach minimum drag distance before showing avatar and really dragging.
      return;
    }
    if (!isDraggingRef.current) {
      // Convert from IDs to the real rows, even children
      var draggedRows = [];
      tableTraverser(rows, function (rowOrChildRow) {
        if (dragRowIdsRef.current.includes(rowOrChildRow.id)) {
          draggedRows.push(rowOrChildRow);
        }
      });

      /* istanbul igore else */
      if (draggedRows.length) {
        // Notify of a drag starting and get the details about what this row does.
        var _onDrag = onDrag(draggedRows),
          preview = _onDrag.preview,
          dropIds = _onDrag.dropIds;
        setDragPreview(preview);
        setCanDropRowIds(new Set(dropIds));
      }
    }

    // We've reach min drag threshold, so we're really dragging now. This will show the avatar and
    // add onenter listeners to rows.
    setIsDragging(true);

    // clientX is *always* relative to the top, left, so we can't just use logical properties. In
    // RTL mode we want to position relative to the right side explicitly, which means the
    // difference between the right vieport edge (doc width) and clientX.
    /* istanbul ignore next */
    var _ref = document.dir === 'rtl' ? {
        right: "calc(".concat(document.documentElement.offsetWidth - startX + getRtlVerticalScrollbarWidth(), "px - 1.5rem)"),
        left: 'auto'
      } : {
        left: "calc(".concat(startX, "px - 1.5rem)"),
        right: 'auto'
      },
      left = _ref.left,
      right = _ref.right;
    if (avatarRef.current) {
      // Update the style directly on the DOM node (not via React) since this is so much faster and
      // we're doing this very frequently.
      Object.assign(avatarRef.current.style, {
        display: '',
        top: "calc(".concat(startY, "px - 1.4rem)"),
        // nudge over a bit, so the cursor is on the tile, not in the very corner.
        left: left,
        right: right,
        transform: "translate(".concat(diffX, "px, ").concat(diffY, "px)")
      });
    }
  }, [isDraggingRef, setIsDragging, rows, dragRowIdsRef, onDrag]);
  var cancel = useCallback(
  /**
   * Cancels the drag. Tears down the events and snaps back avatar.
   * @param {[event]} e Optional event to stop. Mostly for eating the escape key.
   */
  function cancel(e) {
    if (e) e.stopPropagation();
    snapBackAvatar();
    setIsPossibleDrag(false);
  }, []);

  /** Ref to the root DOM element of the row overlay. Used to directly update its style. */
  var overlayRef = useRef(null);
  useEffect(
  /**
   * Once a drag appears to start (user "mousedown"s on a drag handle) this adds the needed event
   * listeners and classes to track the drag. This is the main setup method. We don't technically
   * consider it a real DnD until the mouse has moved a certain threshold though. In that way, we
   * don't set show a drag image (avatar) or update any rows if the user were to just click the
   * drag handle--they need to really drag it a bit.
   *
   * The main dependency of this is `isPossibleDrag`, which is intended to setup the drag events
   * when true, and tear them down when false. The others are just dependencies and not intended
   * to fire this effect per set. In fact, we don't want the other to change while
   * `isPossibleDrag` is true or the drag can be interrupted before it's done. While we can't
   * control what props the caller actually changes, things like `rows` and `onDrop` are unlikely
   * to change during a drag.
   */
  function setupDnd() {
    if (!isPossibleDrag) {
      // Don't connect anything. Everything is torn down in the cleanup from when this this effect
      // was last called.
      cancel();
      return undefined;
    }
    badMoveCountRef.current = 0;
    function handleEscapeKey(e) {
      if (e.key === 'Escape') {
        cancel(e);
      }
    }

    /**
     * Callback when item drag mouse enters li of overflow menu
     * @param {React.MouseEvent} e
     */
    function handleOverflowLiEnter(e) {
      var node = e.currentTarget;
      var hyperLink = node.querySelector(".".concat(prefix, "--overflow-menu-options__btn"));
      hyperLink.classList.add("".concat(prefix, "--breadcrumbmenu--on--row--dropping"));
      activeDropRowIdRef.current = node;
    }

    /**
     * Callback when item drag mouse leaves li of overflow menu
     * @param {React.MouseEvent} e
     */
    function handleOverflowLiLeave(e) {
      var node = e.currentTarget;
      if (activeDropRowIdRef.current === node) {
        activeDropRowIdRef.current = null;
      }
      var hyperLink = node.querySelector(".".concat(prefix, "--overflow-menu-options__btn"));
      if (hyperLink) hyperLink.classList.remove("".concat(prefix, "--breadcrumbmenu--on--row--dropping"));
    }

    /**
     * Callback when the mouse enters a table row. The table needs to add this as an onmouseenter
     * handler.
     * @param {React.MouseEvent} e
     * @returns
     */
    function handleEnterNode(e) {
      /* istanbul ignore if */
      if (!isPossibleDrag) {
        // shouldn't happen
        return;
      }
      var node = e.currentTarget;
      // To have breadcrumb leaf node as disabled by specfying isCurrentPage prop which adds the below class
      // and for the same page drop shouldn't be allowed hence the below condition
      if (!node.classList.contains("".concat(prefix, "--breadcrumb-item--current"))) {
        var link = node.querySelector(".".concat(prefix, "--link"));
        var linkRect = link.getBoundingClientRect();
        // There is another way to have breadcrumb leaf node as disabled by not setting the href,
        // in any case it should be a enabled hyperlink to consider as drop target
        // istanbul ignore else
        if (link.nodeType === Node.ELEMENT_NODE) {
          var verticalPadding = 5;
          var horizontalPadding = 4;
          var style = {
            display: 'block',
            top: "".concat(linkRect.top - verticalPadding, "px"),
            left: "".concat(linkRect.left - horizontalPadding, "px"),
            height: "".concat(linkRect.height + 2 * verticalPadding, "px"),
            width: "".concat(linkRect.width + 2 * horizontalPadding, "px"),
            zIndex: -1
          };
          // istanbul ignore else
          if (overlayRef.current) {
            Object.assign(overlayRef.current.style, style);
            overlayRef.current.classList.add("".concat(iotPrefix, "--breadcrumb-drop-node-overlay"));
          }
          link.classList.add("".concat(prefix, "--row--on--link--dropping"));
          activeDropRowIdRef.current = node;
        }
      }
    }

    /**
     *  Callback when the mouse leaves a breadcrumb node. Removes the class added on mouse enter
     * @param {React.MouseEvent} e
     */
    function handleLeaveNode(e) {
      var node = e.currentTarget;
      /* istanbul ignore else */
      if (activeDropRowIdRef.current === node) {
        activeDropRowIdRef.current = null;
        var link = node.querySelector(".".concat(prefix, "--link"));
        link.classList.remove("".concat(prefix, "--row--on--link--dropping"));
        /* istanbul ignore else */
        if (overlayRef.current) {
          overlayRef.current.classList.remove("".concat(iotPrefix, "--breadcrumb-drop-node-overlay"));
          overlayRef.current.style.display = 'none';
        }
      }
    }

    /**
     * Attaches event listeners to list nodes of overflow menu items of breadcrumb
     */
    function handleMenuItemEnter() {
      var items = document.body.querySelectorAll(".breadcrumb--overflow-items");
      /* istanbul ignore else */
      if (items && items.length > 0) {
        var listItems = items[0].querySelectorAll(".".concat(prefix, "--overflow-menu-options__option"));
        listItems.forEach(function (li) {
          li.addEventListener('mouseenter', handleOverflowLiEnter);
          li.addEventListener('mouseleave', handleOverflowLiLeave);
        });
      }
    }

    /**
     * Removes event listeners from list nodes of overflow menu items of breadcrumb
     */
    function handleMenuItemLeave() {
      var items = document.body.querySelectorAll(".breadcrumb--overflow-items");
      /* istanbul ignore else */
      if (items && items.length > 0) {
        var listItems = items[0].querySelectorAll(".".concat(prefix, "--overflow-menu-options__option"));
        listItems.forEach(function (li) {
          li.removeEventListener('mouseenter', handleOverflowLiEnter);
          li.removeEventListener('mouseleave', handleOverflowLiLeave);
        });
      }
    }

    /**
     * Finds and attaches event listeners to all kind of possible breadcrumb nodes upon drag start setUpDnd is invoked
     */
    function addBreadcrumbEventListners() {
      // Nodes which are visible are selected using common class name
      var elements = document.querySelectorAll(".".concat(prefix, "--breadcrumb-item"));
      elements.forEach(function (element) {
        element.addEventListener('mouseenter', handleEnterNode);
        element.addEventListener('mouseleave', handleLeaveNode);
      });

      // Detects if overflow menu containing hidden nodes is open or not
      var elementExist = !!document.querySelectorAll(".breadcrumb--overflow-items");
      // istanbul ignore else
      if (elementExist) {
        handleMenuItemEnter();
      }
    }

    /**
     * Removes all the event listeners attached to breadcrumb nodes
     */
    function removeBreadcrumbEventListeners() {
      var elements = document.querySelectorAll(".".concat(prefix, "--breadcrumb-item"));
      elements.forEach(function (element) {
        element.removeEventListener('mouseenter', handleEnterNode);
        element.removeEventListener('mouseleave', handleLeaveNode);
      });
      var elementExist = !!document.querySelectorAll(".breadcrumb--overflow-items");
      // istanbul ignore else
      if (elementExist) {
        handleMenuItemLeave();
      }
    }

    /**
     * Stops an event. This is used to stop click event during drag and drop, otherwise the
     * mouseup can trigger a click on the row and the row click handler will be invoked. For
     * expandable rows, this might expand or collapse the row. We add this to the event capture
     * phase to intercept the click before the row can see it.
     * @param {event} e DOM event.
     */
    function stopEvent(e) {
      e.preventDefault();
      e.stopPropagation();
    }
    document.body.addEventListener('click', stopEvent, true);
    document.body.addEventListener('mouseup', handleDrop);
    document.body.addEventListener('mousemove', handleDragMove);
    document.body.addEventListener('keydown', handleEscapeKey);
    // istanbul ignore else
    if (hasBreadcrumbDrop) {
      addBreadcrumbEventListners();
    }

    // If the user goes to another window, cancel the dnd.
    window.addEventListener('blur', cancel);

    // Add global drag state class. This is mostly use to ensure the cursor is "grabbing", but can
    // be used to disable non-drag related hover states. We add it here even though the cursor
    // hasn't moved enough to start a real drag so the user can see a drag can be started from the
    // cursor.
    document.body.classList.add("".concat(iotPrefix, "--is-dragging"));
    return function tearDown() {
      document.body.removeEventListener('click', stopEvent, true);
      document.body.removeEventListener('mouseup', handleDrop);
      document.body.removeEventListener('mousemove', handleDragMove);
      document.body.removeEventListener('keydown', handleEscapeKey);
      // istanbul ignore else
      if (hasBreadcrumbDrop) {
        removeBreadcrumbEventListeners();
        // removes style override of hyperlink on drop which was added on drag start
        // istanbul ignore else
        if (activeDropRowIdRef.current && activeDropRowIdRef.current.children) activeDropRowIdRef.current.children[0].classList.remove("".concat(prefix, "--row--on--link--dropping"));
      }
      window.removeEventListener('blur', cancel);
      document.body.classList.remove("".concat(iotPrefix, "--is-dragging"));
      setDragRowIds([]);
      activeDropRowIdRef.current = null;
      setCanDropRowIds(new Set());
      setIsDragging(false);
    };
  }, [isPossibleDrag, handleDragMove, handleDrop, cancel, setIsDragging, setDragRowIds, hasBreadcrumbDrop]);
  var handleStartPossibleDrag = useCallback(
  /**
   * This is the callback when a user starts a possible drag with a mousedown on a drag handle. At
   * this point we'll set up event listeners to track the mouse movement and decide if it's a real
   * DnD or not. Until it moves past a threshold we don't show a drag image or change the table
   * props yet.
   */
  function handleStartPossibleDrag(e, rowId) {
    dragStartCoordsRef.current = {
      startX: e.clientX,
      startY: e.clientY
    };
    if (selectedIds.includes(rowId)) {
      // we're in the selected set, so drag all selected rows.
      setDragRowIds(selectedIds);
    } else {
      // just drag this one
      setDragRowIds([rowId]);
    }
    setIsPossibleDrag(true);
  }, [setDragRowIds, selectedIds]);
  var handleEnterRow = useCallback(
  /**
   *  Callback when the mouse enters a table row. The table needs to add this as an onmouseenter
   *  handler.
   * @param {string} rowId Row mouse is over.
   * @returns
   */
  function handleEnterRow(rowId, rowEl) {
    /* istanbul ignore if */
    if (!isPossibleDrag) {
      // shouldn't happen
      return;
    }

    // The table can scroll in it container. In that case, the row overlay should not span the
    // entire row width, only as wide as the container.
    var scrollContainer = rowEl.closest(".".concat(prefix, "--data-table-content"));
    var contentRect = scrollContainer.getBoundingClientRect();
    var rowRect = rowEl.getBoundingClientRect();
    var style = {
      display: 'block',
      top: "".concat(rowRect.top + document.documentElement.scrollTop, "px"),
      left: "".concat(contentRect.left + document.documentElement.scrollLeft - getRtlVerticalScrollbarWidth(), "px"),
      width: "".concat(contentRect.width, "px"),
      height: "".concat(rowRect.height, "px"),
      zIndex: zIndex + 1000
    };

    // istanbul ignore else
    if (overlayRef.current) Object.assign(overlayRef.current.style, style);
    rowEl.classList.add("".concat(iotPrefix, "--table__row--dropping"));
    activeDropRowIdRef.current = rowId;
  }, [isPossibleDrag, zIndex]);
  var handleLeaveRow = useCallback(
  /**
   *  Callback when the mouse leaves a table row. The table needs to add this as an onmouseenter
   *  handler.
   * @param {string} rowId A row we are no longer over.
   */
  function handleLeaveRow(rowId, rowEl) {
    /* istanbul ignore else */
    if (activeDropRowIdRef.current === rowId) {
      activeDropRowIdRef.current = null;
      // istanbul ignore else
      if (overlayRef.current) overlayRef.current.style.display = 'none';
      rowEl.classList.remove("".concat(iotPrefix, "--table__row--dropping"));
    }
  }, []);

  // During a drag we show an avatar near the cursor and an overlay over the hovered row, if there
  // is one. These needs to be added to the body element by the caller.
  var dragPreviewAndOverlay = /*#__PURE__*/React__default.createElement(React__default.Fragment, null, isDragging && /*#__PURE__*/React__default.createElement(TableDropRowOverlay, {
    ref: overlayRef
  }), dragPreview &&
  /*#__PURE__*/
  // For breadcrumb overflow menu has zindex of 6000 hence to show the drag above it given
  // value 6001 when breadcrumb drop is enabled.
  React__default.createElement(TableDragAvatar, {
    zIndex: zIndex + hasBreadcrumbDrop ? 6001 : 1001,
    ref: avatarRef
  }, dragPreview));
  return {
    isDragging: isDragging,
    dragPreview: dragPreviewAndOverlay,
    canDropRowIds: canDropRowIds,
    dragRowIds: dragRowIds,
    handleEnterRow: handleEnterRow,
    handleLeaveRow: handleLeaveRow,
    handleStartPossibleDrag: handleStartPossibleDrag
  };
}

export { useTableDnd };
