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

   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)
     * Fabian Jakobs (fjakobs)

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

/**
 * The table pane that shows a certain section from a table. This class handles
 * the display of the data part of a table and is therefore the base for virtual
 * scrolling.
 */
qx.Class.define("qx.ui.table.pane.Pane",
{
  extend : qx.ui.core.Widget,




  /*
  *****************************************************************************
     CONSTRUCTOR
  *****************************************************************************
  */

  /**
   * @param paneScroller {qx.ui.table.pane.Scroller} the TablePaneScroller the header belongs to.
   */
  construct : function(paneScroller)
  {
    this.base(arguments);

    this.__paneScroller = paneScroller;

    this.__lastColCount = 0;
    this.__lastRowCount = 0;

    this.__rowCache = [];
  },


  /*
  *****************************************************************************
     EVENTS
  *****************************************************************************
  */


  events :
  {
    /**
     * Whether the current view port of the pane has not loaded data.
     * The data object of the event indicates if the table pane has to reload
     * data or not. Can be used to give the user feedback of the loading state
     * of the rows.
     */
    "paneReloadsData" : "qx.event.type.Data",

    /**
     * Whenever the content of the table pane has been updated (rendered)
     * trigger a paneUpdated event. This allows the canvas cellrenderer to act
     * once the new cells have been integrated in the dom.
     */
    "paneUpdated" : "qx.event.type.Event"
  },


  /*
  *****************************************************************************
     PROPERTIES
  *****************************************************************************
  */

  properties :
  {
    /** The index of the first row to show. */
    firstVisibleRow :
    {
      check : "Number",
      init : 0,
      apply : "_applyFirstVisibleRow"
    },


    /** The number of rows to show. */
    visibleRowCount :
    {
      check : "Number",
      init : 0,
      apply : "_applyVisibleRowCount"
    },


    /**
     * Maximum number of cached rows. If the value is <code>-1</code> the cache
     * size is unlimited
     */
    maxCacheLines :
    {
      check : "Number",
      init : 1000,
      apply : "_applyMaxCacheLines"
    },

    // overridden
    allowShrinkX :
    {
      refine : true,
      init : false
    }
  },




  /*
  *****************************************************************************
     MEMBERS
  *****************************************************************************
  */

  members :
  {
    __lastRowCount : null,
    __lastColCount : null,

    __paneScroller : null,
    __tableContainer : null,

    __focusedRow : null,
    __focusedCol : null,

    // sparse array to cache rendered rows
    __rowCache : null,
    __rowCacheCount : 0,


    // property modifier
    _applyFirstVisibleRow : function(value, old) {
      this.updateContent(false, value-old);
    },


    // property modifier
    _applyVisibleRowCount : function(value, old) {
      this.updateContent(true);
    },


    // overridden
    _getContentHint : function()
    {
      // the preferred height is 400 pixel. We don't use rowCount * rowHeight
      // because this is typically too large.
      return {
        width: this.getPaneScroller().getTablePaneModel().getTotalWidth(),
        height: 400
      }
    },


    /**
     * Returns the TablePaneScroller this pane belongs to.
     *
     * @return {qx.ui.table.pane.Scroller} the TablePaneScroller.
     */
    getPaneScroller : function() {
      return this.__paneScroller;
    },


    /**
     * Returns the table this pane belongs to.
     *
     * @return {qx.ui.table.Table} the table.
     */
    getTable : function() {
      return this.__paneScroller.getTable();
    },


    /**
     * Sets the currently focused cell.
     *
     * @param col {Integer?null} the model index of the focused cell's column.
     * @param row {Integer?null} the model index of the focused cell's row.
     * @param massUpdate {Boolean ? false} Whether other updates are planned as well.
     *          If true, no repaint will be done.
     */
    setFocusedCell : function(col, row, massUpdate)
    {
      if (col != this.__focusedCol || row != this.__focusedRow)
      {
        var oldRow = this.__focusedRow;
        this.__focusedCol = col;
        this.__focusedRow = row;

        // Update the focused row background
        if (row != oldRow && !massUpdate)
        {
          if (oldRow !== null) {
            this.updateContent(false, null, oldRow, true);
          }
          if (row !== null) {
            this.updateContent(false, null, row, true);
          }
        }
      }
    },


    /**
     * Event handler. Called when the selection has changed.
     */
    onSelectionChanged : function() {
      this.updateContent(false, null, null, true);
    },


    /**
     * Event handler. Called when the table gets or looses the focus.
     */
    onFocusChanged : function() {
      this.updateContent(false, null, null, true);
    },


    /**
     * Sets the column width.
     *
     * @param col {Integer} the column to change the width for.
     * @param width {Integer} the new width.
     */
    setColumnWidth : function(col, width) {
      this.updateContent(true);
    },


    /**
     * Event handler. Called the column order has changed.
     *
     */
    onColOrderChanged : function() {
      this.updateContent(true);
    },


    /**
     * Event handler. Called when the pane model has changed.
     */
    onPaneModelChanged : function() {
      this.updateContent(true);
    },


    /**
     * Event handler. Called when the table model data has changed.
     *
     * @param firstRow {Integer} The index of the first row that has changed.
     * @param lastRow {Integer} The index of the last row that has changed.
     * @param firstColumn {Integer} The model index of the first column that has changed.
     * @param lastColumn {Integer} The model index of the last column that has changed.
     */
    onTableModelDataChanged : function(firstRow, lastRow, firstColumn, lastColumn)
    {
      this.__rowCacheClear();

      var paneFirstRow = this.getFirstVisibleRow();
      var rowCount = this.getVisibleRowCount();

      if (lastRow == -1 || lastRow >= paneFirstRow && firstRow < paneFirstRow + rowCount)
      {
        // The change intersects this pane
        this.updateContent();
      }
    },


    /**
     * Event handler. Called when the table model meta data has changed.
     *
     */
    onTableModelMetaDataChanged : function() {
      this.updateContent(true);
    },


    // property apply method
    _applyMaxCacheLines : function(value, old)
    {
      if (this.__rowCacheCount >= value && value !== -1) {
        this.__rowCacheClear();
      }
    },


    /**
     * Clear the row cache
     */
    __rowCacheClear : function()
    {
      this.__rowCache = [];
      this.__rowCacheCount = 0;
    },


    /**
     * Get a line from the row cache.
     *
     * @param row {Integer} Row index to get
     * @param selected {Boolean} Whether the row is currently selected
     * @param focused {Boolean} Whether the row is currently focused
     * @return {String|null} The cached row or null if a row with the given
     *     index is not cached.
     */
    __rowCacheGet : function(row, selected, focused)
    {
      if (!selected && !focused && this.__rowCache[row]) {
        return this.__rowCache[row];
      } else {
        return null;
      }
    },


    /**
     * Add a line to the row cache.
     *
     * @param row {Integer} Row index to set
     * @param rowString {String} computed row string to cache
     * @param selected {Boolean} Whether the row is currently selected
     * @param focused {Boolean} Whether the row is currently focused
     */
    __rowCacheSet : function(row, rowString, selected, focused)
    {
      var maxCacheLines = this.getMaxCacheLines();
      if (
        !selected &&
        !focused &&
        !this.__rowCache[row] &&
        maxCacheLines > 0
      ) {
        this._applyMaxCacheLines(maxCacheLines);
        this.__rowCache[row] = rowString;
        this.__rowCacheCount += 1;
      }
    },


    /**
     * Updates the content of the pane.
     *
     * @param completeUpdate {Boolean ? false} if true a complete update is performed.
     *      On a complete update all cell widgets are recreated.
     * @param scrollOffset {Integer ? null} If set specifies how many rows to scroll.
     * @param onlyRow {Integer ? null} if set only the specified row will be updated.
     * @param onlySelectionOrFocusChanged {Boolean ? false} if true, cell values won't
     *          be updated. Only the row background will.
     */
    updateContent : function(completeUpdate, scrollOffset, onlyRow, onlySelectionOrFocusChanged)
    {
      if (completeUpdate) {
        this.__rowCacheClear();
      }

      //var start = new Date();

      if (scrollOffset && Math.abs(scrollOffset) <= Math.min(10, this.getVisibleRowCount()))
      {
        //this.debug("scroll", scrollOffset);
        this._scrollContent(scrollOffset);
      }
      else if (onlySelectionOrFocusChanged && !this.getTable().getAlwaysUpdateCells())
      {
        //this.debug("update row styles");
        this._updateRowStyles(onlyRow);
      }
      else
      {
        //this.debug("full update");
        this._updateAllRows();
      }

      //this.debug("render time: " + (new Date() - start) + "ms");
    },


    /**
     * If only focus or selection changes it is sufficient to only update the
     * row styles. This method updates the row styles of all visible rows or
     * of just one row.
     *
     * @param onlyRow {Integer|null ? null} If this parameter is set only the row
     *     with this index is updated.
     */
    _updateRowStyles : function(onlyRow)
    {
      var elem = this.getContentElement().getDomElement();

      if (!elem || !elem.firstChild) {
        this._updateAllRows();
        return;
      }

      var table = this.getTable();
      var selectionModel = table.getSelectionModel();
      var tableModel = table.getTableModel();
      var rowRenderer = table.getDataRowRenderer();
      var rowNodes = elem.firstChild.childNodes;
      var cellInfo = { table : table };

      // We don't want to execute the row loop below more than necessary. If
      // onlyRow is not null, we want to do the loop only for that row.
      // In that case, we start at (set the "row" variable to) that row, and
      // stop at (set the "end" variable to the offset of) the next row.
      var row = this.getFirstVisibleRow();
      var y = 0;

      // How many rows do we need to update?
      var end = rowNodes.length;

      if (onlyRow != null)
      {
        // How many rows are we skipping?
        var offset = onlyRow - row;
        if (offset >= 0 && offset < end)
        {
          row = onlyRow;
          y = offset;
          end = offset + 1;
        } else
        {
          return;
        }
      }

      for (; y<end; y++, row++)
      {
        cellInfo.row = row;
        cellInfo.selected = selectionModel.isSelectedIndex(row);
        cellInfo.focusedRow = (this.__focusedRow == row);
        cellInfo.rowData = tableModel.getRowData(row);

        rowRenderer.updateDataRowElement(cellInfo, rowNodes[y]);
      };
    },


    /**
     * Get the HTML table fragment for the given row range.
     *
     * @param firstRow {Integer} Index of the first row
     * @param rowCount {Integer} Number of rows
     * @return {String} The HTML table fragment for the given row range.
     */
    _getRowsHtml : function(firstRow, rowCount)
    {
      var table = this.getTable();

      var selectionModel = table.getSelectionModel();
      var tableModel = table.getTableModel();
      var columnModel = table.getTableColumnModel();
      var paneModel = this.getPaneScroller().getTablePaneModel();
      var rowRenderer = table.getDataRowRenderer();

      tableModel.prefetchRows(firstRow, firstRow + rowCount - 1);

      var rowHeight = table.getRowHeight();
      var colCount = paneModel.getColumnCount();
      var left = 0;
      var cols = [];

      // precompute column properties
      for (var x=0; x<colCount; x++)
      {
        var col = paneModel.getColumnAtX(x);
        var cellWidth = columnModel.getColumnWidth(col);
        cols.push({
          col: col,
          xPos: x,
          editable: tableModel.isColumnEditable(col),
          focusedCol: this.__focusedCol == col,
          styleLeft: left,
          styleWidth: cellWidth
        });

        left += cellWidth;
      }

      var rowsArr = [];
      var paneReloadsData = false;
      for (var row=firstRow; row < firstRow + rowCount; row++)
      {
        var selected = selectionModel.isSelectedIndex(row);
        var focusedRow = (this.__focusedRow == row);

        var cachedRow = this.__rowCacheGet(row, selected, focusedRow);
        if (cachedRow) {
          rowsArr.push(cachedRow);
          continue;
        }

        var rowHtml = [];

        var cellInfo = { table : table };
        cellInfo.styleHeight = rowHeight;

        cellInfo.row = row;
        cellInfo.selected = selected;
        cellInfo.focusedRow = focusedRow;
        cellInfo.rowData = tableModel.getRowData(row);

        if (!cellInfo.rowData) {
          paneReloadsData = true;
        }

        rowHtml.push('<div ');

        var rowAttributes = rowRenderer.getRowAttributes(cellInfo);
        if (rowAttributes) {
          rowHtml.push(rowAttributes);
        }

        var rowClass = rowRenderer.getRowClass(cellInfo);
        if (rowClass) {
          rowHtml.push('class="', rowClass, '" ');
        }

        var rowStyle = rowRenderer.createRowStyle(cellInfo);
        rowStyle += ";position:relative;" + rowRenderer.getRowHeightStyle(rowHeight)+ "width:100%;";
        if (rowStyle) {
          rowHtml.push('style="', rowStyle, '" ');
        }
        rowHtml.push('>');

        var stopLoop = false;
        for (x=0; x<colCount && !stopLoop; x++)
        {
          var col_def = cols[x];
          for (var attr in col_def) {
            cellInfo[attr] = col_def[attr];
          }
          var col = cellInfo.col;

          // Use the "getValue" method of the tableModel to get the cell's
          // value working directly on the "rowData" object
          // (-> cellInfo.rowData[col];) is not a solution because you can't
          // work with the columnIndex -> you have to use the columnId of the
          // columnIndex This is exactly what the method "getValue" does
          cellInfo.value = tableModel.getValue(col, row);
          var cellRenderer = columnModel.getDataCellRenderer(col);

          // Retrieve the current default cell style for this column.
          cellInfo.style = cellRenderer.getDefaultCellStyle();

          // Allow a cell renderer to tell us not to draw any further cells in
          // the row. Older, or traditional cell renderers don't return a
          // value, however, from createDataCellHtml, so assume those are
          // returning false.
          //
          // Tested with http://tinyurl.com/333hyhv
          stopLoop =
            cellRenderer.createDataCellHtml(cellInfo, rowHtml) || false;
        }
        rowHtml.push('</div>');

        var rowString = rowHtml.join("");

        this.__rowCacheSet(row, rowString, selected, focusedRow);
        rowsArr.push(rowString);
      }
      this.fireDataEvent("paneReloadsData", paneReloadsData);
      return rowsArr.join("");
    },


    /**
     * Scrolls the pane's contents by the given offset.
     *
     * @param rowOffset {Integer} Number of lines to scroll. Scrolling up is
     *     represented by a negative offset.
     */
    _scrollContent : function(rowOffset)
    {
      var el = this.getContentElement().getDomElement();
      if (!(el && el.firstChild)) {
        this._updateAllRows();
        return;
      }

      var tableBody = el.firstChild;
      var tableChildNodes = tableBody.childNodes;
      var rowCount = this.getVisibleRowCount();
      var firstRow = this.getFirstVisibleRow();

      var tabelModel = this.getTable().getTableModel();
      var modelRowCount = 0;

      modelRowCount = tabelModel.getRowCount();

      // don't handle this special case here
      if (firstRow + rowCount > modelRowCount) {
        this._updateAllRows();
        return;
      }

      // remove old lines
      var removeRowBase = rowOffset < 0 ? rowCount + rowOffset : 0;
      var addRowBase = rowOffset < 0 ? 0: rowCount - rowOffset;

      for (var i=Math.abs(rowOffset)-1; i>=0; i--)
      {
        var rowElem = tableChildNodes[removeRowBase];
        try {
          tableBody.removeChild(rowElem);
        } catch(exp) {
          break;
        }
      }

      // render new lines
      if (!this.__tableContainer) {
        this.__tableContainer = document.createElement("div");
      }
      var tableDummy = '<div>';
      tableDummy += this._getRowsHtml(firstRow + addRowBase, Math.abs(rowOffset));
      tableDummy += '</div>';
      this.__tableContainer.innerHTML = tableDummy;
      var newTableRows = this.__tableContainer.firstChild.childNodes;

      // append new lines
      if (rowOffset > 0)
      {
        for (var i=newTableRows.length-1; i>=0; i--)
        {
          var rowElem = newTableRows[0];
          tableBody.appendChild(rowElem);
        }
      }
      else
      {
        for (var i=newTableRows.length-1; i>=0; i--)
        {
          var rowElem = newTableRows[newTableRows.length-1];
          tableBody.insertBefore(rowElem, tableBody.firstChild);
        }
      }

      // update focus indicator
      if (this.__focusedRow !== null)
      {
        this._updateRowStyles(this.__focusedRow - rowOffset);
        this._updateRowStyles(this.__focusedRow);
      }
      this.fireEvent("paneUpdated");
    },


    /**
     * Updates the content of the pane (implemented using array joins).
     */
    _updateAllRows : function()
    {
      var elem = this.getContentElement().getDomElement();
      if (!elem) {
        // pane has not yet been rendered
        this.addListenerOnce("appear", arguments.callee, this);
        return;
      }

      var table = this.getTable();

      var tableModel = table.getTableModel();
      var paneModel = this.getPaneScroller().getTablePaneModel();

      var colCount = paneModel.getColumnCount();
      var rowHeight = table.getRowHeight();
      var firstRow = this.getFirstVisibleRow();

      var rowCount = this.getVisibleRowCount();
      var modelRowCount = tableModel.getRowCount();

      if (firstRow + rowCount > modelRowCount) {
        rowCount = Math.max(0, modelRowCount - firstRow);
      }

      var rowWidth = paneModel.getTotalWidth();
      var htmlArr;

      // If there are any rows...
      if (rowCount > 0)
      {
        // ... then create a div for them and add the rows to it.
        htmlArr =
          [
            "<div style='",
            "width: 100%;",
            (table.getForceLineHeight()
             ? "line-height: " + rowHeight + "px;"
             : ""),
            "overflow: hidden;",
            "'>",
            this._getRowsHtml(firstRow, rowCount),
            "</div>"
          ];
      }
      else
      {
        // Otherwise, don't create the div, as even an empty div creates a
        // white row in IE.
        htmlArr = [];
      }

      var data = htmlArr.join("");
      elem.innerHTML = data;
      this.setWidth(rowWidth);

      this.__lastColCount = colCount;
      this.__lastRowCount = rowCount;
      this.fireEvent("paneUpdated");
    }

  },




  /*
  *****************************************************************************
     DESTRUCTOR
  *****************************************************************************
  */

  destruct : function() {
    this.__tableContainer = this.__paneScroller = this.__rowCache = null;
  }
});
