/**
 * @license
 * Copyright (c) 2016 - 2026 Vaadin Ltd.
 * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
 */
import { addListener, setTouchAction } from '@vaadin/component-base/src/gestures.js';

/**
 * A mixin that provides basic functionality for the
 * `<vaadin-grid-selection-column>`. This includes properties, cell rendering,
 * and overridable methods for handling changes to the selection state.
 *
 * **NOTE**: This mixin is re-used by the Flow component, and as such must not
 * implement any selection state updates for the column element or the grid.
 * Web component-specific selection state updates must be implemented in the
 * `<vaadin-grid-selection-column>` itself, by overriding the protected methods
 * provided by this mixin.
 *
 * @polymerMixin
 */
export const GridSelectionColumnBaseMixin = (superClass) =>
  class GridSelectionColumnBaseMixin extends superClass {
    static get properties() {
      return {
        /**
         * Width of the cells for this column.
         */
        width: {
          type: String,
          value: '58px',
          sync: true,
        },

        /**
         * Override `autoWidth` to enable auto-width
         */
        autoWidth: {
          type: Boolean,
          value: true,
        },

        /**
         * Flex grow ratio for the cell widths. When set to 0, cell width is fixed.
         * @attr {number} flex-grow
         */
        flexGrow: {
          type: Number,
          value: 0,
          sync: true,
        },

        /**
         * When true, all the items are selected.
         * @attr {boolean} select-all
         */
        selectAll: {
          type: Boolean,
          value: false,
          notify: true,
          sync: true,
        },

        /**
         * When true, the active item gets automatically selected.
         * @attr {boolean} auto-select
         */
        autoSelect: {
          type: Boolean,
          value: false,
          sync: true,
        },

        /**
         * When true, rows can be selected by dragging over the selection column.
         * @attr {boolean} drag-select
         */
        dragSelect: {
          type: Boolean,
          value: false,
          sync: true,
        },

        /** @protected */
        _indeterminate: {
          type: Boolean,
          sync: true,
        },

        /** @protected */
        _selectAllHidden: Boolean,

        /**
         * Indicates whether the shift key is currently pressed.
         *
         * @protected
         */
        _shiftKeyDown: {
          type: Boolean,
          value: false,
        },
      };
    }

    static get observers() {
      return [
        '_onHeaderRendererOrBindingChanged(_headerRenderer, _headerCell, path, header, selectAll, _indeterminate, _selectAllHidden)',
      ];
    }

    constructor() {
      super();
      this.__onCellTrack = this.__onCellTrack.bind(this);
      this.__onCellClick = this.__onCellClick.bind(this);
      this.__onCellMouseDown = this.__onCellMouseDown.bind(this);
      this.__onGridInteraction = this.__onGridInteraction.bind(this);
      this.__onActiveItemChanged = this.__onActiveItemChanged.bind(this);
      this.__onTouchStart = this.__onTouchStart.bind(this);
      this.__onSelectRowCheckboxChange = this.__onSelectRowCheckboxChange.bind(this);
      this.__onSelectAllCheckboxChange = this.__onSelectAllCheckboxChange.bind(this);
    }

    /** @protected */
    connectedCallback() {
      super.connectedCallback();
      if (this._grid) {
        this._grid.addEventListener('keyup', this.__onGridInteraction);
        this._grid.addEventListener('keydown', this.__onGridInteraction, { capture: true });
        this._grid.addEventListener('mousedown', this.__onGridInteraction);
        this._grid.addEventListener('active-item-changed', this.__onActiveItemChanged);
        this._grid.addEventListener('touchstart', this.__onTouchStart);
      }
    }

    /** @protected */
    disconnectedCallback() {
      super.disconnectedCallback();
      if (this._grid) {
        this._grid.removeEventListener('keyup', this.__onGridInteraction);
        this._grid.removeEventListener('keydown', this.__onGridInteraction, { capture: true });
        this._grid.removeEventListener('mousedown', this.__onGridInteraction);
        this._grid.removeEventListener('active-item-changed', this.__onActiveItemChanged);
        this._grid.removeEventListener('touchstart', this.__onTouchStart);
      }
    }

    /**
     * Renders the Select All checkbox to the header cell.
     *
     * @override
     */
    _defaultHeaderRenderer(root, _column) {
      let checkbox = root.firstElementChild;
      if (!checkbox) {
        checkbox = document.createElement('vaadin-checkbox');
        checkbox.accessibleName = 'Select All';
        checkbox.classList.add('vaadin-grid-select-all-checkbox');
        checkbox.addEventListener('change', this.__onSelectAllCheckboxChange);
        root.appendChild(checkbox);
      }

      const checked = this.__isChecked(this.selectAll, this._indeterminate);
      checkbox.checked = checked;
      checkbox.indeterminate = this._indeterminate;
      checkbox.style.visibility = this._selectAllHidden ? 'hidden' : '';
    }

    /**
     * Renders the Select Row checkbox to the body cell.
     *
     * @override
     */
    _defaultRenderer(root, _column, { item, selected }) {
      let checkbox = root.firstElementChild;
      if (!checkbox) {
        checkbox = document.createElement('vaadin-checkbox');
        checkbox.accessibleName = 'Select Row';
        checkbox.addEventListener('change', this.__onSelectRowCheckboxChange);
        root.appendChild(checkbox);
        addListener(root, 'track', this.__onCellTrack);
        setTouchAction(root, 'pinch-zoom');
        root.addEventListener('mousedown', this.__onCellMouseDown);
        root.addEventListener('click', this.__onCellClick);
      }

      checkbox.__item = item;
      checkbox.checked = selected;

      const isSelectable = this._grid.__isItemSelectable(item);
      checkbox.readonly = !isSelectable;

      const isHidden = !isSelectable && !selected;
      checkbox.style.visibility = isHidden ? 'hidden' : '';
    }

    /**
     * Updates the select all state when the Select All checkbox is switched.
     * The listener handles only user-fired events.
     *
     * @private
     */
    __onSelectAllCheckboxChange(e) {
      if (this._indeterminate || e.currentTarget.checked) {
        this._selectAll();
      } else {
        this._deselectAll();
      }
    }

    /** @private */
    __onGridInteraction(e) {
      this._shiftKeyDown = e.shiftKey;

      if (this.autoSelect) {
        // Prevent text selection when shift-clicking to select a range of items.
        this._grid.$.scroller.toggleAttribute('range-selecting', this._shiftKeyDown);
      }
    }

    /**
     * Selects or deselects the row when the Select Row checkbox is switched.
     * The listener handles only user-fired events.
     *
     * @private
     */
    __onSelectRowCheckboxChange(e) {
      this.__toggleItem(e.currentTarget.__item, e.currentTarget.checked);
    }

    /** @private */
    __onCellTrack(event) {
      if (!this.dragSelect) {
        return;
      }
      this.__dragCurrentY = event.detail.y;
      this.__dragDy = event.detail.dy;
      if (event.detail.state === 'start') {
        // Don't start drag-select during multi-touch (e.g. pinch-zoom)
        if (this.__multiTouchActive) {
          return;
        }
        const renderedRows = this._grid._getRenderedRows();
        // Get the row where the drag started
        const dragStartRow = renderedRows.find((row) => row.contains(event.currentTarget.assignedSlot));
        // Whether to select or deselect the items on drag
        this.__selectOnDrag = !this._grid._isSelected(dragStartRow._item);
        // Store the index of the row where the drag started
        this.__dragStartIndex = dragStartRow.index;
        // Store the item of the row where the drag started
        this.__dragStartItem = dragStartRow._item;
        // Start the auto scroller
        this.__dragAutoScroller();
      } else if (event.detail.state === 'end') {
        // if drag start and end stays within the same item, then toggle its state
        if (this.__dragStartItem) {
          this.__toggleItem(this.__dragStartItem, this.__selectOnDrag);
        }
        // clear drag state after timeout, which allows preventing the
        // subsequent click event if drag started and ended on the same item
        setTimeout(() => {
          this.__dragStartIndex = undefined;
        });
      }
    }

    /** @private */
    __onCellMouseDown(e) {
      if (this.dragSelect) {
        // Prevent text selection when starting to drag
        e.preventDefault();
      }
    }

    /** @private */
    __onTouchStart(e) {
      if (e.touches.length > 1) {
        this.__multiTouchActive = true;
        // Cancel in-progress drag-select on multi-touch (e.g. pinch-zoom)
        this.__dragStartIndex = undefined;
        this.__dragStartItem = undefined;
      } else {
        this.__multiTouchActive = false;
      }
    }

    /** @private */
    __onCellClick(e) {
      if (this.__dragStartIndex !== undefined) {
        // Stop the click event if drag was enabled. This click event should
        // only occur if drag started and stopped on the same item. In that case
        // the selection state has already been toggled on drag end, and we
        // don't  want to toggle it again from clicking the checkbox or changing
        // the active item.
        e.preventDefault();
      }
    }

    /** @private */
    _onCellKeyDown(e) {
      const target = e.composedPath()[0];
      // Toggle on Space without having to enter interaction mode first
      if (e.keyCode !== 32) {
        return;
      }
      if (target === this._headerCell) {
        if (this.selectAll) {
          this._deselectAll();
        } else {
          this._selectAll();
        }
      } else if (this._cells.includes(target) && !this.autoSelect) {
        const checkbox = target._content.firstElementChild;
        this.__toggleItem(checkbox.__item);
      }
    }

    /** @private */
    __onActiveItemChanged(e) {
      const activeItem = e.detail.value;
      if (this.autoSelect) {
        const item = activeItem || this.__previousActiveItem;
        if (item) {
          this.__toggleItem(item);
        }
      }
      this.__previousActiveItem = activeItem;
    }

    /** @private */
    __dragAutoScroller() {
      if (this.__dragStartIndex === undefined) {
        return;
      }

      // Get the row being hovered over
      const renderedRows = this._grid._getRenderedRows();
      const hoveredRow = renderedRows.find((row) => {
        const rowRect = row.getBoundingClientRect();
        return this.__dragCurrentY >= rowRect.top && this.__dragCurrentY <= rowRect.bottom;
      });

      // Get the index of the row being hovered over or the first/last
      // visible row if hovering outside the grid
      let hoveredIndex = hoveredRow ? hoveredRow.index : undefined;
      const scrollableArea = this.__getScrollableArea();
      if (this.__dragCurrentY < scrollableArea.top) {
        hoveredIndex = this._grid._firstVisibleIndex;
      } else if (this.__dragCurrentY > scrollableArea.bottom) {
        hoveredIndex = this._grid._lastVisibleIndex;
      }

      if (hoveredIndex !== undefined) {
        // Select all items between the start and the current row
        renderedRows.forEach((row) => {
          if (
            (hoveredIndex > this.__dragStartIndex && row.index >= this.__dragStartIndex && row.index <= hoveredIndex) ||
            (hoveredIndex < this.__dragStartIndex && row.index <= this.__dragStartIndex && row.index >= hoveredIndex)
          ) {
            this.__toggleItem(row._item, this.__selectOnDrag);
            this.__dragStartItem = undefined;
          }
        });
      }

      // Start scrolling in the top/bottom 15% of the scrollable area
      const scrollTriggerArea = scrollableArea.height * 0.15;
      // Maximum number of pixels to scroll per iteration
      const maxScrollAmount = 10;

      if (this.__dragDy < 0 && this.__dragCurrentY < scrollableArea.top + scrollTriggerArea) {
        const dy = scrollableArea.top + scrollTriggerArea - this.__dragCurrentY;
        const percentage = Math.min(1, dy / scrollTriggerArea);
        this._grid.$.table.scrollTop -= percentage * maxScrollAmount;
      }
      if (this.__dragDy > 0 && this.__dragCurrentY > scrollableArea.bottom - scrollTriggerArea) {
        const dy = this.__dragCurrentY - (scrollableArea.bottom - scrollTriggerArea);
        const percentage = Math.min(1, dy / scrollTriggerArea);
        this._grid.$.table.scrollTop += percentage * maxScrollAmount;
      }

      // Schedule the next auto scroll
      setTimeout(() => this.__dragAutoScroller(), 10);
    }

    /**
     * Gets the scrollable area of the grid as a bounding client rect. The
     * scrollable area is the bounding rect of the grid minus the header and
     * footer.
     *
     * @private
     */
    __getScrollableArea() {
      const gridRect = this._grid.$.table.getBoundingClientRect();
      const headerRect = this._grid.$.header.getBoundingClientRect();
      const footerRect = this._grid.$.footer.getBoundingClientRect();

      return {
        top: gridRect.top + headerRect.height,
        bottom: gridRect.bottom - footerRect.height,
        left: gridRect.left,
        right: gridRect.right,
        height: gridRect.height - headerRect.height - footerRect.height,
        width: gridRect.width,
      };
    }

    /**
     * Override to handle the user selecting all items.
     * @protected
     */
    _selectAll() {}

    /**
     * Override to handle the user deselecting all items.
     * @protected
     */
    _deselectAll() {}

    /**
     * Override to handle the user selecting an item.
     * @param {Object} item the item to select
     * @protected
     */
    _selectItem(_item) {}

    /**
     * Override to handle the user deselecting an item.
     * @param {Object} item the item to deselect
     * @protected
     */
    _deselectItem(_item) {}

    /**
     * Toggles the selected state of the given item.
     *
     * @param item the item to toggle
     * @param {boolean} [selected] whether to select or deselect the item
     * @private
     */
    __toggleItem(item, selected = !this._grid._isSelected(item)) {
      if (selected === this._grid._isSelected(item)) {
        // Skip selection if the item is already in the desired state.
        // Note, _selectItem and _deselectItem may be overridden in custom
        // selection column implementations, and calling them unnecessarily
        // might affect performance (e.g. vaadin-grid-flow-selection-column).
        return;
      }

      if (selected) {
        this._selectItem(item);
      } else {
        this._deselectItem(item);
      }
    }

    /**
     * IOS needs indeterminate + checked at the same time
     * @private
     */
    __isChecked(selectAll, indeterminate) {
      return indeterminate || selectAll;
    }
  };
