var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { MapToComponentMethod, VegaSlimmer } from 'vega-slimmer/core';
import InternalVegaZIndexManager from '../zindex/internal-vega-z-index-manager';
/**
 * Makes the direct children of a container element sortable via pointer events.
 * This works across shadow DOM boundaries because pointer events are composed.
 */
export class DraggableElementSlimmer extends VegaSlimmer {
  constructor(entry) {
    super();
    this.activePointerId = null;
    this.activeItem = null;
    this.isDragging = false;
    this.startX = 0;
    this.startY = 0;
    this.dragFromIndex = null;
    this.dragToIndex = null;
    this.currentOverItem = null;
    this.currentOverIndex = null;
    this.activeItemHeight = 0;
    this.cachedItems = [];
    this.cachedItemDraggableMap = new WeakMap();
    /**
     * Handles the `pointerdown` event. Validates that the event originated from the trigger element
     * and initiates a drag session for the matched item.
     *
     * @param {PointerEvent} e - The pointer event fired on the container.
     */
    this.onPointerDown = (e) => {
      // check left mouse button only
      if (e.button !== 0)
        return;
      const item = this.getItemFromComposedPath(e, true);
      if (item) {
        this.startDrag(item, e);
        e.preventDefault();
      }
    };
    /**
     * Handles the `pointermove` event. Once the drag threshold is crossed, updates the dragged
     * element's visual position and recalculates the current drop target.
     *
     * @param {PointerEvent} e - The pointer event fired on the window.
     */
    this.onPointerMove = (e) => {
      if (!this.isCurrentPointEvent(e) || !this.hasCrossedDragThreshold(e))
        return;
      this.beginDragging();
      this.syncActiveVisual(e);
      this.updateDropTarget(e);
      this.emitDrag();
      e.preventDefault();
    };
    /**
     * Handles the `pointerup` event. Finalizes the drag and fires the reorder callback
     * if the item was released at a different position.
     *
     * @param {PointerEvent} e - The pointer event fired on the window.
     */
    this.onPointerUp = (e) => {
      if (!this.isCurrentPointEvent(e))
        return;
      this.finishDrag();
    };
    /**
     * Handles the `pointercancel` event. Aborts the active drag session and restores all
     * inline styles to their pre-drag state. Only responds to the active pointer.
     *
     * @param {PointerEvent} e - The pointer event fired on the window.
     */
    this.onPointerCancel = (e) => {
      if (e.pointerId === this.activePointerId)
        this.resetDragState();
    };
    this.entry = entry;
  }
  /**
   * Re-registers pointer event listeners when the host component is re-attached to the DOM after a prior disconnection.
   */
  connectedCallback() {
    this.init();
  }
  /**
   * Removes all pointer event listeners and clears drag state when the host component is detached from the DOM.
   */
  disconnectedCallback() {
    this.destroy();
  }
  /**
   * Registers pointer event listeners once the host component has finished its initial render.
   */
  componentDidLoad() {
    this.init();
  }
  /**
   * Updates the slimmer's draggable state in response to changes in the host component's `rowDraggable` property.
   */
  watchIsDraggable() {
    this.isDraggable ? this.init() : this.destroy();
  }
  /**
   * Initializes the slimmer by attaching pointer event listeners if `isDraggable` is true.
   */
  init() {
    this.isDraggable && this.listenPointerEvents();
  }
  /**
   * Cleans up the slimmer by removing pointer event listeners and resetting all drag state.
   */
  destroy() {
    this.removePointerListeners();
    this.resetDragState();
  }
  /**
   * Attaches `pointerdown` on the container and `pointermove` / `pointerup` / `pointercancel` on the window.
   */
  listenPointerEvents() {
    const container = this.getContainer();
    if (container) {
      container.addEventListener('pointerdown', this.onPointerDown);
      window.addEventListener('pointermove', this.onPointerMove);
      window.addEventListener('pointerup', this.onPointerUp);
      window.addEventListener('pointercancel', this.onPointerCancel);
    }
  }
  /**
   * Removes all pointer event listeners that were registered by {@link listenPointerEvents}.
   */
  removePointerListeners() {
    const container = this.getContainer();
    if (container)
      container.removeEventListener('pointerdown', this.onPointerDown);
    window.removeEventListener('pointermove', this.onPointerMove);
    window.removeEventListener('pointerup', this.onPointerUp);
    window.removeEventListener('pointercancel', this.onPointerCancel);
  }
  /**
   * Initializes all drag state for the given item. Caches the current item list,
   * records the pointer origin, and captures the item height used for sibling offset calculations.
   *
   * @param {HTMLElement} item - The draggable item element that was grabbed.
   * @param {PointerEvent} e - The originating `pointerdown` event.
   */
  startDrag(item, e) {
    this.activePointerId = e.pointerId;
    this.activeItem = item;
    this.isDragging = false;
    this.startX = e.clientX;
    this.startY = e.clientY;
    this.cacheItems();
    this.dragFromIndex = this.getItems().indexOf(item);
    this.dragToIndex = this.dragFromIndex;
    this.currentOverItem = item;
    this.currentOverIndex = this.dragFromIndex;
    this.activeItemHeight = this.getVisualElement(item).getBoundingClientRect().height;
  }
  /**
   * Marks the interaction as an active drag after threshold crossing.
   */
  beginDragging() {
    if (this.isDragging || this.dragFromIndex === null || !this.activeItem)
      return;
    this.isDragging = true;
    this.onDragDrop('start');
  }
  /**
   * Translates the dragged item's visual element to follow the pointer and lifts it
   * above sibling items by applying `zIndex` and disabling its pointer events.
   *
   * @param {PointerEvent} e - The current `pointermove` event.
   */
  syncActiveVisual(e) {
    if (!this.activeItem)
      return;
    const visualElement = this.getVisualElement(this.activeItem);
    const moveY = e.clientY - this.startY;
    visualElement.style.transform = `translateY(${moveY}px)`;
    visualElement.style.position = 'relative';
    visualElement.style.pointerEvents = 'none';
    visualElement.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.15)';
    InternalVegaZIndexManager.setZIndex(visualElement, 'top');
  }
  /**
   * Resolves the item currently under the pointer and updates `dragToIndex`,
   * then applies slot-shift transforms to all sibling items.
   *
   * @param {PointerEvent} e - The current `pointermove` event.
   */
  updateDropTarget(e) {
    if (this.dragFromIndex === null || !this.activeItem)
      return;
    const overItem = this.getItemFromComposedPath(e, false);
    if (!overItem) {
      this.emitDragLeave(this.currentOverIndex, this.currentOverItem);
      this.currentOverItem = null;
      this.currentOverIndex = null;
      return;
    }
    const overIndex = this.getItemIndex(overItem);
    const resolvedDropIndex = this.getResolvedDropIndex(overItem, e.clientY);
    if (this.currentOverItem !== overItem) {
      this.emitDragLeave(this.currentOverIndex, this.currentOverItem);
      this.currentOverItem = overItem;
      this.currentOverIndex = overIndex;
      this.onDragDrop('enter');
    }
    else {
      this.currentOverIndex = overIndex;
    }
    this.dragToIndex = resolvedDropIndex;
    this.onDragDrop('over');
    this.applyLayoutTransforms();
  }
  /**
   * Completes the drag session. Captures the final `fromIndex` and `toIndex` before
   * resetting state, then fires `onReorder` if the item was moved to a new position.
   */
  finishDrag() {
    var _a, _b, _c;
    const fromIndex = this.dragFromIndex;
    const toIndex = this.dragToIndex;
    const shouldReorder = this.isDragging && fromIndex != null && fromIndex !== toIndex;
    const fromItem = fromIndex != null ? (_a = this.getItems().at(fromIndex)) !== null && _a !== void 0 ? _a : null : null;
    const toItem = this.currentOverItem;
    if (this.isDragging && fromIndex != null && fromItem) {
      this.onDragDrop('drop');
    }
    this.resetDragState();
    if (shouldReorder && fromIndex != null && fromItem) {
      (_c = (_b = this.entry).onDragDrop) === null || _c === void 0 ? void 0 : _c.call(_b, {
        status: 'end',
        options: {
          fromIndex,
          fromItem,
          toIndex,
          toItem,
        },
      });
    }
  }
  /**
   * Clears all inline drag styles applied to cached items and resets every
   * drag-related state field to its initial value.
   */
  resetDragState() {
    // Restore inline styles for all cached items.
    this.cachedItems.forEach((item) => {
      const cachedItem = this.cachedItemDraggableMap.get(item);
      if (!cachedItem)
        return;
      const { visualElement, inlineStyle } = cachedItem;
      if (inlineStyle == null) {
        visualElement.removeAttribute('style');
      }
      else {
        visualElement.setAttribute('style', inlineStyle);
      }
    });
    // Clear any zIndex overrides from the active item.
    if (this.activeItem) {
      const activeVisualElement = this.getVisualElement(this.activeItem);
      InternalVegaZIndexManager.removeZIndex(activeVisualElement, 'top');
    }
    // Reset all drag state fields.
    this.activePointerId = null;
    this.activeItem = null;
    this.isDragging = false;
    this.dragFromIndex = null;
    this.dragToIndex = null;
    this.currentOverItem = null;
    this.currentOverIndex = null;
    this.activeItemHeight = 0;
    this.cachedItemDraggableMap = new WeakMap();
    this.cachedItems = [];
  }
  /**
   * Invokes the container factory function to retrieve the current container element.
   *
   * @returns {Nullable<HTMLElement>} The resolved container element, or `null` if the factory throws.
   */
  getContainer() {
    return this.entry.container();
  }
  /**
   * Performs a live DOM query for all elements matching `itemSelector` within the container.
   *
   * @returns {HTMLElement[]} An array of matched item elements, or an empty array if the container is unavailable.
   */
  queryItems() {
    const container = this.getContainer();
    if (!container)
      return [];
    return Array.from(container.querySelectorAll(this.entry.itemSelector));
  }
  /**
   * Returns the cached item list during an active drag session.
   * Falls back to a live DOM query when no drag is in progress.
   *
   * @returns {HTMLElement[]} The current list of draggable item elements.
   */
  getItems() {
    return this.cachedItems.length > 0 ? this.cachedItems : this.queryItems();
  }
  /**
   * Returns the zero-based index of the given element within the current item list.
   *
   * @param {HTMLElement} target - The element to locate.
   * @returns {number} The index of the element, or `-1` if it is not found.
   */
  getItemIndex(target) {
    return this.getItems().indexOf(target);
  }
  /**
   * Resolves the element that should receive drag-related inline styles for a given item.
   * Checks the item's shadow root first, then its light DOM children, then falls back to the item itself.
   *
   * @param {HTMLElement} item - The draggable item element.
   * @returns {HTMLElement} The visual target element for style application.
   */
  getVisualElement(item) {
    const cachedItem = this.cachedItemDraggableMap.get(item);
    if (cachedItem)
      return cachedItem.visualElement;
    return this.resolveVisualElement(item);
  }
  /**
   * Resolves the element that should receive drag-related inline styles for a given item.
   * Checks the item's shadow root first, then its light DOM children, then falls back to the item itself.
   *
   * @param {HTMLElement} item - The draggable item element.
   * @returns {HTMLElement} The visual target element for style application.
   */
  resolveVisualElement(item) {
    var _a, _b;
    const visualElementSelector = this.entry.visualElementSelector;
    if (!visualElementSelector)
      return item;
    const shadowElement = (_a = item.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(visualElementSelector);
    if (shadowElement)
      return shadowElement;
    return (_b = item.querySelector(visualElementSelector)) !== null && _b !== void 0 ? _b : item;
  }
  /**
   * Caches each draggable item using the item as the map key and stores its resolved visual element
   * together with the visual element's original inline style for precise restoration after dragging.
   */
  cacheItems() {
    this.cachedItemDraggableMap = new WeakMap();
    this.cachedItems = this.queryItems();
    this.cachedItems.forEach((cachedItem) => {
      const visualElement = this.resolveVisualElement(cachedItem);
      this.cachedItemDraggableMap.set(cachedItem, {
        visualElement,
        inlineStyle: visualElement.getAttribute('style'),
      });
    });
  }
  /**
   * Walks the event's composed path to find the matching item element.
   * When `requireTriggerMatch` is `true`, also verifies that the event originated
   * inside the configured trigger element; returns `null` if either condition fails.
   *
   * @param {Event} e - The event whose composed path is inspected.
   * @param {boolean} requireTriggerMatch - When `true`, the item is only returned if the trigger element was also matched.
   * @returns {Nullable<HTMLElement>} The matched item element, or `null` if not found or trigger requirement not met.
   */
  getItemFromComposedPath(e, requireTriggerMatch = false) {
    const itemSelector = this.entry.itemSelector;
    const triggerElementSelector = this.entry.triggerElementSelector;
    let matchedItem = null;
    let matchedTrigger = !triggerElementSelector;
    for (const node of e.composedPath()) {
      if (!(node instanceof HTMLElement))
        continue;
      if (!matchedItem && node.matches(itemSelector)) {
        matchedItem = node;
      }
      if (!matchedTrigger && triggerElementSelector && node.matches(triggerElementSelector)) {
        matchedTrigger = true;
      }
      if (matchedItem && matchedTrigger)
        break;
    }
    return !matchedItem || (requireTriggerMatch && !matchedTrigger) ? null : matchedItem;
  }
  /**
   * Translates sibling items up or down to visually make space for the dragged item at its current drop target.
   * Items within the affected index range shift by one item height; all others are reset to their natural position.
   */
  applyLayoutTransforms() {
    if (this.dragFromIndex == null || this.dragToIndex == null || this.activeItemHeight === 0)
      return;
    const fromIndex = this.dragFromIndex;
    const toIndex = this.dragToIndex;
    const offsetY = fromIndex < toIndex ? -this.activeItemHeight : this.activeItemHeight;
    this.getItems().forEach((item, index) => {
      if (item === this.activeItem)
        return;
      const visualElement = this.getVisualElement(item);
      const isInRange = fromIndex < toIndex
        ? index > fromIndex && index <= toIndex
        : index >= toIndex && index < fromIndex;
      visualElement.style.transform = isInRange ? `translateY(${offsetY}px)` : '';
    });
  }
  /**
   * Calculates the resolved drop index based on whether the pointer is above or below
   * the midpoint of the hovered item. The slot only switches after crossing the 50% mark,
   * preventing index jitter at slot boundaries.
   *
   * @param {HTMLElement} overItem - The item element currently under the pointer.
   * @param {number} pointerClientY - The vertical client coordinate of the pointer.
   * @returns {number} The resolved zero-based index where the dragged item would be inserted.
   */
  getResolvedDropIndex(overItem, pointerClientY) {
    const overIndex = this.getItemIndex(overItem);
    const dragFromIndex = this.dragFromIndex;
    if (overIndex < 0 || dragFromIndex == null)
      return overIndex;
    if (overIndex === dragFromIndex)
      return overIndex;
    const overRect = this.getVisualElement(overItem).getBoundingClientRect();
    const isAfterMidpoint = pointerClientY > overRect.top + overRect.height / 2;
    return overIndex > dragFromIndex
      ? isAfterMidpoint
        ? overIndex
        : overIndex - 1
      : isAfterMidpoint
        ? overIndex + 1
        : overIndex;
  }
  /**
   * Checks whether the given pointer event belongs to the pointer that started the current drag session.
   *
   * @param {PointerEvent} e - The pointer event to test.
   * @returns {boolean} `true` if the event's `pointerId` matches the active drag pointer.
   */
  isCurrentPointEvent(e) {
    return this.activePointerId !== null && this.activePointerId === e.pointerId;
  }
  /**
   * Checks whether the pointer has moved at least `DRAG_THRESHOLD` pixels (combined X + Y delta)
   * from its origin, or whether a drag is already in progress.
   *
   * @param {PointerEvent} e - The current `pointermove` event.
   * @returns {boolean} `true` if the drag threshold has been crossed or dragging is already active.
   */
  hasCrossedDragThreshold(e) {
    if (this.isDragging)
      return true;
    const deltaX = Math.abs(e.clientX - this.startX);
    const deltaY = Math.abs(e.clientY - this.startY);
    return deltaX + deltaY >= DraggableElementSlimmer.DRAG_THRESHOLD;
  }
  /**
   * Emits the drag lifecycle callback while the pointer is moving.
   */
  emitDrag() {
    if (this.dragFromIndex === null || !this.activeItem)
      return;
    this.onDragDrop('drag');
  }
  /**
   * Emits a drag-leave lifecycle callback when the pointer exits a hovered item.
   *
   * @param {Nullable<number>} toIndex - Index of the item being left.
   * @param {Nullable<HTMLElement>} toItem - Item element being left.
   */
  emitDragLeave(toIndex, toItem) {
    if (toIndex === null || !toItem)
      return;
    this.onDragDrop('leave');
  }
  /**
   * Emits a drag lifecycle payload to consumers.
   *
   * @param {DragDropEventStatus} status - Current lifecycle phase.
   */
  onDragDrop(status) {
    var _a, _b;
    if (this.dragFromIndex == null || !this.activeItem)
      return;
    (_b = (_a = this.entry).onDragDrop) === null || _b === void 0 ? void 0 : _b.call(_a, {
      status,
      options: {
        fromIndex: this.dragFromIndex,
        fromItem: this.activeItem,
        toIndex: this.dragToIndex,
        toItem: this.currentOverItem,
      },
    });
  }
}
DraggableElementSlimmer.DRAG_THRESHOLD = 4;
__decorate([
  MapToComponentMethod('connectedCallback')
], DraggableElementSlimmer.prototype, "connectedCallback", null);
__decorate([
  MapToComponentMethod('disconnectedCallback')
], DraggableElementSlimmer.prototype, "disconnectedCallback", null);
__decorate([
  MapToComponentMethod('componentDidLoad')
], DraggableElementSlimmer.prototype, "componentDidLoad", null);
