import { generateUUID } from '../../../../../utils/misc';
import { rgbToHex } from '../../../../../utils/ui';
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 { RemoveChildrenAction } from '../../../dto/actions/remove-child-action';
import { UpdateCursorPositionAction } from '../../../dto/actions/update-cursor-position-action';
import { RTETableHeadCellAnnotation } from '../annotations/table-head-cell-annotation';
import { RTETableBodyBlock } from '../dto/table-body-block';
import { RTETableCellBlock } from '../dto/table-cell-block';
import { RTETableHeadBlock } from '../dto/table-head-block';
import { RTETableHeadCellBlock } from '../dto/table-head-cell-block';
import { TableRowCommonUtilitiesStrategy } from './table-row-strategy.abstract';
/**
 * Strategy class for handling converting selected rows to header rows or reverting them back to normal rows.
 */
export class ToggleHeaderRowStrategy extends TableRowCommonUtilitiesStrategy {
  /**
   * @inheritDoc
   */
  handleAction(action, target) {
    const { selectedRows } = action;
    const [headerRows, bodyRows] = this.splitRowsBySection(selectedRows);
    if (bodyRows.length > 0) {
      this.convertToHeaderRows(bodyRows, target);
    }
    else {
      this.revertToBodyRows(headerRows, target);
    }
    const root = this.getRootContent(target);
    if (root) {
      const focusNode = selectedRows[0].getFirstNode();
      focusNode && root.apply(new UpdateCursorPositionAction(focusNode, 0));
    }
  }
  /**
   * Converts normal rows to header rows.
   *
   * @param {RTETableRowBlock[]} rows - The table row blocks to convert.
   * @param {RTETableBlock} table - The table block containing the rows.
   */
  convertToHeaderRows(rows, table) {
    let tableHead = table.children.find((block) => block instanceof RTETableHeadBlock);
    const tableBody = rows[0].parent;
    if (!tableHead) {
      tableHead = RTETableHeadBlock.from({
        id: generateUUID(),
        type: 'table-head',
        children: [],
      });
      table.apply(new InsertChildrenBeforeAction(tableBody, tableHead));
    }
    const endIndex = tableBody.children.indexOf(rows[rows.length - 1]);
    const shouldConvertRows = tableBody.children.slice(0, endIndex + 1);
    const rowsToBeConvertedIndices = this.buildSelectedIndices(shouldConvertRows, tableBody.children);
    const firstSelectedIndex = Math.min(...rowsToBeConvertedIndices);
    const lastSelectedIndex = Math.max(...rowsToBeConvertedIndices);
    if (rowsToBeConvertedIndices.size > 0) {
      if (rowsToBeConvertedIndices.size < tableBody.children.length) {
        const rowCellStartCols = this.getRowCellStartCols(tableBody.children);
        const cellsToMove = this.collectCellsToMove(tableBody.children, rowCellStartCols, rowsToBeConvertedIndices, firstSelectedIndex, lastSelectedIndex);
        this.insertMovedCells(cellsToMove, tableBody.children, rowCellStartCols);
      }
      this.convertBodyRowsToHeaderRows(tableBody, tableHead, shouldConvertRows);
    }
  }
  /**
   * Converts body rows to header rows.
   *
   * @param {RTETableBodyBlock} tableBody - The table body block containing the rows to be converted.
   * @param {RTETableHeadBlock} tableHead - The table head block to which the rows will be moved.
   * @param {RTETableRowBlock[]} shouldConvertRows - The rows to be converted.
   */
  convertBodyRowsToHeaderRows(tableBody, tableHead, shouldConvertRows) {
    for (let i = 0; i < shouldConvertRows.length; i++) {
      const rowBlock = shouldConvertRows[i];
      rowBlock.children = rowBlock.children.map((cell) => this.convertTdToTh(cell));
      tableBody.apply(new RemoveChildrenAction(rowBlock));
    }
    tableHead.apply(new AppendChildrenAction(shouldConvertRows));
  }
  /**
   * Converts header rows back to normal rows.
   *
   * @param {RTETableRowBlock[]} rows - The table row blocks to convert.
   * @param {RTETableBlock} table - The table block containing the rows.
   */
  revertToBodyRows(rows, table) {
    let tableBody = table.children.find((block) => block instanceof RTETableBodyBlock);
    const tableHead = rows[0].parent;
    if (!tableBody) {
      tableBody = RTETableBodyBlock.from({
        id: generateUUID(),
        type: 'table-body',
        children: [],
      });
      table.apply(new InsertChildrenAfterAction(tableHead, tableBody));
    }
    const startIndex = tableHead.children.indexOf(rows[0]);
    const shouldConvertRows = tableHead.children.slice(startIndex);
    const rowsToBeConvertedIndices = this.buildSelectedIndices(shouldConvertRows, tableHead.children);
    if (rowsToBeConvertedIndices.size > 0) {
      const rowCellStartCols = this.getRowCellStartCols(tableHead.children);
      const cellsToMove = this.collectCellsToMove(tableHead.children, rowCellStartCols, rowsToBeConvertedIndices, Math.min(...rowsToBeConvertedIndices), Math.max(...rowsToBeConvertedIndices));
      this.insertMovedCells(cellsToMove, tableHead.children, rowCellStartCols);
      this.convertHeadRowsToBodyRows(tableHead, tableBody, shouldConvertRows);
    }
  }
  /**
   * Convert header rows to body rows.
   *
   * @param {RTETableHeadBlock} tableHead - The table head block containing the rows to be converted.
   * @param {RTETableBodyBlock} tableBody - The table body block to which the rows will be moved.
   * @param {RTETableRowBlock[]} shouldConvertRows - The rows to be converted.
   */
  convertHeadRowsToBodyRows(tableHead, tableBody, shouldConvertRows) {
    const firstBodyRow = tableBody.children[0];
    const rowCellStartCols = this.getRowCellStartCols(shouldConvertRows);
    for (let i = 0; i < shouldConvertRows.length; i++) {
      const rowBlock = shouldConvertRows[i];
      tableHead.apply(new RemoveChildrenAction(rowBlock));
      const headCellLength = firstBodyRow ? this.getHeadCellLength(firstBodyRow) : 0;
      rowBlock.children = this.convertThToTd(rowBlock, rowCellStartCols[i], headCellLength);
      if (firstBodyRow) {
        tableBody.apply(new InsertChildrenBeforeAction(firstBodyRow, rowBlock));
      }
    }
    if (!firstBodyRow) {
      tableBody.apply(new AppendChildrenAction(shouldConvertRows));
    }
  }
  /**
   * Collect cells that need to be moved to the next remaining row.
   *
   * @param {RTETableRowBlock[]} allRows - All rows in the section.
   * @param {number[][]} rowCellStartCols - Logical start columns per row.
   * @param {Set<number>} selectedIndices - Indices of selected rows.
   * @param {number} firstSelectedIndex - First selected row index.
   * @param {number} lastSelectedIndex - Last selected row index.
   * @returns {Map<number, CellToMove[]>} Map of target row index to cells to move.
   */
  collectCellsToMove(allRows, rowCellStartCols, selectedIndices, firstSelectedIndex, lastSelectedIndex) {
    const cellsToMove = new Map();
    const rowCount = allRows.length;
    for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
      const row = allRows[rowIndex];
      const cellCount = row.children.length;
      for (let cellIndex = 0; cellIndex < cellCount; cellIndex++) {
        const cell = row.children[cellIndex];
        const cellRowSpan = cell.rowSpan;
        // Skip cells without rowspan
        if (cellRowSpan <= TableRowCommonUtilitiesStrategy.DEFAULT_CELLSPAN)
          continue;
        const cellEndIndex = rowIndex + cellRowSpan - 1;
        // If the cell's row span does not intersect with the removal range, skip it
        if (!this.intersectsRemovalRange(rowIndex, cellEndIndex, firstSelectedIndex, lastSelectedIndex)) {
          continue;
        }
        const selectedRowsCount = this.countRemovedRows(rowIndex, cellEndIndex, selectedIndices);
        const newRowSpan = cellRowSpan - selectedRowsCount;
        // Scenario 1: Cell starts before selected range and spans into it, head row convert to body row
        if (rowIndex < firstSelectedIndex && cellEndIndex >= firstSelectedIndex) {
          this.updateCellSpan(cell, 'rowspan', newRowSpan);
          const targetRowIndex = firstSelectedIndex;
          if (targetRowIndex < rowCount && selectedIndices.has(targetRowIndex)) {
            const logicalStartCol = rowCellStartCols[rowIndex][cellIndex];
            if (!cellsToMove.has(targetRowIndex)) {
              cellsToMove.set(targetRowIndex, []);
            }
            cellsToMove.get(targetRowIndex).push({
              cell: this.cloneCellWithRowSpan(cell, selectedRowsCount),
              newRowSpan: selectedRowsCount,
              cellIndex,
              logicalStartCol,
            });
          }
          continue;
        }
        // Scenario 2: Cell starts in selected row and spans beyond, we need to move it to the next remaining row and update rowspan. body row convert to head row.
        if (rowIndex >= firstSelectedIndex && cellEndIndex > lastSelectedIndex) {
          this.updateCellSpan(cell, 'rowspan', selectedRowsCount);
          const targetRowIndex = lastSelectedIndex + 1;
          if (targetRowIndex < rowCount && !selectedIndices.has(targetRowIndex)) {
            const logicalStartCol = rowCellStartCols[rowIndex][cellIndex];
            if (!cellsToMove.has(targetRowIndex)) {
              cellsToMove.set(targetRowIndex, []);
            }
            cellsToMove.get(targetRowIndex).push({
              cell: this.cloneCellWithRowSpan(cell, newRowSpan),
              newRowSpan,
              cellIndex,
              logicalStartCol,
            });
          }
        }
      }
    }
    return cellsToMove;
  }
  /**
   * Converts a table data cell to a table header cell.
   *
   * @param {RTETableHeadCellBlock} cell - The table cell block to convert.
   * @returns {RTETableHeadCellBlock} The converted table header cell block.
   */
  convertTdToTh(cell) {
    if (cell instanceof RTETableHeadCellBlock) {
      return cell;
    }
    const headCell = RTETableHeadCellBlock.from(cell.toJSON());
    headCell.parent = cell.parent;
    return headCell;
  }
  /**
   * Converts table header cells to table body cells.
   * Need to check each cell's logical column index to determine whether it should be converted or not.
   *
   * @param {RTETableRowBlock} row - The table row block containing the cells.
   * @param {number[]} rowCellStartCol - The starting column indices for each cell in the row.
   * @param {number} headCellLength - The number of header cells in the reference row.
   * @returns {RTETableCellBlock[]} The converted table body cell blocks.
   */
  convertThToTd(row, rowCellStartCol, headCellLength) {
    return row.children.map((cell, index) => {
      let bodyCell = cell;
      const isHeadCell = rowCellStartCol[index] < headCellLength ? true : false;
      const cellJSON = cell.toJSON();
      bodyCell = isHeadCell
        ? cell
        : RTETableCellBlock.from(this.deleteHeadCellDefaultBackground(cellJSON));
      bodyCell.parent = row;
      return bodyCell;
    });
  }
  /**
   * Clones a table cell with a new rowspan value, used when moving cells during row conversion.
   * For example, when converting body rows contains a cell with rowspan, we need to clone a new empty cell with same colspan and insert it into the remaining row, and update the original cell's rowspan to make sure the table structure is correct after conversion.
   *
   * @param {RTETableCellBlock} cell - The cell to clone.
   * @param {number} newRowSpan - The new rowspan value for the cloned cell.
   * @returns {RTETableCellBlock} The cloned cell with updated rowspan.
   */
  cloneCellWithRowSpan(cell, newRowSpan) {
    // Clone the cell (parent will be updated when inserted)
    const clonedCell = cell.clone(cell.parent);
    clonedCell.id = generateUUID();
    clonedCell.children = []; // Clear children to make it an empty cell
    const paragraph = this.createNewParagraph('');
    paragraph && clonedCell.apply(new AppendChildrenAction([paragraph]));
    // Update the rowspan of the cloned cell
    this.updateCellSpan(clonedCell, 'rowspan', newRowSpan);
    return clonedCell;
  }
  /**
   * Delete the default background color annotation from a header cell when converting it to a body cell.
   *
   * @param {VegaRTETableHeadCellBlock} cell - The table head cell block to process.
   * @returns {VegaRTETableCellBlock} The processed table cell block with default background color annotation removed if applicable.
   */
  deleteHeadCellDefaultBackground(cell) {
    var _a;
    if (cell && cell.annotations && cell.annotations.customStyle) {
      delete cell.annotations.tableCell;
      const backgroundColor = cell.annotations.customStyle.backgroundColor;
      const defaultBackgroundColor = RTETableHeadCellAnnotation.getDefaultBackgroundColor();
      if (backgroundColor &&
        (backgroundColor === defaultBackgroundColor ||
          ((_a = rgbToHex(backgroundColor)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) === defaultBackgroundColor)) {
        delete cell.annotations.customStyle.backgroundColor;
      }
    }
    return cell;
  }
}
