/**
 * This file provides set of helper functions to test nsIAccessibleTable
 * interface.
 *
 * Required:
 *   common.js
 *   role.js
 *   states.js
 */

/**
 * Constants used to describe cells array.
 */
const kDataCell = 1; // Indicates the cell is origin data cell
const kRowHeaderCell = 2; // Indicates the cell is row header cell
const kColHeaderCell = 4; // Indicated the cell is column header cell
const kOrigin = kDataCell | kRowHeaderCell | kColHeaderCell;

const kRowSpanned = 8; // Indicates the cell is not origin and row spanned
const kColSpanned = 16; // Indicates the cell is not origin and column spanned
const kSpanned = kRowSpanned | kColSpanned;

/**
 * Constants to define column header type.
 */
const kNoColumnHeader = 0;
const kListboxColumnHeader = 1;
const kTreeColumnHeader = 2;

/**
 * Constants to define table type.
 */
const kTable = 0;
const kTreeTable = 1;
const kMathTable = 2;

/**
 * Test table structure and related methods.
 *
 * @param  aIdentifier     [in] table accessible identifier
 * @param  aCellsArray     [in] two dimensional array (row X columns) of
 *                          cell types (see constants defined above).
 * @param  aColHeaderType  [in] specifies wether column header cells are
 *                          arranged into the list.
 * @param  aCaption        [in] caption text if any
 * @param  aSummary        [in] summary text if any
 * @param  aTableType      [in] specifies the table type.
 * @param  aRowRoles       [in] array of row roles.
 */
function testTableStruct(aIdentifier, aCellsArray, aColHeaderType,
                         aCaption, aSummary, aTableType, aRowRoles) {
  var tableNode = getNode(aIdentifier);
  var isGrid = tableNode.getAttribute("role") == "grid" ||
    tableNode.getAttribute("role") == "treegrid" ||
    tableNode.localName == "tree";

  var rowCount = aCellsArray.length;
  var colsCount = aCellsArray[0] ? aCellsArray[0].length : 0;

  // Test table accessible tree.
  var tableObj = {
    children: [],
  };
  switch (aTableType) {
    case kTable:
      tableObj.role = ROLE_TABLE;
      break;
    case kTreeTable:
      tableObj.role = ROLE_TREE_TABLE;
      break;
    case kMathTable:
      tableObj.role = ROLE_MATHML_TABLE;
      break;
  }

  // caption accessible handling
  if (aCaption) {
    var captionObj = {
      role: ROLE_CAPTION,
      children: [
        {
          role: ROLE_TEXT_LEAF,
          name: aCaption,
        },
      ],
    };

    tableObj.children.push(captionObj);
  }

  // special types of column headers handling
  if (aColHeaderType) {
    var headersObj = {
      role: ROLE_LIST,
      children: [],
    };

    for (let idx = 0; idx < colsCount; idx++) {
      var headerCellObj = {
        role: ROLE_COLUMNHEADER,
      };
      headersObj.children.push(headerCellObj);
    }

    if (aColHeaderType == kTreeColumnHeader) {
      var columnPickerObj = {
        role: ROLE_PUSHBUTTON,
      };

      headersObj.children.push(columnPickerObj);
    }

    tableObj.children.push(headersObj);
  }

  // rows and cells accessibles
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    let rowObj = {
      role: aRowRoles ? aRowRoles[rowIdx] : ROLE_ROW,
      children: [],
    };

    for (let colIdx = 0; colIdx < colsCount; colIdx++) {
      let celltype = aCellsArray[rowIdx][colIdx];

      var role = ROLE_NOTHING;
      switch (celltype) {
        case kDataCell:
          role = (aTableType == kMathTable ? ROLE_MATHML_CELL :
                  (isGrid ? ROLE_GRID_CELL : ROLE_CELL));
          break;
        case kRowHeaderCell:
          role = ROLE_ROWHEADER;
          break;
        case kColHeaderCell:
          role = ROLE_COLUMNHEADER;
          break;
      }

      if (role != ROLE_NOTHING) {
        var cellObj = { role };
        rowObj.children.push(cellObj);
      }
    }

    tableObj.children.push(rowObj);
  }

  testAccessibleTree(aIdentifier, tableObj);

  // Test table table interface.
  var table = getAccessible(aIdentifier, [nsIAccessibleTable]);

  // summary
  if (aSummary)
    is(table.summary, aSummary,
       "Wrong summary of the table " + prettyName(aIdentifier));

  // rowCount and columnCount
  is(table.rowCount, rowCount,
     "Wrong rows count of " + prettyName(aIdentifier));
  is(table.columnCount, colsCount,
     "Wrong columns count of " + prettyName(aIdentifier));

  // rows and columns extents
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    for (let colIdx = 0; colIdx < colsCount; colIdx++) {
      let celltype = aCellsArray[rowIdx][colIdx];
      if (celltype & kOrigin) {
        // table getRowExtentAt
        var rowExtent = table.getRowExtentAt(rowIdx, colIdx);
        let idx;
        for (idx = rowIdx + 1;
             idx < rowCount && (aCellsArray[idx][colIdx] & kRowSpanned);
             idx++);

        var expectedRowExtent = idx - rowIdx;
        is(rowExtent, expectedRowExtent,
           "getRowExtentAt: Wrong number of spanned rows at (" + rowIdx + ", " +
           colIdx + ") for " + prettyName(aIdentifier));

        // table getColumnExtentAt
        var colExtent = table.getColumnExtentAt(rowIdx, colIdx);
        for (idx = colIdx + 1;
             idx < colsCount && (aCellsArray[rowIdx][idx] & kColSpanned);
             idx++);

        var expectedColExtent = idx - colIdx;
        is(colExtent, expectedColExtent,
           "getColumnExtentAt: Wrong number of spanned columns at (" + rowIdx +
           ", " + colIdx + ") for " + prettyName(aIdentifier));

        // cell rowExtent and columnExtent
        var cell = getAccessible(table.getCellAt(rowIdx, colIdx),
                                 [nsIAccessibleTableCell]);

        is(cell.rowExtent, expectedRowExtent,
           "rowExtent: Wrong number of spanned rows at (" + rowIdx + ", " +
           colIdx + ") for " + prettyName(aIdentifier));

        is(cell.columnExtent, expectedColExtent,
           "columnExtent: Wrong number of spanned column at (" + rowIdx + ", " +
           colIdx + ") for " + prettyName(aIdentifier));
      }
    }
  }
}

/**
 * Test table indexes.
 *
 * @param  aIdentifier  [in] table accessible identifier
 * @param  aIdxes       [in] two dimensional array of cell indexes
 */
function testTableIndexes(aIdentifier, aIdxes) {
  var tableAcc = getAccessible(aIdentifier, [nsIAccessibleTable]);
  if (!tableAcc)
    return;

  var obtainedRowIdx, obtainedColIdx, obtainedIdx;
  var cellAcc;

  var id = prettyName(aIdentifier);

  var rowCount = aIdxes.length;
  for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    var colCount = aIdxes[rowIdx].length;
    for (var colIdx = 0; colIdx < colCount; colIdx++) {
      var idx = aIdxes[rowIdx][colIdx];

      // getCellAt
      try {
        cellAcc = null;
        cellAcc = tableAcc.getCellAt(rowIdx, colIdx);
      } catch (e) { }

      ok(idx != -1 && cellAcc || idx == -1 && !cellAcc,
         id + ": Can't get cell accessible at row = " + rowIdx + ", column = " + colIdx);

      if (idx != -1) {
        // getRowIndexAt
        var origRowIdx = rowIdx;
        while (origRowIdx > 0 &&
               aIdxes[rowIdx][colIdx] == aIdxes[origRowIdx - 1][colIdx])
          origRowIdx--;

        try {
          obtainedRowIdx = tableAcc.getRowIndexAt(idx);
        } catch (e) {
          ok(false, id + ": can't get row index for cell index " + idx + "," + e);
        }

        is(obtainedRowIdx, origRowIdx,
           id + ": row for index " + idx + " is not correct (getRowIndexAt)");

        // getColumnIndexAt
        var origColIdx = colIdx;
        while (origColIdx > 0 &&
               aIdxes[rowIdx][colIdx] == aIdxes[rowIdx][origColIdx - 1])
          origColIdx--;

        try {
          obtainedColIdx = tableAcc.getColumnIndexAt(idx);
        } catch (e) {
          ok(false, id + ": can't get column index for cell index " + idx + "," + e);
        }

        is(obtainedColIdx, origColIdx,
           id + ": column  for index " + idx + " is not correct (getColumnIndexAt)");

        // getRowAndColumnIndicesAt
        var obtainedRowIdxObj = { }, obtainedColIdxObj = { };
        try {
          tableAcc.getRowAndColumnIndicesAt(idx, obtainedRowIdxObj,
                                            obtainedColIdxObj);
        } catch (e) {
          ok(false, id + ": can't get row and column indices for cell index " + idx + "," + e);
        }

        is(obtainedRowIdxObj.value, origRowIdx,
           id + ": row for index " + idx + " is not correct (getRowAndColumnIndicesAt)");
        is(obtainedColIdxObj.value, origColIdx,
           id + ": column  for index " + idx + " is not correct (getRowAndColumnIndicesAt)");

        if (cellAcc) {
          var cellId = prettyName(cellAcc);
          cellAcc = getAccessible(cellAcc, [nsIAccessibleTableCell]);

          // cell: 'table-cell-index' attribute
          var attrs = cellAcc.attributes;
          var strIdx = "";
          try {
            strIdx = attrs.getStringProperty("table-cell-index");
          } catch (e) {
            ok(false,
               cellId + ": no cell index from object attributes on the cell accessible at index " + idx + ".");
          }

          if (strIdx) {
            is(parseInt(strIdx), idx,
                cellId + ": cell index from object attributes of cell accessible isn't corrent.");
          }

          // cell: table
          try {
            is(cellAcc.table, tableAcc,
               cellId + ": wrong table accessible for the cell.");
          } catch (e) {
            ok(false,
               cellId + ": can't get table accessible from the cell.");
          }

          // cell: getRowIndex
          try {
            obtainedRowIdx = cellAcc.rowIndex;
          } catch (e) {
            ok(false,
               cellId + ": can't get row index of the cell at index " + idx + "," + e);
          }

          is(obtainedRowIdx, origRowIdx,
             cellId + ": row for the cell at index " + idx + " is not correct");

          // cell: getColumnIndex
          try {
            obtainedColIdx = cellAcc.columnIndex;
          } catch (e) {
            ok(false,
               cellId + ": can't get column index of the cell at index " + idx + "," + e);
          }

          is(obtainedColIdx, origColIdx,
             id + ": column for the cell at index " + idx + " is not correct");
        }
      }

      // getCellIndexAt
      try {
        obtainedIdx = tableAcc.getCellIndexAt(rowIdx, colIdx);
      } catch (e) {
        obtainedIdx = -1;
      }

      is(obtainedIdx, idx,
         id + ": row " + rowIdx + " /column " + colIdx + " and index " + obtainedIdx + " aren't inconsistent.");
    }
  }
}

/**
 * Test table getters selection methods.
 *
 * @param  aIdentifier  [in] table accessible identifier
 * @param  aCellsArray  [in] two dimensional array (row X columns) of cells
 *                       states (either boolean (selected/unselected) if cell is
 *                       origin, otherwise kRowSpanned or kColSpanned constant).
 * @param  aMsg         [in] text appended before every message
 */
function testTableSelection(aIdentifier, aCellsArray, aMsg) {
  var msg = aMsg ? aMsg : "";
  var acc = getAccessible(aIdentifier, [nsIAccessibleTable]);
  if (!acc)
    return;

  var rowCount = aCellsArray.length;
  var colsCount = aCellsArray[0].length;

  // Columns selection tests.
  var selCols = [];

  // isColumnSelected test
  for (let colIdx = 0; colIdx < colsCount; colIdx++) {
    var isColSelected = true;
    for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
      if (!aCellsArray[rowIdx][colIdx] ||
          aCellsArray[rowIdx][colIdx] == undefined) {
        isColSelected = false;
        break;
      }
    }

    is(acc.isColumnSelected(colIdx), isColSelected,
       msg + "Wrong selection state of " + colIdx + " column for " +
       prettyName(aIdentifier));

    if (isColSelected)
      selCols.push(colIdx);
  }

  // selectedColsCount test
  is(acc.selectedColumnCount, selCols.length,
     msg + "Wrong count of selected columns for " + prettyName(aIdentifier));

  // getSelectedColumns test
  var actualSelCols = acc.getSelectedColumnIndices();

  var actualSelColsCount = actualSelCols.length;
  is(actualSelColsCount, selCols.length,
      msg + "Wrong count of selected columns for " + prettyName(aIdentifier) +
      "from getSelectedColumns.");

  for (let i = 0; i < actualSelColsCount; i++) {
    is(actualSelCols[i], selCols[i],
        msg + "Column at index " + selCols[i] + " should be selected.");
  }

  // Rows selection tests.
  var selRows = [];

  // isRowSelected test
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    var isRowSelected = true;
    for (let colIdx = 0; colIdx < colsCount; colIdx++) {
      if (!aCellsArray[rowIdx][colIdx] ||
          aCellsArray[rowIdx][colIdx] == undefined) {
        isRowSelected = false;
        break;
      }
    }

    is(acc.isRowSelected(rowIdx), isRowSelected,
       msg + "Wrong selection state of " + rowIdx + " row for " +
       prettyName(aIdentifier));

    if (isRowSelected)
      selRows.push(rowIdx);
  }

  // selectedRowCount test
  is(acc.selectedRowCount, selRows.length,
     msg + "Wrong count of selected rows for " + prettyName(aIdentifier));

  // getSelectedRows test
  var actualSelRows = acc.getSelectedRowIndices();

  var actualSelrowCount = actualSelRows.length;
  is(actualSelrowCount, selRows.length,
      msg + "Wrong count of selected rows for " + prettyName(aIdentifier) +
      "from getSelectedRows.");

  for (let i = 0; i < actualSelrowCount; i++) {
    is(actualSelRows[i], selRows[i],
        msg + "Row at index " + selRows[i] + " should be selected.");
  }

  // Cells selection tests.
  var selCells = [];

  // isCellSelected test
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    for (let colIdx = 0; colIdx < colsCount; colIdx++) {
      if (aCellsArray[rowIdx][colIdx] & kSpanned)
        continue;

      var isSelected = !!aCellsArray[rowIdx][colIdx];
      is(acc.isCellSelected(rowIdx, colIdx), isSelected,
         msg + "Wrong selection state of cell at " + rowIdx + " row and " +
         colIdx + " column for " + prettyName(aIdentifier));

      if (aCellsArray[rowIdx][colIdx])
        selCells.push(acc.getCellIndexAt(rowIdx, colIdx));
    }
  }

  // selectedCellCount tests
  is(acc.selectedCellCount, selCells.length,
     msg + "Wrong count of selected cells for " + prettyName(aIdentifier));

  // getSelectedCellIndices test
  var actualSelCells = acc.getSelectedCellIndices();

  var actualSelCellsCount = actualSelCells.length;
  is(actualSelCellsCount, selCells.length,
     msg + "Wrong count of selected cells for " + prettyName(aIdentifier) +
     "from getSelectedCells.");

  for (let i = 0; i < actualSelCellsCount; i++) {
    is(actualSelCells[i], selCells[i],
       msg + "getSelectedCellIndices: Cell at index " + selCells[i] +
       " should be selected.");
  }

  // selectedCells and isSelected tests
  var actualSelCellsArray = acc.selectedCells;
  for (let i = 0; i < actualSelCellsCount; i++) {
    var actualSelCellAccessible =
      actualSelCellsArray.queryElementAt(i, nsIAccessibleTableCell);

    let colIdx = acc.getColumnIndexAt(selCells[i]);
    let rowIdx = acc.getRowIndexAt(selCells[i]);
    var expectedSelCellAccessible = acc.getCellAt(rowIdx, colIdx);

    is(actualSelCellAccessible, expectedSelCellAccessible,
       msg + "getSelectedCells: Cell at index " + selCells[i] +
       " should be selected.");

    ok(actualSelCellAccessible.isSelected(),
       "isSelected: Cell at index " + selCells[i] + " should be selected.");
  }

  // selected states tests
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    for (let colIdx = 0; colIdx < colsCount; colIdx++) {
      if (aCellsArray[rowIdx][colIdx] & kSpanned)
        continue;

      var cell = acc.getCellAt(rowIdx, colIdx);
      var isSel = aCellsArray[rowIdx][colIdx];
      if (isSel == undefined)
        testStates(cell, 0, 0, STATE_SELECTABLE | STATE_SELECTED);
      else if (isSel)
        testStates(cell, STATE_SELECTED);
      else
        testStates(cell, STATE_SELECTABLE, 0, STATE_SELECTED);
    }
  }
}

/**
 * Test unselectColumn method of accessible table.
 */
function testUnselectTableColumn(aIdentifier, aColIdx, aCellsArray) {
  var acc = getAccessible(aIdentifier, [nsIAccessibleTable]);
  if (!acc)
    return;

  var rowCount = aCellsArray.length;
  for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    // Unselect origin cell.
    var [origRowIdx, origColIdx] =
      getOrigRowAndColumn(aCellsArray, rowIdx, aColIdx);
    aCellsArray[origRowIdx][origColIdx] = false;
  }

  acc.unselectColumn(aColIdx);
  testTableSelection(aIdentifier, aCellsArray,
                     "Unselect " + aColIdx + " column: ");
}

/**
 * Test selectColumn method of accessible table.
 */
function testSelectTableColumn(aIdentifier, aColIdx, aCellsArray) {
  var acc = getAccessible(aIdentifier, [nsIAccessibleTable]);
  if (!acc)
    return;

  var rowCount = aCellsArray.length;
  var colsCount = aCellsArray[0].length;

  for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    for (var colIdx = 0; colIdx < colsCount; colIdx++) {
      var cellState = aCellsArray[rowIdx][colIdx];

      if (colIdx == aColIdx) { // select target column
        if (!(cellState & kSpanned)) {
          // Select the cell if it is origin.
          aCellsArray[rowIdx][colIdx] = true;
        } else {
          // If the cell is spanned then search origin cell and select it.
          var [origRowIdx, origColIdx] = getOrigRowAndColumn(aCellsArray,
                                                             rowIdx, colIdx);
          aCellsArray[origRowIdx][origColIdx] = true;
        }
      } else if (!(cellState & kSpanned)) { // unselect other columns
        if (colIdx > aColIdx) {
          // Unselect the cell if traversed column index is greater than column
          // index of target cell.
          aCellsArray[rowIdx][colIdx] = false;
        } else if (!(aCellsArray[rowIdx][aColIdx] & kColSpanned)) {
          // Unselect the cell if the target cell is not row spanned.
          aCellsArray[rowIdx][colIdx] = false;
        } else {
          // Unselect the cell if it is not spanned to the target cell.
          for (var spannedColIdx = colIdx + 1; spannedColIdx < aColIdx;
               spannedColIdx++) {
            var spannedCellState = aCellsArray[rowIdx][spannedColIdx];
            if (!(spannedCellState & kRowSpanned)) {
              aCellsArray[rowIdx][colIdx] = false;
              break;
            }
          }
        }
      }
    }
  }

  acc.selectColumn(aColIdx);
  testTableSelection(aIdentifier, aCellsArray,
                     "Select " + aColIdx + " column: ");
}

/**
 * Test unselectRow method of accessible table.
 */
function testUnselectTableRow(aIdentifier, aRowIdx, aCellsArray) {
  var acc = getAccessible(aIdentifier, [nsIAccessibleTable]);
  if (!acc)
    return;

  var colsCount = aCellsArray[0].length;
  for (var colIdx = 0; colIdx < colsCount; colIdx++) {
    // Unselect origin cell.
    var [origRowIdx, origColIdx] = getOrigRowAndColumn(aCellsArray,
                                                       aRowIdx, colIdx);
    aCellsArray[origRowIdx][origColIdx] = false;
  }

  acc.unselectRow(aRowIdx);
  testTableSelection(aIdentifier, aCellsArray,
                     "Unselect " + aRowIdx + " row: ");
}

/**
 * Test selectRow method of accessible table.
 */
function testSelectTableRow(aIdentifier, aRowIdx, aCellsArray) {
  var acc = getAccessible(aIdentifier, [nsIAccessibleTable]);
  if (!acc)
    return;

  var rowCount = aCellsArray.length;
  var colsCount = aCellsArray[0].length;

  for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    for (var colIdx = 0; colIdx < colsCount; colIdx++) {
      var cellState = aCellsArray[rowIdx][colIdx];

      if (rowIdx == aRowIdx) { // select the given row
        if (!(cellState & kSpanned)) {
          // Select the cell if it is origin.
          aCellsArray[rowIdx][colIdx] = true;
        } else {
          // If the cell is spanned then search origin cell and select it.
          var [origRowIdx, origColIdx] = getOrigRowAndColumn(aCellsArray,
                                                             rowIdx, colIdx);

          aCellsArray[origRowIdx][origColIdx] = true;
        }
      } else if (!(cellState & kSpanned)) { // unselect other rows
        if (rowIdx > aRowIdx) {
          // Unselect the cell if traversed row index is greater than row
          // index of target cell.
          aCellsArray[rowIdx][colIdx] = false;
        } else if (!(aCellsArray[aRowIdx][colIdx] & kRowSpanned)) {
          // Unselect the cell if the target cell is not row spanned.
          aCellsArray[rowIdx][colIdx] = false;
        } else {
          // Unselect the cell if it is not spanned to the target cell.
          for (var spannedRowIdx = rowIdx + 1; spannedRowIdx < aRowIdx;
               spannedRowIdx++) {
            var spannedCellState = aCellsArray[spannedRowIdx][colIdx];
            if (!(spannedCellState & kRowSpanned)) {
              aCellsArray[rowIdx][colIdx] = false;
              break;
            }
          }
        }
      }
    }
  }

  acc.selectRow(aRowIdx);
  testTableSelection(aIdentifier, aCellsArray,
                     "Select " + aRowIdx + " row: ");
}

/**
 * Test columnHeaderCells and rowHeaderCells of accessible table.
 */
function testHeaderCells(aHeaderInfoMap) {
  for (var testIdx = 0; testIdx < aHeaderInfoMap.length; testIdx++) {
    var dataCellIdentifier = aHeaderInfoMap[testIdx].cell;
    var dataCell = getAccessible(dataCellIdentifier, [nsIAccessibleTableCell]);

    // row header cells
    var rowHeaderCells = aHeaderInfoMap[testIdx].rowHeaderCells;
    var rowHeaderCellsCount = rowHeaderCells.length;
    var actualRowHeaderCells = dataCell.rowHeaderCells;
    var actualRowHeaderCellsCount = actualRowHeaderCells.length;

    is(actualRowHeaderCellsCount, rowHeaderCellsCount,
       "Wrong number of row header cells for the cell " +
       prettyName(dataCellIdentifier));

    if (actualRowHeaderCellsCount == rowHeaderCellsCount) {
      for (let idx = 0; idx < rowHeaderCellsCount; idx++) {
        var rowHeaderCell = getAccessible(rowHeaderCells[idx]);
        var actualRowHeaderCell =
          actualRowHeaderCells.queryElementAt(idx, nsIAccessible);
        isObject(actualRowHeaderCell, rowHeaderCell,
                 "Wrong row header cell at index " + idx + " for the cell " +
                 dataCellIdentifier);
      }
    }

    // column header cells
    var colHeaderCells = aHeaderInfoMap[testIdx].columnHeaderCells;
    var colHeaderCellsCount = colHeaderCells.length;
    var actualColHeaderCells = dataCell.columnHeaderCells;
    var actualColHeaderCellsCount = actualColHeaderCells.length;

    is(actualColHeaderCellsCount, colHeaderCellsCount,
       "Wrong number of column header cells for the cell " +
       prettyName(dataCellIdentifier));

    if (actualColHeaderCellsCount == colHeaderCellsCount) {
      for (let idx = 0; idx < colHeaderCellsCount; idx++) {
        var colHeaderCell = getAccessible(colHeaderCells[idx]);
        var actualColHeaderCell =
          actualColHeaderCells.queryElementAt(idx, nsIAccessible);
        isObject(actualColHeaderCell, colHeaderCell,
                 "Wrong column header cell at index " + idx + " for the cell " +
                 dataCellIdentifier);
      }
    }
  }
}

// //////////////////////////////////////////////////////////////////////////////
// private implementation

/**
 * Return row and column of orig cell for the given spanned cell.
 */
function getOrigRowAndColumn(aCellsArray, aRowIdx, aColIdx) {
  var cellState = aCellsArray[aRowIdx][aColIdx];

  var origRowIdx = aRowIdx, origColIdx = aColIdx;
  if (cellState & kRowSpanned) {
    for (var prevRowIdx = aRowIdx - 1; prevRowIdx >= 0; prevRowIdx--) {
      let prevCellState = aCellsArray[prevRowIdx][aColIdx];
      if (!(prevCellState & kRowSpanned)) {
        origRowIdx = prevRowIdx;
        break;
      }
    }
  }

  if (cellState & kColSpanned) {
    for (var prevColIdx = aColIdx - 1; prevColIdx >= 0; prevColIdx--) {
      let prevCellState = aCellsArray[aRowIdx][prevColIdx];
      if (!(prevCellState & kColSpanned)) {
        origColIdx = prevColIdx;
        break;
      }
    }
  }

  return [origRowIdx, origColIdx];
}
