import { InsertChildrenAfterAction } from '../../../dto/actions/insert-children-after-block';
import { RTETableCellBlock } from '../dto/table-cell-block';
import { InsertTableColumnDirection } from '../actions/insert-table-column-action';
import { InsertChildrenBeforeAction } from '../../../dto/actions/insert-children-before-block';
import { getAllTableRows, getDefaultCellBlockTemplate, buildTableGrid, findCellBlockById, setSpanAttributes, } from '../utils/utils';
import { RTETableHeadCellBlock } from '../dto/table-head-cell-block';
import { TableColumnStrategy } from './table-column-strategy.abstract';
/**
 * Strategy to handle the insertion of a table column into the RTETable.
 */
export class InsertTableColumnStrategy extends TableColumnStrategy {
  /**
   * @inheritDoc
   */
  handleAction(action, target) {
    const { insertDirection, tableCellInsertRange } = action;
    const selectedCellIds = new Set(tableCellInsertRange.map((cell) => cell.id));
    const allTableRows = getAllTableRows(target);
    const tableGrid = buildTableGrid(allTableRows);
    const [startCellCol, endCellCol] = this.getBoundaryCellColumnRange(tableCellInsertRange, allTableRows, tableGrid);
    const targetLogicalCol = insertDirection === InsertTableColumnDirection.LEFT ? startCellCol : endCellCol + 1;
    const hasSelectedCellInsideTargetCol = this.hasSelectedCellInsideTargetColumn(allTableRows, tableGrid, targetLogicalCol, selectedCellIds);
    const skipInsertRowIndices = new Set();
    allTableRows.forEach((row, rowIndex) => {
      if (skipInsertRowIndices.has(rowIndex)) {
        return;
      }
      const expandableCell = this.findExpandableCell(row, rowIndex, targetLogicalCol, tableGrid, insertDirection, selectedCellIds, hasSelectedCellInsideTargetCol);
      if (expandableCell) {
        const originalRowSpan = expandableCell.rowSpan;
        setSpanAttributes(expandableCell, expandableCell.colSpan + 1, expandableCell.rowSpan, target, true);
        if (originalRowSpan > 1) {
          for (let offset = 1; offset < originalRowSpan; offset++) {
            skipInsertRowIndices.add(rowIndex + offset);
          }
        }
        return;
      }
      const insertPosition = this.findInsertPosition(row, rowIndex, targetLogicalCol, tableGrid, allTableRows);
      if (insertPosition) {
        const newCell = this.generateTableCell(insertPosition.templateCell);
        if (insertPosition.before) {
          row.apply(new InsertChildrenBeforeAction(insertPosition.anchorCell, newCell));
        }
        else {
          row.apply(new InsertChildrenAfterAction(insertPosition.anchorCell, newCell));
        }
      }
    });
  }
  /**
   * Find a spanning cell that should absorb the inserted logical column by increasing colspan.
   *
   * @param {RTETableRowBlock} row - The row to check.
   * @param {number} rowIndex - The row index in the grid.
   * @param {number} targetCol - The target logical column.
   * @param {string[][]} tableGrid - The table grid.
   * @param {InsertTableColumnDirection} insertDirection - Insert direction.
   * @param {Set<string>} selectedCellIds - The set of selected cell IDs.
   * @param {boolean} hasSelectedCellInsideTargetCol - Whether any selected merged cell covers the target column internally.
   * @returns {Nullable<RTETableCellBlock | RTETableHeadCellBlock>} Expandable cell or null.
   */
  findExpandableCell(row, rowIndex, targetCol, tableGrid, insertDirection, selectedCellIds, hasSelectedCellInsideTargetCol) {
    const gridRow = tableGrid[rowIndex];
    const selectedCellCountInRow = row.children.reduce((count, cell) => count + (selectedCellIds.has(cell.id) ? 1 : 0), 0);
    for (const cell of row.children) {
      const cellStartCol = gridRow.indexOf(cell.id);
      const cellEndColExclusive = cellStartCol + cell.colSpan;
      const isRightBoundaryOfCell = targetCol === cellEndColExclusive;
      const spansToSectionEnd = rowIndex + cell.rowSpan >= tableGrid.length;
      const isSelectedCell = selectedCellIds.has(cell.id);
      const isInsideCell = targetCol > cellStartCol && targetCol < cellEndColExclusive;
      const shouldExpand = insertDirection === InsertTableColumnDirection.LEFT
        ? isInsideCell
        : (isInsideCell && !(isSelectedCell && spansToSectionEnd)) ||
          (isRightBoundaryOfCell &&
            cell.colSpan > 1 &&
            isSelectedCell &&
            !spansToSectionEnd &&
            selectedCellCountInRow === 1 &&
            !hasSelectedCellInsideTargetCol);
      if (shouldExpand) {
        return cell;
      }
    }
    return null;
  }
  /**
   * Check whether any selected merged cell covers the target column internally.
   *
   * @param {RTETableRowBlock[]} allRows - All table rows.
   * @param {string[][]} tableGrid - The 2D grid representation of the table.
   * @param {number} targetCol - The target logical column.
   * @param {Set<string>} selectedCellIds - The set of selected cell IDs.
   * @returns {boolean} True if any selected cell internally covers the target column.
   */
  hasSelectedCellInsideTargetColumn(allRows, tableGrid, targetCol, selectedCellIds) {
    for (let rowIndex = 0; rowIndex < allRows.length; rowIndex++) {
      const row = allRows[rowIndex];
      const gridRow = tableGrid[rowIndex];
      for (const cell of row.children) {
        if (!selectedCellIds.has(cell.id)) {
          continue;
        }
        const cellStartCol = gridRow.indexOf(cell.id);
        const cellEndColExclusive = cellStartCol + cell.colSpan;
        if (targetCol > cellStartCol && targetCol < cellEndColExclusive) {
          return true;
        }
      }
    }
    return false;
  }
  /**
   * Find the correct cell and position to insert a new cell in a row.
   *
   * @param {RTETableRowBlock} row - The row to insert into.
   * @param {number} rowIndex - The index of the row.
   * @param {number} targetCol - The target logical column position.
   * @param {string[][]} tableGrid - The 2D grid representation of the table.
   * @param {RTETableRowBlock[]} allRows - All table rows.
   * @returns {InsertPosition | null} The insert position or null.
   */
  findInsertPosition(row, rowIndex, targetCol, tableGrid, allRows) {
    const gridRow = tableGrid[rowIndex];
    let gridCell = row.children[0];
    if (targetCol >= 0 && targetCol < gridRow.length) {
      const cellId = gridRow[targetCol];
      const foundCell = findCellBlockById(allRows, cellId);
      if (foundCell) {
        gridCell = foundCell;
      }
    }
    let currentCol = 0;
    for (let i = 0; i < row.children.length; i++) {
      const cell = row.children[i];
      while (currentCol < gridRow.length && gridRow[currentCol] !== cell.id) {
        currentCol++;
      }
      if (currentCol >= targetCol) {
        return { anchorCell: cell, templateCell: gridCell, before: true };
      }
      const cellEndCol = currentCol + cell.colSpan;
      if (cellEndCol > targetCol || i === row.children.length - 1) {
        return { anchorCell: cell, templateCell: gridCell, before: false };
      }
      currentCol = cellEndCol;
    }
    return null;
  }
  /**
   * Generate a new table cell based on the original cell type.
   *
   * @param {RTETableCellBlock | RTETableHeadCellBlock} originalCell The original table cell block to base the new cell on.
   * @returns {RTETableCellBlock | RTETableHeadCellBlock} The newly generated table cell block.
   */
  generateTableCell(originalCell) {
    if (originalCell instanceof RTETableHeadCellBlock) {
      return RTETableHeadCellBlock.from(getDefaultCellBlockTemplate(originalCell.type));
    }
    return RTETableCellBlock.from(getDefaultCellBlockTemplate(originalCell.type));
  }
}
