import { RTETableCellBlock } from './dto/table-cell-block';
import { RTETableBlock } from './dto/table-block';
import { RTENode } from '../../dto/nodes/node.abstract';
import stateEntityRenderingRegistry from '../../slimmers/controllers/state-entity-rendering-registry';
import { debounce } from '../../../../utils/timer';
import { tryGetDocument } from '../../../../utils/try-get-document';
import { findParent } from '../../../../utils/ui';
import { getParentBlockByType } from './utils/utils';
import { isNonNullable } from '../../../../types/type-guard';
import changeManager from '../../../../helpers/change-manager/change-manager';
import DomNodeSubjectFactory from '../../../../helpers/change-manager/subject/dom-node-subject/dom-node-subject-factory';
import { VegaInternalRTEExtensionTableCellSelectionChange } from '../../../../helpers/event-manager/event-id/vega-internal-event-id';
import { CommonAnnotationTypeEnum } from '../../dto/annotations/common-annotation.abstract';
/**
 * Table selection helper used for mouse drag selection of cells.
 */
export class RTETableSelection {
  constructor() {
    /**
     *  Indicates whether the user is currently performing a drag selection.
     */
    this.dragging = false;
    /**
     * Anchor cell where the drag selection started.
     */
    this.anchorCell = null;
    /**
     * Collection of currently selected cells.
     */
    this.selectedCells = new Set();
    this.syncRTESelectionWithDebounce = debounce(this.syncRTESelection, 100, this);
    this.handleSelectionChangeWithDebounce = debounce(this.handleSelectionChange, 10, this);
    /**
     * Recursively filter and collect selected table cell blocks within the given node.
     *
     * @param {Range} range - The current selection range.
     * @param {Node} node - The current node to check for selection.
     * @returns {RTETableCellBlock[]} A list of selected table cell blocks.
     */
    this.filterSelectedCell = (range, node) => {
      const selectedCells = new Set();
      if (range.intersectsNode(node)) {
        if (node.nodeType === Node.TEXT_NODE) {
          const focusedNodeDto = stateEntityRenderingRegistry.getEntityByDOM(node.parentElement);
          if (focusedNodeDto) {
            const cell = getParentBlockByType(focusedNodeDto.parent, RTETableCellBlock);
            if (cell) {
              selectedCells.add(cell);
              return Array.from(selectedCells);
            }
          }
        }
        node.childNodes.forEach((child) => this.filterSelectedCell(range, child).forEach((cell) => {
          selectedCells.add(cell);
        }));
      }
      return Array.from(selectedCells);
    };
    /**
     * Update the styles of the selected cells.
     *
     * @param {RTETableCellBlock} cell - The cell to update style for.
     * @param {number} total - The total number of selected cells.
     */
    this.updateSelectedStyles = (cell, total) => {
      const selectedClass = total === 1 ? 'v-rte-table-cell-focused' : 'v-rte-table-cell-selected';
      const cellRef = stateEntityRenderingRegistry.getDOMByEntity(cell);
      if (cellRef) {
        cellRef.classList.add(selectedClass);
      }
    };
    /**
     * Handles the document mouse up event to finish drag selection.
     */
    this.handleDocMouseUp = () => {
      this.endDrag();
      const safeDocument = tryGetDocument();
      if (safeDocument) {
        safeDocument.removeEventListener('mouseup', this.handleDocMouseUp);
      }
    };
    /**
     * Resets the table selection when clicking outside the table.
     */
    this.handleContentClick = () => {
      this.clearSelection();
      const editorContent = 
      // eslint-disable-next-line  @typescript-eslint/no-non-null-assertion
      this.context.host.shadowRoot.querySelector('.rich-text-editor-container');
      if (editorContent) {
        editorContent.removeEventListener('mousedown', this.handleContentClick, true);
      }
    };
  }
  /**
   * Compute all cells inside the rectangle defined by the start and end cells.
   *
   * Note: this only performs the calculation and does not modify the internal `selectedCells` set,
   * so callers can decide how to apply the result.
   *
   * @param {RTETableCellBlock | RTETableHeadCellBlock} anchor - Drag start cell (anchor cell).
   * @param {RTETableCellBlock | RTETableHeadCellBlock} focus - Current drag focus cell.
   * @returns {(RTETableCellBlock | RTETableHeadCellBlock)[]} All cells in the rectangular selection area.
   */
  static computeRectRange(anchor, focus) {
    // Collect all row blocks (from both head and body) under the same table,
    // then compute the rectangle over this flattened row list.
    const anchorRow = anchor.parent;
    const focusRow = focus.parent;
    const anchorSection = anchorRow.parent;
    const focusSection = focusRow.parent;
    const anchorTable = anchorSection.parent;
    const focusTable = focusSection.parent;
    // If they are not in the same table at all, just return empty.
    if (anchorTable !== focusTable) {
      return [];
    }
    // Flatten head rows (if any) + body rows into a single ordered list.
    const allRows = [];
    const tableChildren = anchorTable.children;
    tableChildren.forEach((section) => {
      section.children.forEach((row) => allRows.push(row));
    });
    const { grid, cellMeta } = RTETableSelection.buildCellGrid(allRows);
    const anchorMeta = cellMeta.get(anchor);
    const focusMeta = cellMeta.get(focus);
    if (!anchorMeta || !focusMeta) {
      return [];
    }
    const anchorStartRow = anchorMeta.row;
    const anchorEndRow = anchorMeta.row + anchorMeta.rowSpan - 1;
    const anchorStartCol = anchorMeta.col;
    const anchorEndCol = anchorMeta.col + anchorMeta.colSpan - 1;
    const focusStartRow = focusMeta.row;
    const focusEndRow = focusMeta.row + focusMeta.rowSpan - 1;
    const focusStartCol = focusMeta.col;
    const focusEndCol = focusMeta.col + focusMeta.colSpan - 1;
    const startRow = Math.min(anchorStartRow, focusStartRow);
    const endRow = Math.max(anchorEndRow, focusEndRow);
    const startCol = Math.min(anchorStartCol, focusStartCol);
    const endCol = Math.max(anchorEndCol, focusEndCol);
    const result = new Set();
    for (let r = startRow; r <= endRow; r++) {
      const row = grid[r];
      for (let c = startCol; c <= endCol; c++) {
        const cell = row[c];
        if (cell) {
          result.add(cell);
        }
      }
    }
    return Array.from(result);
  }
  /**
   * Get the rowSpan and colSpan attributes from a cell's DOM element.
   *
   * @param {RTETableCellBlock} cell - The cell to get span values from.
   * @returns {CellSpanType} Object with rowSpan and colSpan properties.
   */
  static getCellSpan(cell) {
    let rowSpan = 1;
    let colSpan = 1;
    // First, try to get from customAttribute annotation (this works even without DOM)
    const customAttr = cell.getAnnotationByType(CommonAnnotationTypeEnum.CUSTOM_ATTRIBUTE);
    if (customAttr && customAttr.customAttribute) {
      const attrs = customAttr.customAttribute;
      if (attrs.rowSpan) {
        rowSpan = parseInt(attrs.rowSpan, 10);
      }
      if (attrs.colSpan) {
        colSpan = parseInt(attrs.colSpan, 10);
      }
    }
    // Fallback to DOM attributes if annotation didn't have the values
    if (rowSpan === 1 && colSpan === 1) {
      const cellDOMElement = stateEntityRenderingRegistry.getDOMByEntity(cell);
      if (cellDOMElement) {
        const rowSpanAttr = cellDOMElement.getAttribute('rowSpan');
        const colSpanAttr = cellDOMElement.getAttribute('colSpan');
        if (rowSpanAttr) {
          rowSpan = parseInt(rowSpanAttr, 10);
        }
        if (colSpanAttr) {
          colSpan = parseInt(colSpanAttr, 10);
        }
      }
    }
    return {
      rowSpan: Number.isFinite(rowSpan) && rowSpan > 0 ? rowSpan : 1,
      colSpan: Number.isFinite(colSpan) && colSpan > 0 ? colSpan : 1,
    };
  }
  /**
   * Build a grid representation of the table that accounts for rowSpan and colSpan.
   * Each cell in the grid array points to the actual cell block that occupies that position.
   *
   * @param {RTETableRowBlock[]} allRows - All rows in the table.
   * @returns {CellGridMetaType} Object with grid and cellMeta properties.
   */
  static buildCellGrid(allRows) {
    const grid = [];
    const cellMeta = new Map();
    for (let r = 0; r < allRows.length; r++) {
      const row = allRows[r];
      if (!grid[r]) {
        grid[r] = [];
      }
      let col = 0;
      for (let c = 0; c < row.children.length; c++) {
        const cell = row.children[c];
        // Find the next available column (skip cells occupied by rowSpan from previous rows)
        while (grid[r][col]) {
          col += 1;
        }
        const { rowSpan, colSpan } = RTETableSelection.getCellSpan(cell);
        cellMeta.set(cell, { row: r, col, rowSpan, colSpan });
        // Fill the grid for all positions this cell occupies
        for (let rowOffset = 0; rowOffset < rowSpan; rowOffset++) {
          const targetRow = r + rowOffset;
          if (!grid[targetRow]) {
            grid[targetRow] = [];
          }
          for (let colOffset = 0; colOffset < colSpan; colOffset++) {
            grid[targetRow][col + colOffset] = cell;
          }
        }
        col += colSpan;
      }
    }
    return { grid, cellMeta };
  }
  /**
   * Clean up event listeners.
   */
  destroy() {
    this.removeSelectionChangeListener();
  }
  /**
   * Inject the extension context and set up selection change listener.
   *
   * @param {RTEExtensionContext} context - The extension context.
   */
  init(context) {
    this.context = context;
    this.addSelectionChangeListener();
  }
  /**
   * Set the selected cells to the given list, replacing any existing selection.
   *
   * @param {RTETableCellBlock} cells - The cells to select.
   */
  setSelectedCells(cells) {
    const firstCell = Array.from(this.selectedCells)[0] || cells[0];
    this.updateSelectedCells(cells);
    this.emitCellSelectionChange(firstCell, cells);
  }
  /**
   * Get all currently selected cells.
   *
   * @returns {(RTETableCellBlock | RTETableHeadCellBlock)[]} A flat list of selected cells.
   */
  getSelectedCells() {
    return Array.from(this.selectedCells);
  }
  /**
   * Handle mouse down on a cell: start drag selection from the given cell.
   *
   * @param {HTMLElement} cellRef - The cell where the drag starts.
   */
  focusCell(cellRef) {
    const cellBlock = stateEntityRenderingRegistry.getEntityByDOM(cellRef);
    if (cellBlock) {
      this.anchorCell = cellBlock;
      this.setSelectedCells([cellBlock]);
      this.addListeners();
    }
  }
  /**
   * Handle drag over a cell (typically called on `mouseenter` while dragging).
   * Maintains a rectangular selection between the anchor cell and the current cell.
   *
   * @param {RTETableCellBlock | RTETableHeadCellBlock} cell - The cell currently under the drag.
   */
  dragOver(cell) {
    if (!this.anchorCell) {
      return;
    }
    // Always compute the rectangle strictly from the original anchorCell
    // (the cell where startDrag was first called) to the current cell.
    const rectCells = RTETableSelection.computeRectRange(this.anchorCell, cell);
    if (rectCells.length > 1) {
      this.startDrag();
    }
    if (this.dragging) {
      this.preventTextSelection(rectCells);
      this.updateSelectedCells(rectCells, { debounceSync: true });
    }
  }
  /**
   * Handle mouse up: stop drag selection.
   */
  endDrag() {
    this.dragging = false;
    // Reset anchor after drag ends so the next drag can start from a new cell.
    this.anchorCell = null;
  }
  /**
   * Emit table-cell selection change notify for the owning table.
   *
   * @param {RTETableCellBlock} firstCell - First cell of the table.
   * @param {RTETableCellBlock[]} cells - The current selected table cells.
   */
  emitCellSelectionChange(firstCell, cells) {
    const table = getParentBlockByType(firstCell, RTETableBlock);
    if (table) {
      const tableRef = stateEntityRenderingRegistry.getDOMByEntity(table);
      if (tableRef) {
        changeManager.notify(DomNodeSubjectFactory.getSubject(tableRef, VegaInternalRTEExtensionTableCellSelectionChange), {
          host: tableRef,
          detail: cells,
        });
      }
    }
  }
  /**
   * Clear the current selection state without changing the drag flag.
   */
  clearSelection() {
    this.selectedCells.forEach((cell) => {
      const cellRef = stateEntityRenderingRegistry.getDOMByEntity(cell);
      if (cellRef) {
        cellRef.classList.remove('v-rte-table-cell-selected', 'v-rte-table-cell-focused');
      }
    });
    this.selectedCells.clear();
  }
  /**
   * Add a selectionchange listener to the document to handle cell click selection.
   */
  addSelectionChangeListener() {
    const safeDocument = tryGetDocument();
    if (safeDocument) {
      safeDocument.addEventListener('selectionchange', this.handleSelectionChangeWithDebounce);
    }
  }
  /**
   * Remove the selectionchange listener from the document.
   */
  removeSelectionChangeListener() {
    const safeDocument = tryGetDocument();
    if (safeDocument) {
      safeDocument.removeEventListener('selectionchange', this.handleSelectionChangeWithDebounce);
    }
  }
  /**
   * Handle selection change events from the document for the cell click selection.
   */
  handleSelectionChange() {
    if (this.dragging) {
      this.syncRTESelectionWithDebounce();
      return;
    }
    const selection = this.context.getSelection();
    if (selection.rangeCount > 0) {
      const range = selection.getRangeAt(0);
      const commonAncestorContainer = range.commonAncestorContainer;
      const rangeInTable = !!findParent(commonAncestorContainer, `table`);
      if (rangeInTable) {
        const selectedCells = this.filterSelectedCell(range, commonAncestorContainer);
        this.setSelectedCells(selectedCells);
      }
    }
  }
  /**
   * Mark the beginning of a drag operation.
   */
  startDrag() {
    this.dragging = true;
  }
  /**
   * Add the given cells to the current selection set.
   *
   * @param {RTETableCellBlock[]} cells - The cells to add to the selection.
   * @param {{ debounceSync?: boolean }} [options] - Options for updating selection.
   * @param {boolean} [options.debounceSync] - Whether to debounce the RTE selection sync.
   */
  updateSelectedCells(cells, { debounceSync } = {}) {
    this.clearSelection();
    cells.forEach((cell) => {
      this.selectedCells.add(cell);
      this.updateSelectedStyles(cell, cells.length);
    });
    if (debounceSync) {
      this.syncRTESelectionWithDebounce();
    }
    else {
      this.syncRTESelection();
    }
  }
  /**
   * Synchronizes the rich text editor selection with the selected table cells.
   */
  syncRTESelection() {
    const cells = Array.from(this.selectedCells);
    const nodes = cells
      .flatMap((cell) => {
      return cell.children.flatMap((block) => {
        return block.children.flatMap((node) => {
          // TODO: Nested table deletion will track in https://gethired.atlassian.net/browse/VD-7860
          if (node instanceof RTENode) {
            return node;
          }
        });
      });
    })
      .filter(isNonNullable);
    if (cells.length > 1) {
      this.context.setSelectedNodes(nodes, {
        startNode: nodes[0],
        endNode: nodes[nodes.length - 1],
        startOffset: 0,
        endOffset: nodes[nodes.length - 1].getRangeEndOffset(),
      });
    }
  }
  /**
   * Prevents text selection during drag operations.
   *
   * @param {RTETableCellBlock[]} selectedCells - The currently selected table cells.
   */
  preventTextSelection(selectedCells) {
    const selection = this.context.getSelection();
    selection.removeAllRanges();
    if (selectedCells.length > 0) {
      const startContainer = stateEntityRenderingRegistry.getDOMByEntity(selectedCells[0]);
      if (startContainer) {
        const newRange = new Range();
        newRange.setStart(startContainer, 0);
        newRange.setEnd(startContainer, 0);
        selection.addRange(newRange);
      }
    }
  }
  /**
   * Adds a mouse up listener to the document to finish drag selection.
   *
   */
  addListeners() {
    const safeDocument = tryGetDocument();
    if (safeDocument) {
      safeDocument.addEventListener('mouseup', this.handleDocMouseUp);
    }
    const editorContent = 
    // eslint-disable-next-line  @typescript-eslint/no-non-null-assertion
    this.context.host.shadowRoot.querySelector('.rich-text-editor-container');
    if (editorContent) {
      editorContent.addEventListener('mousedown', this.handleContentClick, true);
    }
  }
}
