/* ************************************************************************

   qooxdoo - the new era of web development

   http://qooxdoo.org

   Copyright:
     2006 STZ-IDA, Germany, http://www.stz-ida.de

   License:
     LGPL: http://www.gnu.org/licenses/lgpl.html
     EPL: http://www.eclipse.org/org/documents/epl-v10.php
     See the LICENSE file in the project's top-level directory for details.

   Authors:
     * Til Schneider (til132)

************************************************************************ */

/**
 * A simple table model that provides an API for changing the model data.
 */
qx.Class.define("qx.ui.table.model.Simple",
{
  extend : qx.ui.table.model.Abstract,


  construct : function()
  {
    this.base(arguments);

    this.__rowArr = [];
    this.__sortColumnIndex = -1;

    // Array of objects, each with property "ascending" and "descending"
    this.__sortMethods = [];

    this.__editableColArr = null;
  },

  properties :
  {
    /**
     * Whether sorting should be case sensitive
     */
    caseSensitiveSorting :
    {
      check : "Boolean",
      init : true
    }
  },


  statics :
  {
    /**
     * Default ascendeing sort method to use if no custom method has been
     * provided.
     *
     * @param row1 {var} first row
     * @param row2 {var} second row
     * @return {Integer} 1 of row1 is > row2, -1 if row1 is < row2, 0 if row1 == row2
     */
    _defaultSortComparatorAscending : function(row1, row2)
    {
      var obj1 = row1[arguments.callee.columnIndex];
      var obj2 = row2[arguments.callee.columnIndex];
      if (qx.lang.Type.isNumber(obj1) && qx.lang.Type.isNumber(obj2)) {
        var result = isNaN(obj1) ? isNaN(obj2) ?  0 : 1 : isNaN(obj2) ? -1 : null;
        if (result != null) {
          return result;
        }
      }
      return (obj1 > obj2) ? 1 : ((obj1 == obj2) ? 0 : -1);
    },


    /**
     * Same as the Default ascending sort method but using case insensitivity
     *
     * @param row1 {var} first row
     * @param row2 {var} second row
     * @return {Integer} 1 of row1 is > row2, -1 if row1 is < row2, 0 if row1 == row2
     */
    _defaultSortComparatorInsensitiveAscending : function(row1, row2)
    {
      var obj1 = (row1[arguments.callee.columnIndex].toLowerCase ?
            row1[arguments.callee.columnIndex].toLowerCase() : row1[arguments.callee.columnIndex]);
      var obj2 = (row2[arguments.callee.columnIndex].toLowerCase ?
            row2[arguments.callee.columnIndex].toLowerCase() : row2[arguments.callee.columnIndex]);

      if (qx.lang.Type.isNumber(obj1) && qx.lang.Type.isNumber(obj2)) {
        var result = isNaN(obj1) ? isNaN(obj2) ?  0 : 1 : isNaN(obj2) ? -1 : null;
        if (result != null) {
          return result;
        }
      }
      return (obj1 > obj2) ? 1 : ((obj1 == obj2) ? 0 : -1);
    },


    /**
     * Default descending sort method to use if no custom method has been
     * provided.
     *
     * @param row1 {var} first row
     * @param row2 {var} second row
     * @return {Integer} 1 of row1 is > row2, -1 if row1 is < row2, 0 if row1 == row2
     */
    _defaultSortComparatorDescending : function(row1, row2)
    {
      var obj1 = row1[arguments.callee.columnIndex];
      var obj2 = row2[arguments.callee.columnIndex];
      if (qx.lang.Type.isNumber(obj1) && qx.lang.Type.isNumber(obj2)) {
        var result = isNaN(obj1) ? isNaN(obj2) ?  0 : 1 : isNaN(obj2) ? -1 : null;
        if (result != null) {
          return result;
        }
      }
      return (obj1 < obj2) ? 1 : ((obj1 == obj2) ? 0 : -1);
    },


    /**
     * Same as the Default descending sort method but using case insensitivity
     *
     * @param row1 {var} first row
     * @param row2 {var} second row
     * @return {Integer} 1 of row1 is > row2, -1 if row1 is < row2, 0 if row1 == row2
     */
    _defaultSortComparatorInsensitiveDescending : function(row1, row2)
    {
      var obj1 = (row1[arguments.callee.columnIndex].toLowerCase ?
          row1[arguments.callee.columnIndex].toLowerCase() : row1[arguments.callee.columnIndex]);
      var obj2 = (row2[arguments.callee.columnIndex].toLowerCase ?
          row2[arguments.callee.columnIndex].toLowerCase() : row2[arguments.callee.columnIndex]);
      if (qx.lang.Type.isNumber(obj1) && qx.lang.Type.isNumber(obj2)) {
        var result = isNaN(obj1) ? isNaN(obj2) ?  0 : 1 : isNaN(obj2) ? -1 : null;
        if (result != null) {
          return result;
        }
      }
      return (obj1 < obj2) ? 1 : ((obj1 == obj2) ? 0 : -1);
    }

  },


  members :
  {
    __rowArr : null,
    __editableColArr : null,
    __sortableColArr : null,
    __sortMethods : null,
    __sortColumnIndex : null,
    __sortAscending : null,


    // overridden
    getRowData : function(rowIndex)
    {
      var rowData = this.__rowArr[rowIndex];
      if (rowData == null || rowData.originalData == null) {
        return rowData;
      } else {
        return rowData.originalData;
      }
    },


    /**
     * Returns the data of one row as map containing the column IDs as key and
     * the cell values as value. Also the meta data is included.
     *
     * @param rowIndex {Integer} the model index of the row.
     * @return {Map} a Map containing the column values.
     */
    getRowDataAsMap : function(rowIndex)
    {
      var rowData = this.__rowArr[rowIndex];

      if (rowData != null) {
        var map = {};
        // get the current set data
        for (var col = 0; col < this.getColumnCount(); col++) {
          map[this.getColumnId(col)] = rowData[col];
        }

        if (rowData.originalData != null) {
          // merge in the meta data
          for (var key in rowData.originalData) {
            if (map[key] == undefined) {
              map[key] = rowData.originalData[key];
            }
          }
        }

        return map;
      }
      // may be null, which is ok
      return (rowData && rowData.originalData) ? rowData.originalData : null;
    },


    /**
     * Gets the whole data as an array of maps.
     *
     * Note: Individual items are retrieved by {@link #getRowDataAsMap}.
     */
    getDataAsMapArray: function() {
      var len = this.getRowCount();
      var data = [];

      for (var i = 0; i < len; i++)
      {
        data.push(this.getRowDataAsMap(i));
      }

      return data;
    },


    /**
     * Sets all columns editable or not editable.
     *
     * @param editable {Boolean} whether all columns are editable.
     * @return {void}
     */
    setEditable : function(editable)
    {
      this.__editableColArr = [];

      for (var col=0; col<this.getColumnCount(); col++) {
        this.__editableColArr[col] = editable;
      }

      this.fireEvent("metaDataChanged");
    },


    /**
     * Sets whether a column is editable.
     *
     * @param columnIndex {Integer} the column of which to set the editable state.
     * @param editable {Boolean} whether the column should be editable.
     * @return {void}
     */
    setColumnEditable : function(columnIndex, editable)
    {
      if (editable != this.isColumnEditable(columnIndex))
      {
        if (this.__editableColArr == null) {
          this.__editableColArr = [];
        }

        this.__editableColArr[columnIndex] = editable;

        this.fireEvent("metaDataChanged");
      }
    },

    // overridden
    isColumnEditable : function(columnIndex) {
      return this.__editableColArr ? (this.__editableColArr[columnIndex] == true) : false;
    },


    /**
     * Sets whether a column is sortable.
     *
     * @param columnIndex {Integer} the column of which to set the sortable state.
     * @param sortable {Boolean} whether the column should be sortable.
     */
    setColumnSortable : function(columnIndex, sortable)
    {
      if (sortable != this.isColumnSortable(columnIndex))
      {
        if (this.__sortableColArr == null) {
          this.__sortableColArr = [];
        }

        this.__sortableColArr[columnIndex] = sortable;
        this.fireEvent("metaDataChanged");
      }
    },


    // overridden
    isColumnSortable : function(columnIndex) {
      return (
        this.__sortableColArr
        ? (this.__sortableColArr[columnIndex] !== false)
        : true
      );
    },

    // overridden
    sortByColumn : function(columnIndex, ascending)
    {
      // NOTE: We use different comparators for ascending and descending,
      //     because comparators should be really fast.
      var comparator;

      var sortMethods = this.__sortMethods[columnIndex];
      if (sortMethods)
      {
        comparator =
          (ascending
           ? sortMethods.ascending
           : sortMethods.descending);
      }
      else
      {
        if (this.getCaseSensitiveSorting())
        {
          comparator =
            (ascending
             ? qx.ui.table.model.Simple._defaultSortComparatorAscending
             : qx.ui.table.model.Simple._defaultSortComparatorDescending);
        }
        else
        {
          comparator =
            (ascending
             ? qx.ui.table.model.Simple._defaultSortComparatorInsensitiveAscending
             : qx.ui.table.model.Simple._defaultSortComparatorInsensitiveDescending);
        }
      }

      comparator.columnIndex = columnIndex;
      this.__rowArr.sort(comparator);

      this.__sortColumnIndex = columnIndex;
      this.__sortAscending = ascending;

      var data =
        {
          columnIndex : columnIndex,
          ascending   : ascending
        };
      this.fireDataEvent("sorted", data);

      this.fireEvent("metaDataChanged");
    },


    /**
     * Specify the methods to use for ascending and descending sorts of a
     * particular column.
     *
     * @param columnIndex {Integer}
     *   The index of the column for which the sort methods are being
     *   provided.
     *
     * @param compare {Function|Map}
     *   If provided as a Function, this is the comparator function to sort in
     *   ascending order. It takes two parameters: the two arrays of row data,
     *   row1 and row2, being compared. It may determine which column of the
     *   row data to sort on by accessing arguments.callee.columnIndex.  The
     *   comparator function must return 1, 0 or -1, when the column in row1
     *   is greater than, equal to, or less than, respectively, the column in
     *   row2.
     *
     *   If this parameter is a Map, it shall have two properties: "ascending"
     *   and "descending". The property value of each is a comparator
     *   function, as described above.
     *
     *   If only the "ascending" function is provided (i.e. this parameter is
     *   a Function, not a Map), then the "descending" function is built
     *   dynamically by passing the two parameters to the "ascending" function
     *   in reversed order. <i>Use of a dynamically-built "descending" function
     *   generates at least one extra function call for each row in the table,
     *   and possibly many more. If the table is expected to have more than
     *   about 1000 rows, you will likely want to provide a map with a custom
     *   "descending" sort function as well as the "ascending" one.</i>
     *
     * @return {void}
     */
    setSortMethods : function(columnIndex, compare)
    {
      var methods;
      if (qx.lang.Type.isFunction(compare))
      {
        methods =
          {
            ascending  : compare,
            descending : function(row1, row2)
            {
              return compare(row2, row1);
            }
          };
      }
      else
      {
        methods = compare;
      }
      this.__sortMethods[columnIndex] = methods;
    },


    /**
     * Returns the sortMethod(s) for a table column.
     *
     * @param columnIndex {Integer} The index of the column for which the sort
     *   methods are being  provided.
     *
     * @return {Map} a map with the two properties "ascending"
     *   and "descending" for the specified column.
     *   The property value of each is a comparator function, as described
     *   in {@link #setSortMethods}.
     */
    getSortMethods : function(columnIndex) {
      return this.__sortMethods[columnIndex];
    },


    /**
     * Clears the sorting.
     */
    clearSorting : function()
    {
      if (this.__sortColumnIndex != -1)
      {
        this.__sortColumnIndex = -1;
        this.__sortAscending = true;

        this.fireEvent("metaDataChanged");
      }
    },

    // overridden
    getSortColumnIndex : function() {
      return this.__sortColumnIndex;
    },

    /**
     * Set the sort column index
     *
     * WARNING: This should be called only by subclasses with intimate
     *          knowledge of what they are doing!
     *
     * @param columnIndex {Integer} index of the column
     */
    _setSortColumnIndex : function(columnIndex)
    {
      this.__sortColumnIndex = columnIndex;
    },

    // overridden
    isSortAscending : function() {
      return this.__sortAscending;
    },

    /**
     * Set whether to sort in ascending order or not.
     *
     * WARNING: This should be called only by subclasses with intimate
     *          knowledge of what they are doing!
     *
     * @param ascending {Boolean}
     *   <i>true</i> for an ascending sort;
     *   <i> false</i> for a descending sort.
     */
    _setSortAscending : function(ascending)
    {
      this.__sortAscending = ascending;
    },

    // overridden
    getRowCount : function() {
      return this.__rowArr.length;
    },

    // overridden
    getValue : function(columnIndex, rowIndex)
    {
      if (rowIndex < 0 || rowIndex >= this.__rowArr.length) {
        throw new Error("this.__rowArr out of bounds: " + rowIndex + " (0.." + this.__rowArr.length + ")");
      }

      return this.__rowArr[rowIndex][columnIndex];
    },

    // overridden
    setValue : function(columnIndex, rowIndex, value)
    {
      if (this.__rowArr[rowIndex][columnIndex] != value)
      {
        this.__rowArr[rowIndex][columnIndex] = value;

        // Inform the listeners
        if (this.hasListener("dataChanged"))
        {
          var data =
          {
            firstRow    : rowIndex,
            lastRow     : rowIndex,
            firstColumn : columnIndex,
            lastColumn  : columnIndex
          };

          this.fireDataEvent("dataChanged", data);
        }

        if (columnIndex == this.__sortColumnIndex) {
          this.clearSorting();
        }
      }
    },


    /**
     * Sets the whole data in a bulk.
     *
     * @param rowArr {var[][]} An array containing an array for each row. Each
     *          row-array contains the values in that row in the order of the columns
     *          in this model.
     * @param clearSorting {Boolean ? true} Whether to clear the sort state.
     * @return {void}
     */
    setData : function(rowArr, clearSorting)
    {
      this.__rowArr = rowArr;

      // Inform the listeners
      if (this.hasListener("dataChanged"))
      {
        var data =
        {
          firstRow    : 0,
          lastRow     : rowArr.length - 1,
          firstColumn : 0,
          lastColumn  : this.getColumnCount() - 1
        };

        this.fireDataEvent("dataChanged", data);
      }

      if (clearSorting !== false) {
        this.clearSorting();
      }
    },


    /**
     * Returns the data of this model.
     *
     * Warning: Do not alter this array! If you want to change the data use
     * {@link #setData}, {@link #setDataAsMapArray} or {@link #setValue} instead.
     *
     * @return {var[][]} An array containing an array for each row. Each
     *           row-array contains the values in that row in the order of the columns
     *           in this model.
     */
    getData : function() {
      return this.__rowArr;
    },


    /**
     * Sets the whole data in a bulk.
     *
     * @param mapArr {Map[]} An array containing a map for each row. Each
     *        row-map contains the column IDs as key and the cell values as value.
     * @param rememberMaps {Boolean ? false} Whether to remember the original maps.
     *        If true {@link #getRowData} will return the original map.
     * @param clearSorting {Boolean ? true} Whether to clear the sort state.
     */
    setDataAsMapArray : function(mapArr, rememberMaps, clearSorting) {
      this.setData(this._mapArray2RowArr(mapArr, rememberMaps), clearSorting);
    },


    /**
     * Adds some rows to the model.
     *
     * Warning: The given array will be altered!
     *
     * @param rowArr {var[][]} An array containing an array for each row. Each
     *          row-array contains the values in that row in the order of the columns
     *          in this model.
     * @param startIndex {Integer ? null} The index where to insert the new rows. If null,
     *          the rows are appended to the end.
     * @param clearSorting {Boolean ? true} Whether to clear the sort state.
     * @return {void}
     */
    addRows : function(rowArr, startIndex, clearSorting)
    {
      if (startIndex == null) {
        startIndex = this.__rowArr.length;
      }

      // Prepare the rowArr so it can be used for apply
      rowArr.splice(0, 0, startIndex, 0);

      // Insert the new rows
      Array.prototype.splice.apply(this.__rowArr, rowArr);

      // Inform the listeners
      var data =
      {
        firstRow    : startIndex,
        lastRow     : this.__rowArr.length - 1,
        firstColumn : 0,
        lastColumn  : this.getColumnCount() - 1
      };
      this.fireDataEvent("dataChanged", data);

      if (clearSorting !== false) {
        this.clearSorting();
      }
    },


    /**
     * Adds some rows to the model.
     *
     * Warning: The given array (mapArr) will be altered!
     *
     * @param mapArr {Map[]} An array containing a map for each row. Each
     *        row-map contains the column IDs as key and the cell values as value.
     * @param startIndex {Integer ? null} The index where to insert the new rows. If null,
     *        the rows are appended to the end.
     * @param rememberMaps {Boolean ? false} Whether to remember the original maps.
     *        If true {@link #getRowData} will return the original map.
     * @param clearSorting {Boolean ? true} Whether to clear the sort state.
     */
    addRowsAsMapArray : function(mapArr, startIndex, rememberMaps, clearSorting) {
      this.addRows(this._mapArray2RowArr(mapArr, rememberMaps), startIndex, clearSorting);
    },


    /**
     * Sets rows in the model. The rows overwrite the old rows starting at
     * <code>startIndex</code> to <code>startIndex+rowArr.length</code>.
     *
     * Warning: The given array will be altered!
     *
     * @param rowArr {var[][]} An array containing an array for each row. Each
     *          row-array contains the values in that row in the order of the columns
     *          in this model.
     * @param startIndex {Integer ? null} The index where to insert the new rows. If null,
     *          the rows are set from the beginning (0).
     * @param clearSorting {Boolean ? true} Whether to clear the sort state.
     * @return {void}
     */
    setRows : function(rowArr, startIndex, clearSorting)
    {
      if (startIndex == null) {
        startIndex = 0;
      }

      // Prepare the rowArr so it can be used for apply
      rowArr.splice(0, 0, startIndex, rowArr.length);

      // Replace rows
      Array.prototype.splice.apply(this.__rowArr, rowArr);

      // Inform the listeners
      var data =
      {
        firstRow    : startIndex,
        lastRow     : this.__rowArr.length - 1,
        firstColumn : 0,
        lastColumn  : this.getColumnCount() - 1
      };
      this.fireDataEvent("dataChanged", data);

      if (clearSorting !== false) {
        this.clearSorting();
      }
    },


    /**
     * Set rows in the model. The rows overwrite the old rows starting at
     * <code>startIndex</code> to <code>startIndex+rowArr.length</code>.
     *
     * Warning: The given array (mapArr) will be altered!
     *
     * @param mapArr {Map[]} An array containing a map for each row. Each
     *        row-map contains the column IDs as key and the cell values as value.
     * @param startIndex {Integer ? null} The index where to insert the new rows. If null,
     *        the rows are appended to the end.
     * @param rememberMaps {Boolean ? false} Whether to remember the original maps.
     *        If true {@link #getRowData} will return the original map.
     * @param clearSorting {Boolean ? true} Whether to clear the sort state.
     */
    setRowsAsMapArray : function(mapArr, startIndex, rememberMaps, clearSorting) {
      this.setRows(this._mapArray2RowArr(mapArr, rememberMaps), startIndex, clearSorting);
    },


    /**
     * Removes some rows from the model.
     *
     * @param startIndex {Integer} the index of the first row to remove.
     * @param howMany {Integer} the number of rows to remove.
     * @param clearSorting {Boolean ? true} Whether to clear the sort state.
     * @return {void}
     */
    removeRows : function(startIndex, howMany, clearSorting)
    {
      this.__rowArr.splice(startIndex, howMany);

      // Inform the listeners
      var data =
      {
        firstRow    : startIndex,
        lastRow     : this.__rowArr.length - 1,
        firstColumn : 0,
        lastColumn  : this.getColumnCount() - 1,
        removeStart : startIndex,
        removeCount : howMany
      };

      this.fireDataEvent("dataChanged", data);
      if (clearSorting !== false) {
        this.clearSorting();
      }
    },


    /**
     * Creates an array of maps to an array of arrays.
     *
     * @param mapArr {Map[]} An array containing a map for each row. Each
     *          row-map contains the column IDs as key and the cell values as value.
     * @param rememberMaps {Boolean ? false} Whether to remember the original maps.
     *        If true {@link #getRowData} will return the original map.
     * @return {var[][]} An array containing an array for each row. Each
     *           row-array contains the values in that row in the order of the columns
     *           in this model.
     */
    _mapArray2RowArr : function(mapArr, rememberMaps)
    {
      var rowCount = mapArr.length;
      var columnCount = this.getColumnCount();
      var dataArr = new Array(rowCount);
      var columnArr;

      for (var i=0; i<rowCount; ++i)
      {
      columnArr = [];
      if (rememberMaps) {
        columnArr.originalData = mapArr[i];
      }

        for (var j=0; j<columnCount; ++j) {
          columnArr[j] = mapArr[i][this.getColumnId(j)];
        }

        dataArr[i] = columnArr;
      }

      return dataArr;
    }
  },


  destruct : function()
  {
    this.__rowArr = this.__editableColArr = this.__sortMethods =
      this.__sortableColArr = null;
  }
});
