import { generateUUID } from '../../../../../utils/misc';
import { RTENode } from '../../../dto/nodes/node.abstract';
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 { CustomAttributeAnnotation } from '../../../dto/annotations/custom-attribute-annotation';
import { CommonAnnotationTypeEnum } from '../../../dto/annotations/common-annotation.abstract';
import { MergeCellsOperationType } from '../setting-event-handlers/merge-cells-operation';
/**
 * Recursively searches for a parent block of a specific type.
 *
 * @typedef T - Generic type extending RTEBlock.
 * @param {RTEBlock} block - The starting block to search from.
 * @param {ClassType<unknown>} dtoClass - The class type to match the parent block against.
 * @returns {T | null} - The found parent block of the specified type, or null if not found.
 */
export function getParentBlockByType(block, dtoClass) {
  if (block) {
    if (block instanceof dtoClass) {
      return block;
    }
    else {
      return getParentBlockByType(block.parent, dtoClass);
    }
  }
  return null;
}
/**
 * Generate the table body with the specified number of rows and columns.
 *
 * @param {number} rowCount - The number of rows in the table body.
 * @param {number} columnCount - The number of columns in the table body.
 * @returns {VegaRTETableBodyBlock} The generated table body block.
 */
export function generateTableBody(rowCount, columnCount) {
  return {
    id: generateUUID(),
    type: 'table-body',
    children: Array.from({ length: rowCount }, () => generateTableRow(columnCount)),
  };
}
/**
 * Create a table row block with the specified number of columns and cell type.
 *
 * @param {number} columnCount - The number of columns in the row.
 * @returns {VegaRTETableRowBlock} The created table row block.
 */
export function generateTableRow(columnCount) {
  return {
    id: generateUUID(),
    type: 'table-row',
    children: Array.from({ length: columnCount }, () => getDefaultCellBlockTemplate('table-cell')),
  };
}
/**
 * Get all table rows from a table block (including rows from all table bodies and headers).
 *
 * @param {RTETableBlock} tableBlock - The table block.
 * @returns {RTETableRowBlock[]} Array of all table row blocks.
 */
export function getAllTableRows(tableBlock) {
  return tableBlock.children
    .filter((child) => child instanceof RTETableBodyBlock || child instanceof RTETableHeadBlock)
    .flatMap((body) => body.children);
}
/**
 * Find the first node within a block recursively.
 *
 * @param {Nullable<RTEBlock>} block - The block to find the first node in.
 * @returns {Nullable<RTENode>} The focusable node within the block.
 */
export function getFirstNode(block) {
  if (!block || !block.children || !block.children[0])
    return null;
  const firstNode = block.children[0];
  if (firstNode instanceof RTENode) {
    return firstNode;
  }
  return getFirstNode(firstNode);
}
/**
 * Find a cell block by its ID across all table rows.
 *
 * @param {RTETableRowBlock[]} allTableRows - All table rows to search within.
 * @param {string} cellId - The unique identifier of the cell to find.
 * @returns {Nullable<RTETableCellBlock | RTETableHeadCellBlock>} The found cell block, or null if not found.
 */
export function findCellBlockById(allTableRows, cellId) {
  for (const row of allTableRows) {
    const found = row.children.find((cell) => cell.id === cellId);
    if (found) {
      return found;
    }
  }
  return null;
}
/**
 * Get the default cell block template.
 *
 * @param {string} cellType - The type of the cell block.
 * @returns {VegaRTETableCellBlock} The default cell block template.
 */
export function getDefaultCellBlockTemplate(cellType) {
  const id = generateUUID();
  return {
    id,
    type: cellType,
    children: [
      {
        id: `${id}-p`,
        type: 'paragraph',
        nodes: [
          {
            id: `${id}-p-t`,
            type: 'text',
            text: '',
          },
        ],
      },
    ],
  };
}
/**
 * Get the actual col span and row span of a cell.
 *
 * @param {RTETableCellBlock | RTETableHeadCellBlock} cell - The cell to check.
 * @returns {CellSpan} The actual span values.
 */
export function getCellSpan(cell) {
  return { colSpan: cell.colSpan, rowSpan: cell.rowSpan };
}
/**
 * Get the actual column position of a cell considering previous cells' colspan
 * and cells from previous rows with rowspan that extend into the current row.
 *
 * @param {RTETableCellBlock | RTETableHeadCellBlock} cell - The cell to check.
 * @param {RTETableRowBlock[]} allRows - The table all rows.
 * @param {string[][]} grid - The 2D grid representation of the table
 * @returns {number} The actual column position.
 */
export function getActualColumnPosition(cell, allRows, grid) {
  const tableGrid = grid !== null && grid !== void 0 ? grid : buildTableGrid(allRows);
  for (let row = 0; row < tableGrid.length; row++) {
    const cols = tableGrid[row];
    for (let col = 0; col < cols.length; col++) {
      if (cols[col] === cell.id) {
        return col;
      }
    }
  }
  return -1;
}
/**
 * Build a 2D grid representation of a table section where each occupied position
 * contains the cell id covering that (row, col) coordinate, accounting for colSpan/rowSpan.
 *
 * @param {RTETableRowBlock[]} allRows - All rows in the current table section.
 * @returns {string[][]} The table grid.
 */
export function buildTableGrid(allRows) {
  const grid = [];
  allRows.forEach((row, rowIndex) => {
    if (!grid[rowIndex]) {
      grid[rowIndex] = [];
    }
    let colIndex = 0;
    row.children.forEach((cell) => {
      const { colSpan, rowSpan } = getCellSpan(cell);
      while (grid[rowIndex][colIndex] !== undefined) {
        colIndex++;
      }
      for (let r = 0; r < rowSpan; r++) {
        for (let c = 0; c < colSpan; c++) {
          if (!grid[rowIndex + r]) {
            grid[rowIndex + r] = [];
          }
          grid[rowIndex + r][colIndex + c] = cell.id;
        }
      }
      colIndex += colSpan;
    });
  });
  return grid;
}
/**
 * Get the adjacent cell in the specified direction based on the table matrix.
 *
 * @param {RTETableCellBlock | RTETableHeadCellBlock} cell - The current cell.
 * @param {AdjacentCellDirection} direction - The direction to locate the adjacent cell.
 * @returns {RTETableCellBlock | RTETableHeadCellBlock | null} The adjacent cell or null.
 */
export function getAdjacentCell(cell, direction) {
  if (!cell) {
    return null;
  }
  const row = cell.parent;
  const sectionRows = row.parent.children;
  const rowIndex = sectionRows.indexOf(row);
  const { colSpan, rowSpan } = getCellSpan(cell);
  const cellColPosition = getActualColumnPosition(cell, sectionRows);
  let targetRowIndex = rowIndex;
  let targetColPosition = cellColPosition;
  switch (direction) {
    case MergeCellsOperationType.MERGE_CELL_LEFT:
      targetColPosition = cellColPosition - 1;
      break;
    case MergeCellsOperationType.MERGE_CELL_RIGHT:
      targetColPosition = cellColPosition + colSpan;
      break;
    case MergeCellsOperationType.MERGE_CELL_UP:
      targetRowIndex = rowIndex - 1;
      break;
    default:
      targetRowIndex = rowIndex + rowSpan;
      break;
  }
  return findCellAtPosition(sectionRows, targetRowIndex, targetColPosition);
}
/**
 * Find the cell that covers the specified matrix position.
 *
 * @param {RTETableRowBlock[]} rows - Rows in the current table section.
 * @param {number} targetRowIndex - Target matrix row index.
 * @param {number} targetColPosition - Target matrix column position.
 * @returns {RTETableCellBlock | RTETableHeadCellBlock | null} The covering cell or null.
 */
export function findCellAtPosition(rows, targetRowIndex, targetColPosition) {
  const tableGrid = buildTableGrid(rows);
  for (let r = 0; r < rows.length; r++) {
    const currentRow = rows[r];
    for (const candidate of currentRow.children) {
      const { colSpan, rowSpan } = getCellSpan(candidate);
      const candidateColPosition = getActualColumnPosition(candidate, rows, tableGrid);
      const candidateRowStart = r;
      const candidateRowEnd = r + rowSpan - 1;
      const candidateColEnd = candidateColPosition + colSpan - 1;
      const isSameMatrixRow = targetRowIndex >= candidateRowStart && targetRowIndex <= candidateRowEnd;
      const isTargetColumnCovered = targetColPosition >= candidateColPosition && targetColPosition <= candidateColEnd;
      if (isSameMatrixRow && isTargetColumnCovered) {
        return candidate;
      }
    }
  }
  return null;
}
/**
 * Get the rectangular bounds of selected cells.
 * Calculates the bounding box (minRow, maxRow, minCol, maxCol) and returns related information.
 *
 * @param {RTETableCellBlock[] | RTETableHeadCellBlock[]} selectedCells - The selected table cells.
 * @returns {RectangularBounds} The rectangular bounds' information.
 */
export function getRectangularBounds(selectedCells) {
  const firstCell = selectedCells[0];
  const firstRow = firstCell.parent;
  const table = firstRow.parent;
  const allRows = table.children;
  const tableGrid = buildTableGrid(allRows);
  let minRow = Number.POSITIVE_INFINITY;
  let maxRow = -1;
  let minCol = Number.POSITIVE_INFINITY;
  let maxCol = -1;
  const coveredPositions = new Set();
  selectedCells.forEach((cell) => {
    const row = cell.parent;
    const rowIndex = allRows.indexOf(row);
    const actualColPosition = getActualColumnPosition(cell, allRows, tableGrid);
    const { colSpan, rowSpan } = getCellSpan(cell);
    const cellMinRow = rowIndex;
    const cellMaxRow = rowIndex + rowSpan - 1;
    const cellMinCol = actualColPosition;
    const cellMaxCol = actualColPosition + colSpan - 1;
    for (let r = cellMinRow; r <= cellMaxRow; r++) {
      for (let c = cellMinCol; c <= cellMaxCol; c++) {
        coveredPositions.add(`${r},${c}`);
      }
    }
    minRow = Math.min(minRow, cellMinRow);
    maxRow = Math.max(maxRow, cellMaxRow);
    minCol = Math.min(minCol, cellMinCol);
    maxCol = Math.max(maxCol, cellMaxCol);
  });
  const targetCell = selectedCells[0];
  return {
    minRow,
    maxRow,
    minCol,
    maxCol,
    targetCell,
    coveredPositions,
  };
}
/**
 * Calculate the total number of columns in the table.
 * Accounts for cells with col span.
 *
 * @param {RTETableBlock} table - The table block.
 * @returns {number} The total number of columns in the table.
 */
export function getTableColumnCount(table) {
  const rows = getAllTableRows(table);
  // Calculate the total columns from the first row, accounting for col span
  let totalColumns = 0;
  const firstRow = rows[0];
  for (const cell of firstRow.children) {
    const { colSpan } = getCellSpan(cell);
    totalColumns += colSpan;
  }
  return totalColumns;
}
/**
 * Check if the selected cells form a rectangular area.
 * Considers cells with col span and row span.
 *
 * @param {RTETableCellBlock[] | RTETableHeadCellBlock[]} selectedCells - The selected table cells.
 * @returns {boolean} True if the selected cells form a rectangle, false otherwise.
 */
export function isRectangularSelection(selectedCells) {
  if (selectedCells.length === 1) {
    return true;
  }
  const bounds = getRectangularBounds(selectedCells);
  const { minRow, maxRow, minCol, maxCol, coveredPositions } = bounds;
  const expectedPositions = (maxRow - minRow + 1) * (maxCol - minCol + 1);
  if (coveredPositions.size !== expectedPositions) {
    return false;
  }
  return true;
}
/**
 * Set the col span and row span attributes for a target cell.
 * When colSpan equals the table's total columns, rowSpan is removed.
 *
 * @param {RTETableCellBlock | RTETableHeadCellBlock} targetCell - The target cell to set attributes on.
 * @param {number} colSpan - The column span value.
 * @param {number} rowSpan - The row span value.
 * @param {RTETableBlock} table - The table block.
 * @param {boolean} insertColumn - Whether the operation is an insert-column action.
 */
export function setSpanAttributes(targetCell, colSpan, rowSpan, table, insertColumn) {
  const existingAnnotation = targetCell.annotationMap.get(CommonAnnotationTypeEnum.CUSTOM_ATTRIBUTE);
  const existingAttrs = existingAnnotation
    ? Object.assign({}, existingAnnotation['customAttribute']) : {};
  delete existingAttrs.colspan;
  delete existingAttrs.rowspan;
  if (colSpan > 1) {
    existingAttrs.colspan = String(colSpan);
  }
  // When colspan equals table column count, don't set rowspan
  const tableColumnCount = getTableColumnCount(table) + (insertColumn ? 1 : 0);
  if (rowSpan > 1 && colSpan !== tableColumnCount) {
    existingAttrs.rowspan = String(rowSpan);
  }
  if (Object.keys(existingAttrs).length > 0) {
    const customAttributeAnnotation = new CustomAttributeAnnotation(existingAttrs);
    targetCell.annotationMap.set(CommonAnnotationTypeEnum.CUSTOM_ATTRIBUTE, customAttributeAnnotation);
  }
  else {
    targetCell.annotationMap.delete(CommonAnnotationTypeEnum.CUSTOM_ATTRIBUTE);
  }
}
/**
 * Create a new cell of the specified type.
 *
 * @param {string} cellType - The type of cell ('table-cell' or 'table-head-cell').
 * @returns {RTETableCellBlock | RTETableHeadCellBlock} The newly created cell.
 */
export function createNewCell(cellType) {
  const cellTemplate = getDefaultCellBlockTemplate(cellType);
  if (cellType === 'table-head-cell') {
    return RTETableHeadCellBlock.from(cellTemplate);
  }
  return RTETableCellBlock.from(cellTemplate);
}
/**
 * Find the first logical column index in a row that is not a header cell.
 *
 * @param {string[]} gridRow - The grid row array.
 * @param {RTETableRowBlock} row - The table row block.
 * @param {number} maxColLength - The maximum number of columns.
 * @returns {number} The first non-header logical column index, or -1 if not found.
 */
export function findFirstBodyColIndex(gridRow, row, maxColLength) {
  for (let colIndex = 0; colIndex < maxColLength; colIndex++) {
    const cellId = gridRow[colIndex];
    const cellBlock = row.children.find((c) => c.id === cellId);
    if (cellBlock && !(cellBlock instanceof RTETableHeadCellBlock)) {
      return colIndex;
    }
  }
  return -1;
}
/**
 * Build a map of cell id to its first logical column index in the table grid.
 *
 * @param {string[][]} tableGrid - The table grid.
 * @returns {Map<string, number>} Cell id to start column index.
 */
export function buildCellStartColumnMap(tableGrid) {
  const cellStartColMap = new Map();
  tableGrid.forEach((gridRow) => {
    for (let col = 0; col < gridRow.length; col++) {
      const cellId = gridRow[col];
      if (cellId && !cellStartColMap.has(cellId)) {
        cellStartColMap.set(cellId, col);
      }
    }
  });
  return cellStartColMap;
}
/**
 * Find the index range of the table selection.
 * Returns the starting and ending indices of the selected cells.
 *
 * @param {(RTETableCellBlock | RTETableHeadCellBlock)[]} selectedCells - The selected table cells.
 * @param {RTETableBlock} table - The table block
 * @param {RTETableRowBlock[]} allTableRows - All table rows.
 * @param {string[][]} grid - The 2D grid representation of the table
 * @returns {[number, number]} A tuple containing the start and end indices.
 */
export function getSelectedCellsBoundary(selectedCells, table, allTableRows, grid) {
  if (selectedCells.length === 0) {
    return [0, -1];
  }
  const tableRows = allTableRows ? allTableRows : getAllTableRows(table);
  const tableGrid = grid !== null && grid !== void 0 ? grid : buildTableGrid(tableRows);
  let minCol = Number.POSITIVE_INFINITY;
  let maxCol = Number.NEGATIVE_INFINITY;
  selectedCells.forEach((cell) => {
    const logicalCol = getActualColumnPosition(cell, tableRows, tableGrid);
    minCol = Math.min(minCol, logicalCol);
    maxCol = Math.max(maxCol, logicalCol);
  });
  return [minCol, maxCol];
}
