import { generateUUID } from '../../../../../utils/misc';
import { cleanObject } from '../../../../../utils/object';
import { ActionHandleStrategy } from '../../../dto/action-handle-strategies/action-handle-strategy.abstract';
import { AppendChildrenAction } from '../../../dto/actions/append-child-nodes-action';
import { InsertChildrenAfterAction } from '../../../dto/actions/insert-children-after-block';
import { InsertChildrenBeforeAction } from '../../../dto/actions/insert-children-before-block';
import { CommonAnnotationTypeEnum } from '../../../dto/annotations/common-annotation.abstract';
import { CustomAttributeAnnotation } from '../../../dto/annotations/custom-attribute-annotation';
import { RTETableHeadBlock } from '../dto/table-head-block';
import { RTETableHeadCellBlock } from '../dto/table-head-cell-block';
/**
 * Abstract base strategy for table row actions. Provides common utilities for row manipulations.
 */
export class TableRowCommonUtilitiesStrategy extends ActionHandleStrategy {
  /**
   * Calculates the maximum number of cells in any row, accounting for colSpan.
   *
   * @param {RTETableRowBlock[]} rows - The rows to analyze.
   * @returns {number} The maximum cell count across the rows.
   */
  getRowsMaxCellCount(rows) {
    let maxCells = 0;
    for (const row of rows) {
      let rowCols = 0;
      for (const cell of row.children) {
        rowCols += cell.colSpan;
      }
      maxCells = Math.max(maxCells, rowCols);
    }
    return maxCells;
  }
  /**
   * Compute logical start columns for each row's cells, accounting for colSpan and rowSpan.
   *
   * @param {RTETableRowBlock[]} rows - Table rows in a section.
   * @returns {number[][]} Array of row cell start columns by cell index.
   */
  getRowCellStartCols(rows) {
    // Calculate max columns efficiently
    const maxCols = this.getRowsMaxCellCount(rows);
    const rowStartCols = new Array(rows.length);
    // Track occupied columns due to rowSpan
    const spanCarryOver = new Array(maxCols).fill(0);
    for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
      const row = rows[rowIndex];
      const cells = row.children;
      const startCols = new Array(cells.length);
      let currentCol = 0;
      for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
        const cell = cells[cellIndex];
        // Skip columns occupied by previous row's rowspan
        while (currentCol < spanCarryOver.length && spanCarryOver[currentCol] > 0) {
          currentCol++;
        }
        startCols[cellIndex] = currentCol;
        const colSpan = Math.max(cell.colSpan, TableRowCommonUtilitiesStrategy.DEFAULT_CELLSPAN);
        // Mark columns as occupied if cell has rowspan > 1
        if (cell.rowSpan > TableRowCommonUtilitiesStrategy.DEFAULT_CELLSPAN) {
          for (let i = 0; i < colSpan; i++) {
            const col = currentCol + i;
            if (col < spanCarryOver.length) {
              spanCarryOver[col] = Math.max(spanCarryOver[col], cell.rowSpan);
            }
          }
        }
        currentCol += colSpan;
      }
      rowStartCols[rowIndex] = startCols;
      // Decrement all carryOver values at the end of each row
      // This represents "one row has been consumed"
      for (let i = 0; i < spanCarryOver.length; i++) {
        if (spanCarryOver[i] > 0)
          spanCarryOver[i]--;
      }
    }
    return rowStartCols;
  }
  /**
   * Updates the rowspan/colspan attribute of a table cell.
   *
   * @param {RTETableCellBlock | RTETableHeadCellBlock} cell - The cell to update.
   * @param {'rowspan' | 'colspan'} spanKey - The attribute key to update.
   * @param {number} newSpan - The new rowspan/colspan value.
   */
  updateCellSpan(cell, spanKey, newSpan) {
    const customAttrAnnotation = cell.getCustomAttributesAnnotation();
    // Only set rowspan/colspan if > 1, otherwise remove the attribute
    if (newSpan <= TableRowCommonUtilitiesStrategy.DEFAULT_CELLSPAN) {
      if (customAttrAnnotation) {
        const currentAttrs = customAttrAnnotation.render();
        delete currentAttrs[spanKey];
        cell.annotationMap.set(CommonAnnotationTypeEnum.CUSTOM_ATTRIBUTE, new CustomAttributeAnnotation(currentAttrs));
      }
    }
    else {
      const attrs = customAttrAnnotation ? customAttrAnnotation.render() : {};
      attrs[spanKey] = newSpan.toString();
      cell.annotationMap.set(CommonAnnotationTypeEnum.CUSTOM_ATTRIBUTE, new CustomAttributeAnnotation(attrs));
    }
  }
  /**
   * Insert moved cells into target rows using logical column positions.
   *
   * @param {Map<number, CellToMove[]>} cellsToMove - Cells grouped by target row.
   * @param {RTETableRowBlock[]} allRows - All rows in the section.
   * @param {number[][]} rowCellStartCols - Logical start columns per row.
   */
  insertMovedCells(cellsToMove, allRows, rowCellStartCols) {
    cellsToMove.forEach((cells, targetRowIndex) => {
      const targetRow = allRows[targetRowIndex];
      if (targetRow) {
        const targetRowStartCols = rowCellStartCols[targetRowIndex] || [];
        // sort cells in descending order of logical start column to avoid index shifting issues during insertion
        cells.sort((a, b) => {
          if (a.logicalStartCol === b.logicalStartCol) {
            return b.cellIndex - a.cellIndex;
          }
          return b.logicalStartCol - a.logicalStartCol;
        });
        cells.forEach((cellInfo) => {
          const { cell, logicalStartCol } = cellInfo;
          if (targetRow.children.length === 0) {
            targetRow.apply(new AppendChildrenAction([cell]));
            targetRowStartCols[0] = logicalStartCol;
            return;
          }
          const insertIndex = targetRowStartCols.findIndex((startCol) => startCol >= logicalStartCol);
          if (insertIndex === -1) {
            const lastCell = targetRow.children[targetRow.children.length - 1];
            targetRow.apply(new InsertChildrenAfterAction(lastCell, cell));
            targetRowStartCols.push(logicalStartCol);
          }
          else if (insertIndex === 0) {
            targetRow.apply(new InsertChildrenBeforeAction(targetRow.children[0], cell));
            targetRowStartCols.unshift(logicalStartCol);
          }
          else {
            targetRow.apply(new InsertChildrenBeforeAction(targetRow.children[insertIndex], cell));
            targetRowStartCols.splice(insertIndex, 0, logicalStartCol);
          }
        });
      }
    });
  }
  /**
   * Splits the selected rows into head rows and body rows.
   *
   * @param {RTETableRowBlock[]} rows - The rows to be split.
   * @returns {RTETableRowBlock[][]} - An array containing two arrays: the first with head rows and the second with body rows.
   */
  splitRowsBySection(rows) {
    const headRows = [];
    const bodyRows = [];
    rows.forEach((row) => {
      if (row.parent instanceof RTETableHeadBlock) {
        headRows.push(row);
      }
      else {
        bodyRows.push(row);
      }
    });
    return [headRows, bodyRows];
  }
  /**
   * Build indices for selected rows within the section.
   *
   * @param {RTETableRowBlock[]} selectedRows - Selected rows.
   * @param {RTETableRowBlock[]} allRows - All rows in the section.
   * @returns {Set<number>} The set of selected row indices.
   */
  buildSelectedIndices(selectedRows, allRows) {
    return new Set(selectedRows
      .map((row) => allRows.indexOf(row))
      .filter((index) => index >= 0));
  }
  /**
   * Determine whether a cell's row span intersects the removal range.
   *
   * @param {number} cellStartIndex - Cell start row index.
   * @param {number} cellEndIndex - Cell end row index.
   * @param {number} firstRemovalIndex - First row index to remove.
   * @param {number} lastRemovalIndex - Last row index to remove.
   * @returns {boolean} True if the ranges intersect.
   */
  intersectsRemovalRange(cellStartIndex, cellEndIndex, firstRemovalIndex, lastRemovalIndex) {
    return cellStartIndex <= lastRemovalIndex && cellEndIndex >= firstRemovalIndex;
  }
  /**
   * Count how many rows in the cell span are being removed.
   *
   * @param {number} cellStartIndex - Cell start row index.
   * @param {number} cellEndIndex - Cell end row index.
   * @param {Set<number>} removalIndices - Indices of rows to remove.
   * @returns {number} Count of removed rows in the span.
   */
  countRemovedRows(cellStartIndex, cellEndIndex, removalIndices) {
    let removedRowsCount = 0;
    for (let i = cellStartIndex; i <= cellEndIndex; i++) {
      if (removalIndices.has(i)) {
        removedRowsCount++;
      }
    }
    return removedRowsCount;
  }
  /**
   * Get the column head end index in a table row.
   *
   * @param {RTETableRowBlock} tableRow - The table row block.
   * @returns {number} The end index of the column head.
   */
  getHeadCellLength(tableRow) {
    let length = 0;
    for (let i = 0; i < tableRow.children.length; i++) {
      const cellBlock = tableRow.children[i];
      if (cellBlock instanceof RTETableHeadCellBlock) {
        length += cellBlock.colSpan;
      }
      else {
        break;
      }
    }
    return length;
  }
  /**
   * Gets the starting cell for a given cell index by checking row spans.
   *
   * @param {RTETableRowBlock[]} allRows - All rows in the section to check for row spans.
   * @param {number[][]} rowsStartCols - Pre-calculated starting column indices for each cell in each row.
   * @param {number} rowIndex - The index of the row to start checking from.
   * @param {number} cellIndex - The logical cell index to find the starting cell for.
   * @returns {LogicalStartSpanCell} - The starting cell and its row index if found, otherwise the cell is undefined.
   */
  getTheStartSpanCell(allRows, rowsStartCols, rowIndex, cellIndex) {
    const result = { rowIndex };
    for (let i = rowIndex; i >= 0; i--) {
      const cellStartCols = rowsStartCols[i];
      if (cellStartCols.includes(cellIndex)) {
        result.cell = allRows[i].children[cellStartCols.indexOf(cellIndex)];
        result.rowIndex = i;
        break;
      }
    }
    return result;
  }
  /**
   * Get the default cell block template.
   *
   * @param {string} cellType - The type of the cell block.
   * @param {number} colSpan - The colspan attribute for the cell.
   * @param {number} rowSpan - The rowspan attribute for the cell.
   * @returns {VegaRTETableCellBlock | VegaRTETableHeadCellBlock} The default cell block template.
   */
  getDefaultCellBlockTemplate(cellType, colSpan, rowSpan = 1) {
    const customAttribute = cleanObject({
      colspan: colSpan > 1 ? colSpan.toString() : null,
      rowspan: rowSpan > 1 ? rowSpan.toString() : null,
    });
    const id = generateUUID();
    const cellBlock = {
      id,
      type: cellType,
      children: [
        {
          id: `${id}-p`,
          type: 'paragraph',
          nodes: [
            {
              id: `${id}-p-t`,
              type: 'text',
              text: '',
            },
          ],
        },
      ],
    };
    if (Object.keys(customAttribute).length > 0) {
      cellBlock.annotations = {
        customAttribute,
      };
    }
    return cellBlock;
  }
  /**
   * Convert a numeric-keyed map to an array sorted by ascending key.
   *
   * @typedef T - The type of the values in the map.
   * @param {Map<number, T>} valueMap - The map with numeric keys to convert.
   * @returns {T[]} An array of values sorted by their numeric keys.
   */
  getSortedValuesByNumericKey(valueMap) {
    return Array.from(valueMap.entries())
      .sort(([aKey], [bKey]) => aKey - bKey)
      .map(([, value]) => value);
  }
}
TableRowCommonUtilitiesStrategy.DEFAULT_CELLSPAN = 1;
