/**
  *  Gradebook data grid
  *
  *  PORTIONS OF THIS FILE ARE BASED ON RICO LIVEGRID 1.1.2
  *
  *  Copyright 2005 Sabre Airline Solutions
  *
  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
  *  file except in compliance with the License. You may obtain a copy of the License at
  *
  *         http://www.apache.org/licenses/LICENSE-2.0
  *
  *  Unless required by applicable law or agreed to in writing, software distributed under the
  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
  *  either express or implied. See the License for the specific language governing permissions
  *  and limitations under the License.
  *
  * File:
  * Authors(s): Bill Richard
  * Description: Main controller class for gradebook2 grid.
  * Version:
  **/

var GradebookGridUtil =
{

  showInlineReceipt : function(message)
  {
    window.location.href = window.viewSpreadsheetURL + '&inline_receipt_message=' + message;
  },

  showAlertLightbox : function( flyoutFormId, title, firstElement )
  {
    var lightboxParam =
    {
      dimensions :
      {
        w : 600,
        h : 150
      },
      title : title,
      closeOnBodyClick : false,
      showCloseLink : false,
      contents :
      {
        id : flyoutFormId,
        move : true
      }
    }; 
    Gradebook.alertLightbox = new lightbox.Lightbox( lightboxParam );
    var submitButton = $( flyoutFormId + 'Submit');
    var firstElement = $( firstElement );
    Gradebook.alertLightbox.open(function()
    {
      firstElement.focus();
      Gradebook.alertLightbox.lastLink = submitButton;
      Gradebook.alertLightbox.firstLink = firstElement;
    });
    GradebookGridUtil.resizeLightbox( flyoutFormId );
  },
  
  resizeLightbox : function( content )
  {
    var newHeight = $( content ).getHeight( ) + 60;
    if ( newHeight > 150 )
    {
      Gradebook.alertLightbox.resize( {w:600, h:newHeight } );
    }
  },
  
  //on firefox/mac scroll bars will show ontop of anything if not shimmed
  shimDiv : function(menuDiv)
  {
    var shimIFrame = $('shimDiv');
    if (!shimIFrame)
    {
      return;
    }
    shimIFrame.style.width = menuDiv.offsetWidth;
    shimIFrame.style.height = menuDiv.offsetHeight;
    var position = Position.page(menuDiv);
    shimIFrame.style.top = position[1];
    shimIFrame.style.left = position[0];
    shimIFrame.style.zIndex = 2;
    shimIFrame.style.display = "block";
  },

  clearShim : function()
  {
    if ($("shimDiv"))
    {
      $("shimDiv").style.display = "none";
    }
  }
};

var GradebookScrollContext = Class.create(
{
  initialize: function( accessibleMode, currentView )
  {
    this.accessibleMode = accessibleMode;
    this.currentView = currentView;

    if ( this.accessibleMode )
    {
      this.accessibleScrollSettings = {};
    }
    else
    {
      this.inaccessibleScrollSettings = {};
    }
  },

  saveScrollCoordinates: function()
  {
    if ( this.accessibleMode )
    {
      this.saveScreenReaderModeScrollCoordinates();
    } else
    {
      this.saveInteractiveModeScrollCoordinates(theGradeCenter.grid.viewPort);
    }
  },

  saveScreenReaderModeScrollCoordinates: function()
  {
    var container = document.getElementById('table1_accessible_container');
    this.accessibleScrollSettings.y = container.scrollTop;
    this.accessibleScrollSettings.x = container.scrollLeft;
  },

  saveInteractiveModeScrollCoordinates: function(viewPort)
  {
    if ( viewPort.scrollerDiv )
    {
      this.inaccessibleScrollSettings.scrollTop = viewPort.scrollerDiv.scrollTop;
    }
    if ( viewPort.scrollerDivH )
    {
      this.inaccessibleScrollSettings.scrollLeft = viewPort.scrollerDivH.scrollLeft;
    }
    this.inaccessibleScrollSettings.lastVScrollPos = viewPort.lastVScrollPos;
    this.inaccessibleScrollSettings.lastHScrollPos = viewPort.lastHScrollPos;
  }
});

GradebookScrollContext.Constants =
{
  GRADEBOOK_SCROLL_CONTEXT: 'GradebookScrollContext'
};


GradebookScrollContext.getNewInstance = function( model, accessibleMode ) 
{
    var gradebookScrollContext = new GradebookScrollContext( accessibleMode, model.currentView );
    model.setObject( GradebookScrollContext.Constants.GRADEBOOK_SCROLL_CONTEXT , gradebookScrollContext );
    return gradebookScrollContext;
};

GradebookScrollContext.getExistingInstance = function( model, accessibleMode ) 
{
  var scrollContext = model.getObject( GradebookScrollContext.Constants.GRADEBOOK_SCROLL_CONTEXT  );
  if (!scrollContext)
  {
    return null;
  }
  if (scrollContext.currentView != model.currentView)
  {
    return null;
  }
  if (scrollContext.accessibleMode != model.accessibleMode)
  {
    return null;
  }
  return scrollContext;
};

Gradebook.Grid = Class.create();

Gradebook.Grid.prototype =
{

  initialize : function(tableId, gradebookService, options, model)
  {

    this.options =
    {
      scrollerBorderRight : '1px solid #ababab',
      sortBlankImg : 'images/blank.gif',
      topArrowLImg : 'images/toparrowL.gif',
      topArrowRImg : 'images/toparrowR.gif',
      botArrowLImg : 'images/botarrowL.gif',
      botArrowRImg : 'images/botarrowR.gif',
      numFrozenColumns : 0,
      accessibleMode : false
    };
    Object.extend(this.options, options ||
    {});

    this.tableId = tableId;
    this.table = $(tableId);

    this.currentSelectedCell = null;

    if (model)
    {
      this.model = model;
      this.model.removeModelListeners();
      this.model.gradebookService = gradebookService;
    }
    else
    {
      this.model = new Gradebook.GridModel(gradebookService);
    }
    this.model.addModelListener(this);
    // setting the scroll context stored in model to survive page refresh
    var scrollContext = this.model.getObject( "scrollContext" );
    if ( !scrollContext )
    {
       scrollContext = this.model.newObject( "scrollContext" );
       scrollContext.x = 0;
       scrollContext.y = 0;
    }
    this.scrollContext = scrollContext;

    this.initClearAttemptsFlyOut();
    if (this.model.getNumColDefs() === 0)
    {
      this.model.requestLoadData();
    }
    else
    {
      this.model.requestUpdateData();
    }

    this.wrapTable();
  },
  
  /**
   * Determines the html table row index of the specified course membership id.
   *
   * Returns an object with two properties: a boolean indicating whether the row was found
   * and if found, the row index of the specified course member.
   */
  getHtmlRowIndexByUserId: function(userId)
  {
    var visibleRows = this.model.visibleRows;
    var result = {found:false};
    var scrollableColRowIterators = this.model.getRowIterators();

    for (var i=0; i < visibleRows.length; i++)
    {
      if ( scrollableColRowIterators[i].dataArray[0].uid == userId )
      {
        result.index = i;
        result.found = true;
        break;
      }
    }
    return result;
  },

  wrapTable : function()
  {
    var table = this.table;
    // wrap table with a new container div: relative see IE7 bug:
    // http://rowanw.com/bugs/overflow_relative.htm
    table.insert(
    {
      before : "<div id='" + this.tableId + "_container' style='position:relative;'></div>"
    });
    table.previousSibling.appendChild(table);

    if (!this.options.accessibleMode)
    {
      // wrap table with a new viewport div
      table.insert(
      {
        before : "<div id='" + this.tableId + "_viewport'></div>"
      });
      table.previousSibling.appendChild(table);
    }
  },

  modelChanged : function()
  {
    $('loadStatusMsg').update(GradebookUtil.getMessage('creatingGridMsg'));
    this.model.removeModelListeners();
    setTimeout(this.createView.bind(this), 50);
  },

  modelError : function(exception, serverReply)
  {
    this.loaded = true;
    window.model = null;
    parent.gbModel = null;
    if (serverReply)
    {
      // server returned error page instead of json data
      if (exception.name && exception.message)
      {
        document.write(exception.name + ': ' + exception.message + '     ');
      }
      document.write(serverReply);
      document.close();
    }
    else
    {
      $('loadstatus').hide();
      $('loadingGridErrorMsg').update(GradebookUtil.getMessage('errorParsingDataMsg'));
      $('errorLoadingGrid').show();
      if (exception)
      {
        if (exception.name && exception.message)
        {
          $('loadingGridError').update(exception.name + ': ' + exception.message);
        }
        else
        {
          $('loadingGridError').update(exception);
        }
      }
    }
  },

  initClearAttemptsFlyOut : function()
  {
    var clearAttempsFormPanel = $('clearAttemptsFlyOut');
    // direct root child to solve absolute positioning issues
    // clearAttempsFormPanel.remove();
    // document.getElementsByTagName('body')[0].appendChild(
    // clearAttempsFormPanel );
    Event.observe(clearAttempsFormPanel, 'click', function(event)
    {
      Gradebook.doNotCloseAttemptsForm = true;
    });
    Event.observe($('dp_bbDateTimePicker_start_date'), 'click', function(event)
    {
      $('clearAttemptsOptionRange').checked = true;
    });
    Event.observe($('dp_bbDateTimePicker_end_date'), 'click', function(event)
    {
      $('clearAttemptsOptionRange').checked = true;
    });
    Event.observe($('dp_bbDateTimePicker_start_date'), 'change', function(event)
    {
      $('clearAttemptsOptionRange').checked = true;
    });
    Event.observe($('dp_bbDateTimePicker_end_date'), 'change', function(event)
    {
      $('clearAttemptsOptionRange').checked = true;
    });
    Event.observe('selectOption', 'change', function(event)
    {
      $('clearAttemptsOptionSelect').checked = true;
    });
    Gradebook.clearAttemptsFormDefault =
    {};
    Gradebook.clearAttemptsFormDefault.defaultSelect = $('selectOption').value;
    Gradebook.clearAttemptsFormDefault.defaultStartDate = $('dp_bbDateTimePicker_start_date').value;
    Gradebook.clearAttemptsFormDefault.defaultEndDate = $('dp_bbDateTimePicker_end_date').value;
    Gradebook.clearAttemptsFormDefault.defaultStartDateHidden = $('bbDateTimePickerstart').value;
    Gradebook.clearAttemptsFormDefault.defaultEndDateHidden = $('bbDateTimePickerend').value;

    Event.observe('clearAttemptsFlyOutCancel', 'click', function(event)
    {
      $("clearAttemptsFlyOut").style.display = "none";
    });
  },

  createView : function()
  {
    this.options.numFrozenColumns = window.model.getNumFrozenColumns();
    this.modelSortIndex = this.model.getSortIndex();
    this.sortDir = this.model.getSortDir();
    this._initializeHTML();
    this.viewPort = new Gradebook.GridViewPort(this.table, this.model, this.options, this);
    this.model.addModelListener(this.viewPort);
    this.viewPort.refreshContentsH();
    this.updateSortImage();
    this.restoreFocus();

    if ( this.options.onLoadComplete ){
      this.options.onLoadComplete();
    }

    this.loaded=true;
  },


  /**
   * Scrolls the viewport horizontally to display the specified grade item 
   */
  scrollGradeItemIntoViewPort: function( gradableItemId )
  {
    var htmlColumnIndex = this.model.getVisibleColDefIndex( gradableItemId );
    if (htmlColumnIndex == -1)
    {
      return false;
    }
    var htmlColumn = theGradeCenter.grid.isHtmlColumnIndexVisible( htmlColumnIndex );
    if (!htmlColumn.found)
    {
      // grade column is not currently visible.  scroll horizontally to ensure it's in view
      theGradeCenter.grid.viewPort.scrollCols(htmlColumn.diff, true);
    }
    return true;
  },
  
  /**
   * Scrolls the viewport vertically to display the specified course member
   */
  scrollCourseMemberIntoViewPort: function( userId )
  {
    var htmlRow = theGradeCenter.grid.getHtmlRowIndexByUserId( userId );
    if (!htmlRow)
    {
      return false;
    }

    if (!htmlRow.found)
    {
      return false;
    }

    htmlRow = theGradeCenter.grid.isHtmlRowIndexVisible( htmlRow.index );
    if (!htmlRow.isVisible)
    {
      // student row is not currently visible.  scroll vertically to ensure it's in view
      theGradeCenter.grid.viewPort.scrollRows(htmlRow.diff, true);
    }
    return true;
  },

  /**
   * Returns whether the specified domElement is currently viewable in the grid
   */
  isGridCellInView: function( domElement )
  {
    var docViewTop =  document.viewport.getScrollOffsets().top;
    var docViewBottom = docViewTop + $(document).viewport.getHeight();
    var viewPortOffset = $(domElement).viewportOffset();
    var elemTop = viewPortOffset.top;
    var elemBottom = elemTop + $(domElement).getHeight();

    return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) &&
            (elemBottom <= docViewBottom) &&  (elemTop >= docViewTop) );
  },
  
  /**
   * Returns whether the specified row is visible in the viewport
   */
  isHtmlRowIndexVisible: function(index)
  {
    var results = {isVisible:false};
    var numRows = this.viewPort.numVisibleRows;
    var lastRowPos = this.viewPort.lastRowPos;

    if ( index < lastRowPos || index > lastRowPos + numRows )
    {
      results.diff = index - lastRowPos ;
    }
    else
    {
      results.isVisible = true;
    }
    return results;
  },
  
  /**
   * Returns whether the specified column is visible in the viewport
   */
  isHtmlColumnIndexVisible: function(index)
  {
    var numFrozenColumns = this.viewPort.options.numFrozenColumns;
    var numVisibleColumns = this.viewPort.numVisibleCols;
    var colOffset = this.viewPort.colOffset;
    if ( numFrozenColumns >= numVisibleColumns )
    {
      return -1;
    }
    var results = {found:false};
    var items = this.model.getColDefs(false, false);
    var lastColIndex = numVisibleColumns + colOffset - 1;
    var lastViewableColumn = items[ lastColIndex ];

    for (var i = 0, idx = 1; i < items.length; i++)
    {
      if ( i == index )
      {
        results.index = i;
        if ( lastColIndex > results.index )
        {
          results.diff = results.index - lastColIndex;
        }
        else if (results.index > lastColIndex)
        {
          results.diff = results.index - lastColIndex;
        }
        else
        {
         results.found = true;
        }
        break;
      }
    }
    return results;
  },

  setAccessibleViewportSize : function()
  {
    // container div will scroll in accessible mode
    var contDiv = $(this.tableId + '_container');
    var oneRowHeight = 20;
    if (this.table.rows.length > 0)
    {
      oneRowHeight = this.table.rows[this.table.rows.length - 1].offsetHeight + 1 /*
                                                                                   * border
                                                                                   * spacing
                                                                                   */;
    }
    // need to account for the wider header row
    var h = Math.max(this.options.tableHeight, ((this.model.getMinimumRows() + 1) * oneRowHeight + 10));
    h = Math.min(h, this.table.offsetHeight + 19);
    var w = this.options.tableWidth;
    contDiv.style.height = h + "px";
    contDiv.style.width = w + "px";
    contDiv.style.overflow = "auto";
  },

  _initializeHTML : function()
  {
    this._sizeHTMLTable();
    if (this.options.accessibleMode)
    {
      this.setAccessibleViewportSize();
      return;
    }

    var viewportDiv = $(this.tableId + '_viewport');
    viewportDiv.style.height = (this.table.offsetHeight) + "px";
    viewportDiv.style.overflow = "hidden";

    var c, numHCols;

    // add controllers to table cells
    var tableHeader = $(this.table.id + '_header');
    if (tableHeader)
    {
      numHCols = tableHeader.rows[0].cells.length;
      for (c = 0; c < numHCols; c++)
      {
        new Gradebook.CellController(tableHeader.rows[0].cells[c], this, 0, c, true, numHCols);
      }
    }
    var numRows = this.table.rows.length;
    for ( var r = 0; r < numRows; r++)
    {
      var numCols = this.table.rows[0].cells.length;
      for (c = 0; c < numCols; c++)
      {
        var cell = this.table.rows[r].cells[c];
        new Gradebook.CellController(cell, this, r, c, false, numHCols);
      }
    }

    if (document.onClickHandler)
    {
      Event.stopObserving(document, 'click', document.onClickHandler);
    }
    document.onClickHandler = this.onDocumentClickHandler.bindAsEventListener(this);
    Event.observe(document, 'click', document.onClickHandler);

    if (document.onKeydownHandler)
    {
      Event.stopObserving(document, 'keydown', document.onKeydownHandler);
    }
    document.onKeydownHandler = this.onDocumentKeyDownHandler.bindAsEventListener(this);
    Event.observe(document, 'keydown', document.onKeydownHandler);
  },

  unload: function() {
  GradebookScrollContext.getNewInstance( this.model, this.options.accessibleMode ).saveScrollCoordinates();
    var numRows = this.table.rows.length;
    var c, cell;
    for ( var r = 0; r < numRows; r++)
    {
      var numCols = this.table.rows[0].cells.length;
      for (c = 0; c < numCols; c++)
      {
        cell = this.table.rows[r].cells[c];
        if (cell.controller)
        {
          cell.controller.unload();
        }
      }
    }
    var tableHeader = $(this.table.id + '_header');
    if (tableHeader)
    {
      var numHCols = tableHeader.rows[0].cells.length;
      for (c = 0; c < numHCols; c++)
      {
        cell = tableHeader.rows[0].cells[c];
        if (cell.controller)
        {
          cell.controller.unload();
        }
      }
    }
    if (this.viewPort)
    {
      this.viewPort.unload();
    }
    this.model.removeModelListeners();
    this.table = null;
    this.model = null;
    this.viewPort = null;
    this.options = null;
    this.sortCell = null;
  },

  _sizeHTMLTable : function()
  {
    var tbl = this.table;
    var tableHeader = $(this.table.id + '_header');
    var numRows = 0;
    var numCols = 0;
    var numFrozenColumns = this.options.numFrozenColumns;
    var i;
    // presence of th impacts the calculation of the row height
    // so we remove it before the calculation occurs
    if (numFrozenColumns === 0)
    {
      for (i = 0; i < tbl.rows.length; i++)
      {
        tbl.rows[i].deleteCell(1);
        if ( tableHeader ) 
        {
          // no table header in accessible view
          tableHeader.rows[i].deleteCell(1);
        }
      }
      // region is now too small to display msg and count
      $("selectedRowMsg").style.display = 'none';
    }
    else
    {
      $("selectedRowMsg").style.display = 'inline';
    }
    if (this.options.accessibleMode)
    {
      numRows = this.model.getNumRows() + 1 ; //in accessible mode, the same table has the header and content
      numCols = this.model.getNumColDefs();
    }
    else
    {
      var cell = this.table.rows[this.table.rows.length - 1].cells[1]; // skip
                                                                        // checkbox
                                                                        // column
      cell.height = cell.offsetHeight;
      numRows = parseInt(this.options.tableHeight/cell.offsetHeight, 10);
      if ( numRows < this.model.getMinimumRows() )
      {
        numRows = this.model.getMinimumRows() ;
      }
      if ( this.model.getNumRows() < numRows ) 
      {
        numRows = this.model.getNumRows() ;
      }
      numCols = parseInt(this.options.tableWidth / cell.offsetWidth, 10);
    }

    // at least one non-frozen column must be shown
    if (numFrozenColumns + 1 >= numCols)
    {
      numFrozenColumns = numCols - 1;
      this.options.numFrozenColumns = numFrozenColumns;
    }

    // assumes the table has at least 1 row & 2 cols
    // the first column is a frozen column
    // the second column is a non-frozen column

    // clone frozen columns
    for (i = 0; i < numFrozenColumns - 1; i++)
    {
      this._cloneColumn(1); // skip check box column
    }

    // clone non-frozen columns
    var numNonFrozenColumns = numCols - numFrozenColumns - 1;
    for (i = 0; i < numNonFrozenColumns; i++)
    {
      this._cloneColumn(numFrozenColumns + 1); // skip check box column
    }

    var checkColumnWidth = this.table.rows[0].cells[0].offsetWidth;
    var visibleWidth = this.table.offsetWidth;
    this.avgColWidth = (visibleWidth - checkColumnWidth) / numCols;
    var frozenWidth = (numFrozenColumns * this.avgColWidth) + checkColumnWidth;
    $("selectedRows").style.width = this.isIE ? frozenWidth + "px" : frozenWidth - 2 + "px";

    // clone rows
    var numRowsToAdd = numRows - tbl.rows.length;

    var rowToClone = tbl.rows[this.table.rows.length - 1];
    for (i = 0; i < numRowsToAdd; i++)
    {
      tbl.tBodies[0].appendChild(rowToClone.cloneNode(true));
    }

    // remove table rows if html table is bigger than numRows
    while (tbl.rows.length > numRows)
    {
      if (tbl.rows.length > 0)
      {
        tbl.deleteRow(tbl.rows.length - 1);
      }
    }

    // remove table columns if html table is bigger than model
    var allRows = tbl.rows;
    while (tbl.rows.length > 0 && tbl.rows[0].cells.length - 1 > this.model.getNumColDefs())
    {
      for (i = 0; i < allRows.length; i++)
      {
        if (allRows[i].cells.length > 1)
        {
          allRows[i].deleteCell(-1);
        }
      }
    }
    while (tableHeader && tableHeader.rows[0].cells.length - 1 > this.model.getNumColDefs())
    {
      tableHeader.rows[0].deleteCell(-1);
    }
  },

  _cloneColumn : function(colIndex)
  {
    var tbl = this.table;
    var i, origCell, newCell;
    for (i = 0; i < tbl.rows.length; i++)
    {
      origCell = tbl.rows[i].cells[colIndex];
      newCell = origCell.cloneNode(true);
      tbl.rows[i].insertBefore(newCell, origCell);
    }
    var tableHeader = $(this.table.id + '_header');
    if (tableHeader)
    {
      tbl = tableHeader;
      for (i = 0; i < tbl.rows.length; i++)
      {
        origCell = tbl.rows[i].cells[colIndex];
        newCell = origCell.cloneNode(true);
        tbl.rows[i].insertBefore(newCell, origCell);
      }
    }
  },

  getAbbrColIndexes : function()
  {
    if (!this.abbrColIndexes)
    {
      this.abbrColIndexes = [];
      /*
       * Add abbr attributes to specific columns to allow screen readers to
       * announce meaningful column headers based on the following rules:
       *
       * 1. If both first and last name are visible, use those. 2. If the
       * username is visible, use that. 3. If neither of the first cases pass,
       * use the first column as the header.
       */
      var lastNameColIndex = this.model.getVisibleColDefIndex('LN');
      var firstNameColIndex = this.model.getVisibleColDefIndex('FN');
      var userNameColIndex = this.model.getVisibleColDefIndex('UN');
      if (lastNameColIndex != -1 && firstNameColIndex != -1)
      {
        this.abbrColIndexes[ lastNameColIndex ] = true;
        this.abbrColIndexes[ firstNameColIndex ] = true;
      }
      else if (userNameColIndex != -1)
      {
        this.abbrColIndexes[ userNameColIndex ] = true;
      }
      else
      {
        this.abbrColIndexes.push[ 0 ] = true;
      }
    }
    return this.abbrColIndexes;
  },

  onDocumentClickHandler : function(evt)
  {
    if (document.ignoreOnClick || Gradebook.alertLightbox)
    {
      return;
    }
    Gradebook.CellController.prototype.closePopupsAndRestoreFocus(evt);
  },

  onDocumentKeyDownHandler : function(evt)
  {
    if (Gradebook.alertLightbox)
    {
      return;
    }
    if (!Gradebook.CellController.prototype.tableHasFocus)
    {
      return;
    }
    var ek = evt.keyCode;
    var visibleRowCount = this.viewPort.getNumVisibleRows();
    var deltaRow = 0;
    var deltaCol = 0;
    /*
     * the model grid cell index is R2L agnostic: thus moving right in L2R is
     * moving towards the next col (+1), while in R2L it is going towards the
     * previous col (-1).
     */
    switch (ek)
    {
      case (Event.KEY_LEFT):
        deltaCol = page.util.isRTL() ? 1 : -1;
        break;
      case (Event.KEY_RIGHT):
        deltaCol = page.util.isRTL() ? -1 : 1;
        break;
      case (Event.KEY_UP):
        deltaRow = -1;
        break;
      case (Event.KEY_DOWN):
        deltaRow = 1;
        break;
      case (33/* page up */):
        if (!this.options.accessibleMode)
        {
          deltaRow = -visibleRowCount;
        }
        break;
      case (34/* page down */):
        if (!this.options.accessibleMode)
        {
          deltaRow = visibleRowCount;
        }
        break;
      case (Event.KEY_TAB):
        if (!Gradebook.CellController.currentSelectedCell || !Element.descendantOf(evt.element(), Gradebook.CellController.currentSelectedCell.controller.htmlCell))
        {
          break;
        }
        if (evt.shiftKey)
        {
          if (!evt.element().hasClassName('cmimg') && !this.isFirstCell())
          {
            deltaCol = -1;
            break;
          }
        }
        else if (!this.isLastCell() && (evt.element().hasClassName('cmimg') || this.isCurrentCellWithoutMenu()))
        {
          deltaCol = 1;
        }
        break;
    }
    if (deltaRow === 0 && deltaCol === 0)
    {
      return;
    }
    else
    {
      if (evt)
      {
        Event.stop(evt);
      }
      this.selectRelativeCell(deltaRow, deltaCol);
      Gradebook.CellController.prototype.closePopups(evt);
    }
  },

  isLastCell : function()
  {
    if (!Gradebook.CellController.currentSelectedCell)
    {
      return false;
    }
    // last cell if it is the last displayed cell with no more scroll available
    // right or down
    var nextSelectedCol = Gradebook.CellController.currentSelectedCell.controller.col;
    var nextSelectedRow = Gradebook.CellController.currentSelectedCell.controller.row + 1;

    return (nextSelectedCol >= this.viewPort.numVisibleCols) && (nextSelectedRow >= this.viewPort.numVisibleRows) &&
       ((this.viewPort.lastRowPos/* offset */+ this.viewPort.numVisibleRows) == this.model.getNumRows()) &&
       ((this.viewPort.colOffset + this.viewPort.numVisibleCols) == this.model.getNumColDefs());
  },

  isFirstCell : function()
  {
    if (!Gradebook.CellController.currentSelectedCell)
    {
      return false;
    }
    return (Gradebook.CellController.currentSelectedCell.controller.col == 1/* checkbox */ &&
        Gradebook.CellController.currentSelectedCell.controller.row === 0 &&
        (!this.viewPort.scrollerDiv /* null if no vertical scroll */  || this.viewPort.scrollerDiv.scrollTop === 0 )
    );
  },

  isCurrentCellWithoutMenu : function()
  {
    // the only cell type that does not display a context menu are calculated
    // columns
    if (!Gradebook.CellController.currentSelectedCell)
    {
      return false;
    }
    var gridCell = Gradebook.CellController.currentSelectedCell.controller.getGridCell();
    if (!gridCell)
    {
      return true;
    }
    return gridCell.isGrade() && !gridCell.canEdit();
  },

  selectRelativeCell : function(deltaRow, deltaCol)
  {
    var visibleRowCount = this.viewPort.getNumVisibleRows();
    var visibleColumnCount = this.viewPort.getNumVisibleCols();
    var modelRowCount = this.model.getNumRows();
    var modelColumnCount = this.model.getNumColDefs();

    var cellController = this.currentCellController;
    if (Gradebook.CellController.currentSelectedCell)
    {
      cellController = Gradebook.CellController.currentSelectedCell.controller;
    }
    var currentSelectedRow = cellController.row;
    var currentSelectedCol = cellController.col - 1; // skip checkbox col
    var selectDelay = 100;

    currentSelectedRow += deltaRow;
    if (currentSelectedRow < 0 || currentSelectedRow >= visibleRowCount)
    {
      currentSelectedRow -= deltaRow;
      selectDelay = 500; // need longer delay to select cell until scroll
                          // completes
      if (!this.viewPort.scrollRows(deltaRow))
      {
        if (deltaRow < 0)
        {
          // wrap to bottom of previous col
          if (currentSelectedCol === 0)
          {
            return;
          }
          deltaRow = modelRowCount - visibleRowCount;
          currentSelectedRow = visibleRowCount - 1;
          currentSelectedCol -= 1;
        }
        else
        {
          // wrap to top of next col
          deltaRow = visibleRowCount - modelRowCount;
          currentSelectedRow = 0;
          if (currentSelectedCol < visibleColumnCount - 1)
          {
            currentSelectedCol += 1;
          }
          else
          {
            this.viewPort.scrollCols(1);
          }
        }
        this.viewPort.scrollRows(deltaRow);
      }
    }
    currentSelectedCol += deltaCol;
    if ((currentSelectedCol < this.options.numFrozenColumns && deltaCol < 0) || currentSelectedCol >= visibleColumnCount)
    {
      currentSelectedCol -= deltaCol;
      selectDelay = 500; // need longer delay to select cell until scroll
                          // completes
      if (!this.viewPort.scrollCols(deltaCol))
      {
        if (deltaCol < 0)
        {
          if (currentSelectedCol > 0)
          { // navigate in frozen columns
            currentSelectedCol += deltaCol;
          }
          else
          {
            // wrap to end of previous row
            if (currentSelectedRow === 0)
            {
              return;
            }
            deltaCol = modelColumnCount - visibleColumnCount;
            currentSelectedCol = visibleColumnCount - 1;
            currentSelectedRow -= 1;
          }
        }
        else
        {
          // wrap to beginning of next row
          deltaCol = visibleColumnCount - modelColumnCount;
          currentSelectedCol = 0;
          if (currentSelectedRow < visibleRowCount - 1)
          {
            currentSelectedRow += 1;
          }
          else
          {
            this.viewPort.scrollRows(1);
          }
        }
        this.viewPort.scrollCols(deltaCol);
      }
    }
    // select the current cell after servicing the main event loop to allow
    // current events to complete
    // this was needed for AS-110508 to apply the left/right arrow event to cell
    // navigation only and not to cell editing too.
    this.currentCellController = this.table.rows[currentSelectedRow].cells[currentSelectedCol + 1].controller;
    setTimeout(this.selectCell.bind(this), selectDelay);
  },

  selectCell : function()
  {
    this.currentCellController.selectCell();
  },

  sortColumn : function(newSortCell, sortDir)
  {
    if (newSortCell != this.sortCell)
    {
      this.sortDir = 'ASC';
      if (this.sortCell)
      {
        this.sortCell.setSortImage('NO_SORT'); // remove current sort image
      }
    }
    else
    {
      this.sortDir = (this.sortDir == 'ASC') ? 'DESC' : 'ASC'; // toggle
    }
    if (sortDir)
    {
      this.sortDir = sortDir;
    }
    this.sortCell = newSortCell;
    this.sortCell.setSortImage(this.sortDir); // show new sort image

    // sort the model
    this.modelSortIndex = this.viewPort.toModelIndex(this.sortCell.col - 1); // skip
                                                                              // checkbox
                                                                              // column
    this.model.sort(this.modelSortIndex, this.sortDir);

    // refresh the view
    this.viewPort.moveScroll(0);
    this.viewPort.refreshContents(0);
  },

  updateSortImage : function()
  {
    if (!this.viewPort)
    {
      return;
    }
    if (this.sortCell)
    {
      this.sortCell.setSortImage('NO_SORT'); // remove current sort image
    }
    var viewSortIndex = this.viewPort.toViewIndex(this.modelSortIndex);
    if (viewSortIndex < 0)
    {
      this.sortCell = null;
    }
    else
    {
      var headerTable = $(this.table.id + '_header');
      if (!headerTable)
      {
        return;
      }
      this.sortCell = headerTable.rows[0].cells[viewSortIndex + 1].controller; // add
                                                                                // 1 to
                                                                                // account
                                                                                // for
                                                                                // check
                                                                                // column
      this.sortCell.setSortImage(this.sortDir);
    }
  },

  // focused is restored only in AX view since user has to leave the page for
  // update
  restoreFocus : function()
  {
    if (!this.options || !this.options.accessibleMode || !Gradebook.getModel().lastFocusedRow || !Gradebook.getModel().lastFocusedCol)
    {
      return;
    }
    if ( GradebookUtil.isIE() )
    {
      setTimeout(this.doRestoreFocus.bind(this), 0 );
    }
    else
    {
      this.doRestoreFocus();
    }
  },

  doRestoreFocus : function()
  {
    var lastFocusedRow = Gradebook.getModel().lastFocusedRow;
    var lastFocusedCol = Gradebook.getModel().lastFocusedCol;
    this.table.rows[lastFocusedRow].cells[lastFocusedCol].controller.selectCell();
    Gradebook.getModel().lastFocusedRow = null;
    Gradebook.getModel().lastFocusedCell = null;
  }

};

// Gradebook.GridViewPort --------------------------------------------------
Gradebook.GridViewPort = Class.create();

Gradebook.GridViewPort.prototype =
{

  initialize: function(table, model, options,grid)
  {
    this.isIE = GradebookUtil.isIE();
    this.table = table;
    this.model = model;
    this.options = options;
    this.grid = grid;
    this.lastPixelOffset = 0;
    this.colOffset = 0;
    this.lastRowPos = 0;
    this.startScrollLeft = 0;
    this.headerTableId = this.table.id + '_header';
    this.headerTable   = $(this.headerTableId);
    if (!this.headerTable)
    {
      this.headerTable = this.table;
    }
    this.numVisibleRows = this.table.rows.length;
    if ( this.headerTable.rows[0] )
    {
      this.numVisibleCols = this.headerTable.rows[0].cells.length-1; // don't include check column
    }
    this.div = this.table.parentNode;
    this.initScrollers();
    this.updateLastModifiedTS();
    this.restoreInteractiveModeScrollCoordinates();
  },

  unload: function()
  {
    this.grid = null;
    this.model = null;
    this.table = null;
    this.headerTable = null;
    this.div = null;
    this.scrollerDiv = null;
    this.heightDiv = null;
    this.scrollerDivH  = null;
    this.widthDiv = null;
    this.options = null;
  },

  restoreInteractiveModeScrollCoordinates: function() {
    var gradebookScrollContext = GradebookScrollContext.getExistingInstance( this.model, this.options.accessibleMode );
    if ( !gradebookScrollContext || gradebookScrollContext.accessibleMode )
    {
      return;
    }
    if ( this.scrollerDiv )
    {
      this.scrollerDiv.scrollTop = gradebookScrollContext.inaccessibleScrollSettings.scrollTop;
    }

    this.lastVScrollPos = gradebookScrollContext.inaccessibleScrollSettings.lastVScrollPos;
    if ( this.scrollerDivH )
    {
      this.scrollerDivH.scrollLeft = gradebookScrollContext.inaccessibleScrollSettings.scrollLeft;
      this.setColOffsetFromScrollOffset();
    }
    this.lastHScrollPos = gradebookScrollContext.inaccessibleScrollSettings.lastHScrollPos;
    if ( this.scrollerDiv )
    {
      var contentOffset = parseInt( this.scrollerDiv.scrollTop / parseInt(this.rowHeight, 10), 10 );
      this.lastRowPos = contentOffset;
    }
  },

  modelChanged: function()
  {
    this.updateLastModifiedTS();
    this.refreshContentsH();
  },

  updateLastModifiedTS: function()
  {
    var t = this.model.lastLogEntryTS;
    if (!t)
    {
      return;
    }

    var gcFrame = (top.content.gradecenterframe) ? top.content.gradecenterframe : top.content;
    $( gcFrame.document.getElementById('timeStampDiv')).update( gcFrame.LastSavedMsg + t );
  },

  getHeaderGridCell: function(col)
  {
    if (col > 0)
    {
      col -= 1; // skip check col
    }
    if (col >= this.options.numFrozenColumns)
    {
      col += this.colOffset;
    }
    var iterator = this.model.getColDefIterator(col);
    if (!iterator || !iterator.hasNext())
    {
      GradebookUtil.error('getHeaderGridCell cannot get header cell for col: '+col);
    }    
    return iterator.next();
  },

  getNumVisibleRows: function()
  {
    return this.numVisibleRows;
  },

  getNumVisibleCols: function() {
    return this.numVisibleCols;
  },

  populateRow: function(htmlRow, frozenColRowIterator, scrollableColRowIterator)
  {
    var numFrozenColumns = this.options.numFrozenColumns;
    for (var j=0; j < (this.numVisibleCols); j++) 
    {
      var iterator = (j < numFrozenColumns)?frozenColRowIterator:scrollableColRowIterator;
      var dataCell = iterator.next();
      var htmlCell = htmlRow.cells[j+1];
      // set check box column based on isRowChecked flag for first data cell
      if (j === 0)
      {
        var checkInput = GradebookUtil.getChildElementByClassName(htmlRow.cells[0], 'input', 'checkInput');
        checkInput.checked = dataCell.metaData.isRowChecked;
      }
      htmlCell.controller.renderHTML(dataCell);
    }
  },

  refreshContents: function(rowOffset)
  {
    if (this.model.getNumRows() === 0)
    {
      return;
    }
    if (this.options.accessibleMode)
    {
      this.refreshAccessibleContents();
      return;
    }
    var numRows = this.numVisibleRows;
    var numModelRows = this.model.getNumRows();
    if (rowOffset + numRows > numModelRows)
    {
      rowOffset = numModelRows - numRows - 1;
    }
    var numFrozenColumns = this.options.numFrozenColumns;
    var frozenColRowIterators = this.model.getRowIterators(rowOffset, numRows, 0);
    var scrollableColRowIterators = frozenColRowIterators;
    if (this.numVisibleCols > numFrozenColumns)
    {
      scrollableColRowIterators = this.model.getRowIterators(rowOffset, numRows, numFrozenColumns+this.colOffset);
    }
    for (var i=0; i < numRows; i++)
    {
      this.populateRow(this.table.rows[i], frozenColRowIterators[i], scrollableColRowIterators[i]);
    }
    this.lastRowPos = rowOffset;
  },

  restorePreviousAccessibleModeScrollCoordinates: function()
  {
    var gradebookScrollContext = GradebookScrollContext.getExistingInstance( this.model, this.options.accessibleMode );
    if ( gradebookScrollContext && gradebookScrollContext.accessibleMode )
    {
      var accessibleContainer = document.getElementById('table1_accessible_container');
      accessibleContainer.scrollTop = gradebookScrollContext.accessibleScrollSettings.y;
      accessibleContainer.scrollLeft = gradebookScrollContext.accessibleScrollSettings.x;
    }
  },

  refreshAccessibleContents : function()
  {
    var numModelRows = this.model.getNumRows();
    var iters = this.model.getRowIterators();
    var numCols = this.table.rows[0].cells.length - 1; // skip check column
    var start = new Date().getTime();
    if (this.refreshRowCounter === undefined || this.refreshRowCounter === null)
    {
      this.refreshRowCounter = 0;
    }
    var abbrColIndexes = this.grid.getAbbrColIndexes();
    for ( var i = this.refreshRowCounter; i < numModelRows; i++)
    {
      var htmlRowIndex = i + 1; // skip header row
      var htmlRow = this.table.rows[htmlRowIndex];
      var htmlCell;
      // if we are rendering for more than 3 seconds, give Firefox some time to
      // get
      // rid of the "unresponsive script" message.
      if (new Date().getTime() - start > 3000)
      {
        setTimeout(this.refreshAccessibleContents.bind(this), 0);
        return;
      }
      var rowTitle = GradebookUtil.getMessage('selectUserMsg');
      for ( var j = 0; j < numCols; j++)
      {
        var dataCell = iters[i].next();
        htmlCell = htmlRow.cells[j + 1]; // skip check column
        if (!htmlCell.controller)
        {
          new Gradebook.CellController(htmlCell, this.grid, htmlRowIndex, j + 1, false /* not a header cell */ );
        }
        htmlCell.controller.renderHTML(dataCell);
        if ( abbrColIndexes[ j ] )
        {
          htmlCell.abbr = htmlCell.controller.getGridCell().getValue();
          htmlCell.scope = 'row';
          rowTitle += " " + htmlCell.controller.getGridCell().getValue();
        }
        // set check box column based on isRowChecked flag for first grid cell
        if (j === 0)
        {
          htmlCell = htmlRow.cells[0];
          if (!htmlCell.controller)
          {
            new Gradebook.CellController(htmlCell, this.grid, htmlRowIndex, j, false /* not a header cell */ );
          }
          var checkInput = $(htmlCell).down('input');
          checkInput.checked = dataCell.metaData.isRowChecked;
        }
      }
      $(htmlRow.cells[0]).down('input').title = rowTitle;

      this.refreshRowCounter++;
    }
    this.refreshRowCounter = null;
    setTimeout(this.restorePreviousAccessibleModeScrollCoordinates.bind(this), 0 );
  },

  refreshContentsH : function()
  {
    // refresh data cells
    this.refreshContents(this.lastRowPos);
    // refresh the header cells
    var numFrozenColumns = this.options.numFrozenColumns;
    var hdrCells = null;
    var hdr = $(this.table.id + '_header');
    if (hdr)
    {
      hdrCells = hdr.rows[0].cells;
    }
    else
    {
      hdrCells = this.table.rows[0].cells;
    }
    if (!hdrCells)
    {
      return;
    }
    var frozenColIterator = this.model.getColDefIterator(0);
    var scrollableColIterator = null;
    if (this.numVisibleCols > numFrozenColumns)
    {
      scrollableColIterator = this.model.getColDefIterator(numFrozenColumns + this.colOffset);
    }
    for ( var i = 0; i < this.numVisibleCols; i++)
    {
      var iterator = (i < numFrozenColumns) ? frozenColIterator : scrollableColIterator;
      var htmlCell = hdrCells[i + 1]; // skip check column
      var colDef = iterator.next();
      if (!htmlCell.controller)
      {
        new Gradebook.CellController(htmlCell, this.grid, 0, i + 1, true);
      }
      htmlCell.controller.renderHeaderCellHTML(colDef);
    }
    // add the check all listener if not present
    if (!hdrCells[0].controller)
    {
      new Gradebook.CellController(hdrCells[0], this.grid, 0, 0, true);
    }
    this.grid.updateSortImage();
  },

  visibleHeight : function()
  {
    return parseInt(GradebookUtil.getElementsComputedStyle(this.div, 'height'), 10);
  },

  toViewIndex : function(modelSortIndex)
  {
    var numFrozenColumns = this.options.numFrozenColumns;
    if (modelSortIndex < numFrozenColumns)
    {
      return modelSortIndex;
    }
    var vi = (modelSortIndex - this.colOffset);
    if (numFrozenColumns <= vi && vi < this.numVisibleCols)
    {
      return vi;
    }
    else
    {
      return -1;
    }
  },

  toModelIndex : function(viewSortIndex)
  {
    if (viewSortIndex == -1)
    {
      return -1;
    }

    var numFrozenColumns = this.options.numFrozenColumns;
    var mi = (viewSortIndex < numFrozenColumns) ? viewSortIndex : (this.colOffset + viewSortIndex);
    return mi;
  },

  // scrolling management

  initScrollers : function()
  {
    this.createVScrollBar();
    this.createHScrollBar();
    this.lastVScrollPos = 0;
    if (this.scrollerDivH)
    {
      this.lastHScrollPos = this.scrollerDivH.scrollLeft;
    }
    else
    {
      this.lastHScrollPos = 0;
    }
    this.startScrollLeft = this.lastHScrollPos;
  },

   createVScrollBar : function()
  {
    // see comments on createHScroolBar()
    if (this.table.rows.length >= this.model.getNumRows())
    {
      return;
    }
    var visibleHeight = this.visibleHeight();
    // rule of third: we have X rows to display, only Y are visible
    // and the height for the Y is visibleHeight, what should be the
    // height for all? totalHeight = ( visibleHeight / Y ) * X
    var numVisibleRows = this.table.rows.length;
    this.rowHeight = parseInt(visibleHeight / numVisibleRows, 10);
    visibleHeight = this.rowHeight * numVisibleRows; // just in case rowHeight
                                                      // was rounded
    var divHeight = this.rowHeight * this.model.getNumRows();

    // create the outer div...
    this.scrollerDiv = document.createElement("div");
    var scrollerStyle = this.scrollerDiv.style;
    scrollerStyle.borderRight = this.options.scrollerBorderRight;
    scrollerStyle.position = "absolute";
    var tableWidth = this.isIE ? this.table.offsetWidth - 2 + "px" : this.table.offsetWidth - 3 + "px";
    if (document.documentElement.dir == 'rtl')
    {
      scrollerStyle.right = tableWidth;
    }
    else
    {
      scrollerStyle.left = tableWidth;
    }
    scrollerStyle.top = "0px";
    scrollerStyle.width = "19px";
    scrollerStyle.height = visibleHeight + "px";
    scrollerStyle.overflow = "scroll";

    // create the inner div...
    this.heightDiv = document.createElement("div");
    this.heightDiv.style.width = "1px";

    this.heightDiv.style.height = parseInt(divHeight, 10) + "px";
    this.scrollerDiv.appendChild(this.heightDiv);
    Event.observe(this.scrollerDiv, 'scroll', this.handleVScroll.bindAsEventListener(this));

    this.table.parentNode.parentNode.insertBefore(this.scrollerDiv, this.table.parentNode.nextSibling);
    var eventName = this.isIE ? "mousewheel" : "DOMMouseScroll";
    Event.observe(this.table, eventName, function(evt)
    {
      if (evt.wheelDelta >= 0 || evt.detail < 0) // wheel-up
        {
          this.scrollerDiv.scrollTop -= (2 * this.rowHeight);
        }
        else
        {
          this.scrollerDiv.scrollTop += (2 * this.rowHeight);
        }
        this.handleVScroll();
      }.bindAsEventListener(this), false);
  },

   createHScrollBar : function()
  {
    // logic here is to create an div the same width that the non frozen
    // columns
    // then put inside it an invisible inner div that would be the width of
    // the non
    // frozen if they were all visible; by setting the parent with overflow:
    // auto
    // scroll bars will appear, and the scrolling events are captured to decide
    // what
    // portion of the table should be displayed.
    if (!this.headerTable.rows[0] || this.headerTable.rows[0].cells.length > this.model.getNumColDefs())
    {
      return;
    }
    var totalColumnCount = this.model.getNumColDefs();
    var visibleColumnCount = this.numVisibleCols;
    var numFrozenColumns = this.options.numFrozenColumns;
    this.maxColOffset = totalColumnCount - (visibleColumnCount - numFrozenColumns);

    var visibleHeight = this.isIE ? this.table.offsetHeight - 23 : this.table.offsetHeight - 3;
    var checkColumnWidth = this.headerTable.rows[0].cells[0].offsetWidth;
    // set avg col width to be based on actual cell width (not including
    // padding, etc.)
    // this will allow scrolling to be more accurate
    this.avgColWidth = this.headerTable.rows[0].cells[1].offsetWidth;
    var frozenWidth = (numFrozenColumns * this.avgColWidth) + checkColumnWidth;
    var visibleWidth = (visibleColumnCount - numFrozenColumns) * this.avgColWidth;

    // create the outer div...
    this.scrollerDivH = document.createElement("div");
    var scrollerStyle = this.scrollerDivH.style;
    scrollerStyle.position = "absolute";
    if (document.documentElement.dir == 'rtl')
    {
      scrollerStyle.right = frozenWidth + "px";
    }
    else
    {
      scrollerStyle.left = frozenWidth + "px";
    }

    scrollerStyle.top = visibleHeight + "px";
    scrollerStyle.height = this.isIE ? "40px" : "19px";
    scrollerStyle.width = visibleWidth + "px";
    scrollerStyle.overflow = "auto";

    // create the inner div...
    this.widthDiv = document.createElement("div");
    this.widthDiv.style.height = "1px";
    this.widthDiv.style.direction = 'ltr';
    this.widthDiv.style.width = (this.avgColWidth * (totalColumnCount - numFrozenColumns)) + "px";
    this.scrollerDivH.appendChild(this.widthDiv);
    Event.observe(this.scrollerDivH, 'scroll', this.handleHScroll.bindAsEventListener(this));

    if (this.scrollerDiv)
    {
      this.table.parentNode.parentNode.insertBefore(this.scrollerDivH, this.scrollerDiv.nextSibling);
    }
    else
    {
      this.table.parentNode.parentNode.insertBefore(this.scrollerDivH, this.table.parentNode.nextSibling);
    }
  },

   rowToPixel : function(rowOffset)
  {
    return (rowOffset / this.model.getNumRows()) * this.heightDiv.offsetHeight;
  },

  moveScroll : function(rowOffset)
  {
    if (this.scrollerDiv)
    {
      this.scrollerDiv.scrollTop = this.rowToPixel(rowOffset);
    }
  },

  /* When scrolling, IE sends multiple onscroll events for a single scroll action by the user.
     To get around this, we set a timer and wait until the dust settles before doing the scroll
     Here is info on the work around: http://support.microsoft.com/kb/238004
  */
  // scroll numRows, can be negative. returns false if scroll request is out of range
  scrollRows : function(numRows)
  {
    if (!this.scrollerDiv)
    {
      return false;
    }
    if ((numRows < 0 && this.scrollerDiv.scrollTop === 0) || (numRows > 0 && this.lastRowPos == (this.model.getNumRows() - this.numVisibleRows)))
    {
      return false;
    }
    this.ignoreOnVscroll = true;
    this.scrollerDiv.scrollTop += (numRows * this.rowHeight);
    setTimeout(this.doVScroll.bind(this), 200 );
    return true;
  },

  handleVScroll : function(evt)
  {
    if ( this.ignoreOnVscroll || Gradebook.alertLightbox )
    {
      return;
    }
    this.ignoreOnVscroll = true;
    setTimeout(this.doVScroll.bind(this), 200);
  },

  doVScroll : function()
  {
    var incomingscrollTop = this.scrollerDiv.scrollTop;
    var scrollDiff = this.lastVScrollPos - this.scrollerDiv.scrollTop;
    if (scrollDiff !== 0.00)
    {
      // Only tell the cellcontroller if we're actually going to do something.
      Gradebook.CellController.prototype.onGridScroll();
      var r = this.scrollerDiv.scrollTop % this.rowHeight;
      if (r !== 0)
      {
        if (scrollDiff < 0)
        {
          this.scrollerDiv.scrollTop += (this.rowHeight - r);
        }
        else
        {
          this.scrollerDiv.scrollTop -= r;
        }
      }
      var contentOffset = parseInt(this.scrollerDiv.scrollTop / parseInt(this.rowHeight, 10), 10);
      this.refreshContents(contentOffset);
      this.lastVScrollPos = this.scrollerDiv.scrollTop;
    }
    this.ignoreOnVscroll = false;
  },

  handleHScroll : function(evt)
  {
    if (this.ignoreOnHscroll || Gradebook.alertLightbox )
    {
      return;
    }
    this.ignoreOnHscroll = true;
    setTimeout(this.doHScroll.bind(this), 200);
  },

  // scroll numCols, can be negative. returns false if scroll request is out of
  // range
  scrollCols : function(numCols)
  {
    if (!this.scrollerDivH)
    {
      return false;
    }
    var totalColumnCount = this.model.getNumColDefs();

    if ((numCols < 0 && this.scrollerDivH.scrollLeft === 0) || (numCols > 0 && this.colOffset == (this.model.getNumColDefs() - this.numVisibleCols)))
    {
      return false;
    }
    this.ignoreOnHscroll = true;
    /*
     * so here we need to translate delta to actual scroll value. The delta is
     * screen orientation agnostic (we need to move to that col in the model) we
     * need to translate the move in a pixel move to the left: a move to the
     * left in l2r means we move to the next col, while in r2l it means we move
     * to previous col, thus the inversion of orientation if r2l.
     */
    this.scrollerDivH.scrollLeft += (numCols * this.avgColWidth * (page.util.isRTL() ? -1 : 1));
    setTimeout(this.doHScroll.bind(this), 200);
    return true;
  },

  doHScroll : function()
  {
    var scrollDiff = this.lastHScrollPos - this.scrollerDivH.scrollLeft;
    if (scrollDiff !== 0.00)
    {
      // Only tell the cellcontroller if we're actually going to do something.
      Gradebook.CellController.prototype.onGridScroll();
      // To align the column scroll - we move by column increment
      var r = this.scrollerDivH.scrollLeft % this.avgColWidth;
      if (r !== 0)
      {
        if (scrollDiff < 0)
        {
          this.scrollerDivH.scrollLeft += (this.avgColWidth - r);
        }
        else
        {
          this.scrollerDivH.scrollLeft -= r;
        }
      }
      this.setColOffsetFromScrollOffset();
      this.refreshContentsH();
      this.lastHScrollPos = this.scrollerDivH.scrollLeft;
    }
    this.ignoreOnHscroll = false;
  },
  
  setColOffsetFromScrollOffset: function()
  {
    var offset = 0;
    if ( document.documentElement.dir == 'rtl' )  
    {
      // Subtract the max scroll left with the current one and divide with the avgColWidth
      offset = parseInt( (this.startScrollLeft - this.scrollerDivH.scrollLeft) / this.avgColWidth, 10);
      if (offset < 0)
      {
        // IE8 Standards mode generates a negative offset with the above logic, but IE7 does not.
        // In an attempt to resolve this without figuring out every single line of this fake scrolling 
        // support, let's go with the number that works in this case.
        offset = parseInt(this.scrollerDivH.scrollLeft / this.avgColWidth, 10);
      }
    } 
    else 
    {
      offset = parseInt(this.scrollerDivH.scrollLeft / this.avgColWidth, 10);
    }
    this.colOffset = Math.min( offset, this.maxColOffset );   
  }

};

