/**
 *  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.
 *
 *  @author "Bill Richard"
 *  @version  
 *
 *
 **/

//Gradebook.GridModel -----------------------------------------------------

Gradebook.GridModel = Class.create();

Gradebook.GridModel.prototype = {

    initialize: function(gradebookService) {
    this.gradebookService = gradebookService;
    this.courseId = gradebookService.courseId;
    this.rows = new Array();
    this.colDefs = new Array();
    this.colOrderMap = new Array();
    this.customViews = new Array();
    this.listeners = new Array();
    this.accessibleMode = false;
    this.resizingWindow = false;
    this.minimumRows = 10;
    this.isolatedStudentId = '';
    // singleton on this document
    this._initMessages();
    // stuff that needs to be stored to survive page refresh but not used by the model
    this.gridColorScheme = null;
    this.store = {};
    gbModel = this;
  },
  
  getObject: function( name )
  {
    return this.store[ name ];
  },
  
  newObject: function( name )
  {
    var newObject = {};
    this.store[ name ] = newObject;
    return newObject;
  },
  
  setObject: function( name, object )
  {
    this.store[ name ] = object;
    return object;
  },
  
  removeObject: function( name )
  {
    delete this.store[ name ];
  },
  
  newArray: function( name )
  {
    this[ name ] = new Array();
    return this[ name ];
  },
  
  getCustomViews: function() {
    this.customViews.sort(
        function(a,b){
          var aa = a.name.toLowerCase();
          var bb = b.name.toLowerCase();
          if (aa == bb) return 0;
          else if (aa < bb) return -1;
          else return 1;
        }
    );
    return this.customViews;
  },
  
  // notify registered listeners that model data has changed
  fireModelChanged: function() {
    if ( !this.messages && this.loadingLocalizedMessages )
    {
      // wait for the messages to be loaded before to do a reload
      window.setTimeout( this.fireModelChanged.bind( this ), 50 );
    }
    else
    {
      for (var i = 0; i < this.listeners.length; i++){
        this.listeners[i].modelChanged();
      }
    }
  },
  
  // notify registered listeners that model error has occured
  fireModelError: function( exception, serverReply) {
    for (var i = 0; i < this.listeners.length; i++){
      if (this.listeners[i].modelError){
        this.listeners[i].modelError( exception, serverReply);
      }
    }
  },
  
  addModelListener: function(listener) {
    this.listeners.push(listener);
  },
  
  removeModelListeners: function() {
    this.listeners = new Array();
  },
  
  updateGrade: function( newValue, newTextValue, userId, colDefId) {
    this.gradebookService.updateGrade((this.updateGradeCallback).bind(this), this.version, newValue, newTextValue, userId, colDefId);
  },

  clearAll: function( isDelete, userId, colDefId) {
    this.gradebookService.clearAll((this.updateGradeCallback).bind(this), this.version, isDelete, userId, colDefId);
  },
  
  clearSelected: function( attemptIds, isDelete, userId, colDefId) {
    this.gradebookService.clearSelected((this.updateGradeCallback).bind(this), this.version, attemptIds, isDelete, userId, colDefId);
  },

  deleteColumn: function(colDefId) {
    this.gradebookService.deleteColumn(colDefId);
  },
  
  modifyColumn: function(colDefId, colType) {
    this.gradebookService.modifyColumn(colDefId, colType);
  },
  
  
  viewItemStats: function(itemId) {
    this.gradebookService.viewItemStats(itemId);
  },
  
  viewSingleStudentGrades: function(userId) {
    this.isolatedStudentId =  userId;
    this.gradebookService.reloadGrid();
  },
  
  restoreFromSingleStudentView: function() {
    this.isolatedStudentId =  '';
    this.gradebookService.reloadGrid();
  },
  
  viewStudentStats: function(userId) {
    this.gradebookService.viewStudentStats(userId);
  },
  
  viewAdaptiveRelease: function(userName) {
    this.gradebookService.viewAdaptiveRelease(userName);
  },
  
  hideColumn: function(colDefId) {
    // decrement numFrozenColumns if hiding a frozen column
    var idx = this.colDefMap[colDefId];
    for (var i = 0; i < this.colOrderMap.length; i++) {
      if (this.colOrderMap[i] == idx)
      { 
        if (i < this.numFrozenColumns && this.numFrozenColumns > 1)
        { 
          this.numFrozenColumns--;
          this.gradebookService.updateNumFrozenColumns( this.numFrozenColumns );
        }
        break;
      }
    }
    this.gradebookService.hideColumn(colDefId);
  },
  
  setColumnStudentVisibility: function (  colDefId, visible ) {
    this.gradebookService.setColumnStudentVisibility 
    ( ( this.setColumnStudentVisibilityCallback).bind(this), colDefId, visible );
  },
  
  showGradeDetails: function(userId, colDefId){
    this.gradebookService.showGradeDetails( userId, colDefId );
  },
  
  onAddComment: function(userId, colDefId){
    this.gradebookService.loadComments( userId, colDefId, "studentComments", "instructorComments" );
  },
  
  exemptGrade: function(userId, colDefId){
    this.gradebookService.setExemption( (this.updateGradeCallback).bind(this), this.version, userId, colDefId, true );
  },
  
  clearExemption: function(userId, colDefId){
    this.gradebookService.setExemption( (this.updateGradeCallback).bind(this), this.version, userId, colDefId, false );
  },
  
  setComments: function(userId, colDefId, studentComments, instructorComments){
    this.gradebookService.setComments( userId, colDefId, studentComments, instructorComments );
  },
  
  getRowByUserId: function(userId) {
    var rowIndex = this.rowUserIdMap[userId];
    if ( rowIndex == undefined || this.rows[rowIndex][0].uid != userId ){
      return null;
    }
    return this.rows[rowIndex];
  },
  
  _getGradesForItemId: function(itemId, includeUnavailable) {
    var grades = new Array()
    var colIndex = this.colDefMap[itemId];
    if (colIndex == undefined){
      GradebookUtil.error('GridModel _getGradesForItemId contains data for invalid column id: '+itemId);
      return grades;
    }
    var rows = ( includeUnavailable ) ? this.rows : this.visibleRows;
    for (var i = 0, len = rows.length; i < len; i++){
      var data = rows[i][colIndex];
      if (data.metaData == undefined) data.metaData = rows[i][0];
      if (includeUnavailable || data.metaData.isAvailable){
        if (data.colDef == undefined) data.colDef = this.colDefs[colIndex];
        grades.push( new Gradebook.GridCell( data ) );
      }
    }
    return grades;
  },

  getColDefById: function (itemId) {
    return  this.colDefs[this.colDefMap[itemId]];
  },
  
  
  setColumnStudentVisibilityCallback: function (retData) {   
    if (!retData){
      GradebookUtil.error('GridModel error updating column visibility');
      return;
    }       
    this.getColDefById(retData.columnId).vis = retData.vis;
    this.fireModelChanged(); 
  },
  
  
  
  updateGradeCallback: function(retData) {
    if (!retData || retData.length == 0){
      GradebookUtil.error('GridModel error updating grade');
      return;
    }
    for (var i = 0, len = retData.length; i < len; i++)
    {
      var data = retData[i];
      var colDefId = data.itemId;
      var userId = data.courseUserId;
      var score = data.score;
      var textInput = data.textInput;
      var row = this.getRowByUserId(userId);
      var colIndex = this.colDefMap[colDefId];
      if (colIndex == undefined){
        // ignore
        continue;
      }
      var gridCell = row[colIndex];
      gridCell.tv = textInput;
      if (textInput.length == 0 && score == 0){
        gridCell.v = '-';
      } else {
        gridCell.v = score;
      }
      gridCell.or = (data.override)?"y":null;
      gridCell.x = (data.exempt)?"y":null;
      gridCell.ng = (data.needsGrading)?"y":null;
      gridCell.ip = (data.inProgress)?"y":null;
      gridCell.notExcluded = !data.excluded;
      gridCell.mp = data.points;
      gridCell.attemptsInfo = null;
    }
    this.lastLogEntryTS = new Date().getTime();
    this.fireModelChanged();
  },
  
  setResizingWindow: function(f) {
    this.resizingWindow = f;
  },
  
  getResizingWindow: function() {
    return this.resizingWindow;
  },
  
  setMinimumRows: function( minRows )
  {
    if ( minRows < 5  ) minRows = 5;
    if ( minRows > 50 ) minRows = 50;
    this.minimumRows = minRows;
  },
  
  getMinimumRows: function()
  {
    return this.minimumRows;
  },
  
  getColorScheme: function( gradeCell )
  {
    if ( gradeCell.needsGrading() )
    {     
      return "cs_ng";
    }
    if ( gradeCell.attemptInProgress() )
    {
      return "cs_ip";
    }
    if ( gradeCell.isExempt() )
    {
      return "cs_ex";
    }
    var colorScheme = this.gridColorScheme;
    if ( colorScheme && gradeCell.isGraded() )
    {
      var normalizedPoints =  gradeCell.getNormalizedGrade();
      if ( normalizedPoints !== null )
      {
        normalizedPoints = normalizedPoints * 100;
        for ( var i = 0; i < colorScheme.length; ++i )
        {
          var range = colorScheme[ i ];
          if ( range.u && normalizedPoints > range.u )
          {
            continue;
          }
          if ( range.l && normalizedPoints < range.l )
          {
            continue;
          }
          return range.cid;
        }
      }
    }
    return "";
  },
  
  _reportException: function(e) {
    GradebookUtil.error('exception getting data from server: '+ e.message);
    this.fireModelError( e, e.message )
  },
  
  _reportError: function(t) {
    GradebookUtil.error('error getting data from server: '+ t.status + ' -- ' + t.statusText);
    this.fireModelError( 'error getting data from server', t.statusText );
  },
  
  getVisibleColDefIndex: function(id) {
    var colnum = this.colDefMap[id]; 
    if (colnum === undefined || this.colDefs[colnum] === undefined ||
        !this.colDefs[colnum].gbvis ){
      return -1;
    }
    for (var i = 0, len = this.colOrderMap.length; i < len; i++){
      if (this.colOrderMap[i] == colnum) return i;
    }
    return -1;
  },
  
  updateUserVisibility: function ( userId, visible ) {
    this.gradebookService.updateUserVisibility( userId, visible );
  },
  
  
  _hasNewUsers: function(jsonBook) {
    if (!jsonBook || !jsonBook.rows) return false;
    for (var i = 0; i < jsonBook.rows.length; i++){
      if (this.getRowByUserId(jsonBook.rows[i][0].uid) == null) return true;
    }
    return false;
  },
  
  _containsUser: function(rows, userId) {
    for (var i = 0; i < rows.length; i++){
      if (rows[i][0].uid == userId) return true;
    }
    return false;
  },
  
  
  // called by view to get a window of row data
  // returns iterators to get row data in correct order while skipping hidden columns
  getRowIterators: function(startRow, numRows, startCol) {
    var rows = this.visibleRows;
    if (!startRow) startRow = 0;
    if (!startCol) startCol = 0;
    if (!numRows) numRows = rows.length;
    var endRow = startRow + numRows
  
    if (startRow < 0 || startRow >= rows.length) {
      GradebookUtil.error('getRowIterators startRow out of range. Max is: '+rows.length-1+' startRow is: '+startRow);
      return null;
    }
    if (numRows < 0 || numRows > rows.length) {
      GradebookUtil.error('getRowIterators numRows out of range. Max is: '+rows.length+' numRows is: '+numRows);
      return null;
    }
    if (startCol < 0 || startCol >= this.colOrderMap.length) {
      GradebookUtil.error('getRowIterators startCol out of range. Max is: '+this.orderMap.length+' startCol is: '+startCol);
      return null;
    }
    if ( endRow > rows.length){
      endRow = rows.length
      GradebookUtil.error('Error: GridModel getRowIterators input args requesting too much data. startRow = '+startRow+' numRows = '+numRows+' rows.length = '+rows.length);
      return null;
    }
  
    var results = new Array()
    var index = 0;
    for ( var i=startRow ; i < endRow; i++ ) {
      results[index++] = new Gradebook.GridRowIterator(rows[i],this.colOrderMap,startCol,this.colDefs);
    }
    return results;
  },
  
  // called by view to get the column definitions
  // returns iterator to get definitions in correct order while skipping hidden columns
  getColDefIterator: function(startCol) {
    if (!startCol) startCol = 0;
    if (startCol < 0 || startCol >= this.colOrderMap.length) {
      GradebookUtil.error('getColDefIterator startCol out of range. Max is: '+this.orderMap.length+' startCol is: '+startCol);
      return null;
    }
    return new Gradebook.ColDefIterator(this.colDefs,this.colOrderMap,startCol);
  },
  
  // called by view to determine how much vertical scroll is needed
  getNumRows: function() {
    if (this.visibleRows)
      return this.visibleRows.length;
    else 
      return 0;
  },
  
  // called by view to determine how much horizontal scroll is needed
  getNumColDefs: function() {
    return this.colOrderMap.length;
  },
  
  // called by view to determine how many columns to freeze
  getNumFrozenColumns: function() {
    return this.numFrozenColumns;
  },
  
  getSortDir: function(){
    return this.sortDir;
  }, 
  
  getSortIndex: function(){
    if (this.sortColumnId === undefined){
      return -1;
    }
    var colnum = this.colDefMap[this.sortColumnId];
    if (colnum === undefined){
      return -1;
    } else {
      var sortColumn = this.colOrderMap[colnum];
      if (sortColumn === undefined || this.colDefs[sortColumn] === undefined ||
          this.colDefs[sortColumn].deleted == "Y"){
        return -1;
      } else {
        return colnum;
      }
    }
  }, 
  
  reSort: function() {
    if (this.sortColumnId === undefined || this.sortDir === undefined) return;
    var colnum = this.getSortIndex();
    if (colnum == -1) return;
    this.sort(colnum, this.sortDir);
  },
  
  setDefaultView: function(view) {
    this.defView = view;
    this.gradebookService.setDefaultView( view );
  },  
  
  getDefaultView: function() {
    if ( !this._isValidView( this.defView ) ) this.defView = 'fullGC';
    return this.defView;
  },  
  
  setCategoryFilter: function(category) {
    this.categoryFilter = category; // override category for current view
  },
  
  setStatusFilter: function(status) {
    if (status.startsWith("stat_")) status = status.substr( 5, status.length - 5 );
    this.statusFilter = status; // override status for current view
  },
  
  setInitialCurrentView: function(view) {
    this.initialView = view;
  },
  
  // set the current view to a fullGC, custom view, or grading period
  //   view param is: 
  //     'fullGC' for full
  //     'cv_123' for custom views 
  //     '456' for grading periods
  // if specified view is invalid, use default, if default is invalid, use full
  // 
  setCurrentView: function(view) {
  
    this.categoryFilter = null;  // clear category override
    this.statusFilter = null;  // clear status override
    this.currentCustomView = null;
    this.currentGradingPeriodId = null;
  
    if (!this._isValidView(view)) {
      view = this.defView;
    }
    if (!this._isValidView(view)) {
      view = 'fullGC'; 
    }
  
    if (view == 'fullGC') {
      // use a custom view for full grade center to allow category/status overrides
      this.currentCustomView = Gradebook.CustomView.getFullGC(this); 
    } else if (view.startsWith('cv_')){
      var idx = this.customViewMap[view.substring(3)];
      this.currentCustomView = this.customViews[idx];
    } else if (view.startsWith('gp_')){
      this.currentGradingPeriodId = view.substring(3);
    }
    this.currentView = view;
  },
  
  _isValidView: function( view ) {
    if (!view) return false;
    if (view == 'fullGC') return true;
    if (view.startsWith('cv_') && this.customViewMap[view.substring(3)] != undefined){
      var idx = this.customViewMap[view.substring(3)];
      return this.customViews[idx].evaluate();
    }
    if (view.startsWith('gp_')){
      return this.gradingPeriodMap && (this.gradingPeriodMap[view.substring(3)] != undefined);
    }
    return false;
  },
  
  _applyCustomView: function() {
    if ( this.isolatedStudentId )
    {
      this.visibleRows = new Array();
      var row = this.getRowByUserId(this.isolatedStudentId);
      this.visibleRows.push( row );
    }
    if(this.currentCustomView == null || this.currentCustomView == undefined){
      return;
    }
    this.currentCustomView.evaluate( this );
    var userIds = this.currentCustomView.getUserIds();
    if ( this.isolatedStudentId ) return;
    this.visibleRows = new Array();
    // loop through custom view users and add to visibleRows
    for (var i = 0, len = userIds.length; i < len; i++){
      var row = this.getRowByUserId(userIds[i]);
      if (row) this.visibleRows.push( row );
    }
  },
  
  getCustomView: function(cvId) {
    var idx = this.customViewMap[cvId]; 
    if (idx == undefined){
      return null;
    } else {
      return this.customViews[idx];
    }
  },
  
  getCurrentCustomView: function( ) {
    return this.currentCustomView;
  },
  
  getCurrentStatus: function() {
    if (!this.isStatusView()) return 'stat_ALL';
    else if (this.statusFilter) return this.statusFilter;
    else return this.currentCustomView.display.items
  },
  
  getCurrentCategory: function() {
    if (!this.isStatusView()) return 'c_all';
    else if (this.categoryFilter) return this.categoryFilter;
    else if (this.currentCustomView.category == 'c_all') return 'c_all';
    else return 'c_'+this.currentCustomView.aliasMap[this.currentCustomView.category];
  },
  
  isStatusView: function() {
    return this.currentCustomView && this.currentCustomView.searchType == 'status'; 
  },
  
  getCurrentViewName: function() {
    if (this.currentCustomView) 
      return this.currentCustomView.name;
    else if ( this.currentGradingPeriodId )
      return this.gradingPeriodMap[ this.currentGradingPeriodId ].name;
    else
      return "";
  },
  
  getCurrentViewStatus: function() {
    var s = this.getCurrentStatus();
    if (s.startsWith("stat_")) s = s.substr( 5, status.length - 5 );
    if (s == "ALL") return this.getMessage('all_statusesMsg');
    else if (s == "NA") return this.getMessage('not_attemptedMsg');
    else if (s == "C") return this.getMessage('completedMsg');
    else if (s == "NG") return this.getMessage('needs_gradingMsg');
    else if (s == "IP") return this.getMessage('in_progressMsg');
    else if (s == "EM") return this.getMessage('edited_manuallyMsg');
  },
  
  sortColumns: function(sortBy) {
    if (this.sortColAscending == undefined){
      this.sortColAscending = true;
    }
  
    if (sortBy){
      if (this.currentSortColumnBy == sortBy){
        this.sortColAscending = !this.sortColAscending;
      } else {
        this.sortColAscending = true;
        this.currentSortColumnBy = sortBy;
      }
    }
    else if (!this.currentSortColumnBy){
      this.currentSortColumnBy = 'pos';
    }
    var sortFunc = null;
    sortBy = this.currentSortColumnBy;
    if (sortBy == 'pos'){
      sortFunc = this._sortColByPosFunc.bind(this);
    } else if (sortBy == 'categories'){
      sortFunc = this._sortColByCategoriesFunc.bind(this);
    } else if (sortBy == 'dueDate') {
      sortFunc = this._sortColByDueDateFunc.bind(this);
    } else if (sortBy == 'creationdate') {
      sortFunc = this._sortColByCreationDateFunc.bind(this);
    } else if (sortBy == 'points') {
      sortFunc = this._sortColByPointsFunc.bind(this);
    } else if (sortBy == 'name') {
      sortFunc = this._sortColByNameFunc.bind(this);
    }
  
    var tempColDefs = new Array();
  
    if(this.currentCustomView){
      var colIds = this.currentCustomView.getDisplayItemIds();
      tempColDefs = this._getVisibleToAll( this.currentCustomView.includeHiddenItems, colIds );
      for (var i = 0, len = colIds.length; i < len; i++){
        var cd = this.colDefs[this.colDefMap[colIds[i]]];
        tempColDefs.push(cd);
      }
    } else {
      // filter out colDefs that are: deleted, hidden, not in all grading periods
      // or not in current grading period
      for (var i = 0, len = this.colDefs.length; i < len; i++){
        var cd = this.colDefs[i];
        if (cd.deleted || !cd.gbvis) continue;
        var cgp = this.currentGradingPeriodId;
        var ingp = (cgp == undefined || cgp == cd.gpid || cgp == 'all' || (cgp == 'none' && cd.gpid == ''));
        if (cd.visAll || !cd.isGrade() || ingp){
          tempColDefs.push(cd);
        }
      }
    }
    tempColDefs.sort(sortFunc);
  
    // compute colOrderMap based on the sorted columns
    this.colOrderMap = new Array();
    for (var i = 0, len = tempColDefs.length, idx = 0; i < len; i++){
      this.colOrderMap[idx++] = this.colDefMap[tempColDefs[i].id];
    }    
  },
  
  _getVisibleToAll: function( includeHidden, excludeIds ) {
    var tempColDefs = new Array();
    for (var i = 0, len = this.colDefs.length; i < len; i++){
      var cd = this.colDefs[i];
      if (excludeIds.indexOf( cd.id ) != -1) continue;
      var visAll = cd.visAll || !cd.isGrade();
      if (cd.deleted || !visAll || (!includeHidden && !cd.gbvis)) continue;
      tempColDefs.push(cd);
    }
    return tempColDefs;
  },
  
  // if both a & b are NOT visible to all, returns null
  // if both a & b are visible to all, sorts by position
  // if a is visible to all, returns -1 so visible to all columns come first
  // if b is visible to all, returns 1 so visible to all columns come first
  _sortVisibleToAll: function(a,b) {
    var aVisAll = a.visAll || !a.isGrade();
    var bVisAll = b.visAll || !b.isGrade();
    if (!aVisAll && !bVisAll) {
      return null;
    } else if (aVisAll && bVisAll) {
      return a.pos - b.pos;
    } else if (aVisAll) {
      return -1;
    } else if (bVisAll) {
      return 1;
    }
  },
  
  _sortColDir: function(result) {
    return (this.sortColAscending)?result:result*-1;
  },
  
  _sortColByPosFunc: function(a,b) {
    var sf = this._sortVisibleToAll(a,b);
    if (sf != null) return sf;
    var gpPosA = (a.gpid.blank())?-1:this.gradingPeriodMap[a.gpid].pos;    
    var gpPosB = (b.gpid.blank())?-1:this.gradingPeriodMap[b.gpid].pos;  
    var res;  
    if (gpPosA == gpPosB){
      res = a.pos - b.pos;
    } else if (gpPosA >= 0 && gpPosB >= 0){
      res = gpPosA - gpPosB;
    } else if (gpPosB == -1) {
      res = -1;
    } else {
      res = 1;
    }
    return this._sortColDir( res );
  },
  
  _sortColByPointsFunc: function(a,b) {
    var sf = this._sortVisibleToAll(a,b);
    if (sf != null) return sf;
    var aa = a.points;
    var bb = b.points;
    var res;  
    if (aa==bb) 
      res = a.cdate - b.cdate;
    else if (aa<bb) 
      res = -1;
    else
      res = 1;
    return this._sortColDir( res );
  },
  
  _sortColByNameFunc: function(a,b) {
    var sf = this._sortVisibleToAll(a,b);
    if (sf != null) return sf;
    var aa = a.name.toLocaleLowerCase();
    var bb = b.name.toLocaleLowerCase();
    var res;  
    if (aa==bb) 
      res = a.cdate - b.cdate;
    else if (aa<bb) 
      res = -1;
    else
      res = 1;
    return this._sortColDir( res );
  },
  
  _sortColByDueDateFunc: function(a,b) {
    var sf = this._sortVisibleToAll(a,b);
    if (sf != null) return sf;
    var aa = a.due;
    var bb = b.due;
    var res;  
    if (aa==bb) 
      res = a.cdate - b.cdate;
    else if (aa==0) 
      res = -1; // items with no due date, appear before items with due date
    else if (bb==0) 
      res = 1; // items with no due date, appear before items with due date
    else if (aa<bb) 
      res = -1;
    else 
      res = 1;
    return this._sortColDir( res );
  },
  
  _sortColByCreationDateFunc: function(a,b) {
    var sf = this._sortVisibleToAll(a,b);
    if (sf != null) return sf;
    var res = a.cdate - b.cdate;
    return this._sortColDir( res );
  },
  
  _sortColByCategoriesFunc: function(a,b) {
    var sf = this._sortVisibleToAll(a,b);
    if (sf != null) return sf;
    var aa = a.getCategory();
    var bb = b.getCategory();
    var res;
    if (aa==bb) 
      res = a.cdate - b.cdate;
    else if (aa<bb) 
      res = -1;
    else
      res = 1;
    return this._sortColDir( res );
  },
  
  sort: function(colnum, sortdir) {
    if (colnum < -1 || colnum >= this.colOrderMap.length) {
      GradebookUtil.error('sort colnum out of range. Max is: '+this.orderMap.length+' colnum is: '+colnum);
      return;
    }
    GradebookUtil.debug('GridModel sort called. colnum: '+colnum+' sortdir: '+sortdir);
    this.sortDir = sortdir;
    var sortFunc;
    if (colnum == -1){
      this.sortColumnId = null;
      if (sortdir=='ASC')
        sortFunc = this._sortCheckedASC.bind(this);
      else
        sortFunc = this._sortCheckedDESC.bind(this);
    } else {
      var sortColumn = this.colOrderMap[colnum];
      var secondarySortColumn = this.colDefMap['LN'];
      var colDef = this.colDefs[sortColumn];
      this.sortColumnId = colDef.id;
      if ( this.sortColumnId == 'LN' ){
        secondarySortColumn = this.colDefMap['FN'];
      }
      sortFunc = colDef.getSortFunction(sortColumn, sortdir, secondarySortColumn);
    }
    this.visibleRows.sort(sortFunc);
  },
  
  _sortCheckedASC: function(a,b) {
    var aa = a[0].isRowChecked?1:0;
    var bb = b[0].isRowChecked?1:0;
    if (aa==bb) return 0;
    if (aa<bb) return -1;
    return 1;
  },
  
  _sortCheckedDESC: function(a,b) {
    var aa = a[0].isRowChecked?1:0;
    var bb = b[0].isRowChecked?1:0;
    if (aa==bb) return 0;
    if (bb<aa) return -1;
    return 1;
  },
  
  // called by cumultive item authoring
  // returns gradable items
  getColDefs: function (gradableOnly, includeHidden) {
    var colDefs =  this.colDefs;
    var retColDefs = new Array();
    for (var i = 0, len = colDefs.length; i < len; i++){
      var c = colDefs[i];
      if (!c.deleted && (!gradableOnly || c.isGrade()) && (includeHidden || !c.isHidden())){
        retColDefs.push( c );
      }
    }
    return retColDefs;
  },
  
  // called by grade detail page
  getCurrentColDefs: function (includeCalculated) {
    var colDefs =  this.colDefs;
    var retColDefs = new Array();
    for (var i = 0, len = this.colOrderMap.length; i < len; i++){
      var c = colDefs[this.colOrderMap[i]];
      if (c.isGrade() && (includeCalculated || !c.isCalculated()) ){
        retColDefs.push( c );
      }
    }
    return retColDefs;
  },
  
  // called by grade detail page
  getNextColDefId: function (colDefs, colDefId) {
    for (var i = 0; i < colDefs.length-1; i++){
      if (colDefs[i].getID() == colDefId){
        return colDefs[i+1].getID();
      }
    }
    return null;
  },
  
  // called by grade detail page
  getPrevColDefId: function (colDefs, colDefId) {
    for (var i = 1; i < colDefs.length; i++){
      if (colDefs[i].getID() == colDefId){
        return colDefs[i-1].getID();
      }
    }
    return null;
  },

  // called by grade detail page
  getStudents : function( includeHidden )
  {
    var rows = ( includeHidden ) ? this.rows : this.visibleRows;
    var students = new Array();
    var LAST_NAME_COL_IDX = 0;
    var FIRST_NAME_COL_IDX = 1;
    var USER_NAME_COL_IDX = 2;
    if ( rows != null )
    {
      rows.sort( function( a,b )
      {
        var aa = a[LAST_NAME_COL_IDX].v;
        var bb = b[LAST_NAME_COL_IDX].v;
        if (aa == bb) return 0;
        else if (aa < bb) return -1;
        else return 1;
      });      
      for ( var i = 0; i < rows.length; i++ )
      {
        var s = {};
        var row = rows[ i ];
        s.last = row[ LAST_NAME_COL_IDX ].v;
        s.first = row[ FIRST_NAME_COL_IDX ].v;
        s.user = row[ USER_NAME_COL_IDX ].v;
        s.id = row[ 0 ].uid;
        s.hidden = row[ 0 ].isHidden;
        s.available = row[ 0 ].isAvailable;
        students.push( s );
      }
    }
    return students;
  },
  
  // called by cumulative item page
  getGradingPeriods : function()
  {
    return this.gradingPeriods;
  },
  
  // called by cumulative item page
  getCategories: function () {        
    return this.categories;
  },
  
  // called by grade detail page
  getNextUserId: function (userId) {
    for (var i = 0; i < this.visibleRows.length-1; i++){
      if (this.visibleRows[i][0].uid == userId){
        return this.visibleRows[i+1][0].uid;
      }
    }
    return null;
  },
  
  // called by grade detail page
  getPrevUserId: function (userId) {
    for (var i = 1; i < this.visibleRows.length; i++){
      if (this.visibleRows[i][0].uid == userId){
        return this.visibleRows[i-1][0].uid;
      }
    }
    return null;
  },
  
  // called by grade detail page; returns null if invalid colId
  getRawValue: function (colId, displayValue) {
    var colIndex = this.colDefMap[colId]; 
    if (colIndex === undefined){
      return null;
    }
    var colDef = this.colDefs[colIndex];
    return colDef.getRawValue(displayValue);
  },
  
  // called by grade detail page; returns null if invalid colId
  getDisplayValue: function (colId, rawValue) {
    var colIndex = this.colDefMap[colId]; 
    if (colIndex === undefined){
      return null;
    }
    var colDef = this.colDefs[colIndex];
    return colDef.getDisplayValue(rawValue);
  },
  
  // called by grade detail page; returns null if invalid colId 
  getDisplayType: function(colId) {
    var colIndex = this.colDefMap[colId]; 
    if (colIndex === undefined){
      return null;
    }
    var colDef = this.colDefs[colIndex];
    return colDef.getDisplayType();
  },
  
  // called by grade detail page; returns validate error or null if no error
  validate: function (colId, newValue) {
    var colIndex = this.colDefMap[colId]; 
    if (colIndex === undefined){
      return null;
    }
    var colDef = this.colDefs[colIndex];
    return colDef.validate(newValue);
  },
  
  getCheckedStudentIds: function () {
    var rows =  this.visibleRows;
    var students = new Array();
    for (var i = 0, len = rows.length; i < len; i++){
      if (rows[i][0].isRowChecked){ 
        students.push(rows[i][0].uid);
      }
    }
    return students;
  },
  
  checkedAllStudents: function () {
    var rows =  this.visibleRows;
    for (var i = 0, len = rows.length; i < len; i++){
      rows[i][0].isRowChecked = true;
    }
    this.fireModelChanged();
  
  },
  
  checkedNoStudents: function () {
    var rows =  this.visibleRows;
    for (var i = 0, len = rows.length; i < len; i++){
      rows[i][0].isRowChecked = false;
    }
    this.fireModelChanged();
  },
  
  invertCheckedStudents: function () {
    var rows =  this.visibleRows;
    for (var i = 0, len = rows.length; i < len; i++){
      rows[i][0].isRowChecked = !rows[i][0].isRowChecked;
    }
    this.fireModelChanged();
  },
  
  checkedRangeOfStudents: function (uid1,uid2) {
    var startId;
    var rows =  this.visibleRows;
    for (var i = 0, len = rows.length; i < len; i++){
      var uid = rows[i][0].uid;
      if (!startId && (uid != uid1 && uid != uid2)) continue;
      else if (!startId && uid == uid1) startId = uid;
      else if (!startId && uid == uid2) startId = uid;
      else if (uid == uid1 || uid == uid2) break;
      else rows[i][0].isRowChecked = true;
    }
    this.fireModelChanged();
  },
  
  
  clearAttempts: function (colId, clearOption, startDate, endDate) {
    this.gradebookService.clearAttempts(colId, clearOption, startDate, endDate);
  },
  
  updateGroups: function () {
    var crsId = this.courseId;
    if (crsId.indexOf("_") >= 0) {
      crsId = crsId.split("_")[1];
    }
    var gradeCenterContentFrame = window.frames['gradecenterframe']; // Grade Center Frame in SSL mode
    if (!gradeCenterContentFrame) gradeCenterContentFrame = window.frames['content']; // regular course content frame
    if (!gradeCenterContentFrame.GradebookDWRFacade) gradeCenterContentFrame = window.frames['content'].frames['main']; 
    gradeCenterContentFrame.GradebookDWRFacade.getGroups(crsId, Gradebook.GridModel.prototype.updateGroupsCallback );
  },
  
  updateGroupsCallback: function (retData) {
    var groupsMap = new Array();
    var groups = new Array();
    var h = $H(retData);
    h.each(function(pair) {
      var g = {};
      g.id = pair.key;
      g.uids = pair.value;
      groupsMap[g.id] = groups.length;
      groups.push( g );
    });
    var model = Gradebook.getModel();   
    model.groupsMap = groupsMap;
    model.groups = groups;
  },
  
  // used by reporting
  getReportData: function (reportDef) {
    var LAST_NAME_COL_IDX = 0;
    // get rows for students to include in report
    var userIds = null;
    if (reportDef.students == 'BYGROUPS'){
      if ( reportDef.groupIds == null ){
        GradebookUtil.error('GridModel error getReportData: no reportDef.groupIds');
        return null;
      }
      userIds = this._getUserIdsByGroupIds( reportDef.groupIds );
    } else if (reportDef.students == 'BYSTUDENT') {
      if ( reportDef.studentIds == null ){
        GradebookUtil.error('GridModel error getReportData: no reportDef.studentIds');
        return null;
      }
      userIds = reportDef.studentIds;
    }
    var rows = this._getRowsByUserIds( userIds ); 
    if ( !reportDef.includeHiddenStudents )
      rows = this._removeHiddenStudents(rows);
    // get columns to include in report
    var colDefs = this.getColDefs(true,true);
    if (reportDef.columns == 'BYITEM'){
      colDefs = this._getColDefsById( reportDef.itemIds );
    } else if (reportDef.columns == 'BYGP'){
      colDefs = this._getColDefsByGradingPeriodId( reportDef.gradingPeriodIds );
    } else if (reportDef.columns == 'BYCAT'){
      colDefs = this._getColDefsByCategoryId( reportDef.categoryIds );
    }
    if (!reportDef.includeHiddenColumns){
      colDefs = this._removeHiddenColumns( colDefs );
    }
  
  //before printing the report, sort on student's last name,according to PM's requirement
    rows.sort( function( a,b ){
      var aa = a[LAST_NAME_COL_IDX].v;
      var bb = b[LAST_NAME_COL_IDX].v;
      if (aa == bb) return 0;
      else if (aa < bb) return -1;
      else return 1;
    });
    // create return data structure
    var reportData = {};
    reportData.columnInfoMap = new Array();
    reportData.studentGradeInfo = new Array();
  
    // add column data
    for (var i = 0, len = colDefs.length; i < len; i++){
      var cdef = colDefs[i];
      var cdata = {};
      reportData.columnInfoMap[cdef.id] = cdata;
      cdata.name = cdef.getName();
      if (reportDef.columnInfoDescription){
        cdata.description = 'tbd';    // server will provide desc map
      }
      if (reportDef.columnInfoDueDate){
        cdata.dueDate = cdef.getDueDate();
      }
      if (reportDef.columnInfoStatsMedian || reportDef.columnInfoStatsAverage){
        var stats = cdef.getStats( true ); // include unavailable students
        cdata.statsMedian = stats.median;
        cdata.statsAverage = stats.avg;
      }
    }
  
    // add student data
    for (var i = 0, len0 = rows.length; i < len0; i++){
      var row = rows[i];
      var rd = {};
      reportData.studentGradeInfo.push(rd);
  
      if (reportDef.firstName){
        rd.firstName = this._getStudentAttribute( row, 'FN' );
      }
      if (reportDef.lastName){
        rd.lastName = this._getStudentAttribute( row, 'LN' );
      }
      if ( reportDef.studentId ){
        rd.studentId = this._getStudentAttribute( row, 'SI' );
      }
      if ( reportDef.userName ){
        rd.userName = this._getStudentAttribute( row, 'UN' );
      }
      if ( reportDef.lastAccessed ){
        rd.lastAccessed = this._getStudentAttribute( row, 'LA' );
        if (rd.lastAccessed && rd.lastAccessed > 0){ 
          var date = new Date();
          date.setTime(rd.lastAccessed);
          rd.lastAccessed = formatDate(date,'MMM d, y');
        }
      }
      rd.grades = new Array();
      for (var c = 0, len1 = colDefs.length; c < len1; c++){
        var g = {};
        g.cid = colDefs[c].id;
        var gridCell = this._getGrade( row, colDefs[c]);
        if (gridCell.attemptInProgress() && !gridCell.isOverride()) 
          g.grade = this.getMessage('inProgressMsg');
        else if (gridCell.needsGrading() && !gridCell.isOverride()) 
          g.grade = this.getMessage('needsGradingMsg');
        else 
          g.grade = gridCell.getCellValue();
        rd.grades.push( g );
      }
    }
    return reportData;
  },
  
  _getGrade: function( row, colDef ) {
    var colIndex = this.colDefMap[colDef.id];
    if (colIndex == undefined){
      GradebookUtil.error('GridModel _getGrade invalid column id: '+colDef.id);
      return null;
    }
    var data = row[colIndex];
    if (data.metaData == undefined) data.metaData = row[0];
  if (data.colDef == undefined) data.colDef = colDef;
  return new Gradebook.GridCell( data );
  },
  
  _getStudentAttribute: function( row, colDefId ) {
    var colIndex = this.colDefMap[colDefId];
    if (colIndex == undefined){
      GradebookUtil.error('GridModel _getStudentAttribute invalid column id: '+colDefId);
      return null;
    }
    return row[colIndex].v;
  },
  _removeHiddenStudents: function( students ) {
    var retStudents = new Array();
    for (var i = 0, len = students.length; i < len; i++){
      if (!students[i][0].isHidden){  
        retStudents.push( students[i] );
      }
    }
    return retStudents;
  },
  
  _removeHiddenColumns: function( colDefs ) {
    var retColDefs = new Array();
    for (var i = 0, len = colDefs.length; i < len; i++){
      if (!colDefs[i].isHidden()){  
        retColDefs.push( colDefs[i] );
      }
    }
    return retColDefs;
  },
  
  _getColDefsById: function( itemIds ) {
    var colDefs = new Array();
    for (var i = 0, len = this.colDefs.length; i < len; i++){
      if (itemIds.indexOf( this.colDefs[i].id ) != -1){  
        colDefs.push( this.colDefs[i] );
      }
    }
    return colDefs;
  },
  
  _getColDefsByCategoryId: function( categoryIds ) {
    var colDefs = new Array();
    for (var i = 0, len = this.colDefs.length; i < len; i++){
      if (categoryIds.indexOf( this.colDefs[i].catid ) != -1){  
        colDefs.push( this.colDefs[i] );
      }
    }
    return colDefs;
  },
  
  _getColDefsByGradingPeriodId: function( gradingPeriodIds ) {
    var colDefs = new Array();
    for (var i = 0, len = this.colDefs.length; i < len; i++){
      if (gradingPeriodIds.indexOf( this.colDefs[i].gpid ) != -1){  
        colDefs.push( this.colDefs[i] );
      }
    }
    return colDefs;
  },
  
  _getRowsByUserIds: function( userIds ) {
    var rows = this.rows;
    if (userIds == null){
      return rows;
    }
    var retRows = new Array();
    for (var i = 0, len = rows.length; i < len; i++){
      if (userIds.indexOf(rows[i][0].uid) != -1){
        retRows.push(rows[i]);
      }
    }
    return retRows;
  },
  
  _getUserIdsByGroupIds: function( groupIds ) {
    if ( !this.groupsMap || !this.groups ){
      GradebookUtil.error('GridModel error getUserIdsByGroupIds: no groups');
      return null;
    }
    var userIds = new Array();
    for (var i = 0; i < groupIds.length; i++){
      var index = this.groupsMap[Number(groupIds[i])];
      if (index == undefined){
        GradebookUtil.error('GridModel error getUserIdsByGroupIds: no group for id: '+groupIds[i]);
        continue;
      }
      var group = this.groups[index];
      for (var g = 0; g < group.uids.length; g++){
        if (userIds.indexOf(group.uids[g]) == -1){
          userIds.push( String( group.uids[g] ) );
        }
      }
    }
    return userIds;
  },
  
  // called by student stats page
  getStudentStats: function( userId, currentViewOnly ) {
    var studentStats = {};
    studentStats.catStats = new Array();
    var catMap = new Array();
  
  
    // get columns, either all or current view
    var colDefs = new Array();
    var len = currentViewOnly ? this.colOrderMap.length : this.colDefs.length;
    for (var i = 0; i < len; i++){
      var idx = currentViewOnly ? this.colOrderMap[i] : i;
      var c = this.colDefs[idx];
      if (!c.deleted && c.isGrade() && !c.isCalculated()) {
        colDefs.push( c );
      }
    }
  
    var row = this.getRowByUserId(userId);
  
    for (var i = 0; i < colDefs.length; i++){
      var colDef = colDefs[i];
      var catId = colDef.getCategoryID();
      var catStat = catMap[catId];
      if (catStat == null){
        catStat = {};
        catStat.name = colDef.getCategory();
        catStat.qtyGraded = 0;
        catStat.qtyInProgress = 0;
        catStat.qtyNeedsGrading = 0;
        catStat.qtyExempt = 0;
        catStat.sum = 0;
        catStat.avg = 0;
        catMap[catId] = catStat;
        studentStats.catStats.push( catStat );
      }
      var grade = this._getGrade( row, colDef);
      var val = grade.getSortValue();
      var isNull = (val == '-');
      var isIP = grade.attemptInProgress();
      var isNG = grade.needsGrading();
      var isExempt = grade.isExempt();
      var isVal = (!isNull && !isIP && !isNG && !isExempt);
      if (isIP) 
        catStat.qtyInProgress++;
      else if (isNG) 
        catStat.qtyNeedsGrading++;
      else if (isExempt) 
        catStat.qtyExempt++;
  
      if (isVal){
        catStat.qtyGraded++;
        if (colDef.isCalculated()){
          val = parseFloat(val)/parseFloat(grade.getPointsPossible()) * 100.0;  
        }
        catStat.sum += parseFloat( val );
      }
    }
    studentStats.numItemsCompleted = 0;
    var totNumExempt = 0;
    for (var i = 0; i < studentStats.catStats.length; i++){
      var catStat = studentStats.catStats[i];
      if (catStat.sum > 0){
        catStat.avg = catStat.sum/parseFloat( catStat.qtyGraded );
        catStat.avg = NumberFormatter.getDisplayFloat( catStat.avg.toFixed(2) );
      }
      totNumExempt += catStat.qtyExempt;
      studentStats.numItemsCompleted += (catStat.qtyNeedsGrading + catStat.qtyGraded);
    }
    studentStats.numItems = colDefs.length - totNumExempt;
    return studentStats;
  },
  
  getAccessibleMode: function() {
    return this.accessibleMode;
  },
  
  setAccessibleMode: function( accessibleMode ) {
    this.accessibleMode = accessibleMode;
  },
  
  setLoggerDebugLevel: function(logger) {
    this.logger.setLevel(top.content.log4javascript.Level.DEBUG);
  },
  
  setLogger: function(logger) {
    this.logger = logger;
  },
  
  getLogger: function() {
    return this.logger;
  },
  
  setMessages: function (messages) {
    this.messages = messages;
  },
  
  getMessage: function (key) {
    if (this.messages){
      return this.messages[key];
    } else {
      return key;
    }
  }


};

////////////////////////////Utility //////////////////////////////////////

Gradebook.GridRowIterator = Class.create();

Gradebook.GridRowIterator.prototype = {
    initialize: function(dataArray, orderMap, startIndex, colDefs) {
    this.dataArray = dataArray;
    this.orderMap = orderMap;
    this.currentIndex = startIndex;
    this.colDefs = colDefs;
  },
  hasNext: function() {return this.currentIndex < this.orderMap.length;},
  next: function() {
    if (this.currentIndex >= this.orderMap.length) {
      GradebookUtil.error('GridRowIterator out of data. length = '+this.orderMap.length);
      return null;
    }
    var idx = this.orderMap[this.currentIndex++];
    var data = this.dataArray[idx];
    // add colDef & metedata reference to cell data, if not already there 
    if ( !data.colDef ) data.colDef = this.colDefs[idx];
    if ( !data.metaData ) data.metaData = this.dataArray[0];  // first cell is extended with metadata
    return data;
  }
};

Gradebook.ColDefIterator = Class.create();

Gradebook.ColDefIterator.prototype = {
    initialize: function(dataArray, orderMap, startIndex) {
    this.dataArray = dataArray;
    this.orderMap = orderMap;
    this.currentIndex = startIndex;
  },
  hasNext: function() {return this.currentIndex < this.orderMap.length;},
  next: function() {
    if (this.currentIndex >= this.orderMap.length) {
      GradebookUtil.error('ColDefIterator out of data. length = '+this.orderMap.length);
      return null;
    }
    return this.dataArray[this.orderMap[this.currentIndex++]];
  }
};

Gradebook.numberComparator = function( a, b ) { return a - b; }

var NumberFormatter = {

    // usually called from frameset scope and re-set when grid is initialized with grid page LOCALE SETTINGS
    needToConvert: ( ( window.LOCALE_SETTINGS ) ? LOCALE_SETTINGS.getString('number_format.decimal_point') == ',' : page.bundle.getString('number_format.decimal_point') == ','),

    getDisplayFloat: function ( f ) {
    if (!NumberFormatter.needToConvert) return f;
    f = ''+f;
    return f.replace('.',',');
  },
  
  getDotFloat: function ( f ) {
    if (!NumberFormatter.needToConvert) return f;
    f = ''+f;
    return f.replace(',','.');
  }
};


