/**
 * @fileoverview Plugin for southprojects Grid to calculate totals.
 *
 * <pre>
 * Copyright (c) 2006 by SouthProjects
 * http://www.southprojects.com
 * Santiago, Chile,
 * Version 1.2 Dec 2006.
 * All rights reserved.
 * </pre>
 */

/* $Id: spgrid-aggregate.js 4672 2006-10-10 13:09:04Z shacka $ */

/**
 * Gets called from aggregate functions to calculate totals for one or several
 * columns.
 *
 * <pre>
 * Takes in following object:
 * {
 *   aggregate: [function] one of internal aggregate callbacks,
 *   grid: [object] grid object,
 *   rows: [object] array of row objects,
 *   column: [object or number] array of zero-based column numbers or single
 *    zero-based column number to calculate,
 *   label: [string, optional] label for the value,
 *   labelColumn: [string, optional] zero-based column number where to put label
 *    (by default label is placed before the value in the same cell)
 * }
 *
 * Returns following object:
 * {
 *   rows:
 *   [
 *     {
 *       cells:
 *       [
 *         {
 *           v: [string] value to display
 *         },
 *         ...
 *       ]
 *     }
 *   ]
 * }
 * </pre>
 *
 * @private
 * @param {object} objArgs Arguments object
 * @return Row with totals
 * @type object
 */
southprojects.Grid.aggregate = function(objArgs) {
  // Check arguments
  if (!objArgs || typeof objArgs.aggregate != 'function') {
    return;
  }
  // Get grid
  var objGrid = objArgs.grid;
  if (!(objGrid instanceof southprojects.Grid)) {
    return;
  }
  // Get fields
  var arrFields = objGrid.getFields();
  // Get columns
  var arrColumns = [];
  objArgs.totals = [];
  if (objArgs.column instanceof Array) {
    // Multiple columns
    for (var iCol = 0; iCol < objArgs.column.length; iCol++) {
      var iColId = objArgs.column[iCol];
      if (typeof arrFields[iColId] != 'undefined') {
        arrColumns.push(iColId);
        objArgs.totals[iColId] = 0;
      }
    }
  } else if (typeof arrFields[objArgs.column] != 'undefined') {
    // Single column
    arrColumns.push(objArgs.column);
    objArgs.totals[objArgs.column] = 0;
  }
  if (!arrColumns.length) {
    return;
  }
  objArgs.column = arrColumns;
  // Calculate totals
  if ((objArgs.rows instanceof Array) && objArgs.rows.length) {
    objArgs.aggregate(objArgs);
  }
  // Get label column
  var iLabelColumn;
  if (typeof objArgs.labelColumn != 'undefined' &&
   typeof arrFields[objArgs.labelColumn] != 'undefined') {
    iLabelColumn = objArgs.labelColumn;
  }
  // Form prefix
  var strPrefix = '';
  if (typeof iLabelColumn == 'undefined' && objArgs.label) {
    strPrefix = objArgs.label + ' ';
  }
  // Form result
  objResult = {
    rows: [
      {
        cells: []
      }
    ]
  };
  for (var iCol = 0; iCol < arrFields.length; iCol++) {
    var val = objArgs.totals[iCol];
    if (typeof val == 'undefined') {
      val = '';
    } else {
      val = strPrefix + val;
    }
    objResult.rows[0].cells.push({v: val});
  }
  // Add label
  if (typeof iLabelColumn != 'undefined' && objArgs.label) {
    objResult.rows[0].cells[iLabelColumn].v = objArgs.label;
  }
  return objResult;
};

/**
 * Calculates sum for one or several columns.
 *
 * <pre>
 * Takes in following object:
 * {
 *   grid: [object] grid object,
 *   rows: [object] array of row objects,
 *   column: [object or number] array of zero-based column numbers or single
 *    zero-based column number to calculate,
 *   label: [string, optional] label for the value (overrides standard label),
 *   labelColumn: [string, optional] zero-based column number where to put label
 *    (by default label is placed before the value in the same cell)
 * }
 *
 * Returns following object:
 * {
 *   rows:
 *   [
 *     {
 *       cells:
 *       [
 *         {
 *           v: [string] value to display
 *         },
 *         ...
 *       ]
 *     }
 *   ]
 * }
 * </pre>
 *
 * @param {object} objArgs Arguments object
 * @return Row with totals
 * @type object
 */
southprojects.Grid.sum = function(objArgs) {
  objArgs.aggregate = southprojects.Grid.aggregateSum;
  // Default label
  if (typeof objArgs.label == 'undefined') {
    objArgs.label = 'Total:'
  }
  return southprojects.Grid.aggregate(objArgs);
};

/**
 * Internal aggregate callback to calculate sum of the column. Gets called from
 * {@link southprojects.Grid#aggregate}.
 *
 * <pre>
 * Takes in following object:
 * {
 *   totals: [object] array having as many members as there are fields in the
 *    grid and initializied with 0 for each column to calculate; 0 will be
 *    replaced with sum of the column by this function,
 *   grid: [object] grid object,
 *   rows: [object] array of row objects,
 *   column: [object] array of zero-based column numbers to calculate
 * }
 *
 * Instead of returning result, modifies array passed with "totals" property of
 * input object.
 * </pre>
 *
 * @private
 * @param {object} objArgs Arguments object
 */
southprojects.Grid.aggregateSum = function(objArgs) {
  // Get grid
  var objGrid = objArgs.grid;
  // Calculate sum and get precisions
  var arrPresisions = objArgs.totals.slice();
  for (var iRow = 0; iRow < objArgs.rows.length; iRow++) {
    var objRow = objArgs.rows[iRow];
    for (var iCol = 0; iCol < objArgs.column.length; iCol++) {
      var iColId = objArgs.column[iCol];
      // Convert to number
      var val = objGrid.getCellValue(objGrid.getCellByRow(objRow, iColId)) * 1;
      if (!isNaN(val)) {
        // Save precision
        var iPrecision = southprojects.Utils.getPrecision(val);
        if (iPrecision > arrPresisions[iColId]) {
          arrPresisions[iColId] = iPrecision;
        }
        // Add to the sum
        objArgs.totals[iColId] += val;
      }
    }
  }
  // Correct precisions
  for (var iCol = 0; iCol < objArgs.column.length; iCol++) {
    var iColId = objArgs.column[iCol];
    objArgs.totals[iColId] = southprojects.Utils.setPrecision(objArgs.totals[iColId],
     arrPresisions[iColId]);
  }
};

/**
 * Calculates average for one or several columns. Input and output object format
 * is the same as for {@link southprojects.Grid#sum}.
 *
 * @param {object} objArgs Arguments object
 * @return Row with totals
 * @type object
 */
southprojects.Grid.avg = function(objArgs) {
  objArgs.aggregate = southprojects.Grid.aggregateAvg;
  // Default label
  if (typeof objArgs.label == 'undefined') {
    objArgs.label = 'AVG:'
  }
  return southprojects.Grid.aggregate(objArgs);
};

/**
 * Internal aggregate callback to calculate average of the column. Gets called
 * from {@link southprojects.Grid#aggregate}.
 *
 * <pre>
 * Takes in following object:
 * {
 *   totals: [object] array having as many members as there are fields in the
 *    grid and initializied with 0 for each column to calculate; 0 will be
 *    replaced with average of the column by this function,
 *   grid: [object] grid object,
 *   rows: [object] array of row objects,
 *   column: [object] array of zero-based column numbers to calculate
 * }
 *
 * Instead of returning result, modifies array passed with "totals" property of
 * input object.
 * </pre>
 *
 * @private
 * @param {object} objArgs Arguments object
 */
southprojects.Grid.aggregateAvg = function(objArgs) {
  // Get sum
  southprojects.Grid.aggregateSum(objArgs);
  // Get number of rows
  var iRows = objArgs.rows.length;
  // Get precision and calculate average
  for (var iCol = 0; iCol < objArgs.column.length; iCol++) {
    var iColId = objArgs.column[iCol];
    var iPrecision = southprojects.Utils.getPrecision(objArgs.totals[iColId]);
    var fAvg = objArgs.totals[iColId] / iRows;
    objArgs.totals[iColId] = southprojects.Utils.setPrecision(fAvg, iPrecision);
  }
};

/**
 * Calculates min value for one or several columns. Input and output object
 * format is the same as for {@link southprojects.Grid#sum}.
 *
 * @param {object} objArgs Arguments object
 * @return Row with totals
 * @type object
 */
southprojects.Grid.min = function(objArgs) {
  objArgs.aggregate = southprojects.Grid.aggregateCompare;
  objArgs.sign = '<';
  // Default label
  if (typeof objArgs.label == 'undefined') {
    objArgs.label = 'Min:'
  }
  return southprojects.Grid.aggregate(objArgs);
};

/**
 * Calculates max value for one or several columns. Input and output object
 * format is the same as for {@link southprojects.Grid#sum}.
 *
 * @param {object} objArgs Arguments object
 * @return Row with totals
 * @type object
 */
southprojects.Grid.max = function(objArgs) {
  objArgs.aggregate = southprojects.Grid.aggregateCompare;
  objArgs.sign = '>';
  if (typeof objArgs.label == 'undefined') {
    objArgs.label = 'Max:'
  }
  return southprojects.Grid.aggregate(objArgs);
};

/**
 * Internal aggregate callback to calculate min or max of the column. Gets
 * called from {@link southprojects.Grid#aggregate}.
 *
 * <pre>
 * Takes in following object:
 * {
 *   totals: [object] array having as many members as there are fields in the
 *    grid and initializied with 0 for each column to calculate; 0 will be
 *    replaced with min or max of the column by this function,
 *   sign: [string] sign to use, e.g. '<' to get min and '>' to get max,
 *   grid: [object] grid object,
 *   rows: [object] array of row objects,
 *   column: [object] array of zero-based column numbers to calculate
 * }
 *
 * Instead of returning result, modifies array passed with "totals" property of
 * input object.
 * </pre>
 *
 * @private
 * @param {object} objArgs Arguments object
 */
southprojects.Grid.aggregateCompare = function(objArgs) {
  // Get grid
  var objGrid = objArgs.grid;
  // Init totals
  var objRow = objArgs.rows[0];
  for (var iCol = 0; iCol < objArgs.column.length; iCol++) {
    var iColId = objArgs.column[iCol];
    objArgs.totals[iColId] = objGrid.getCellByRow(objRow, iColId);
  }
  // Find max cell
  for (var iRow = 0; iRow < objArgs.rows.length; iRow++) {
    objRow = objArgs.rows[iRow];
    for (var iCol = 0; iCol < objArgs.column.length; iCol++) {
      var iColId = objArgs.column[iCol];
      var objCell = objGrid.getCellByRow(objRow, iColId);
      // Compare cells
      if (eval('objGrid.getCellValueCompare(objCell)' + objArgs.sign +
       'objGrid.getCellValueCompare(objArgs.totals[iColId])')) {
        objArgs.totals[iColId] = objCell;
      }
    }
  }
  // Convert to values
  for (var iCol = 0; iCol < objArgs.column.length; iCol++) {
    var iColId = objArgs.column[iCol];
    objArgs.totals[iColId] = objGrid.getCellValue(objArgs.totals[iColId]);
  }
};

/**
 * Calculates count for one or several columns. Input and output object format
 * is the same as for {@link southprojects.Grid#sum}.
 *
 * @param {object} objArgs Arguments object
 * @return Row with totals
 * @type object
 */
southprojects.Grid.count = function(objArgs) {
  objArgs.aggregate = southprojects.Grid.aggregateCount;
  if (typeof objArgs.label == 'undefined') {
    objArgs.label = 'Count:'
  }
  return southprojects.Grid.aggregate(objArgs);
};

/**
 * Internal aggregate callback to get count of the column. Gets called from
 * {@link southprojects.Grid#aggregate}.
 *
 * <pre>
 * Takes in following object:
 * {
 *   totals: [object] array having as many members as there are fields in the
 *    grid and initializied with 0 for each column to calculate; 0 will be
 *    replaced with count of the column by this function,
 *   grid: [object] grid object,
 *   rows: [object] array of row objects,
 *   column: [object] array of zero-based column numbers to calculate
 * }
 *
 * Instead of returning result, modifies array passed with "totals" property of
 * input object.
 * </pre>
 *
 * @private
 * @param {object} objArgs Arguments object
 */
southprojects.Grid.aggregateCount = function(objArgs) {
  // Get grid
  var objGrid = objArgs.grid;
  // Get number of rows
  var iRows = objArgs.rows.length;
  // Set count
  for (var iCol = 0; iCol < objArgs.column.length; iCol++) {
    objArgs.totals[objArgs.column[iCol]] = iRows;
  }
};

/**
 * Calculates count of distinct values for one or several columns. Input and
 * output object format is the same as for {@link southprojects.Grid#sum}.
 *
 * @param {object} objArgs Arguments object
 * @return Row with totals
 * @type object
 */
southprojects.Grid.countDistinct = function(objArgs) {
  objArgs.aggregate = southprojects.Grid.aggregateCountDistinct;
  if (typeof objArgs.label == 'undefined') {
    objArgs.label = 'Distinct:'
  }
  return southprojects.Grid.aggregate(objArgs);
};

/**
 * Internal aggregate callback to get count of distinct values of the column.
 * Gets called from {@link southprojects.Grid#aggregate}.
 *
 * <pre>
 * Takes in following object:
 * {
 *   totals: [object] array having as many members as there are fields in the
 *    grid and initializied with 0 for each column to calculate; 0 will be
 *    replaced with count of distinct values of the column by this function,
 *   grid: [object] grid object,
 *   rows: [object] array of row objects,
 *   column: [object] array of zero-based column numbers to calculate
 * }
 *
 * Instead of returning result, modifies array passed with "totals" property of
 * input object.
 * </pre>
 *
 * @private
 * @param {object} objArgs Arguments object
 */
southprojects.Grid.aggregateCountDistinct = function(objArgs) {
  // Get grid
  var objGrid = objArgs.grid;
  // Prepare buffer
  var arrDistinct = [];
  for (var iCol = 0; iCol < objArgs.column.length; iCol++) {
    arrDistinct[iCol] = {};
  }
  // Count distinct
  for (var iRow = 0; iRow < objArgs.rows.length; iRow++) {
    var objRow = objArgs.rows[iRow];
    for (var iCol = 0; iCol < objArgs.column.length; iCol++) {
      var iColId = objArgs.column[iCol];
      var strVal =
       objGrid.getCellValueString(objGrid.getCellByRow(objRow, iColId));
      // Check distinct
      if (typeof arrDistinct[iCol][strVal] == 'undefined') {
        arrDistinct[iCol][strVal] = true;
        objArgs.totals[iColId]++;
      }
    }
  }
};

/**
 * Returns array of rows for totals section.
 *
 * @return Array of rows for totals section
 * @type object
 */
southprojects.Grid.prototype.getTotals = function() {
  // Check if there are totals
  if (!(this.config.totals instanceof Array)) {
    return;
  }
  // Calculate totals
  var arrTotals = [];
  for (var iTotal = 0; iTotal < this.config.totals.length; iTotal++) {
    var objTotal = this.config.totals[iTotal];
    // Get callback
    if (typeof objTotal.callback != 'function') {
      objTotal.callback = southprojects.Grid.sum;
    }
    // Calculate rows
    var objResult = objTotal.callback({
      grid: this,
      rows: this.getFilteredRows(),
      column: objTotal.column,
      label: objTotal.label,
      labelColumn: objTotal.labelColumn
    });
    // Add rows
    if (objResult && (objResult.rows instanceof Array)) {
      for (var iRow = 0; iRow < objResult.rows.length; iRow++) {
        arrTotals.push(objResult.rows[iRow]);
      }
    }
  }
  return arrTotals;
};

/**
 * Forms grid output.
 *
 * @private
 * @param {object} arrHtml Output array
 * @param {object} arrCols Array with field objects to output
 * @param {object} arrTotals Array with row objects to output
 * @param {object} iRow Last row number
 * @param {boolean) boolFixed (Optional) Indicates that this is fixed part of
 * row
 */
southprojects.Grid.prototype.outputTotals = function(arrHtml, arrCols, arrTotals, boolFixed) {
  if (typeof this.config.callbackTotalsDisplay == 'function' || 
   typeof this.config.callbackTotalDisplay == 'function' || !arrTotals) {
    return;
  }
  for (var iTotal = 0; iTotal < arrTotals.length; iTotal++) {
    this.outputTotalsRow(arrHtml, arrCols, arrTotals, iTotal, boolFixed);
  }
};

/**
 * Forms grid output.
 *
 * @private
 * @param {object} arrHtml Output array
 * @param {object} arrCols Array with field objects to output
 * @param {object} arrRows Array with row objects to output
 * @param {object} iRow Index in arrRows array
 * @param {boolean) boolFixed (Optional) Indicates that this is fixed part of
 * row
 */
southprojects.Grid.prototype.outputTotalsRow = function(arrHtml, arrCols, arrRows, iRow, boolFixed) {
  // Get row
  var objRow = arrRows[iRow];
  if (!objRow) {
    return;
  }
  var iRowN = iRow + 1;
  var strRow = iRow.toString();
  var arrClass = [];
  arrClass.push('spgridRow spgridRowOdd spgridRowTotals spgridRowTotals');
  arrClass.push(strRow);
  arrClass.push(' spgridRowTotals');
  arrClass.push(iRow % 2 == 1 ? 'Odd' : 'Even');
  if (iRowN == arrRows.length) { // Last row
    arrClass.push(' spgridRowLast spgridRowTotalsLast');
  }
  var strClass = arrClass.join('');
  arrHtml.push('<tr id="spgrid');
  arrHtml.push(this.id);
  arrHtml.push('Total');
  arrHtml.push(iRow);
  if (boolFixed) {
    arrHtml.push('Fixed');
  }
  arrHtml.push('" class="');
  arrHtml.push(strClass);
  arrHtml.push('" style="');
  if (objRow.style) {
    arrHtml.push(objRow.style);
  }
  arrHtml.push('">');
  // Display cells
  var iFields = this.fields.length;
  var iShow = boolFixed ? this.config.fixedLeft : iFields;
  var iCols = arrCols.length;
  var boolHiddenCols = (!boolFixed && this.config.fixedLeft);
  for (var iField = 0, iCol = 0; iField < iFields && iField < iShow; iField++) {
    var boolHidden = (boolHiddenCols && iCol < this.config.fixedLeft);
    iCol += this.outputTotalsCell(arrHtml, iRow, objRow, iCols, iField, iCol,
     boolHidden);
  }
  arrHtml.push('</tr>');
};

/**
 * Forms grid output.
 *
 * @private
 * @param {object} arrHtml Output array
 * @param {object} iRow Number of row on the page
 * @param {object} objRow Row object
 * @param {number} iCols Visible column count
 * @param {number} iField Column id
 * @param {number} iCol Visible column number
 * @param {boolean} boolHidden (Optional) Indicates that field is hidden
 * @return Number of displayed cells (0 or 1)
 * @type number
 */
southprojects.Grid.prototype.outputTotalsCell = function(arrHtml, iRow, objRow, iCols, iField, iCol, boolHidden) {
  // Get field
  var objField = this.fields[iField];
  // Skip hidden columns
  if (!objField) {
    return 0;
  }
  // Get cell
  var objCell = objRow.cells[iField];
  if (!objCell) {
    return 0;
  }
  var strCol = iCol.toString();
  var arrClass = [];
  arrClass.push('spgridCell spgridCellTotals spgridCell');
  arrClass.push(strCol);
  arrClass.push(' spgridCellTotals');
  arrClass.push(strCol);
  arrClass.push(' spgridCell');
  arrClass.push(iCol % 2 == 1 ? 'Odd' : 'Even');
  arrClass.push(' spgridCellTotals');
  arrClass.push(iCol % 2 == 1 ? 'Odd' : 'Even');
  if (iCol + 1 == iCols) { // Last cell
    arrClass.push(' spgridCellLast spgridCellTotalsLast');
  }
  var strClass = arrClass.join('');
  arrHtml.push('<td class="');
  arrHtml.push(strClass);
  arrHtml.push('" id="spgrid');
  arrHtml.push(this.id);
  arrHtml.push('Total');
  arrHtml.push(iRow);
  arrHtml.push('Cell');
  arrHtml.push(iField);
  if (boolHidden) {
    arrHtml.push('Hidden"');
  } else {
    arrHtml.push('"');
  }
  var strStyle = this.getCellStyle(objCell, iRow);
  if (strStyle) {
    arrHtml.push(' style="');
    arrHtml.push(strStyle);
    arrHtml.push('"');
  }
  arrHtml.push('>');
  this.outputTotalsCellValue(arrHtml, objField, objRow, objCell);
  arrHtml.push('</td>');
  return 1;
};

/**
 * Forms grid output.
 *
 * @private
 * @param {object} arrHtml Output array
 * @param {object} objField Field object
 * @param {object} objRow Row object
 * @param {object} objCell Cell object
 */
southprojects.Grid.prototype.outputTotalsCellValue = function(arrHtml, objField, objRow, objCell) {
  // div is needed to be able to set column width
  arrHtml.push('<div class="spgridDiv');
  // Set cell type
  if (this.getClassByType) {
    arrHtml.push(' ');
    arrHtml.push(this.getClassByType(this.getFieldType(objField)));
  }
  arrHtml.push('">');
  arrHtml.push(objCell.v);
  arrHtml.push('</div>');
};

/**
 * Redraws totals for the specified columns.
 *
 * <pre>
 * Takes in following object:
 * {
 *   column: [object or number, optional] array of zero-based column numbers or
 *    single zero-based column number to redraw (default is all columns)
 * }
 * </pre>
 *
 * @param {object} objArgs Arguments object
 */
southprojects.Grid.prototype.redrawTotals = function(objArgs) {
  // If we are responsible for visualisation
  if (this.visualize) {
    // Get totals
    var arrTotals = this.getTotals();
    if (!arrTotals) {
      return;
    }
    // Get fields
    var arrFields = objGrid.getFields();
    // Redraw cells
    var iGridId = this.id;
    for (var iTotal = 0; iTotal < arrTotals.length; iTotal++) {
      var objRow = arrTotals[iTotal];
      for (var iField = 0; iField < arrFields.length; iField++) {
        var objField = arrFields[iField];
        var objCell = objRow.cells[iField];
        // Get table cell element
        var strCellId = 'spgrid' + iGridId + 'Total' + iTotal + 'Cell' + iField;
        var objTd = document.getElementById(strCellId);
        if (objTd) {
          // Redraw cell
          var arrHtml = [];
          this.outputCellValue(arrHtml, objField, objRow, objCell);
          var strHtml = arrHtml.join('');
          objTd.innerHTML = strHtml;
          // Redraw hidden cell
          var objTdHidden = document.getElementById(strCellId + 'Hidden');
          if (objTdHidden) {
            objTdHidden.innerHTML = strHtml;
          }
        }
      }
    }
  }
};
