/* Copyright (c) 2005, 2023, Oracle and/or its affiliates. */
/* All rights reserved.*/


/* TODO: 
- in all client-side code, in all the places where we're doing arithmetic on slice nums 
  to determine whether a block is the first/last block in the logical pivot table, 
  we should instead use the _firstBlock and _lastBlock attributes (for header blocks) and the 
  _firstBlockHoriz, _lastBlockHoriz, _firstBlockVert, and _lastBlockVert attributes 
  (for databody blocks) now that those attributes are available.  
*/


/**
 * @export
 * Utility class used by the PivotTable peer
 */
function AdfPivotTableUtils(pivotTable, pivotTablePeer, domElement) {
    if (arguments.length > 0)
        this.Init(pivotTable, pivotTablePeer, domElement);
}

AdfObject.createSubclass(AdfPivotTableUtils);

/**
 * @export
 */
AdfPivotTableUtils.InitClass = function() { ///////////////////////////////////////////// DONE (except nice-to-have of deleting edge-specific class vars)
    // "this" refers to AdfPivotTableUtils (the class, not an instance), so these are class constants
  
    this.DATABODY = -1;
    this.COLUMN_EDGE = 0;
    this.ROW_EDGE = 1;
    this.PAGE_EDGE = 2;
    this.ROW_PIVOT_HANDLES=2;
    this.COL_PIVOT_HANDLES=3;

    // TODO: replace literals with this constant
    this.SEPARATOR = "::"; // between element of cell ID's

    this.MAYBE = -1 // for use in tri-valued logic

    // The row/colCount constants indicate whether blocks have a dummy row and dummy column
    // at the top or left of the blocks.  By making them 1 and 0, we can use them either as 
    // indexing offsets or as booleans.  
    // TODO: make all logic that adjusts for dummy rows/columns use these constants instead 
    // of a hardcoded number 1.  In particular, setBlockRowsToExistingHeights and setBlockColumnsToExistingWidths
    this.COLUMN_EDGE_DUMMY_COLUMN_COUNT = 1;
    this.COLUMN_EDGE_DUMMY_ROW_COUNT = 1;
    this.COLUMN_EDGE_DUMMY_ROWCOL_COUNT = [this.COLUMN_EDGE_DUMMY_COLUMN_COUNT, 
                                           this.COLUMN_EDGE_DUMMY_ROW_COUNT];

    this.ROW_EDGE_DUMMY_COLUMN_COUNT = 1;
    this.ROW_EDGE_DUMMY_ROW_COUNT = 1;
    this.ROW_EDGE_DUMMY_ROWCOL_COUNT = [    this.ROW_EDGE_DUMMY_COLUMN_COUNT, 
                                            this.ROW_EDGE_DUMMY_ROW_COUNT];

    this.DATABODY_DUMMY_COLUMN_COUNT = 0;
    this.DATABODY_DUMMY_ROW_COUNT = 0;
    this.DATABODY_DUMMY_ROWCOL_COUNT = [    this.DATABODY_DUMMY_COLUMN_COUNT, 
                                            this.DATABODY_DUMMY_ROW_COUNT];

    this.DUMMY_ROWCOL_COUNT = [    this.COLUMN_EDGE_DUMMY_ROWCOL_COUNT, 
                                   this.ROW_EDGE_DUMMY_ROWCOL_COUNT];
    this.DUMMY_ROWCOL_COUNT["-1"] =     this.DATABODY_DUMMY_ROWCOL_COUNT;
    // e.g. AdfPivotTableUtils.DUMMY_ROWCOL_COUNT[0][1] is whether column edge blocks have dummy rows, 
    // and AdfPivotTableUtils.DUMMY_ROWCOL_COUNT[-1][0] is whether databody blocks have dummy columns. 


    // TODO - get working        this.MIN_ROW_HEADER_COLUMN_WIDTH = 30;

    // TODO: don't hardcode these values, so they are skinnable.
    // this.CELL_EXTRA_WIDTH  = 9; // never used.  4px left padding + 4px right padding  + 1px right border (left border in RTL)
    this.CELL_EXTRA_HEIGHT = 3; // used only in IE7.  1px top padding  + 1px bottom padding + 1px bottom border.  In newer skins in which row header cells have both top and bottom border, this constant is correct only for databody cells, and row header extra cell height is 4.
    AdfPivotTableUtils.initSizeConstants(); // decomped setting of some constants to this method so PrintablePage can call it too.
    this.SCROLLBAR_SPACER_EXTRA_LENGTH  = [0,0]; // [horizScrollbarSpacerExtraWidth, vertScrollbarSpacerExtraHeight].  no padding/borders/margin
    this.SCROLLBAR_SPACER_EXTRA_BREADTH = [1,1]; // [horizScrollbarSpacerExtraHeight, vertScrollbarSpacerExtraWidth].  1px top border.  1px left border (right border in RTL)
    this.PLACEHOLDER_ROW_HEIGHT = 1;

    /*
     * Controls the size of the viewport cushion, which is the area on the four sides 
     * of the viewport for which we always fetch data in anticipation of a scroll.  
     * For example, if the viewport is 400px tall, and the vertical cushion factor 
     * is 0.50, then the cushions on the top and bottom of the viewport are both 200px, 
     * so that the total viewport+cushion area is 800px tall.  
     * TODO: actually use these.  When we do, use a getter that calcs factor*vpSize, 
     * rather than an ivar that must be kept up to date.
     */
    this.VIEWPORT_CUSHION_FACTOR_VERT  = 1.0;
    this.VIEWPORT_CUSHION_FACTOR_HORIZ = 1.0;

    /*
     * Return values of identifyScrollType:
     */
    this.SCROLLTYPE_LONG_SCROLL = 0;
    this.SCROLLTYPE_SHORT_SCROLL = 1;
    this.SCROLLTYPE_NO_FETCH = 2;
    
    // Paging Control values:
    this.PAGE_PREV = [[], []];
    this.PAGE_FIRST = [[], []];
    this.PAGE_NEXT = [[], []];
    this.PAGE_LAST = [[], []];
    
    this.PAGE_PREV[0][0] = 0;
    this.PAGE_PREV[0][1] = 1;
    this.PAGE_PREV[1][0] = 2;
    this.PAGE_PREV[1][1] = 3;
    this.PAGE_FIRST[0][0] = 4;
    this.PAGE_FIRST[0][1] = 5;
    this.PAGE_FIRST[1][0] = 6;
    this.PAGE_FIRST[1][1] = 7;
    this.PAGE_NEXT[0][0] = 8;
    this.PAGE_NEXT[0][1] = 9;
    this.PAGE_NEXT[1][0] = 10;
    this.PAGE_NEXT[1][1] = 11;
    this.PAGE_LAST[0][0] = 12;
    this.PAGE_LAST[0][1] = 13;
    this.PAGE_LAST[1][0] = 14;
    this.PAGE_LAST[1][1] = 15;
    
    this.DELAY_INITIAL = 0;
    this.DELAY_FILL_VIEWPORT = 0; 
    this.DELAY_SHORT_SCROLL = 20; //50;
    this.DELAY_LONG_SCROLL = 200;

    // The minimum number of pixels allowed between the last block (in any of the 4 directions) 
    // in the buffer and the end of the virtual canvas.  This minimum distance is applied iff 
    // that block is not the last block in the logical pivotTable.  This is to guarantee that the 
    // user is able to keep scrolling iff there is something to scroll to.
    this.END_SCROLLING_MIN_PIXELS = 10; //100;

    // For shortcutting asserts in non-obfuscated code.  Idiom is:
    //   AdfAssert.assert(AdfPivotTableUtils.assertsOff || condition, message);
    // or for asserts that access the DOM and should therefore usually be off:
    //   AdfAssert.assert(AdfPivotTableUtils.domAssertsOff || condition, message);
    // or for asserts where even the message arg accesses the dom: 
    //   if(!AdfPivotTableUtils.domAssertsOff) AdfAssert.assert(...);
    this.assertsOff = !(AdfAssert.DEBUG); // should normally be set to !(AdfAssert.DEBUG)
    this.domAssertsOff = true; // should normally be set to true


    this.DATABODY_CELL_STYLE="_st_dbc";
    this.DATABODY_CELL_SELECTED_STYLE="_st_dbc_s";
    this.DATABODY_CELL_FOCUSED_STYLE="_st_dbc_f";
    this.ROW_HEADER_CELL_STYLE="_st_rhc";
    this.ROW_HEADER_CELL_SELECTED_STYLE = "_st_rhc_s";
    this.ROW_HEADER_CELL_FOCUSED_STYLE = "_st_rhc_f";
    this.COLUMN_HEADER_CELL_STYLE="_st_chc";
    this.COLUMN_HEADER_CELL_SELECTED_STYLE = "_st_chc_s";
    this.COLUMN_HEADER_CELL_FOCUSED_STYLE = "_st_chc_f";
    this.ROW_PIVOT_IMAGE_CELL_LEFT_STYLE="_st_rp_icl";
    this.ROW_PIVOT_SWAP_DROP_TARGET="_st_rpsdt";
    this.COLUMN_PIVOT_SWAP_DROP_TARGET="_st_cpsdt";
    this.ROW_PIVOT_IMAGE_CELL_RIGHT_STYLE="_st_rp_icr";
    this.ROW_PIVOT_IMAGE_CELL_CENTER_STYLE="_st_rp_icc";
    this.ROW_PIVOT_LABEL_CELL_STYLE="_st_rp_lc";
    this.ROW_PIVOT_INDICATOR_HEADER_CELL_STYLE="_st_rp_ihc";
    this.ROW_PIVOT_INDICATOR_PIVOT_HANDLE_STYLE="_st_rp_iph";
    this.COL_PIVOT_IMAGE_CELL_TOP_STYLE="_st_cp_ict";
    this.COL_PIVOT_LABEL_CELL_STYLE="_st_cp_lc";
    this.COL_PIVOT_IMAGE_CELL_CENTER_STYLE="_st_cp_icc";
    this.COL_PIVOT_IMAGE_CELL_BOTTOM_STYLE="_st_cp_icb";
    this.COLUMN_PIVOT_INDICATOR_HEADER_CELL_STYLE="_st_cp_ihc";
    this.COLUMN_PIVOT_INDICATOR_PIVOT_HANDLE_STYLE="_st_cp_iph";
    this.SELECTED_STATE_CLASS="st_sel";
    this.DATABODY_FROZEN_STYLE="_st_db_fz";
    
    this.COLUMN_HEADER_SIZING_CELL_CLASS = "af_dvt-pivotTable_column-sizing-cell";
}

// decomped from InitClass (which isn't called for PrintablePage) so that PrintablePage code can call it too.  Reason: the entire point of this code is to define these constants in exactly one place.
AdfPivotTableUtils.initSizeConstants = function() {
    // TODO: don't hardcode these values, so they are skinnable.
    AdfPivotTableUtils.CONTAINER_EXTRA_SIZE = [2,2]; // [extraWidth, extraHeight].  Extra size of outer PT div.  1px left border + 1px right border.  1px top  border + 1px bottom border
    AdfPivotTableUtils.DATABODY_EXTRA_SIZE  = [1,1]; // [databodyExtraWidth, databodyExtraHeight].  1px left border (right border in RTL).  1px top border 
    AdfPivotTableUtils.HEADER_EXTRA_PIXEL_EXTENT = [1,1]; // [colHeaderExtraWidth, rowHeaderExtraHeight].  1px left border (right border in RTL).  1px top border.  "Extent" means size slice-wise, not layer-wise.
    AdfPivotTableUtils.HEADER_EXTRA_PIXEL_DEPTH  = [0,0]; // [colHeaderExtraHeight, rowHeaderExtraWidth].  no padding/borders/margin.  "Depth" means size layer-wise, not slice-wise.
    AdfPivotTableUtils.HEADER_OUTER_PIXEL_DEPTH_NO_DATA = [33, 67]; // [colHeaderOuterHeight, rowHeaderOuterWidth].  Header sizes when showing the "no data" message, per "UX spec" provided in Bug 7373119.  TODO: skin this.
}

AdfPivotTableUtils.prototype.Init = function(pivotTable, pivotTablePeer, domElement) { ///////////////////////////////////////////// TODO: delete edge-specific ivars when all refs gone.  O/W DONE.
  AdfPivotTableUtils.superclass.Init.call(this);

  this._id             = domElement.id;
  this._pivotTable     = pivotTable;
  this._pivotTablePeer = pivotTablePeer;
  this._container      = domElement;
  var containerId = this._container.id + "::";
  this._innerContainer = AdfDhtmlPivotTablePeer._getChildById(domElement, containerId+"Inner");
  
  // local vars used only to populate the ivars:
  
  var rowHeader0                  = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"RowHeader0");
  var rowHeader1                  = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"RowHeader1");
  var colHeader0                  = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"ColHeader0");
  var colHeader1                  = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"ColHeader1");
  
  var databody00                  = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"Databody00");
  var databody01                  = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"Databody01");
  var databody10                  = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"Databody10");
  var databody11                  = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"Databody11");

  if (AdfPivotTableUtils.isMobileTouchDevice()) {
    var databodyCanvas00          = databody00.childNodes[0];
    var databodyCanvas01          = databody01.childNodes[0];
    var databodyCanvas10          = databody10.childNodes[0];
    var databodyCanvas11          = databody11.childNodes[0];
  }

  var databodyScroller01          = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"DatabodyScroller01");
  var databodyScrollerCanvas01    = databodyScroller01.childNodes[0];
  var databodyScroller10          = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"DatabodyScroller10");
  var databodyScrollerCanvas10    = databodyScroller10.childNodes[0];
  var databodyScroller11          = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"DatabodyScroller11");
  var databodyScrollerCanvas11    = databodyScroller11.childNodes[0];
  
  var verticalScrollbarSpacer0    = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"ColHeaderScrollbarSpacer0");
  var verticalScrollbarSpacer1    = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"ColHeaderScrollbarSpacer1");
  var horizontalScrollbarSpacer0  = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"RowHeaderScrollbarSpacer0");
  var horizontalScrollbarSpacer1  = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"RowHeaderScrollbarSpacer1");
  
  // ivars populated from those local vars:
  
  this._corner                    = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"Corner");
  
  this._rowHeaders                = [rowHeader0, rowHeader1];
  this._colHeaders                = [colHeader0, colHeader1];
  this._headers                   = [this._colHeaders, this._rowHeaders];
  
  this._databodies                = [[ databody00               , databody01               ], 
                                     [ databody10               , databody11               ]];
  this._databodyScrollers         = [[           null           , databodyScroller01       ], 
                                     [ databodyScroller10       , databodyScroller11       ]];
  this._databodyScrollerCanvases  = [[           null           , databodyScrollerCanvas01 ], 
                                     [ databodyScrollerCanvas10 , databodyScrollerCanvas11 ]];
  if (AdfPivotTableUtils.isMobileTouchDevice()) {
    this._databodyCanvases        = [[ databodyCanvas00 , databodyCanvas01 ], 
                                     [ databodyCanvas10 , databodyCanvas11 ]];
  }
                                     
  // In the ByEdgeAndPane arrays, the edge (first array index) indicates scroll orientation (horizontal scroll: edge==0, vertical scroll: edge==1).  
  // The pane (second array index) indicates which pane of that edge to scroll.
  // E.g. to do a vertical scroll (edge==1, the row edge) of pane 0 (the top pane) of that edge, you set scrollPos of Scroller01 (the top right pane's scroller), since that is the one whose scrollbar controls the vertical scroll position of the top pane.
  this._databodiesByEdgeAndPane                = [[ databody10               , databody11               ], 
                                                  [ databody01               , databody11               ]];
  this._databodyScrollersByEdgeAndPane         = [[ databodyScroller10       , databodyScroller11       ], 
                                                  [ databodyScroller01       , databodyScroller11       ]];
//  this._databodyScrollerCanvasesByEdgeAndPane  = [[ databodyScrollerCanvas10 , databodyScrollerCanvas11 ],    // delete this ivar if never find a use for it.
//                                                  [ databodyScrollerCanvas01 , databodyScrollerCanvas11 ]];
                                     
  this._statusBar                 = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"StatusBar");
  this._hasStatusBar              = (this._statusBar != null); // set the ivar from whether the status bar was actually rendered, rather than checking the statusBarRendered property, since sometimes we don't render it even though the property is true.  We only render it when the property is true AND we're in non-ADA mode AND Selection is enabled.  (The final condition, which is not relevant until SelectionMode is implemented, is because the status bar is currently used only for Selection.)
  
  this._scrollbarSpacers          = [[ horizontalScrollbarSpacer0 , horizontalScrollbarSpacer1 ], 
                                     [ verticalScrollbarSpacer0 , verticalScrollbarSpacer1 ]];

  this._cornerScrollbarSpacer     = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"CornerScrollbarSpacer");
  
  if (pivotTablePeer._isPaging)
  {
      this._pagingControl         = [[], []];
      this._pagingControl[0][0]   = AdfDhtmlPivotTablePeer._getChildById(this._container, containerId+"hPc").childNodes[1];
      this._pagingControl[0][1]   = AdfDhtmlPivotTablePeer._getChildById(this._container, containerId+"hPc").childNodes[0];
      this._pagingControl[1][0]   = AdfDhtmlPivotTablePeer._getChildById(this._container, containerId+"vPc1").childNodes[0];
      this._pagingControl[1][1]   = AdfDhtmlPivotTablePeer._getChildById(this._container, containerId+"vPc").childNodes[0];
      
      this._pagingControlInputId         = [[], []];
      this._pagingControlInputId[0][0]   = ":hPIc1";
      this._pagingControlInputId[0][1]   = ":hPIc";
      this._pagingControlInputId[1][0]   = ":vPIc1";
      this._pagingControlInputId[1][1]   = ":vPIc";
  }
  
  this._resizeIndicator           = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"ri");
  this._fetchStatus               = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"FetchStatus");
  this._pivotDropTarget           = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"pdt");
  this._scrollbarTester           = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"ScrollbarTesterParent");
  this._summaryDiv                = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"Summary");
  this._pivotStatus               = AdfDhtmlPivotTablePeer._getChildById(this._innerContainer, containerId+"PivotStatus");

  // whether we're using a right-to-left language like Arabic:
  this._rightToLeft = AdfPage.PAGE.getLocaleContext().isRightToLeft();
  
  // First array index is edge (horizontal scroll: edge==0, vertical scroll: edge==1).  
  // Second array index indicates which pane of that edge to scroll.
  this._lastScrollPos = [[0,0],
                         [0,0]];
  this._maxScrollPos = [[0,0],  // create the arrays here just in case it's accessed before sizePT() sets it, to avoid NPE in that case
                        [0,0]];
  if (AdfPivotTableUtils.isMobileTouchDevice() && !pivotTablePeer._isPaging)
    this._scrollWaitForFetch = false;
  this._splitPercentage = [0.5, 0.5]; // init to default value (each edge splits 50/50) in case app sets split on us before user does.
  
  
//  this.clientSizeAttr  = ["clientWidth",     "clientHeight"]; // not currently used
  this.offsetSizeAttr  = ["offsetWidth",     "offsetHeight"];
  this.sizeAttr        = ["width",           "height"];
  this._sizeAttr       = ["_width",          "_height"];
  this._usableSizeAttr = ["_usableWidth",    "_usableHeight"];
//  this.posAttrLeft     = ["left",            "top"]; // not currently used
//  this._posAttrLeft    = ["_left",           "_top"]; // not currently used
  this._posAttr        = ["_horizPos",       "_top"];
  this._canvasPosAttr  = ["_canvasHorizPos", "_canvasTop"];
  this.scrollPosAttr   = ["scrollLeft",      "scrollTop"];
  this.sliceAttr       = ["column",          "row"];

  this.initBuffer();
}

// Helper method to "init" case of DomReplaceNotify.  Inits info like splitSlice, startSlice, edgeExtent, etc.
AdfPivotTableUtils.prototype.initInfo = function(pprTarget, component) { ///////////////////////////////////////////// DONE (except nice-to-have of deleting edge-specific ivars)
    
    // Set the startRow/Col properties to what the server sent, rather than using the existing client-side values.  Why this makes sense:
    // - initial render: there *are* no existing client-side values
    // - pivot: value sent by server is 0, which is the desired value.  The client-side value reflects the pre-pivot scroll position (and any scrolls that have occured since the request was sent), which is not relevant.  
    // - drill: The server's value reflects the scroll position at the time the drill request was sent, while the client's value reflects any scrolls that have occurred since then.  The server's start slice definitely exists in the block, which keeps things simple, while use of the client's start slice would honor any intervening scrolls, which would be desirable, but which might introduce the possibility of having scrolled all the way off the block.  For now, we're using the server's value.  TBD: should drills use the client's value?
    // TBD: If it ever becomes possible for any of these expandos not to get rendered by the server, this logic should detect that case and either set those properties to 0 or not set them at all, whichever is the intended semantics of not rendering them on the server.  The former is probably the right thing if the startSlice was rendered but the startPixel wasn't.  In the latter case, the Peer c'r must set them to default values.
    var viewportStartRow      = AdfAgent.AGENT.getIntAttribute(pprTarget, "_startRow");
    var viewportStartCol      = AdfAgent.AGENT.getIntAttribute(pprTarget, "_startCol");
    var viewportStartRowPixel = AdfAgent.AGENT.getIntAttribute(pprTarget, "_startRowPixel");
    var viewportStartColPixel = AdfAgent.AGENT.getIntAttribute(pprTarget, "_startColPixel");
    var rowHeaderStartLayer   = AdfAgent.AGENT.getIntAttribute(pprTarget, "_rowHeaderStartLayer");
    var colHeaderStartLayer   = AdfAgent.AGENT.getIntAttribute(pprTarget, "_colHeaderStartLayer");
    var rowHeaderStartPixel   = AdfAgent.AGENT.getIntAttribute(pprTarget, "_rowHeaderStartPixel");
    var colHeaderStartPixel   = AdfAgent.AGENT.getIntAttribute(pprTarget, "_colHeaderStartPixel");
    
    component.setProperty(AdfRichPivotTable.__START_ROW_KEY,                 viewportStartRow);
    component.setProperty(AdfRichPivotTable.__START_COLUMN_KEY,              viewportStartCol);
    component.setProperty(AdfRichPivotTable.__START_ROW_PIXEL_KEY,           viewportStartRowPixel);
    component.setProperty(AdfRichPivotTable.__START_COLUMN_PIXEL_KEY,        viewportStartColPixel);
    component.setProperty(AdfRichPivotTable.__ROW_HEADER_START_LAYER_KEY,    rowHeaderStartLayer);
    component.setProperty(AdfRichPivotTable.__COLUMN_HEADER_START_LAYER_KEY, colHeaderStartLayer);
    component.setProperty(AdfRichPivotTable.__ROW_HEADER_START_PIXEL_KEY,    rowHeaderStartPixel);
    component.setProperty(AdfRichPivotTable.__COLUMN_HEADER_START_PIXEL_KEY, colHeaderStartPixel);
    
    // as soon as the split slice changes, i.e. as soon as a split operation occurs, we go up to server to fetch 
    // all new blocks, so we can treat the split slices as unchanging rather than accessing the properties.
    var splitCol     = AdfAgent.AGENT.getIntAttribute(pprTarget,"_splitCol");
    var splitRow     = AdfAgent.AGENT.getIntAttribute(pprTarget,"_splitRow");
    this._splitSlice = [splitCol, splitRow];
    var splitSlice   = this._splitSlice;
    component.setProperty(AdfRichPivotTable.__SPLIT_COLUMN_KEY, splitCol);
    component.setProperty(AdfRichPivotTable.__SPLIT_ROW_KEY,    splitRow);
    
    // if neither edge split, then reset to default value (each edge splits 50/50) in case app sets split on us before user does.
    if (splitCol == -1 && splitRow == -1) {
        this._splitPercentage[0] = 0.5;
        this._splitPercentage[1] = 0.5;
    }

    if (splitCol != -1) {
        var secondStartCol      = AdfAgent.AGENT.getIntAttribute(pprTarget, "_secondStartCol");
        var secondStartColPixel = AdfAgent.AGENT.getIntAttribute(pprTarget, "_secondStartColPixel");
        component.setProperty(AdfRichPivotTable.__SECOND_START_COLUMN_KEY,       secondStartCol);
        component.setProperty(AdfRichPivotTable.__SECOND_START_COLUMN_PIXEL_KEY, secondStartColPixel);
    }
    
    if (splitRow != -1) {
        var secondStartRow      = AdfAgent.AGENT.getIntAttribute(pprTarget, "_secondStartRow");
        var secondStartRowPixel = AdfAgent.AGENT.getIntAttribute(pprTarget, "_secondStartRowPixel");
        component.setProperty(AdfRichPivotTable.__SECOND_START_ROW_KEY,       secondStartRow);
        component.setProperty(AdfRichPivotTable.__SECOND_START_ROW_PIXEL_KEY, secondStartRowPixel);
    }
    
    var colExtent    = AdfAgent.AGENT.getIntAttribute(pprTarget,"_cExtent");
    var rowExtent    = AdfAgent.AGENT.getIntAttribute(pprTarget,"_rExtent");
    this._edgeExtent = [colExtent, rowExtent];
    var edgeExtent   = this._edgeExtent;
    
    var colLayerCount = AdfAgent.AGENT.getIntAttribute(pprTarget,"_cLayerCount");
    var rowLayerCount = AdfAgent.AGENT.getIntAttribute(pprTarget,"_rLayerCount");
    this._layerCount  = [colLayerCount, rowLayerCount];
    
    var colBlockSize = AdfAgent.AGENT.getIntAttribute(pprTarget,"_cBlockSize");
    var rowBlockSize = AdfAgent.AGENT.getIntAttribute(pprTarget,"_rBlockSize");
    this._blockSize  = [colBlockSize, rowBlockSize];
    
    this._startPane  = []; // [colEdgeStartPane, rowEdgeStartPane]. For each edge, start pane is the first pane in use for that edge, i.e. 0 if edge split, 1 otherwise.  
    this._canvasStartSlice = [[],[]]; // indexed by edge/pane, i.e. [[leftPaneStartCol, rightPaneStartCol],
                                      //                             [topPaneStartRow, bottomPaneStartRow]]
    this._canvasEndSlice   = [[],[]]; // indexed by edge/pane, i.e. [[leftPaneEndCol, rightPaneEndCol],
                                      //                             [topPaneEndRow, bottomPaneEndRow]]
    this._paneExtent = [[],[]]; // # of slices in pane.  Indexed by edge/pane, i.e. [[leftPaneColCount, rightPaneColCount],
                                //                                                   [topPaneRowCount, bottomPaneRowCount]]
    
    var startPane        = this._startPane;
    var canvasStartSlice = this._canvasStartSlice;
    var canvasEndSlice   = this._canvasEndSlice;
    var paneExtent       = this._paneExtent;
    
    for (var edge=0; edge<2; ++edge) {
        if (splitSlice[edge] == -1) {
            startPane[edge] = 1;
        } else {
            startPane[edge] = 0;
            canvasStartSlice[edge][1] = splitSlice[edge];
            canvasEndSlice  [edge][0] = splitSlice[edge];
        }
        canvasStartSlice[edge][startPane[edge]] = 0;
        canvasEndSlice[edge][1] = edgeExtent[edge];
    
        for (var pane=startPane[edge]; pane<2; ++pane) {
//            console.log("splitSlice[edge] " + splitSlice[edge] + " canvasEndSlice[edge][pane] " + canvasEndSlice[edge][pane] + " canvasStartSlice[edge][pane] " + canvasStartSlice[edge][pane]);
            paneExtent[edge][pane] = canvasEndSlice[edge][pane] - canvasStartSlice[edge][pane];
        }
    }
  
    // colHeaderHeight and rowHeaderWidth allocation.  Both default to 50% (i.e. 0.5 and '%') if app didn't set them
    // For 15.3%, number is 0.153, not 0.15 or 15.3 or 15.  For px, number is int.
    this._headerSizeAllocation = [Number(AdfAgent.AGENT.getAttribute(pprTarget,"_sizeAlloc0")), // casts won't bomb; server validates they are valid numbers
                                  Number(AdfAgent.AGENT.getAttribute(pprTarget,"_sizeAlloc1"))];
    this._headerSizeAllocUnit  = [AdfAgent.AGENT.getAttribute(pprTarget,"_sizeUnit0"),  // server ensures units are always '%' or 'px'
                                  AdfAgent.AGENT.getAttribute(pprTarget,"_sizeUnit1")];
    
    /////////////////// TODO: Eventually remove all these edge-specific ivars and just keep the arrays.
    this._colExtent = colExtent;
    this._rowExtent = rowExtent;
    this._colLayerCount = colLayerCount;
    this._rowLayerCount = rowLayerCount;
    this._colBlockSize = colBlockSize;
    this._rowBlockSize = rowBlockSize;
    // END edge-specific ivars to eventually remove
}

/**
 * Helper method to "init" case of DomReplaceNotify.
 * For each edge, shows/hides the header, databody, and scroller divs for pane 0 depending on whether that edge is split.
 * This must be called before methods like sizeInitialHTMLBlocks, since if an in-use pane is hidden, its sizes are reported as 0.
 */
AdfPivotTableUtils.prototype.setVisibilityOfPanes = function() { //////////////// DONE
    var splitSlice = this._splitSlice;
    var headers    = this._headers;
    var databodies = this._databodies;
    var scrollers  = this._databodyScrollers;
    
    for (var edge=0; edge<2; ++edge) {
        var display = (splitSlice[edge]==-1) ? "none" : "block";
        headers[edge][0]['style'].display = display;
        (edge ? databodies[0][1] : databodies[1][0])['style'].display = display;
        if (!AdfPivotTableUtils.isMobileTouchDevice()) // in touchscreen, scrollers are always hidden
            (edge ? scrollers[0][1] : scrollers[1][0])['style'].display = display;
    }
    
    // For top left pane, show databody div iff both edges split.  Note that there's no scroller in that pane.
    databodies[0][0]['style'].display = (splitSlice[0]!=-1 && splitSlice[1]!=-1) ? "block" : "none";
}

// TBD: when an agent method becomes available for this, use that rather than a home-grown check.
AdfPivotTableUtils.isMobileSafari = function() { //////////////////////////////////////////// DONE
    var agentName = navigator.userAgent.toLowerCase();
    return (agentName.indexOf("mobile") != -1 && agentName.indexOf("safari") != -1);
}

AdfPivotTableUtils.isMobileTouchDevice = function() {
    return ((AdfAgent.guessOS() == AdfAgent.ANDROID_OS) || AdfPivotTableUtils.isMobileSafari());
}

// TBD: When we create a Paging JS partition, move this method to it.  Since this is the only method (I've found so far) that should go to that partition 
// that is Utils rather than Peer, consider moving it to Peer so can have just one JS file in that partition.  (Note that it's static.)
// Looks like this method is referenced only in RPTRenderer, but verify.  
AdfPivotTableUtils.handlePagingValueChange = function(valueChangeEvent)
{
    var pagingInput = valueChangeEvent.getSource();
    var pivotTablePeer = pagingInput.getParent().getPeer();
    
    var verticalIndex = pagingInput.getClientId().indexOf("vPIc");
    if (verticalIndex != -1)
    {
        var edge = AdfPivotTableUtils.ROW_EDGE;
        if (verticalIndex == pagingInput.getClientId().length - 4)
            var pane = 1;
        else
            pane = 0;
    }
    else
    {
        edge = AdfPivotTableUtils.COLUMN_EDGE;
        var horizontalIndex = pagingInput.getClientId().indexOf("hPIc");
        if (horizontalIndex == pagingInput.getClientId().length - 4)
            pane = 1;
        else
            pane = 0;
    }
    
    // If the paging input was updated, but enter was not pressed, than reset to old value
    // May be able to remove this block if autoSubmit is turned off on the input control text box
    if (!pivotTablePeer._keepPagingValueChange[edge][pane] && !pivotTablePeer._updatedPagingInputValue)
    {
        var newValue = valueChangeEvent.getNewValue();
        var oldValue = valueChangeEvent.getOldValue();
        if (oldValue != newValue)
        {
            pivotTablePeer._keepPagingValueChange[edge][pane] = true;
            pagingInput.setValue(oldValue);
        }
    }
    else if (pivotTablePeer._updatedPagingInputValue)
    {
        pivotTablePeer._updatedPagingInputValue = false;
        var revertValue = true;
        newValue = valueChangeEvent.getNewValue();
        oldValue = valueChangeEvent.getOldValue();
        
        /* TBD: Look into using _canvasStartSlice here instead of calculating the edgeStart value */
        var splitSlice = pivotTablePeer._ptUtils._splitSlice[edge];
        if (pane == 1 && splitSlice != -1)
            var edgeStart = splitSlice;
        else
            edgeStart = 0;
        
        if (newValue > edgeStart && newValue <= pivotTablePeer._ptUtils._edgeExtent[edge] &&
            !(newValue > oldValue && pivotTablePeer._ptUtils.getScrollPos(edge, pane) == pivotTablePeer._ptUtils._maxScrollPos[edge][pane]))
        {
            pivotTablePeer.setPageStartSlice(edge, newValue - 1);
            revertValue = false;
        }
        
        if (revertValue)
        {
            pivotTablePeer._keepPagingValueChange[edge][pane] = true;
            pagingInput.setValue(oldValue);
        }
    }
    else if (pivotTablePeer._keepPagingValueChange[edge][pane])
        pivotTablePeer._keepPagingValueChange[edge][pane] = false;
}

AdfPivotTableUtils.prototype.getScroller = function(rowPane, colPane) { //////////////// DONE
    if (AdfPivotTableUtils.isMobileTouchDevice())
        return this._databodies[rowPane][colPane];
    else
        return this._databodyScrollers[rowPane][colPane];
}

// for touchScreen, there's not just one scroller for the specified edge and pane, since both panes in that row/col of panes can be finger-scrolled.  So for touchScreen this method should be used only in cases where either one will do, e.g. getting info that is the same for both, not for setting info that must be set on both.
AdfPivotTableUtils.prototype.getScrollerByEdgeAndPane = function(edge, pane) { //////////////// DONE
    if (AdfPivotTableUtils.isMobileTouchDevice())
        return this._databodiesByEdgeAndPane[edge][pane];
    else
        return this._databodyScrollersByEdgeAndPane[edge][pane];
}

AdfPivotTableUtils.prototype.getScrollerCanvas = function(rowPane, colPane) { //////////////// DONE
    if (AdfPivotTableUtils.isMobileTouchDevice())
        return this._databodyCanvases[rowPane][colPane];
    else
        return this._databodyScrollerCanvases[rowPane][colPane];
}

AdfPivotTableUtils.prototype.removeExistingBlocksFromDOM = function() /////////////////////// DONE
{
    var databodyArray = AdfPivotTableUtils.isMobileTouchDevice() ? this._databodyCanvases : this._databodies; // normally the databody divs contain the blocks, but for iOS, the databody canvases do.  (The scroller canvases never do.)
    
    // note that most loops over panes must start at startPane[edge], not 0.  Don't copy this elsewhere.
    for (var i=0; i<2; ++i) {
        for (var j=0; j<2; ++j) {
            this.removeChildren(this._headers[i][j]); // i and j are edge and pane
            this.removeChildren(databodyArray[i][j]); // i and j are rowPane and colPane
        }
    }
}

/** 
 * @param node the node from which to remove children
 * @param childNodes an array of childNodes to remove from node.  If omitted, all children are removed.
 *
 * If childNodes contains any arrays, this method is recursively called on those arrays.  
 * This is convenient since the databody buffer is a 2D array (array of arrays).
 * If childNodes contains any non-array elements that aren't children of node, they are ignored 
 * with no error.  This facilitates the use of placeholder objects in the buffer.
 */
AdfPivotTableUtils.prototype.removeChildren = function(node, childNodes) //////////////////////// NOTHING TO DO
{
    if (!childNodes) 
        childNodes = node.childNodes;

    var length = childNodes.length;
    // iterate backwards since array will be live in the "remove all children" case:
    for (var i=length-1; i>=0; --i) {
        var child = childNodes[i];
        if (child instanceof Array) {
            this.removeChildren(node, child);
        } else if (child.parentNode==node) {
            this._unregisterNode(child);
            node.removeChild(child); 
        }
    }
}

AdfPivotTableUtils.prototype._unregisterNode = function(node) //////////////////////// NOTHING TO DO
{
  // We must remove the components from the subtree and their corresponding elements to support
  // virtualized scrolling.  When we get rid of blocks, we want to notify stamped components that
  // they are being removed, just like the PPR code does.
  AdfPage.PAGE.removeComponentsInSubtree(node);
  AdfAgent.AGENT.elementsRemoved(node);
}


/**
 * Preconditions and requirements: 
 * - There must be exactly one rowHeader, colHeader, and databody block in each pane.  
 * - for each pane, (blockStartSlice <= viewportStartSlice < blockEndSlice) must be satisfied, 
 *   unless viewportStartSlice >= edgeExtent.  (i.e. viewportStartSlice must be in this block)
 *   
 * @param sizeBeforeViewport an object having heightBeforeViewport and widthBeforeViewport 
 *        properties, indicating the number of pixels in the (full and partial) slices 
 *        that precede the viewport
 */
AdfPivotTableUtils.prototype.positionInitialBlocks = function(sizeBeforeViewport) { ///////////////////////////// DONE
//    console.log("pib100");

    var scrollPos = [[],[]];
    var startPane = this._startPane;
    
    
    for (var edge=1; edge>=0; --edge) {
        for (var pane=startPane[edge]; pane<2; ++pane) {
            var headerBlock = this._buffers2[edge][pane][0];
            var databodyBlocks = (edge==1) 
                                 ? (this._splitSlice[0] > 0) ? [this._databodyBuffers[pane][0][0][0], this._databodyBuffers[pane][1][0][0]] : [this._databodyBuffers[pane][1][0][0]]
                                 : (this._splitSlice[1] > 0) ? [this._databodyBuffers[0][pane][0][0], this._databodyBuffers[1][pane][0][0]] : [this._databodyBuffers[1][pane][0][0]];
            var blockStartSlice = this.getIntAttribute(headerBlock, "_startSlice"); 
            var canvasStartSlice = this._canvasStartSlice[edge][pane];
            var canvasEndSlice = this._canvasEndSlice[edge][pane];
            var viewportStartSlice = this._pivotTablePeer.getViewportStartSliceProperty(edge, pane, false);
            var sizeObj = (edge) ? sizeBeforeViewport.heightBeforeViewport[pane]: sizeBeforeViewport.widthBeforeViewport[pane];
            
//            console.log("pib300");
            
            scrollPos[edge][pane] = 
              this._positionInitialBlocks(edge, pane, canvasStartSlice, canvasEndSlice, 
                                          headerBlock, databodyBlocks, blockStartSlice, this._defaultSliceSize[edge], 
                                          this._maxScrollPos[edge][pane], sizeObj, viewportStartSlice);
        }
    }
                                 
//    console.log("pib600");

    // While _pIB() computes pos, canvasPos, and scrollPos, we delay the actual setting of scrollPos to here, so that all calls to 
    // setScrollPos can be together.  Since setting the scrollPos seems to trigger a repaint in IE, setting the horiz and vert scrollPos 
    // together appears less likely to lead to performance issues, and seems to reduce/eliminate visible "blinks" on long scrolls in IE.
    
    // Note that the scrollPos of the headers and layerLabelBlock is set earlier, in callees of sizeInitialHTMLBlocks(), which is where the headers acquire a scrollbar, to 
    // hopefully minimize the visibility of the jump of the header scrollbars to the beginning and then back down to the correct value.  This 
    // doesn't help very much.  
    // TODO: To really fix this, we prob. need to keep the old header block (or a same-size facsimile) in the CH until the new block is added, rather 
    // than removing all blocks and THEN adding the new one, since in the meantime the scrollbar obviously disappears and when it returns it defaults 
    // to 0 until we set it, hence the jump.  At that time, move the set-header-scrollpos calls back to here if possible.  
    
    if (this._pivotTablePeer.__logEnabled) {
      for (edge=0; edge<2; ++edge) {
        for (pane=startPane[edge]; pane<2; ++pane) {
          this._pivotTablePeer.putInTestLog("positionInitialBlocks calling setScrollPos(" + edge + ", " + pane + ", " + scrollPos[edge][pane] + ")");
        }
      }
    }
    
//    console.log("pib700");

    var scrollPos2 = [[],[]]; // used only in assert.  TBD: remove?
    
    for (edge=0; edge<2; ++edge) {
      for (pane=startPane[edge]; pane<2; ++pane) {
        scrollPos2[edge][pane] = this.setScrollPos(edge, pane, scrollPos[edge][pane]);
      }
    }
    
//    console.log("pib800");

    if (!AdfPivotTableUtils.assertsOff) {
        var outOfBounds = false;
        outer:
        for (edge=0; edge<2; ++edge) {
          for (pane=startPane[edge]; pane<2; ++pane) {
            if (scrollPos[edge][pane] != scrollPos2[edge][pane]) {
              outOfBounds = true;
              break outer;
            }
          }
        }
        if (outOfBounds) {
          var output = "positionInitialBlocks: scrollPoses out of bounds.  ";
          for (edge=0; edge<2; ++edge) {
            for (pane=startPane[edge]; pane<2; ++pane) {
              output += "edge " + edge + ", pane " + pane + ", scrollPos " + scrollPos[edge][pane] + ", scrollPos2 " + scrollPos2[edge][pane];
            }
          }
          AdfAssert.assert(false, output);
        }
    }
    
    // TODO: why was this added?  In the other code that positions colheader blocks (e.g. positionNewHtmlBlocks) we set style['top'] but not _top, and 
    // here we're doing the opposite.  We're not doing the analogous thing to the row header block (setting _left) (which would also be irregular).  
//    colHeaderBlock._top = 0; 
}

/**
 * Private helper for positionInitialBlocks.  No other method should call it.  
 * 
 * CanvasPos (position relative to canvas):
 * Each pane's canvas is sized (in sizePivotTable) and the blocks are positioned on the canvas (in this method) as they would be if all the missing blocks were present 
 * with all their slices sized to defaultSliceSize.  
 * 
 * Pos (position relative to viewport):
 * For the specified dimension, this method positions the blocks so that the first slice in the viewport is as specified by the pivot table's [second]startRow/Col (hereafter 
 * startSlice) and startPixel properties, which indicate which slice and which pixel of that slice should be at the start of the viewport.  If that pixel of that 
 * slice is less than one viewport away from the end of the canvas, so that it isn't possible to honor those properties without scrolling past the end of 
 * the canvas (impossible), we scroll all the way to the end, so that the highest possible slice and pixel are at the beginning of the viewport.  
 * 
 * If the startSlice property >= canvasEndSlice, that means "scroll to the end, no matter what startSlice that produces."  So we do so. 
 * 
 * Since this method can change the viewport startSlice and startPixel (in all the aforementioned cases), we reset those properties.  In particular, 
 * in the case where startSlice >= canvasEndSlice, we reset startSlice from its >=canvasEndSlice value to the slice that winds up at the beginning of the viewport.
 */
AdfPivotTableUtils.prototype._positionInitialBlocks = function(edge, pane, canvasStartSlice, canvasEndSlice, 
                                                               headerBlock, databodyBlocks, blockStartSlice, defSliceSize, 
                                                               maxScrollPos, sizeBeforeViewport, viewportStartSlice) { ///////////////////////////// DONE.
    var needToResetViewportStartSlice = true;
    var canvasPos = (blockStartSlice-canvasStartSlice) * defSliceSize; 
    
    if (viewportStartSlice >= canvasEndSlice) {
    // Case 1: "scrollToEnd": This is a long jump to the last slice, or an initial render where the startSlice property was set to >= canvasEndSlice to specify "the end 
    // of the pane."  Thus, the block is the last block of the pane.  It may also be the first block (iff this is the only block of the edge in that pane).  There 
    // may or may not be a scrollbar.  Drills/pivots/sorts never hit this case (unless app set scrollPos in response to the event).  Note that this case doesn't use 
    // sizeBeforeViewport, which is not set to a meaningful value in this case.
        AdfAssert.assert(AdfPivotTableUtils.assertsOff || headerBlock.getAttribute("_lastBlock"), "In scrollToEnd case, the block must be the last block.");
        var scrollPos = maxScrollPos; // scroll to the end.  maxScrollPos is guaranteed to be in-bounds. 
        var pos = canvasPos - scrollPos; // position the block (relative to the viewport) in the only position that's consistent with the chosen canvasPos and scrollPos.
    } else { // Case 2: This is not a scrollToEnd scenario.  There may or may not be a scrollbar.  Relevant to all cases: initial render, pivot, drill, sort, and longScroll.  
        var desiredPos = -sizeBeforeViewport; // the pos that honors viewportStartSlice
        var minPos = canvasPos - maxScrollPos; // desiredPos is below this value iff the specified viewportStartSlice is within one viewport of the end of the canvas, in which case the viewportStartSlice can't be honored (the desiredPos can't be used) without attempting to set scrollPos above maxScrollPos (i.e. scroll past the end).
        // maxPos is canvasPos, since greater values would make scrollPos negative (move the viewport off the beginning of the canvas), but since (desiredPos <= 0 <= canvasPos), there's no need for an explicit maxPos check.
        pos = Math.max(desiredPos, minPos);
        if (pos==desiredPos)
            needToResetViewportStartSlice = false;
        scrollPos = canvasPos - pos; // put the scroll handle in the only position that's consistent with the chosen canvasPos and pos.  Guaranteed in-bounds (between 0 and maxScrollPos) due to the preceding pos checks.
    }
//    console.log("_pib100");
    
    var databodyPos = (AdfPivotTableUtils.isMobileTouchDevice()) ? canvasPos : pos;

    // Note: not currently combining these for-loops to match the pre-split-panes order of ops.
    for (var i=0; i<databodyBlocks.length; ++i) {
        this.setPos(edge, databodyPos, databodyBlocks[i]);
    }
//    console.log("_pib200");

    this.setPos(edge, pos, headerBlock);
//    console.log("_pib300");
    
    for (var i=0; i<databodyBlocks.length; ++i) {
        this.setCanvasPos(edge, canvasPos, databodyBlocks[i]);
    }
//    console.log("_pib400");

    this.setCanvasPos(edge, canvasPos, headerBlock);
//    console.log("_pib500");

    if (needToResetViewportStartSlice) {
        //AdfLogger.LOGGER.severe("edge " + edge + ", pane " + pane + ", resetting VSS from " + viewportStartSlice);
        this.setViewportStartSliceFromPosition(edge, pane); // Must be reset for Case 1 always, and for Case 2 in the situations where it is unable to honor the vSS.
//    } else {
//        AdfLogger.LOGGER.severe("--NOT-- resetting VSS");
    }
    
    // TBD: if/when ER 11706073 is fixed, and the logic to set the header scrollPos is moved from coordinateRowHeaderColumnWidths / coordinateColumnHeaderRowHeights to here, AND we've started tracking whether the startLayer/Pixel is outdated to enable getChanges() to skip the calc if not outdated, then uncomment this code (which hasn't been tested and may have typos):
    
//    var needToResetHeaderStartLayer = true;
//    
//    if (headerStartLayer >= layerCount) {
//    // Case 1: "scrollToEnd": This is an initial render where the startLayer property was set to >= layerCount to specify "the end of the header."  There 
//    // may or may not be a header scrollbar.  Drills/pivots/sorts never hit this case (unless app set header scrollPos in response to the event).  Note that this 
//    // case doesn't use sizeBeforeHeader, which is not set to a meaningful value in this case.
//        var headerScrollPos = maxHeaderScrollPos; // scroll to the end.  maxScrollPos is guaranteed to be in-bounds. 
//    } else { // Case 2: This is not a scrollToEnd scenario.  There may or may not be a scrollbar.  Relevant to all cases: initial render, pivot, drill, and longScroll.  
//        var desiredHeaderScrollPos = sizeBeforeHeader; // the scrollPos that honors headerStartLayer.  desiredScrollPos is above maxHeaderScrollPos iff the specified startLayer is within one viewport of the end of the header, in which case the startLayer can't be honored without attempting to set headerScrollPos above maxHeaderScrollPos (i.e. scroll past the end).
//        headerScrollPos = Math.min(desiredHeaderScrollPos, maxHeaderScrollPos);
//        if (headerScrollPos==desiredHeaderScrollPos)
//            needToResetHeaderStartLayer = false;
//    }
//    
//    // TODO: SET HEADER SCROLLPOS HERE!!
//    
//    if (needToResetHeaderStartLayer) {
//        //AdfLogger.LOGGER.severe("edge " + edge + ", resetting headerStartLayer from " + headerStartLayer);
//        this.setHeaderStartLayerOutdated(edge); // TODO: IMPL CALLEE.  Unlike databody scrolling, we handle this lazily.  (TBD: consider making lazy for databody too.)  Must be reset for Case 1 always, and for Case 2 in the situations where it is unable to honor the headerStartLayer.
////    } else {
////        AdfLogger.LOGGER.severe("--NOT-- resetting headerStartLayer");
//    }
    
    // END OF TBD
    
    return scrollPos;
}

/**
 * Sets the viewport start slice and start pixel properties from the current 
 * position of the blocks.  If there is no first block of the viewport due to 
 * whitespace, the existing values are not overwritten.  
 */
// This method is called by (1) scrollHTMLBlocks, (2) _positionInitialBlocks (scrollToEnd and desiredSliceWithinViewportOfEnd cases), and (3) DRN (shortScroll and fillViewport cases).
// Performance notes:  This method is one of our most expensive, per Firebug profiler.  Ways to address this:
// - Consider calling this method lazily, by having each existing caller just set a dirty bit, and calling it from GetChanges and whatever else needs the prop?  Does framework provide a way to override the autogenerated getter method for the prop to make it lazy?
// - Revisit whether there are times when we can avoid calling it from DRN.
// - (Old TBD based on the existing eager paradigm) This method, and its caller scrollHTMLBlocks, should probably take bounds args, which could be passed thru to the callee firstSliceInShiftedViewport, so it doesn't have to recompute them.  HandleDomScroll already has computed the bounds (based on the new, appropriate viewport), but currently its distant callee firstSliceInShiftedViewport must recompute them.  
AdfPivotTableUtils.prototype.setViewportStartSliceFromPosition = function(edge, pane, bounds, cacheOffset) { /////////////////// DONE
    if (edge === undefined) {
        for (edge=0; edge<2; ++edge) {
            for (pane = this._startPane[edge]; pane<2; ++pane) {
                this.setViewportStartSliceFromPosition(edge, pane, bounds, cacheOffset);
            }
        }
        return;
    }

    viewportStartSliceObj = this.firstSliceInShiftedViewport(edge, pane, 0, bounds, cacheOffset);
    viewportStartSlice = viewportStartSliceObj.slice;
    
    if (viewportStartSlice != -1) {
        viewportStartPixel = viewportStartSliceObj.pixel;
        
        this._pivotTablePeer.setViewportStartSliceProperty(edge, pane, false, viewportStartSlice);
        this._pivotTablePeer.setViewportStartSliceProperty(edge, pane, true,  viewportStartPixel);
    }
}

AdfPivotTableUtils.prototype.setViewportStartLayerFromPosition = function(edge) { ///////////////////// DONE
    if (edge === undefined) {
        this.setViewportStartLayerFromPosition(AdfPivotTableUtils.COLUMN_EDGE);
        this.setViewportStartLayerFromPosition(AdfPivotTableUtils.ROW_EDGE);
        return;
    }

    var viewportStartLayerObj = this.firstLayerInViewport(edge);
    
    this._pivotTablePeer.setHeaderStartLayerProperty(edge, false, viewportStartLayerObj.layer);
    this._pivotTablePeer.setHeaderStartLayerProperty(edge, true,  viewportStartLayerObj.pixel);
}

/** 
 * Gets the scrollPos from the last time the blocks were positioned.
 * 
 * Precondition: There must be at least one block in the header buffer for the 
 * specified edge.  
 */
AdfPivotTableUtils.prototype.getOldScrollPos = function(edge, pane) { ////////////////////// DONE
    var canvasPos = this._canvasPosAttr[edge];
    var pos = this._posAttr[edge];
    var testBlock = this._buffers2[edge][pane][0]; // the first header block in the buffer.  This can never be a placeholder, so this works as long as the buffer is non-empty.
    return testBlock[canvasPos] - testBlock[pos]; // since canvasPos = scrollPos + pos, this gives us the scrollPos from the last time the blocks were positioned (the last time pos was set).
}

/** 
 * Gets the position (pos) that the block will have as soon as the blocks are allowed to 
 * catch up to the scrollbar position (scrollPos).
 */
AdfPivotTableUtils.prototype.getNewBlockPos = function(edge, block, newScrollPos) { ////////////////////// DONE
    var canvasPos = this._canvasPosAttr[edge];
    return block[canvasPos] - newScrollPos; // since canvasPos = scrollPos + pos, this gives us the pos the block will have as soon as it is updated to reflect the new scrollPos
}

/**
 * This method sets the pos of all blocks to (canvasPos - scrollPos), which is useful 
 * for several purposes.  First, it makes the blocks "catch up" to the position of 
 * the scrollbar after a scroll.  Second, it is useful for setting the initial position 
 * of new blocks.  Third, in RTL, it can update style['left'] to once again 
 * be consistent with horizPos in case viewportWidth or blockWidth changed.
 * 
 * @param bBlocksNew indicates that some blocks are new and haven't ever had their position 
 *                   set, so that this method must do so even if no scrolling has occurred
 * @param bounds row and column bounds, optional
 * @param cacheOffset whether to use and store cached offset value, optional
 * @param scrollDatabody whether to scroll databody or not, optional
 */
AdfPivotTableUtils.prototype.scrollHTMLBlocks = function(bBlocksNew, bounds, cacheOffset, scrollDatabody) { ////////////////////////////////// DONE
    var databodyBuffers = this._databodyBuffers;

    for (var edge=1; edge>=0; --edge) { // iterate backwards to match order from before this became a for-loop
        var otherEdge = 1-edge;
        var headerBuffers      = this._buffers2[edge];
        var otherHeaderBuffers = this._buffers2[otherEdge];
        
        for (var pane=this._startPane[edge]; pane<2; ++pane) {
            var scrollPos = this.getScrollPos(edge, pane);
            
            // Move the blocks vertically (edge 1) or horizontally (edge 0), if the scroll position in that dimension has changed or if some blocks are new:
            var somethingToDo = (scrollDatabody != undefined) || bBlocksNew 
                || (scrollPos - this.getOldScrollPos(edge, pane)) != 0 // the change in scroll position, i.e. the distance all blocks must move, is the current scrollPos minus the scrollPos when the blocks were last positioned.  If this is 0, there is nothing to do.
                || (edge==0 && this._rightToLeft); // in RTL, _horizPos can be out of synch with block._left if there has been a change to viewportWidth, which is an input to the formula linking horizPos and left, so that the preceding test is unreliable.  
                                                   // TBD: maybe chg rtl to (rtl && horizPos out of synch with left)    
            
            if (somethingToDo) {
                var headerBuffer = headerBuffers[pane];
            
                for (var i=0; i<headerBuffer.length; i++) {
                    var headerBlock = headerBuffer[i];
                    if (!headerBlock['_placeholder']) {
                        var newPos = this.getNewBlockPos(edge, headerBlock, scrollPos);
                        this.setPos(edge, newPos, headerBlock);
                    }
                    
                    for (var otherPane=this._startPane[otherEdge]; otherPane<2; ++otherPane) {
                        var otherHeaderBufferLength = otherHeaderBuffers[otherPane].length;
                        var databodyBuffer = (edge==1) ? databodyBuffers[pane][otherPane] : databodyBuffers[otherPane][pane];
                        for (var j=0; j<otherHeaderBufferLength; j++) {
                            var databodyBlock = (edge==1) ? databodyBuffer[i][j] : databodyBuffer[j][i];
                            if (databodyBlock) {
                                newPos = (AdfPivotTableUtils.isMobileTouchDevice()) 
                                         ? databodyBlock[this._canvasPosAttr[edge]] 
                                         : this.getNewBlockPos(edge, databodyBlock, scrollPos);

                                if (scrollDatabody !=undefined && !scrollDatabody)
                                    AdfDhtmlCommonUtils.addCSSClassName(databodyBlock, this._pivotTablePeer._getStyleClass(AdfPivotTableUtils.DATABODY_FROZEN_STYLE));                                
                                else
                                {
                                    if (scrollDatabody != undefined)
                                        AdfDhtmlCommonUtils.removeCSSClassName(databodyBlock, this._pivotTablePeer._getStyleClass(AdfPivotTableUtils.DATABODY_FROZEN_STYLE));                                

                                    this.setPos(edge, newPos, databodyBlock);
                                }
                            }
                        }
                    }
                }

                this.setViewportStartSliceFromPosition(edge, pane, bounds == null ? null : bounds[edge][0], cacheOffset);
            }
        }
    }
}

/**
 * This method is intended for use after a shortScroll or fillViewport response is received.  It sets the canvasPos of all existing and new 
 * blocks, and it adjusts scrollPos so that when scrollHTMLBlocks subsequently sets the pos of all blocks to canvasPos - scrollPos, the blocks 
 * move the desired number of pixels.  This method does not set pos directly. 
 * 
 * CanvasPos details: The canvas is sized (in sizePivotTable) and the blocks are positioned on the canvas (in this method) as they would be if 
 * all the missing blocks were present with all their slices sized to defaultSliceSize.  
 */
AdfPivotTableUtils.prototype.positionNewHTMLBlocks = function(deltas) { /////////////////// DONE.  (called only by DRN:SS and DRN:FV.)
    var edge, pane, i, j; // loop vars
    var scrollPos = [[], []];
    
    // 1. Position header blocks on canvas: 
    for (edge=1; edge>=0; --edge) { // iterate backwards to match order from before this became a for-loop
        var headerBuffers = this._buffers2[edge];
        var _canvasPosAttr = this._canvasPosAttr[edge];
        var _sizeAttr = this._sizeAttr[edge]
        var _posAttr = this._posAttr[edge];
        for (pane=this._startPane[edge]; pane<2; ++pane) {
            var headerBuffer = headerBuffers[pane];
            var length = headerBuffer.length;
            var oldHeaderBlock = null; // this will hold a block that existed before the fetch
            
            var block = headerBuffer[0];
            if (!block['_new']) {
                oldHeaderBlock = block;
            }
            var blockStartSlice = this.getIntAttribute(block, "_startSlice");
            var slicesBeforeBlock = blockStartSlice - this._canvasStartSlice[edge][pane];
            this.setCanvasPos(edge, this._defaultSliceSize[edge] * slicesBeforeBlock, block);
          
            for (i=1; i<length; i++) { // note loop starts at 1
                var prevBlock = block;
                block = headerBuffer[i];
                if (oldHeaderBlock == null && !block['_new']) {
                    oldHeaderBlock = block;
                }
                this.setCanvasPos(edge, prevBlock[_canvasPosAttr] + prevBlock[_sizeAttr], block);
                block['style'][edge ? "left" : "top"] = "0px"; // TODO: Do we really need this line?  If so, then in RTL mode, should we be right-aligning the block rather than left-aligning it?  Or is it a perfect fit anyway?  
            }
            
            var desiredBlockPos = oldHeaderBlock[_posAttr] - deltas[edge][pane];
            var canvasPos = oldHeaderBlock[_canvasPosAttr];
            scrollPos[edge][pane] = canvasPos - desiredBlockPos; // see comments on setScrollPos below.
        }
    }

    // 2: Position databody blocks on canvas:
    var rowHeaderBuffers = this._rowHeaderBuffers;
    var colHeaderBuffers = this._colHeaderBuffers;
    var databodyBuffers = this._databodyBuffers;
    
    for (var rowPane=this._startPane[1]; rowPane<2; ++rowPane) {
        var rowHeaderBuffer      = rowHeaderBuffers[rowPane];
        var rowOfDatabodyBuffers = databodyBuffers[rowPane];
        
        for (var colPane=this._startPane[0]; colPane<2; ++colPane) {
            var colHeaderBuffer = colHeaderBuffers[colPane];
            var databodyBuffer  = rowOfDatabodyBuffers[colPane];
            length = databodyBuffer.length;
            
            for (i=0; i<length; i++) {
                var rowHeaderBlock      = rowHeaderBuffer[i];
                var rowOfDatabodyBlocks = databodyBuffer[i];
                var rLength = rowOfDatabodyBlocks.length;
                
                for(j=0; j<rLength; j++) {
                    // synch with the positions already on the row/col header blocks:
                    var colHeaderBlock = colHeaderBuffer[j];
                    var databodyBlock = rowOfDatabodyBlocks[j];
                    this.setCanvasPos(1, rowHeaderBlock['_canvasTop'],      databodyBlock);
                    this.setCanvasPos(0, colHeaderBlock['_canvasHorizPos'], databodyBlock);
                }
            }
        }
    }

    // 3: Set scrollPos:
    // The scrollDelta (passed in by the caller) is the distance the scrollbar has moved since the blocks 
    // were last allowed to catch up to the scrollbar.  When this catch-up is done by a later method, we 
    // would like the blocks to move scrollDelta px as planned, so we set scrollPos to the value that will 
    // effect this.  (Since a previous method has resized the canvas, which can change scrollPos, and this 
    // method has changed canvasPos, the current value of scrollPos is probably not the value that will 
    // effect this.)  If the desired scrollPos is out-of-bounds, the closest in-bounds value will be set.  This
    // happens when the new block was smaller than expected, so that there are now fewer than scrollDelta pixels 
    // between the end of the viewport and the end of the canvas.  
    
    for (edge=1; edge>=0; --edge) { // iterate backwards to match order from before this became a for-loop
        for (pane=this._startPane[edge]; pane<2; ++pane) {
            if (this._pivotTablePeer.__logEnabled) {
                this._pivotTablePeer.putInTestLog("positionNewHTMLBlocks calling setScrollPos(" + edge + ", " + pane + ", " + scrollPos[edge][pane] + ")");
            }
            this.setScrollPos(edge, pane, scrollPos[edge][pane]);
        }
    }
}

/**
 * Moves the affected blocks down the specified number of pixels.  The affected blocks are all non-new rowHeader and databody 
 * blocks in the specified pane (top or bottom), from the specified row of blocks to the last row of blocks in the pane.
 */
AdfPivotTableUtils.prototype.moveExistingBlocksTop = function(rowPane, startBlockIndex, topDelta) ////////////////////////////////// DONE
{
  var rowHeaderBuffer = this._rowHeaderBuffers[rowPane];
  var length = rowHeaderBuffer.length;
  for(i=startBlockIndex;i<length;i++)
  {
    var block = rowHeaderBuffer[i];
    if(!block['_placeholder'] && !block['_new'])
    {
      this.setCanvasPos(1, block['_canvasTop'] + topDelta, block);
      this.setPos(      1, block['_top']       + topDelta, block);
    }
  }

  for (var colPane=this._startPane[0]; colPane<2; ++colPane) {
      var databodyBuffer = this._databodyBuffers[rowPane][colPane];
      length = databodyBuffer.length;
      for(i=startBlockIndex;i<length;i++)
      {
        var row = databodyBuffer[i];
        var rLength = row.length;
        for(j=0;j<rLength;j++)
        {
          block = row[j];      
          if(block && !block['_new'])
          {
            this.setCanvasPos(1, block['_canvasTop'] + topDelta, block);
            this.setPos(      1, block['_top']       + topDelta, block);
          }
        }
      }
  }
}


//============================================================================//
//                              Buffer code                                   //
//============================================================================//


/*
 * Buffer invariants
 * 
 * In these notes, <--> represents two-way implication, and --> and <-- represent implication 
 * in one direction but definitely not the other.
 * 
 * Whenever the buffer is in a steady state (not in the middle of an insertion, deletion, etc.), 
 * we have the following invariants.  (For invariants that apply in the middle of deleting 
 * and inserting blocks from/into the buffer, see the Javadoc for the corresponding methods.)
 *
 * Regularity of structure:
 *
 * 1 The databody buffer is a rectangular (not ragged) 2D array, where each element of the outer 
 *   array represents a row.  
 * 2 The row (column) header buffer is a non-null 1D array whose length equals the number of 
 *   rows (columns) of blocks in the databody buffer.  
 * 5 The outer array of the databody buffer is always non-null.  (Per Invariant 2, its length 
 *   is 0 iff the row header buffer's length is 0.)  If the outer array has non-zero length, then 
 *   each inner array is non-null.  (Per Invariant 2, each inner array has length 0 iff the column 
 *   header buffer's length is 0.)
 * 2A All three arrays are indexed starting at zero.  In particular, if the first row (column) 
 *   of blocks is removed, the remaining elements of the row (column) header buffer are shifted so 
 *   that the first array element is again indexed zero, and the remaining rows (columns) of the 
 *   databody buffer are likewise shifted.  
 * 3 For each row (column) of blocks, the row (column) header block and all databody blocks in that 
 *   row (column) have the same startRow (startCol) and numRows (numCols).
 * 4 For each row (column) of blocks, the last slice in that row (column) of blocks is one less than 
 *   the first slice in the next row (column) of blocks.  For example, if one row (column) of blocks 
 *   has startSlice 42 and numSlices 18, then the next row (column) of blocks must have startSlice 60.
 *
 * Missing databody blocks and placeholder rows/columns of blocks:
 *
 * 6 Some databody blocks may be missing (i.e. some slots of the databody buffer may be empty).
 * 7 In unusual cases, an entire row (column) of blocks may be missing.  Such a row (column) is called 
 *   a placeholder row (column) since it was created purely to satisfy Invariant #4.  
 * 8/9 There are never any empty slots in the header buffers.  If the (real) header block is 
 *   not present, then that slot of the header buffer contains a placeholder block, which is 
 *   a dummy object that contains only metadata such as the startSlice and numSlices.  Placeholder 
 *   blocks have a _placeholder expando with the value true.  Real blocks have no such expando.  
 * 10 At least one databody block in a row (column) is present -->
 *    the header block in that row (column) of blocks is real (not a placeholder).
 *    We say that a row (column) of blocks is a placeholder row (column) iff the header block 
 *    is a placeholder.
 * 11 The first and last row (column) of blocks are never placeholder rows (columns).  A 
 *    special case is that a placeholder row (column) can't be the only row (column) in the buffer.
 * 12 A placeholder row/col cannot be adjacent to another placeholder row/col, as they would 
 *    have been consolidated into one.   
 * TODO: revisit this one and the discussion of it under Rationale:
 * 13 If, hypothetically, our fetch algorithm were guaranteed to fetch all blocks in the extended viewport 
 *    (viewport + cushion), where the cushion is defined by the fetch threshold, and if we made the fetch 
 *    threshold and the retention threshold equal, there would never be any missing blocks.  
 *    
 * TODO: I changed #5 and 10 not to require/assume that RH buffer empty iff CH buffer empty, 
 * but I haven't actually changed all the code that conforms to the old versions!!  (The 
 * oldCode file has the original versions of these invariants.)
 *
 * Derived invariants:  
 * 
 * 14 Each placeholder row (column) must have a non-placeholder row (column) on both sides of it.  
 *    Follows from 11 and 12.
 * 
 * Relationship to DOM
 * 
 * 15 TODO: is this always true: A block is in the (row header / column header / databody) buffer <--> 
 *    it is in the (row header / column header / databody) portion of the DOM.
 *
 * Rationale for the above invariants:
 *
 * - The "Regularity of Structure" invariants are maintained for sanity's sake.
 * - Rationale for "Header block not placeholder iff >=1 DB block present" invariant:  
 *   - TODO: Update this rationale to reflect the fact that the implication is one-way 
 *     now, per #10.  This rationale was faulty.  The missing 
 *     piece was that trims *can* remove DB blocks w/o removing corresponding header blocks, 
 *     because the scroll was in one dimension only, so that the other header and the DB 
 *     are emptied but not this header.  
 *   - This is true at the outset.
 *   - Fetches preserve the invariant.  Whenever we fetch a databody block, we fetch the corresponding header 
 *     blocks if we don't already have them.  Likewise, we fetch a header block only if we're fetching at 
 *     least one corresponding databody block.  
 *   - Trimming the buffer preserves the invariant, since we always remove entire rows/columns of blocks, 
 *     never removing the header block without the corresponding databody blocks or vice versa.  
 * - Possibility of missing blocks:
 *   - In the hypothetical "fetch threshold = retention threshold" case described above, no holes would be 
 *     possible, because 
 *     - the initial fetch leaves us with no holes (rectangular databody buffer is full, as are corresponding 
 *       header buffers), 
 *     - each trim of the buffer will take entire rows and columns on the sides of the buffer, leaving a full 
 *       rectangle, 
 *     - each addition to the buffer will add entire rows and columns on the sides of the buffer, leaving a full 
 *       rectangle.  
 *   - However, if the fetch threshold is smaller than the retention threshold, then part 3 of the preceding is 
 *     no longer true.  When the buffer is augmented, some blocks in the new rows and columns of blocks (if any) 
 *     will not be fetched.  (We say "if any" since unlike the previous case, a fetch doesn't necessarily mean 
 *     adding new rows and columns of blocks to the buffer.  It might mean filling in some of the holes in 
 *     existing rows/columns.)  
 *   - TODO: REVISIT:  
 *     Also, since we're using an algorithm that doesn't guarantee fetching all blocks in the extended viewport, 
 *     clearly holes are possible.  However, since our alg will immediately refetch if it turns out that 
 *     some extended-viewport blocks are missing, until the extended viewport is full, if we define "steady 
 *     state" to exclude the period between an incomplete fetch and the fetch that completes it, then we're back 
 *     to the case where holes are impossible if fetch threshold = retention threshold.  
 */
///////////////////////////////////////////////////////////////////// DONE 
AdfPivotTableUtils.prototype.initBuffer = function() {
  // The databody buffer for each pane is a 2D array, implemented as an array of arrays where each nested array is a row of blocks.
  // Note that these 4 are local vars only; the ivar is below.
  var databodyBuffer00  = []; // top left pane's buffer
  var databodyBuffer01  = []; // top right pane's buffer
  var databodyBuffer10  = []; // bottom left pane's buffer
  var databodyBuffer11  = []; // bottom right pane's buffer
  
  // _databodyBuffers[i][j] is the databody buffer for the pane in the i'th row of panes and the j'th column of panes.  
  // _databodyBuffers[i][j][r][c] is the block in that buffer in the r'th row of blocks and the c'th column of blocks.
  this._databodyBuffers = [[databodyBuffer00, databodyBuffer01], 
                           [databodyBuffer10, databodyBuffer11]];

  // _colHeaderBuffers[i] is the colHeader buffer for pane i.
  // _colHeaderBuffers[i][j] is the j'th block in that buffer.
  this._colHeaderBuffers = [[], []];
  this._rowHeaderBuffers = [[], []];
  
  //////////////////////////////////////////////////////////////// TODO: rename this and all references to remove the 2, after the other one is gone.
  // Parameterized by edge:
  this._buffers2 = [this._colHeaderBuffers, this._rowHeaderBuffers];
  this._buffers2[AdfPivotTableUtils.DATABODY] = this._databodyBuffers;
}

/**
 * This method removes unneeded blocks from both the buffer and the DOM.  
 * 
 *   @param vertDelta optional, default is 0
 *   @param horizDelta optional, default is 0
 */

/* TODO: clean up/ reduce these comments as part of buffer-code cleanup:
 * Before this method is called, we know that the buffer is "regular" in that it satisfies 
 * all the invariants listed in the class Javadoc.  Furthermore, the blocks that this 
 * method removes from the buffer satisfy the following invariants.  (In these notes, <--> 
 * represents two-way implication, and --> and <-- represent implication in one 
 * direction but definitely not the other.)
 * - We always remove an entire row (column) of blocks, including the header block and 
 *   all databody blocks in that row (column) of blocks.  In particular, we never remove 
 *   a header block but not the corresponding databody block(s) or vice versa.  (Not to 
 *   imply that this method only removes one row/column; it can remove multiple rows and 
 *   multiple columns.)
 *   
DEBUGGING CHECKLIST: (delete later)
- Don't get caught by case where deleting rows/columns changes your indexes!
- consider case that retvals of callee are -1 or length.
- make sure works even if buffer size and/or vp size are 0, 1, etc.
- make sure works whether or not buffer and vp overlap.
- be sure to trim all 3 buffers in lockstep.
- after trimming rows/columns, ensure that the first and last row/column are not placeholder 
  rows/cols.  If they are delete them.  THEN do the following check:
- when the column (row) count reaches zero, make sure the row (column) count is made 
  zero as well.  This means clearing out the other header buffer and removing all the 
  inner arrays in the databody buffer.

TODO:
- By calling rangeOfBlocksOverlappingShiftedViewport, clearly this is redoing some of the 
  work done by fetchNearbyBlocks.  Maybe both methods could take the results of this 
  method as an input so it must only be called once.
- Consider changing method name now that it trims both buffer and DOM 
- Once the to-do in the longJump method is completed, this method will be called when 
  a long jump response is received, and it will need to keep the row (col) header block even 
  if it removes all databody and col (row) header blocks.  Does it handle 
  this correctly, or is it too aggressive in removing the row (col) header block in this case?  
  (Actually, code thruout these classes assumes one header block is empty iff the other is, and 
  I think this was an invariant.  This illustrates that the invariant is inappropriate and the 
  code that relies on it s/b revised.)
 */
AdfPivotTableUtils.prototype.trimBuffer = function(deltas) { ///////////////// DONE: METHOD, CALLER (only DRN:SS/FV), AND CALLEES.
    // At this point we know that the buffer has at least one row and one column of blocks, 
    // so that it is safe to call rangeOfBlocksOverlappingShiftedViewport.
    
    var boundses = [[], []];
    var bufferEmpty = false;
    var noOverlap = false;
    
    // TBD: can this loop be merged with the following foreach edge/foreach pane loop?  Or must this loop complete for all edges/panes before starting the first edge/pane in next loop?
    outer:
    for (var edge=0; edge<2; ++edge) {
        for (var pane=this._startPane[edge]; pane<2; ++pane) {
            var bufferLength = this._buffers2[edge][pane].length;
            
            if (bufferLength==0) {
                bufferEmpty = true;
                break outer;
            }
            
            var delta = deltas ? deltas[edge][pane] : 0;
            var bounds = this.rangeOfBlocksOverlappingShiftedViewport(edge, pane, delta);
            
            if (!this.doesBufferOverlapShiftedViewport(edge, pane, bounds)) {
                noOverlap = true;
                break outer;
            }
            
            // Make start/end row/col reflect the first/last row of overlap, even if they were 
            // beyond the end of the buffer to indicate that the viewport continued on:
            bounds.start = Math.max(bounds.start, 0);
            bounds.end   = Math.min(bounds.end, bufferLength-1);
            
            boundses[edge][pane] = bounds;
        }
    }
    
    if (bufferEmpty) {
        AdfLogger.LOGGER.severe("trimBuffer found that the buffer was already empty.  This is unexpected.");
        this.removeAllBlocks(); // make sure the other edge and db are empty too, and that the DOM is empty.  
                                // This should always be the case already, so probably replace this line with an assert.
        return; // return because there's nothing to trim
    }
    
    if (noOverlap) {
        AdfLogger.LOGGER.severe("trimBuffer is removing all blocks due to no overlap.  This is unexpected.");
        this.removeAllBlocks(); // no overlap, so clear all contents from buffer and DOM
        return; 
    }
    
    // At this point we know that, for each databody pane:
    // - that pane's databody buffer has at least one row and one column of blocks.
    // - the rectangular range of blocks in that pane that overlaps that pane's 
    //   viewport has at least one row and one column of blocks.  
    
    // Delete rows first (edge 1), so there are fewer rows to iterate over for the column deletions (edge 0).  
    for (var edge=1; edge>=0; --edge) {
        for (var pane=this._startPane[edge]; pane<2; ++pane) {
            var bounds = boundses[edge][pane];
            
            // Delete the end rows (cols) before the start rows (cols), to avoid having the index numbers suddenly point to different rows (cols).
            
            // delete rows/cols at end:
            var deleteStart = bounds.end + 1;
            var bufferLength = this._buffers2[edge][pane].length;
            var deleteCount = bufferLength - deleteStart;
            this._trimBuffer(edge, pane, deleteStart, deleteCount);
            
            // delete rows/cols at beginning:
            deleteStart = 0;
            deleteCount = bounds.start
            this._trimBuffer(edge, pane, deleteStart, deleteCount);
        }
    }
    
    // At this point, since we knew before that there was at least one row and column of overlap, 
    // we know that the buffer still has at least one row and one column.  However, the indices 
    // of the rows and columns may be different.
    
    // TBD: can any of these last 3 foreach edge/foreach pane loops be merged?
    
    // If the first/last row/col of the buffer is a placeholder row/col, delete it.
    // As before, we do the end before the beginning to avoid messing up indices.
    
    // Check end
    for (var edge=1; edge>=0; --edge) {
        for (var pane=this._startPane[edge]; pane<2; ++pane) {
            var headerBuffer = this._buffers2[edge][pane];
            var lastIndex = headerBuffer.length-1;

            if (headerBuffer[lastIndex]['_placeholder']) {
                deleteStart = lastIndex;
                deleteCount = 1;
                this._trimBuffer(edge, pane, deleteStart, deleteCount);
            }
        }
    }
    
    // Check beginning.  Now we don't have the guarantee that there's at least one row and one col.  
    
    for (var edge=1; edge>=0; --edge) {
        for (var pane=this._startPane[edge]; pane<2; ++pane) {
            var headerBuffer = this._buffers2[edge][pane];
            var firstBlock = headerBuffer[0];
            if (firstBlock && firstBlock['_placeholder']) {
                deleteStart = 0;
                deleteCount = 1;
                this._trimBuffer(edge, pane, deleteStart, deleteCount);
            }
        }
    }
    
    // Check buffer lengths yet again.  If the row (column) count is now zero, make the 
    // column (row) count zero as well.  This means clearing out all 3 buffers and the DOM.
    for (var edge=0; edge<2; ++edge) {
        for (var pane=this._startPane[edge]; pane<2; ++pane) {
            if (this._buffers2[edge][pane].length == 0) {
                AdfLogger.LOGGER.severe("trimBuffer found overlap betweeen the buffer and the viewport, but that overlap was exclusively placeholder rows/columns, so it removed all blocks.  This is unexpected.");
                this.removeAllBlocks(); 
            }
        }
    }
}

/** 
 * Helper to trimBuffer
 */
AdfPivotTableUtils.prototype._trimBuffer = function(edge, pane, deleteStart, deleteCount) {
    // trim header
    var headerBuffer = this._buffers2[edge][pane];
    var header = this._headers[edge][pane];
    this._trimBuffer2(headerBuffer, header, deleteStart, deleteCount)
    
    // trim databody
    var databodyBlockParentArray = (AdfPivotTableUtils.isMobileTouchDevice()) ? this._databodyCanvases : this._databodies;
    for (var otherPane=this._startPane[1-edge]; otherPane<2; ++otherPane) {
        if (edge) {
            var rowPane = pane;
            var colPane = otherPane;
        } else {
            rowPane = otherPane;
            colPane = pane;
        }
        
        var databodyBuffer      = this._databodyBuffers[rowPane][colPane];
        var databodyBlockParent = databodyBlockParentArray[rowPane][colPane];
        
        if (edge) {
            this._trimBuffer2(databodyBuffer, databodyBlockParent, deleteStart, deleteCount)
        } else {
            for (var i=0; i<databodyBuffer.length; ++i) {
                this._trimBuffer2(databodyBuffer[i], databodyBlockParent, deleteStart, deleteCount)
            }
        }
    }
}

/** 
 * Helper to helper to trimBuffer
 * 
 * @param buffer can be a header pane's buffer (i.e. block[]), a databody pane's buffer (i.e. block[][]), or row of a databody pane's buffer (i.e. block[]).  
 *               In all cases it's an array whose elements are blocks or block[]'s, and some of those elements will get spliced out.
 */
AdfPivotTableUtils.prototype._trimBuffer2 = function(buffer, blockParent, deleteStart, deleteCount) {
    var deletedBlocks = buffer.splice(deleteStart, deleteCount);
    this.removeChildren(blockParent, deletedBlocks);
}

/**
 * Removes all blocks from both the buffer and the DOM.  
 */
AdfPivotTableUtils.prototype.removeAllBlocks = function() { ///////////////////////// DONE
    this.initBuffer();
    this.removeExistingBlocksFromDOM();
}

/**
 * This method adds all blocks in the pprTarget to the buffer, and also adds them to the DOM.  
 */
 
/* TODO: clean up/ reduce these comments as part of buffer-code cleanup:
 * Before this method is called, we know that the buffer is "regular" in that it satisfies 
 * all the invariants listed in the class Javadoc.  Furthermore, the set of new blocks 
 * arriving from the server satisfy the following invariants.  (In these notes, <--> 
 * represents two-way implication, and --> and <-- represent implication in one 
 * direction but definitely not the other.)
 * 
 * - We receive a new row (column) header block --> we receive at least one databody 
 *   block in that row (column) of blocks.
 * - We need to insert a slot into the row (column) header buffer <--> we need to insert 
 *   a row (column) of slots into the databody buffer.  (Note that inserting a slot is 
 *   different than inserting a block into an existing slot.)  
 * - Each arriving block satisfies exactly one of the following conditions.  (Note that 
 *   this method handles each of these cases differently.)
 *
 *   1 It will be the first block in the buffer since the buffer is empty.
 *   2 Its startSlice and numSlices match an existing non-placeholder row (column) of blocks.
 *   3 Its startSlice and numSlices are such that it fits entirely within an existing 
 *     placeholder row (column) of blocks, with or without room to spare on one or both sides.  
 *   4 Its startSlice is less than the startSlice of the first existing row (column) of 
 *     blocks, and the sum of the new block's startSlice and numSlices equals the 
 *     startSlice of the first existing row (column) of blocks.  
 *   5 Its startSlice is less than the startSlice of the first existing row (column) of 
 *     blocks, and the sum of the new block's startSlice and numSlices is less than the 
 *     startSlice of the first existing row (column) of blocks.  
 *   6 Its startSlice is greater than the startSlice of the last existing row (column) 
 *     of blocks, and the sum of the startSlice and numSlices of the last existing row 
 *     (column) of blocks equals the startSlice of the new block.
 *   7 Its startSlice is greater than the startSlice of the last existing row (column) 
 *     of blocks, and the sum of the startSlice and numSlices of the last existing row 
 *     (column) of blocks is less than the startSlice of the new block.
 *
 *   Note that (2) is relevant to databody blocks only.  Further, since we're deliberately 
 *   handling header blocks first, (2) is the only case that ever applies to databody blocks.  
 *   Thus 1,3-7 apply to header blocks only, and 2 applies to databody blocks only.  
 *
 * Per the invariants listed elsewhere, when this method starts, the buffer is contiguous 
 * in that no rows or columns are skipped, so that if one row (column) of blocks 
 * has startSlice 42 and numSlices 18, then the next row (column) of blocks must 
 * have startSlice 60.  Thus, in most cases, if a row or column needs to be added, 
 * it will be added at the end, not in the middle.  As an exception, if an existing 
 * row (column) is a placeholder row (column), then it was created to fill the gap between two 
 * non-placeholder rows (columns), and it is possible that multiple rows (columns) should 
 * have been created instead.  If a header block arrives that fits in this placeholder row 
 * (column), we need to check whether it contains enough slices to completely fill 
 * that row (column).  If not, a placeholder row (column) needs to be inserted before 
 * and/or after this row (column) of blocks.  
 
TODO: 
- are the beginning of buffer cases and the end of buffer cases a special case of 
  the middle-of-buffer logic?  If so, can they be merged with that logic?
- It would probably be faster to put the col header section before the row header 
  section, so there are fewer databody rows to iterate over for each column insertion.
  Does everything still work if we do that?  
- Put asserts thruout for all the invariants.  Maybe pre- and post-conditions too.
- addRowHeaderBlocksToBuffer and addColHeaderBlocksToBuffer are 99% the same.  Try to 
  combine the two.
- identify private methods and prepend them with "_"
- if/when move to asynchronous fetches, must check whether the blocks are still 
  relevant before adding them.  
- since the caller has already looked up the pprTarget, perhaps this and similar 
  methods should take a pprTarget arg, to avoid looking it up twice.  
- add "new" or "arriving" to method name.  
 */
AdfPivotTableUtils.prototype.addBlocksToBuffer = function() { /////////////////////////////////// DONE
    // First we add all row header blocks, adding rows to the buffer as necessary, 
    // after which all rows of blocks will be present in the row header and databody 
    // buffer (per the second invariant above):
    this.addRowHeaderBlocksToBuffer();

    // Now we add all column header blocks, adding columns to the buffer as necessary, 
    // after which all columns of blocks will be present in the column header and 
    // databody buffer (per the second invariant above).  
    this.addColHeaderBlocksToBuffer();
    
    // Now we add all databody blocks.  By doing this last, we are guaranteed 
    // that a slot for each block already exists.  
    this.addDatabodyBlocksToBuffer();
    
    // Now delete all the comment nodes and any other nodes that might be in the 
    // pprTarget, to prevent a memory leak.  (Since the outer text of the ppr node 
    // gets blown away each time a new ppr response arrives, we don't really need this 
    // line, but it's safer to keep it.)
    var pprTarget = AdfDhtmlPivotTablePeer._getChildBySubId(this._container, "PPRTarget");
    this.removeChildren(pprTarget);

}

AdfPivotTableUtils.prototype.addRowHeaderBlocksToBuffer = function() { ////////////////////// DONE
    var rowHeaderBuffers = this._rowHeaderBuffers;
    var databodyBuffers  = this._databodyBuffers;
    
    var colCounts = (this._splitSlice[0] > 0) ? [this._colHeaderBuffers[0].length, this._colHeaderBuffers[1].length] : [this._colHeaderBuffers[1].length]; // lengths of the colHeader buffers currently in use
    var splitSlice = this._splitSlice[1];
    
    var pprTarget = AdfDhtmlPivotTablePeer._getChildBySubId(this._container, "PPRTarget");
    var newRowHeaderBlocks = this.getArrivingBlocks(pprTarget, this._id, AdfPivotTableUtils.ROW_EDGE);
    var newRowHeaderBlockCount = newRowHeaderBlocks.length;
    
    for (var b=0; b<newRowHeaderBlockCount; ++b) {
        var block = newRowHeaderBlocks[b];
        var blockStartSlice = this.getIntAttribute(block, "_startSlice");
        var blockNumSlices  = this.getIntAttribute(block, "_numSlices");
        
        var pane = (blockStartSlice < splitSlice) ? 0 : 1; // this works when splitSlice == -1, since we use pane 1 in that case
        
        var rowHeaderBuffer = rowHeaderBuffers[pane];
        var rowOfDatabodyBuffers = (this._splitSlice[0] > 0) ? databodyBuffers[pane] : [databodyBuffers[pane][1]]; // contains either both of, or just one of, the databody buffers in the row of databody buffers corresponding to the rowHeader buffer, depending on whether columns are split (i.e on whether both buffers are in use)

        this.addBlockToDOM(this._rowHeaders[pane], block);
        
        var headerBufferBlockCount = rowHeaderBuffer.length;
        
        // For the current block, we distinguish between the 7 cases listed in the method 
        // Javadoc.  First we handle the case where the buffer is empty:

        if (headerBufferBlockCount == 0) {
            // Case 1: adding to empty buffer
            rowHeaderBuffer[0] = block; // add block to row header buffer
            for (var i=0; i<rowOfDatabodyBuffers.length; ++i) {
                rowOfDatabodyBuffers[i][0] = new Array(colCounts[i]); // add row of blocks to databody buffer
            }
            continue;
        } 

        // Now we handle the 2 cases where the block goes at the beginning of the 
        // buffer: 

        var firstBufferBlock = rowHeaderBuffer[0];
        var firstBufferBlockStartSlice = this.getIntAttribute(firstBufferBlock, "_startSlice");
        
        if (blockStartSlice < firstBufferBlockStartSlice) {
            var nextStartSlice = blockStartSlice + blockNumSlices;
            if (nextStartSlice == firstBufferBlockStartSlice) {
                // Case 4: prepending block only to buffer
                // It's the block immediately before the first existing block
                rowHeaderBuffer.unshift(block); // prepend block to beginning of row header buffer
                for (var i=0; i<rowOfDatabodyBuffers.length; ++i) {
                    rowOfDatabodyBuffers[i].unshift(new Array(colCounts[i])); // prepend row of blocks to beginning of databody buffer
                }
            } else {
                // Case 5: prepending block and placeholder to buffer
                // There's a gap between the end of the new block and the beginning of the 
                // first existing block.
                // At the beginning of the row header buffer, prepend the block and then a
                // placeholder for the block(s) in the gap:
                var skippedSlices = firstBufferBlockStartSlice - nextStartSlice;
                var placeholder = {_startSlice: nextStartSlice, 
                                   _numSlices: skippedSlices, 
                                   _placeholder: true};
                rowHeaderBuffer.unshift(block, placeholder); 
                for (var i=0; i<rowOfDatabodyBuffers.length; ++i) {
                    rowOfDatabodyBuffers[i].unshift(new Array(colCounts[i]), new Array(colCounts[i])); // prepend 2 rows of blocks to beginning of databody buffer
                }
            }
            continue;
        } 
        
        // Now we handle the 2 cases where the block goes at the end of the buffer:
        
        var lastBufferBlock = rowHeaderBuffer[headerBufferBlockCount-1];
        var lastBufferBlockStartSlice = this.getIntAttribute(lastBufferBlock, "_startSlice");
        var lastBufferBlockNumSlices  = this.getIntAttribute(lastBufferBlock, "_numSlices");

        if (blockStartSlice > lastBufferBlockStartSlice) {
            var nextStartSlice = lastBufferBlockStartSlice + lastBufferBlockNumSlices;
            if (nextStartSlice == blockStartSlice) {
                // Case 6: appending block only to buffer
                // It's the block immediately after the last existing block
                rowHeaderBuffer.push(block); // append block to end of row header buffer
                for (var i=0; i<rowOfDatabodyBuffers.length; ++i) {
                    rowOfDatabodyBuffers[i].push(new Array(colCounts[i])); // append row of blocks to end of databody buffer
                }
            } else {
                // Case 7: appending placeholder and block to buffer
                // There's a gap between the end of the last existing block and the beginning of the 
                // new block.
                // At the end of the row header buffer, append a placeholder for the 
                // block(s) in the gap and then the block:
                var skippedSlices = blockStartSlice - nextStartSlice;
                var placeholder = {_startSlice: nextStartSlice, 
                                   _numSlices: skippedSlices, 
                                   _placeholder: true};
                rowHeaderBuffer.push(placeholder, block); 
                for (var i=0; i<rowOfDatabodyBuffers.length; ++i) {
                    rowOfDatabodyBuffers[i].push(new Array(colCounts[i]), new Array(colCounts[i])); // append 2 rows of blocks to end of databody buffer
                }
            }
            continue;
        }
        
        // Now we handle the 2 cases where the block goes in the middle of the buffer.
        // In these cases we are replacing the placeholder block with the new one, 
        // and potentially inserting a row before and/or after this one.  
        
        for (var i=0; i<headerBufferBlockCount; ++i) {
            var bufferBlock = rowHeaderBuffer[i];
            var bufferBlockStartSlice = this.getIntAttribute(bufferBlock, "_startSlice");
            var bufferBlockNumSlices  = this.getIntAttribute(bufferBlock, "_numSlices");
            var nextBlockStartSlice = bufferBlockStartSlice + bufferBlockNumSlices;
            
            if (blockStartSlice == bufferBlockStartSlice) {
                rowHeaderBuffer[i] = block; // replace placeholder block with the new block.  
                if (blockNumSlices < bufferBlockNumSlices) {
                    // Case 3b: add to middle of buffer; add PH row after.
                    // The new block has fewer slices than the placeholder row, so 
                    // we must insert a new placeholder row after the current row to 
                    // represent the remaining slices.  
                    var placeholder = {_startSlice: blockStartSlice + blockNumSlices, 
                                       _numSlices: bufferBlockNumSlices - blockNumSlices, 
                                       _placeholder: true};
                    rowHeaderBuffer.splice(i+1, 0, placeholder);   
                    for (var j=0; j<rowOfDatabodyBuffers.length; ++j) {
                        rowOfDatabodyBuffers[j].splice(i+1, 0, new Array(colCounts[j]));
                    }
                } // else Case 3a: add to middle of buffer; don't add PH row.
                break;
            }
            if (blockStartSlice < nextBlockStartSlice) {
                rowHeaderBuffer[i] = block; // replace placeholder block with the new block.  
                // We must insert a new placeholder row before the current row to 
                // represent the skipped slices.  
                var placeholder = {_startSlice: bufferBlockStartSlice, 
                                   _numSlices: blockStartSlice - bufferBlockStartSlice, 
                                   _placeholder: true};
                rowHeaderBuffer.splice(i, 0, placeholder);   
                for (var j=0; j<rowOfDatabodyBuffers.length; ++j) {
                    rowOfDatabodyBuffers[j].splice(i, 0, new Array(colCounts[j]));
                }
                
                var sliceAfterNewBlock = blockStartSlice + blockNumSlices;
                if (sliceAfterNewBlock < nextBlockStartSlice) {
                    // Case 3d: add to middle of buffer; add PH row before and after.
                    // We must insert a new placeholder row after the new block's row to 
                    // represent the remaining slices.  Since the new block's row is now i+1, 
                    // we must insert before row i+2.
                    var placeholder = {_startSlice: sliceAfterNewBlock, 
                                       _numSlices: nextBlockStartSlice - sliceAfterNewBlock, 
                                       _placeholder: true};
                    rowHeaderBuffer.splice(i+2, 0, placeholder);   
                    for (var j=0; j<rowOfDatabodyBuffers.length; ++j) {
                        rowOfDatabodyBuffers[j].splice(i+2, 0, new Array(colCounts[j]));
                    }
                } // else Case 3c: add to middle of buffer; add PH row before.
                break;
            }
        }
    }
    
    // TBD: Remove this check once all code is able to handle placeholder rows and columns.
    for (var pane=0; pane<2; ++pane) {
        for (var i=0, length=rowHeaderBuffers[pane].length; i<length; ++i) {
            if (rowHeaderBuffers[pane][i]['_placeholder']) throw new Error("Attempting to add one or more placeholder rows!");
        }
    }
}

AdfPivotTableUtils.prototype.addColHeaderBlocksToBuffer = function() { /////////////////////////////////////// DONE
    var colHeaderBuffers = this._colHeaderBuffers;
    var splitSlice = this._splitSlice[0];
    
    var pprTarget = AdfDhtmlPivotTablePeer._getChildBySubId(this._container, "PPRTarget");
    var newColHeaderBlocks = this.getArrivingBlocks(pprTarget, this._id, AdfPivotTableUtils.COLUMN_EDGE);
    var newColHeaderBlockCount = newColHeaderBlocks.length;

    for (var b=0; b<newColHeaderBlockCount; ++b) {
        var block = newColHeaderBlocks[b];
        var blockStartSlice = this.getIntAttribute(block, "_startSlice");
        var blockNumSlices  = this.getIntAttribute(block, "_numSlices");
        
        var pane = (blockStartSlice < splitSlice) ? 0 : 1; // this works when splitSlice == -1, since we use pane 1 in that case
        var colHeaderBuffer = colHeaderBuffers[pane];
        this.addBlockToDOM(this._colHeaders[pane], block);
        
        var headerBufferBlockCount = colHeaderBuffer.length; 
        
        // For the current block, we distinguish between the 7 cases listed in the method 
        // Javadoc.  First we handle the case where the buffer is empty:
        
        if (headerBufferBlockCount == 0) {
            // Case 1: adding to empty buffer
            colHeaderBuffer[0] = block; // add block to col header buffer
            this.insertColumnIntoDatabodyBuffer(pane, 0); // add col of blocks to databody buffer
            continue;
        } 

        // Now we handle the 2 cases where the block goes at the beginning of the 
        // buffer: 

        var firstBufferBlock = colHeaderBuffer[0];
        var firstBufferBlockStartSlice = this.getIntAttribute(firstBufferBlock, "_startSlice");
        
        if (blockStartSlice < firstBufferBlockStartSlice) {
            var nextStartSlice = blockStartSlice + blockNumSlices;
            if (nextStartSlice == firstBufferBlockStartSlice) {
                // Case 4: prepending block only to buffer
                // It's the block immediately before the first existing block
                colHeaderBuffer.unshift(block); // prepend block to beginning of col header buffer
                this.insertColumnIntoDatabodyBuffer(pane, 0); // prepend col of blocks to beginning of databody buffer
            } else {
                // Case 5: prepending block and placeholder to buffer
                // There's a gap between the end of the new block and the beginning of the 
                // first existing block.
                // At the beginning of the col header buffer, prepend the block and then a
                // placeholder for the block(s) in the gap:
                var skippedSlices = firstBufferBlockStartSlice - nextStartSlice;
                var placeholder = {_startSlice: nextStartSlice, 
                                   _numSlices: skippedSlices, 
                                   _placeholder: true};
                colHeaderBuffer.unshift(block, placeholder); 
                // At the beginning of the databody buffer, prepend two cols of blocks:
                this.insertColumnIntoDatabodyBuffer(pane, 0); 
                this.insertColumnIntoDatabodyBuffer(pane, 0); 
            }
            continue;
        } 
        
        // Now we handle the 2 cases where the block goes at the end of the buffer:
        
        var lastBufferBlock = colHeaderBuffer[headerBufferBlockCount-1];
        var lastBufferBlockStartSlice = this.getIntAttribute(lastBufferBlock, "_startSlice");
        var lastBufferBlockNumSlices  = this.getIntAttribute(lastBufferBlock, "_numSlices");

        if (blockStartSlice > lastBufferBlockStartSlice) {
            var nextStartSlice = lastBufferBlockStartSlice + lastBufferBlockNumSlices;
            if (nextStartSlice == blockStartSlice) {
                // Case 6: appending block only to buffer
                // It's the block immediately after the last existing block
                colHeaderBuffer.push(block); // append block to end of col header buffer
                this.insertColumnIntoDatabodyBuffer(pane, headerBufferBlockCount); // append col of blocks to end of databody buffer
            } else {
                // Case 7: appending placeholder and block to buffer
                // There's a gap between the end of the last existing block and the beginning of the 
                // new block.
                // At the end of the col header buffer, append a placeholder for the 
                // block(s) in the gap and then the block:
                var skippedSlices = blockStartSlice - nextStartSlice;
                var placeholder = {_startSlice: nextStartSlice, 
                                   _numSlices: skippedSlices, 
                                   _placeholder: true};
                colHeaderBuffer.push(placeholder, block); 
                // At the end of the databody buffer, append two cols of blocks:
                this.insertColumnIntoDatabodyBuffer(pane, headerBufferBlockCount);  
                this.insertColumnIntoDatabodyBuffer(pane, headerBufferBlockCount);  
            }
            continue;
        }
        
        // Now we handle the 2 cases where the block goes in the middle of the buffer.
        // In these cases we are replacing the placeholder block with the new one, 
        // and potentially inserting a column before and/or after this one.  
        
        for (var i=0; i<headerBufferBlockCount; ++i) {
            var bufferBlock = colHeaderBuffer[i];
            var bufferBlockStartSlice = this.getIntAttribute(bufferBlock, "_startSlice");
            var bufferBlockNumSlices  = this.getIntAttribute(bufferBlock, "_numSlices");
            var nextBlockStartSlice = bufferBlockStartSlice + bufferBlockNumSlices;
            
            if (blockStartSlice == bufferBlockStartSlice) {
                colHeaderBuffer[i] = block; // replace placeholder block with the new block.  
                if (blockNumSlices < bufferBlockNumSlices) {
                    // Case 3b: add to middle of buffer; add PH row after.
                    // The new block has fewer slices than the placeholder column, so 
                    // we must insert a new placeholder column after the current column to 
                    // represent the remaining slices.  
                    var placeholder = {_startSlice: blockStartSlice + blockNumSlices, 
                                       _numSlices: bufferBlockNumSlices - blockNumSlices, 
                                       _placeholder: true};
                    colHeaderBuffer.splice(i+1, 0, placeholder);   
                    this.insertColumnIntoDatabodyBuffer(pane, i+1);  
                } // else Case 3a: add to middle of buffer; don't add PH row.
                break;
            }
            if (blockStartSlice < nextBlockStartSlice) {
                colHeaderBuffer[i] = block; // replace placeholder block with the new block.  
                // We must insert a new placeholder column before the current column to 
                // represent the skipped slices.  
                var placeholder = {_startSlice: bufferBlockStartSlice, 
                                   _numSlices: blockStartSlice - bufferBlockStartSlice, 
                                   _placeholder: true};
                colHeaderBuffer.splice(i, 0, placeholder);   
                this.insertColumnIntoDatabodyBuffer(pane, i);  
                
                var sliceAfterNewBlock = blockStartSlice + blockNumSlices;
                if (sliceAfterNewBlock < nextBlockStartSlice) {
                    // Case 3d: add to middle of buffer; add PH row before and after.
                    // We must insert a new placeholder column after the new block's column to 
                    // represent the remaining slices.  Since the new block's column is now i+1, 
                    // we must insert before column i+2.
                    var placeholder = {_startSlice: sliceAfterNewBlock, 
                                       _numSlices: nextBlockStartSlice - sliceAfterNewBlock, 
                                       _placeholder: true};
                    colHeaderBuffer.splice(i+2, 0, placeholder);   
                    this.insertColumnIntoDatabodyBuffer(pane, i+2);  
                } // else Case 3c: add to middle of buffer; add PH row before.
                break;
            }
        }
    }
    
    // TBD: Remove this check once all code is able to handle placeholder rows and columns.
    for (var pane=0; pane<2; ++pane) {
        for (var i=0, length=colHeaderBuffers[pane].length; i<length; ++i) {
            if (colHeaderBuffers[pane][i]['_placeholder']) throw new Error("Attempting to add one or more placeholder cols!");
        }
    }
}

/**
 * This method should be called after addRowHeaderBlocksToBuffer and addColHeaderBlocksToBuffer 
 * have already run, since it assumes that all rows and columns of blocks are already in place
 * in the buffer, so that there is a slot ready and waiting for each databody block.  
 */
AdfPivotTableUtils.prototype.addDatabodyBlocksToBuffer = function() { //////////////////////////////// DONE
    var pprTarget      = AdfDhtmlPivotTablePeer._getChildBySubId(this._container, "PPRTarget");
    var newDatabodyBlocks = this.getArrivingBlocks(pprTarget, this._id, AdfPivotTableUtils.DATABODY);
    var newDatabodyBlockCount = newDatabodyBlocks.length;
    
    for (var b=0; b<newDatabodyBlockCount; ++b) {
        var block = newDatabodyBlocks[b];
        
        var blockStartRow = this.getIntAttribute(block, "_startRow");
        var blockStartCol = this.getIntAttribute(block, "_startCol");

        var rowPane = (blockStartRow < this._splitSlice[1]) ? 0 : 1; // this works when splitSlice == -1, since we use pane 1 in that case
        var colPane = (blockStartCol < this._splitSlice[0]) ? 0 : 1; // this works when splitSlice == -1, since we use pane 1 in that case
        
        var rowHeaderBuffer = this._rowHeaderBuffers[rowPane];
        var colHeaderBuffer = this._colHeaderBuffers[colPane];
        var databodyBuffer = this._databodyBuffers[rowPane][colPane];
        
        var rowCount = rowHeaderBuffer.length;
        var colCount = colHeaderBuffer.length;
    
        if (AdfPivotTableUtils.isMobileTouchDevice())
            this.addBlockToDOM(this._databodyCanvases[rowPane][colPane], block);
        else
            this.addBlockToDOM(this._databodies[rowPane][colPane], block);
        
        var r=0;
        var c=0;
        var startSlice;
        
        for (; r < rowCount; ++r) {
            startSlice = this.getIntAttribute(rowHeaderBuffer[r], "_startSlice");
            if (blockStartRow == startSlice) break;
        }
        for (; c < colCount; ++c) {
            startSlice = this.getIntAttribute(colHeaderBuffer[c], "_startSlice");
            if (blockStartCol == startSlice) break;
        }
        // TBD: We may need to replace this assert with code that correctly handles the case where a block arrives that replaces an existing block.
        AdfAssert.assert(AdfPivotTableUtils.assertsOff || !databodyBuffer[r][c], "adding databody block to non-empty buffer slot.");
        
        databodyBuffer[r][c] = block;
    }
}

/**
 * This method adds the pivot handles to the pivotTable's container, but doesn't make them visible yet. 
 */
// TODO - Currently, addBlocksToBuffer() must be called after this method, since it clears the pprTarget, which would 
// remove the pivot handles as well. Fix this by making the removal of objects from the pprTarget a separate method
// that is called after the pprTarget's contents are no longer needed. 
AdfPivotTableUtils.prototype.addPivotHandlesAndLayerLabelBlock = function(pprTarget) {    ///////////////////////////////////////////////////////////// NO CHANGES NEEDED
    // add pivot handles:
    var pivotHandles = this.getArrivingElements(pprTarget, this._id + "::ph");    
    var length = pivotHandles.length;    
    for (var i=0; i<length; i++) {
        this._corner.appendChild(pivotHandles[i]);
    }
    
    // add layer label block and set ivar:
    this._layerLabelBlock = AdfDhtmlPivotTablePeer._getChildById(pprTarget, this._id + "::LayerLabelBlock"); // null if persistent layer labels not enabled
    if (this._layerLabelBlock)
        this.addBlockToDOM(this._corner, this._layerLabelBlock);
}

AdfPivotTableUtils.prototype.removePivotHandlesAndLayerLabelBlock = function() {    ///////////////////////////////////////////////////////////// NO CHANGES NEEDED
    // TBD: maybe replace all this code with one-line call to this.removeChildren(this._corner).  Simpler, prob faster, and automatically takes care of the _unregisterNode todo below.  First, verify that the corner never has any children other than pivot handles and the layer label block, and maybe have callee skip _unregisterNode() until stamping is enabled.
    // TBD: if stamping is ever enabled for pivot handles or for persistent layer labels, this code must call this._unregisterNode(child) before removeChild(), like removeChildren() does.
    
    var pivotHandles = this.getArrivingElements(this._corner, this._id + "::ph");    
    var length = pivotHandles.length;    
    for (var i=0; i<length; i++) {
        this._corner.removeChild(pivotHandles[i]);
    }
    
    if (this._layerLabelBlock)
        this._corner.removeChild(this._layerLabelBlock);
}

/**
 * This method does not insert the blocks in any particular order, since the buffer 
 * is the only place that must be kept in order.  
 *
 * WARNING: This method does NOT set _horizPos, _canvasHorizPos, _top, and _canvasTop 
 * on the blocks, so that none of the usual invariants involving these attributes 
 * (including their existence) are satisfied.  This method MUST be immediately followed 
 * by one of the positioning methods that set these attributes on all blocks.  
 * 
 * This method positions the blocks offscreen, so that they will not be visible 
 * until a subsequent method positions them properly.  The only final positioning that
 * this methods does is to set the top of colHeader blocks, and the left of rowHeader blocks, 
 * to 0.  
 */
AdfPivotTableUtils.prototype.addBlockToDOM = function(parent, block) { /////////////////////// DONE
    parent.appendChild(block);
//    AdfAgent.AGENT.elementsAdded(block);
    block['_new'] = true;
    
    // position 1000px beyond the entire pivotTable:
    block['style']['top']  = (parent == this._colHeaders[0] || parent == this._colHeaders[1] || parent == this._corner) 
                       ? "0px"
                       : (this._pivotTablePeer._maxHeight + 1000) + "px"; 
    if(parent==this._rowHeaders[0] || parent == this._rowHeaders[1] || parent == this._corner)
    {
        if(this._rightToLeft)    
          block['style']['right'] = "0px";
        else
          block['style']['left'] = "0px";
    } else
      block['style']['left'] = (this._pivotTablePeer._maxWidth + 1000) + "px";     
    
}

AdfPivotTableUtils.prototype.markBlocksNotNew = function() { ///////////////////// DONE
  var buffers,buffer,block;
  
  var startPane = this._startPane;
  
  for (var edge=0; edge<2; ++edge) {
    buffers = this._buffers2[edge];
    for (var pane=startPane[edge]; pane<2; ++pane) {
      buffer = buffers[pane];
      for (var i=0; i<buffer.length; ++i) {
        block = buffer[i];
        block['_new'] = false;
      }
    }
  }
  
  buffers = this._databodyBuffers;
  for (var rowPane=startPane[1]; rowPane<2; ++rowPane) {
    var rowOfBuffers = buffers[rowPane];
    for (var colPane=startPane[0]; colPane<2; ++colPane) {
      buffer = rowOfBuffers[colPane];
      for (var i=0; i<buffer.length; ++i) {
        var rowOfBlocks = buffer[i];
        for (var j=0; j<rowOfBlocks.length; ++j) {
          block = rowOfBlocks[j];
          block['_new'] = false;
        }
      }
    }
  }
  
  // if persistent layer labels enabled
  if (this._layerLabelBlock)
    this._layerLabelBlock['_new'] = false;
}


/**
 * insertColumnIntoDatabodyBuffer does not attempt to preserve invariants like the following:
 * 
 * - colHeaderBufferLength == databodyBufferLength
 * - colCountIsZero <--> rowCountIsZero
 * - first/last columns are not placeholders
 * 
 * Instead, maintenance of invariants is the responsibility of the caller.
 */
AdfPivotTableUtils.prototype.insertColumnIntoDatabodyBuffer = function(colPane, colNum) { //////////////////////////////////// DONE
    for (var rowPane=this._startPane[1]; rowPane<2; ++rowPane) {
        var databodyBuffer = this._databodyBuffers[rowPane][colPane];
        var rowCount = databodyBuffer.length;
        for (var i=0; i<rowCount; ++i) {
            databodyBuffer[i].splice(colNum, 0, null);
        }
    }
}

/** 
 * This method is used to fetch blocks near the viewport.  In other words, it is 
 * intended for situations where we need to augment the existing buffer rather than 
 * throw out all buffer contents and start fresh.  Thus, it is appropriate for short 
 * scrolls and for checking whether the blocks that just arrived from the server were 
 * sufficient to fill the viewport.  
 * 
 * This method identifies which rows and columns of blocks overlap the viewport, and 
 * identifies whether additional rows/columns of blocks are needed on the four sides 
 * of the current buffer.  Then, if any blocks in that range are not present, it 
 * queues a fetch request for them.
 *
 * @param vertDelta optional, default is 0.  Ignored if rowBounds is specified.
 * @param horizDelta optional, default is 0.  Ignored if colBounds is specified.
 * @param rowBounds Optional parameter allowing this method to avoid calling 
 *                  rangeOfBlocksOverlappingShiftedViewport if it has already been called.
 * @param colBounds Optional parameter allowing this method to avoid calling 
 *                  rangeOfBlocksOverlappingShiftedViewport if it has already been called.
 * @return true iff a (delayed or immediate) request was queued
 */

/* TODO: clean up/ reduce these comments as part of buffer-code cleanup:
 *
 *- only approx is that we get only 1 col of blocks on the end in augmentation case.
 * 
 * - Preconditions:
 *   - Buffer contains at least one row of blocks and at least one col of blocks.  
 *     (Otherwise some other method should be called.)
 *   - all positioning of existing blocks (at a minimum, header blocks) must have already 
 *     been done when this method is called, as this method uses the current position of each 
 *     header block to identify overlap with viewport.  (If needed, we could reverse this 
 *     assumption by offsetting each current position by the scroll distance.)
 *   - There is some overlap between the buffer and viewport.
 * - General invariants this method depends on (non-exhaustive list): TODO: remove this section after dev't
 *   - Either 8 (header buffer slots never empty) or 10 (real header block present iff 
 *     any block in col present), and that either way, they all have same offsetLeft/Width.
 *     (Thus, we can look at only the header buffers to determine which rows/cols of 
 *     blocks overlap the viewport and thus need to be filled.
 *   - 12: placeholder row/cols never back-to-back
 *   - 11. first/last row/col never placeholder row/col.
 * - Assumptions (non-exhaustive list):
 *   - It's cheap to query each block's offsetLeft/Width, since it's expensive only 
 *     if it has changed and needs recomputing, which shouldn't be the case here.
 *     (Thus it w/b pointless to cache _offsetLeft/Width each time we set offsetLeft/Width 
 *     and then use the cached value instead.)
 * - Optimizations
 *   - could run this logic while doing the post-scroll positioning, to avoid the need for a 
 *     separate iteration over the 3 buffers, and to avoid the need to retrieve offsetLeft, etc., 
 *     an extra time.  
 *   - could cache _offsetLeft/Width expando each time we set offsetLeft/Width and then 
 *     use the cached value instead.  (This is pointless under above assumption.)
 * - Placeholder columns:
 *   - If a column of blocks is a placeholder column (i.e. if all databody blocks in the column 
 *     are missing and the header block is a placeholder block), there are 4 cases, as follows.  
 *     Invariant 14 ("Each placeholder row (column) must have a non-placeholder row (column) on both 
 *     sides of it") guarantees that this is an exhaustive list of cases, since we can just 
 *     enumerate the possible combinations of the surrounding columns' viewport status, where 
 *     the possible statuses are "to left of viewport," "overlapping viewport," and "to right of viewport."
 *     1 it is in the middle of the vp.  i.e. the two adjacent non-placeholder 
 *       columns are both in the viewport.  Fetch all slices of that placeholder block.
 *     2 it is adjacent to the vp.  i.e. one adjacent block is in the viewport, and the other 
 *       is not.  If the viewport ends in the adjacent column rather than extending into 
 *       the placeholder column, don't fetch the placeholder block.  Otherwise, if the 
 *       viewport column is to the left (right) of the placeholder column, fetch the first 
 *       (last) n slices of that placeholder column, where n is 
 *       min(defaultSlicesPerBlock, numColsInPlaceholder).  (Perhaps if n-defaultSlices is sufficiently 
 *       small, go ahead and add the remaining few slices to those blocks.)  Or, at same time we 
 *       upgrade the augmentation logic to be smart about number of cols/blocks to fetch, 
 *       could make same upgrade here.  
 *     3 it is the only col in the vp.  i.e. one adjacent block is to the left of the viewport, and the other 
 *       is to the right of the viewport.  In this case, in short term could just fetch the col 
 *       using whatever numCols it has, even though that could in principle be really big.  
 *       Later could try to be smarter.  
 *     4 it is outside and non-adjacent to the vp.  i.e. both adjacent blocks are outside the viewport 
 *       and to the same side of it.  In this case it is no different than any other col
 *       that is outside the viewport--we don't fetch it.
 *   - Note that in case #1, once Active Data is enabled, we won't necessarily know the number 
 *     of slices in the missing column; we'll only know start/end QDR.  (ActiveData will probably 
 *     affect a lot more than this...)
 * - TODO: 
 *   - invariants, etc.
 *     - Now that the logic has been broken up into multiple methods, identify 
 *       which of the precond's/inv'ts/etc. apply to which pieces, and add to their javadoc.  
 *     - under inv'ts or assumptions, record whiteboard logic about 0+ V's, etc.
 *     - after coding, revisit which precond's, inv'ts, and assumptions are really needed.  
 *   - Logic improvements:
 *     - maybe make it not dependent on the preconditions.
 *     - Right now, assumes no cushion, using VP instead of extended VP.  Fix.  
 *     - instead of fetching one row/col of blocks on the ends, estimate how many we need.
 *     - doesn't handle placeholder row/cols yet.  Fix, handling the 4 cases.  
 *     - either make sure that placeholder blocks have all necessary metadata (e.g. offsetLeft) 
 *       or test for _placeholder status first and work around it.  
 *   - Replace Errors with assertions and logging, and in general fail gracefully.
 */
AdfPivotTableUtils.prototype.fetchNearbyBlocks = function(isFillViewport, deltas, bounds) { ///////////// DONE: METHOD, CALLEES, AND CALLERS
    if (bounds == undefined) {
        bounds = [[],[]];
        
        for (var edge=1; edge>=0; --edge) {
            for (var pane=this._startPane[edge]; pane<2; ++pane) {
                var delta = (deltas) ? deltas[edge][pane] : 0;
                bounds[edge][pane] = this.rangeOfBlocksOverlappingShiftedViewport(edge, pane, delta);
            }
        }
    }
    
    // TODO: assert here that there was some overlap both horizontally and vertically.  O/w a long-scroll method s/h/b called.  
    
    // Callee will populate these arrays
    var blocks = []; // list of needed blocks to be passed into the fetch event
    var colHeaderSizeMap = {}; // map from col header block start slice to its sizing param object.  Use a map to avoid duplicates.
    var rowHeaderSizeMap = {}; // map from row header block start slice to its sizing param object.  Use a map to avoid duplicates.
    
    for (var rowPane=this._startPane[1]; rowPane<2; ++rowPane) {
        var rowBounds = bounds[1][rowPane];
        for (var colPane=this._startPane[0]; colPane<2; ++colPane) {
          var colBounds = bounds[0][colPane];
            this.findBlocksInRangeNeedingFetch(blocks, colHeaderSizeMap, rowHeaderSizeMap, colPane, colBounds.start, colBounds.end, rowPane, rowBounds.start, rowBounds.end);
        }
    }
    
    // Now, if we've accumulated at least one block to fetch, send off the request:
    
    if (blocks.length>0) {
        var databodySizing = []; // databody sizing to be passed into the fetch event
        for (var i in rowHeaderSizeMap) {
            if (rowHeaderSizeMap.hasOwnProperty(i)) // in case someone added stuff like convenience methods to Object prototype
                databodySizing.push(rowHeaderSizeMap[i]);
        }
        for (var i in colHeaderSizeMap) {
            if (colHeaderSizeMap.hasOwnProperty(i)) // in case someone added stuff like convenience methods to Object prototype
                databodySizing.push(colHeaderSizeMap[i]);
        }
        
        // for the moment always include the header heights/widths
        var rowHeaderWidths  = this.getRowHeaderWidths();
        var colHeaderHeights = this.getColumnHeaderHeights();
        
        if(isFillViewport) {
            var fetchType = AdfPivotTableDataFetchEvent.FETCH_TYPE_INITIAL_FILL_VIEWPORT;
            var delay = AdfPivotTableUtils.DELAY_FILL_VIEWPORT;
        } else {
            fetchType = AdfPivotTableDataFetchEvent.FETCH_TYPE_SHORT_SCROLL;
            delay = AdfPivotTableUtils.DELAY_SHORT_SCROLL;
        }
        
        this._pivotTablePeer.fetchBlocks(fetchType, delay, blocks, null, databodySizing, rowHeaderWidths, colHeaderHeights);
        return true;
    } else {
        return false;
    }
}
    
/** 
 * @param bounds Output of rangeOfBlocksOverlappingShiftedViewport 
 */
AdfPivotTableUtils.prototype.isShiftedViewportSatisfied = function(edge, pane, bounds) { /////////////////////// DONE: METHOD, THE 1 CALLER.  HAS NO CALLEES.
    // There are several ways that the viewport can fail to be satisfied:  
    // - There can be viewport whitespace to the right of the last header block in the buffer 
    //   (unless that block is the rightmost block of the pivot table), and likewise for the 
    //   other 3 directions.  (Depending on edge, this method either tests left and right or up 
    //   and down, not all 4 directions.)
    // - There can be whitespace due to a placeholder block in the middle of the header buffer.
    // - There can be whitespace in the databody due to missing databody blocks even though the 
    //   corresponding header blocks are present.  
    // The latter two causes are impossible under the current fetch logic.  Also, we don't have 
    // to worry about the "unless" clause, since the last block of the PT is always flush with 
    // the end of the canvas.  So we just test for whitespace beyond the last header block in 
    // both of the relevant directions:  
    return (bounds.start != -1 && 
            bounds.end != this._buffers2[edge][pane].length);
} 

/**
 * @param bounds Output of rangeOfBlocksOverlappingShiftedViewport
 */
AdfPivotTableUtils.prototype.doesBufferOverlapShiftedViewport = function(edge, pane, bounds) { //////////////// DONE: METHOD, THE 1 CALLER.  HAS NO CALLEES.
    var start = bounds.start;
    var end   = bounds.end;
    return !(start == end && (start == -1 || start == this._buffers2[edge][pane].length));
}

/**
 * @param bounds Output of rangeOfBlocksOverlappingShiftedViewport
 * @return either SCROLLTYPE_LONG_SCROLL, SCROLLTYPE_SHORT_SCROLL, or SCROLLTYPE_NO_FETCH.
 */
 
/* TODO:
- same issues as other methods in group.  
- We need to handle the case where the viewport is satisfied but the extended viewport is not.  
  Do we need a 4th return value for this?
*/
AdfPivotTableUtils.prototype.identifyScrollType = function(bounds) { /////////////////////// DONE: METHOD, THE 1 CALLER, THE 2 CALLEES
    // TBD: it would be more efficient to only long-scroll the no-overlap panes, short-scroll the some-overlap panes, and just adjust the other panes, rather than treating all panes like the worst-case pane.

    // For any pane, if the shifted viewport and the buffer don't overlap, it's a long scroll, in which case we throw out the buffer and start over
    for (var edge=0; edge<2; ++edge) {
        for (var pane=this._startPane[edge]; pane<2; ++pane) {
            if (!this.doesBufferOverlapShiftedViewport(edge, pane, bounds[edge][pane]))
                return AdfPivotTableUtils.SCROLLTYPE_LONG_SCROLL;
        }
    }
    
    // Now that we know it's not a long scroll, it's either a short-scroll (if at least one viewport isn't satisfied) or a no-fetch scroll (otherwise)
    for (var edge=0; edge<2; ++edge) {
        for (var pane=this._startPane[edge]; pane<2; ++pane) {
            if (!this.isShiftedViewportSatisfied(edge, pane, bounds[edge][pane]))
                return AdfPivotTableUtils.SCROLLTYPE_SHORT_SCROLL;
        }
    }
    
    return AdfPivotTableUtils.SCROLLTYPE_NO_FETCH;
}


/**
 * This method defines the "shifted viewport" as the region the same size as the 
 * viewport but either delta pixels to the right of the viewport (if the edge param 
 * is AdfPivotTableUtils.COLUMN_EDGE) or delta pixels below the viewport (if the edge param 
 * is AdfPivotTableUtils.ROW_EDGE), where delta is a param to the method.  The delta param 
 * may be omitted, in which case it is assumed to be 0.  In right-to-left mode, 
 * "right" becomes "left" in the preceding discussion.
 * 
 * This method returns a new object with start and end properties, which are set to the indices of 
 * the first and last block, respectively, that overlap the shifted viewport in the dimension 
 * indicated by the edge param.  For example, if AdfPivotTableUtils.COLUMN_EDGE is passed in, 
 * if there are 4 columns of blocks in the buffer (indexed 0 to 3), and if the shifted viewport 
 * overlaps only the middle 2 columns of blocks (indexed 1 to 2), then this method 
 * returns {start: 1, end: 2}.  If the shifted viewport extends before the first block, start 
 * is set to -1, and if the shifted viewport extends after the last block, end is set to the 
 * buffer length.  If the end of the shifted viewport precedes the first buffer block, with no 
 * overlap, this method returns {start: -1, end: -1}.  If the start of the shifted viewport 
 * follows the last buffer block, with no overlap, this method returns 
 * {start: bufferLength, end: bufferLength}.  
 * 
 * All preconditions of fetchNearbyBlocks apply to this method, except the overlap precondition.  
 * Precondition: There must be at least 1 header block on the specified edge.  
 * 
 * @param delta optional, default is 0
 */
// TBD: since none of the callers can add a block to the buffer, propagate the "at least 1 header block" precond recursively to all callers.
AdfPivotTableUtils.prototype.rangeOfBlocksOverlappingShiftedViewport = function(edge, pane, delta) { ///////////////////////////////////////// DONE: METHOD, CALLERS, AND CALLEES.
    if (delta == undefined) 
        delta = 0;
    
    var pos = this._posAttr[edge];
    var size = this._sizeAttr[edge];
    var headerBuffer = this._buffers2[edge][pane];
    var canvasEndSlice = this._canvasEndSlice[edge][pane];

    // The header divs sometimes have scrollbars, and are sometimes obscured by scrollbar 
    // "spacer" divs, both of which would have to be subtracted from the inner size of the 
    // header div to get the "viewport size."  Since the databody never has either of these 
    // things, its inner width can be used without modification.  Thus, we use the databody, 
    // not the header divs, as the current viewport that is offset to get the shifted viewport.  
    var viewport = this._databodiesByEdgeAndPane[edge][pane];
    
    // Throughout this method, the beginning edge of the viewport is zero in our coordinate 
    // system, so that the beginning edge of the shifted viewport is at delta pixels.
    var shiftedViewportPos = delta;
    // the pixel position of the end (right/bottom edge) of the shifted viewport. (In RTL mode, the left/bottom edge)
    var shiftedViewportEndPos = delta + viewport[size]; 
    
    var headerLength = headerBuffer.length;

    // row/column of blocks at which the shifted viewport starts and ends:
    var shiftedViewportStartBlock = -2; 
    var shiftedViewportEndBlock = headerLength;
    
    var firstBlock = headerBuffer[0];
    var firstBlockPos = firstBlock[pos];
    
    if (firstBlockPos > shiftedViewportPos) { // shifted viewport starts before first block
        shiftedViewportStartBlock = -1;
    } else { // shifted viewport starts at or after first block
        for (var i=0; i < headerLength; ++i) {
            var block = headerBuffer[i];
            
            var beginPos    = block[pos]; // the position of the left/top edge of the block (right/top in RTL mode)
            var widthHeight = block[size]; 
            var endPos      = beginPos + widthHeight; // the position of the right/bottom edge of the block (left/bottom in RTL mode)
            
            // for each block we reach, we know that the shifted viewport starts at or 
            // after that block due to our previous checks.
            if (shiftedViewportPos < endPos) { // shifted viewport begins with this block
                shiftedViewportStartBlock = i;
                break;
            } // else shifted viewport begins after this block, so iterate until we find it.
        }
    }
    
    if (shiftedViewportStartBlock == -2) { // if it didn't get set in the loop
        // the end of the buffer precedes the beginning of the shifted viewport
        return {start: headerLength, end: headerLength};
    }
    
    if (shiftedViewportStartBlock == -1) {
        if (shiftedViewportEndPos <= firstBlockPos) { 
            // the end of the shifted viewport precedes the beginning of the buffer
            return {start: -1, end: -1};
        }
        var firstPossibleEndBlock = 0;
        var block = headerBuffer[0];
        startSlice = this.getIntAttribute(block, "_startSlice");
        if (startSlice == 0) {
            shiftedViewportStartBlock=0;
        }
    } else {
        var firstPossibleEndBlock = shiftedViewportStartBlock;
    }

    // At this point we've verified that the buffer and shifted viewport overlap horizontally 
    // or vertically (whichever direction we're handling), with neither 
    // one completely below/to the right of the other.
    
    for (var i=firstPossibleEndBlock; i < headerLength; ++i) {
        var block = headerBuffer[i];
        
        var beginPos    = block[pos]; // the position of the left/top edge of the block (right/top in RTL mode)
        var widthHeight = block[size]; 
        var endPos      = beginPos + widthHeight; // the position of the right/bottom edge of the block (left/bottom in RTL mode)
        
        // for each block we reach, we know that the shifted viewport ends at or 
        // after that block due to our previous checks.
        if (shiftedViewportEndPos <= endPos) { // shifted viewport ends with this block
            shiftedViewportEndBlock = i;
            break;
        } // else shifted viewport ends after this block, so iterate until we find it.
    }
    
    // At this point, shiftedViewportEndBlock got set in the for loop iff the shifted viewport ended at one 
    // of the blocks in the buffer.  The other possibility is that the shifted viewport extends beyond 
    // the buffer, in which case shiftedViewportEndBlock still has its default value of headerLength, 
    // which is the appropriate value in this case.  The exception is if there are no more rows/cols 
    // in the pivotTable because we've reached the row/col extent, in which case we must decrement the 
    // end block by one.
    
    if (shiftedViewportEndBlock == headerLength) {
        var block = headerBuffer[headerLength-1];
        startSlice = this.getIntAttribute(block, "_startSlice");
        numSlices  = this.getIntAttribute(block, "_numSlices");
        var endSlice = startSlice + numSlices;
        if (endSlice >= canvasEndSlice) {
            --shiftedViewportEndBlock;
        }
    }
    
    return {start: shiftedViewportStartBlock, end: shiftedViewportEndBlock};
}

// calculate offset of an element, see firstSliceInShiftedViewport.  This is mainly used to minimize
// offset calls for cases like dom scrolling
AdfPivotTableUtils.prototype._calcOffset = function(elem, which)
{
    var cacheKey, offset;
    if (which == "offsetHeight")
    {
        // firefox it's in the element style
        offset = parseInt(elem.style.height);
        if (isNaN(offset))
        {
            // .. and other browsers
            if (elem.firstElementChild)
                offset = parseInt(elem.firstElementChild.style.height);

            if (isNaN(offset))
                cacheKey = "_oh";
        }
    }
    else
    {
        offset = parseInt(elem.style.width);
        if (isNaN(offset))
            cacheKey = "_ow";
    }

    // theorectically we would never get to this point, but if we do, cache the value
    if (cacheKey != null)
    {
        if (agent.hasAttribute(elem, cacheKey))
            return this.getIntAttribute(elem, cacheKey);
        else
        {
            offset = elem[which];
            agent.setAttribute(elem, cacheKey, offset);
        }
    }

    return offset;
}

/**
 * Returns a new object whose slice property is the index of the first slice in 
 * the shifted viewport for the given edge, and whose pixel property specifies which 
 * pixel of that slice is the first pixel of the viewport.  If there is no such 
 * slice because there is no block at the beginning of the shifted viewport, this 
 * method returns {slice: -1, pixel: -1}. 
 * 
 * @param edge 
 * @param pane 
 * @param delta 
 * @param bounds Optional parameter allowing this method to avoid calling 
 *               rangeOfBlocksOverlappingShiftedViewport if it has already been called.
 * @param cacheOffset Optional how offset value is calculated and cached
 */
AdfPivotTableUtils.prototype.firstSliceInShiftedViewport = function(edge, pane, delta, bounds, cacheOffset) { /////////////// DONE: THIS METHOD, CALLERS, AND CALLEES
    if (bounds==undefined) 
        bounds = this.rangeOfBlocksOverlappingShiftedViewport(edge, pane, delta);

    var buffer = this._buffers2[edge][pane];
    var startBlockIndex = bounds.start;
    
    // if there is no block at the beginning of the viewport, return -1
    if (startBlockIndex == -1 || startBlockIndex == buffer.length) 
        return {slice: -1, pixel: -1, pixelsInsideViewport: -1};

    // the first block in the viewport, thus guaranteed to contain the first slice in the viewport:
    var startBlock = buffer[startBlockIndex];
    var table = startBlock.firstChild;
    var rows = table.rows;
    
    var position = startBlock[this._posAttr[edge]] - delta; // position of block relative to shifted viewport
    var slices = (edge) ? rows : rows[0].cells;             // rows or cols, depending on edge
    var offsetHeightWidth = this.offsetSizeAttr[edge];      // height or width, depending on edge

    // Having initialized the position variable with the position of the top (left) 
    // of the block, relative to the shifted viewport, we increment it with the 
    // height (width) of each row (column) until it becomes positive, at which point 
    // we've found the first row (column) in the shifted viewport.
    for (var sliceNum=1; sliceNum<slices.length; ++sliceNum) { // start at 1 because slice 0 is a sizing slice with size 0
        var slice = slices[sliceNum];
        var sliceSize = cacheOffset ? this._calcOffset(slice, offsetHeightWidth) : slice[offsetHeightWidth];
        position += sliceSize; // now position is the position of the *next* slice
        if (position > 0) {
            var startSliceOfBlock = this.getIntAttribute(startBlock, "_startSlice");
            return {slice: startSliceOfBlock + sliceNum - 1, // -1 b/c of sizing slice
                    // (position - sliceSize) is the position of *this* slice, which is non-positive, and the 
                    // negation of that is the (non-negative) pixel of the slice at which the viewport begins:
                    pixel: sliceSize - position,
                    pixelsInsideViewport: position}; // (positive) number of pixels of the slice that are inside the viewport}; 
        }
    }
    
    AdfAssert.assert(AdfPivotTableUtils.assertsOff || false, "firstSliceInShiftedViewport " +
        "completed without finding a first slice, which shouldn't happen"); 
}

AdfPivotTableUtils.prototype.lastSliceInShiftedViewport = function(edge, pane, delta, bounds) {
    if (bounds==undefined) 
        bounds = this.rangeOfBlocksOverlappingShiftedViewport(edge, pane, delta);

    var buffer = this._buffers2[edge][pane];
    var endBlockIndex = bounds.end;
    
    // if there is no block at the end of the viewport, return -1
    if (endBlockIndex == -1 || endBlockIndex == buffer.length) 
        return {slice: -1, pixelsInsideViewport: -1, pixelsOutsideViewport: -1};

    // the last block in the viewport, thus guaranteed to contain the last slice in the viewport:
    var endBlock = buffer[endBlockIndex];
    var table = endBlock.firstChild;
    var rows = table.rows;
    
    var scroller = this._databodyScrollersByEdgeAndPane[edge][pane];
    var databodySize = (edge) ? scroller['_usableHeight'] : scroller['_usableWidth'];
    
    var position = endBlock[this._posAttr[edge]] - delta; // position of block relative to shifted viewport
    var size = endBlock[this._sizeAttr[edge]];            // size of block
    var endPosition = databodySize - (position + size);   // position of end of block relative to end of shifted viewport
    var slices = (edge) ? rows : rows[0].cells;           // rows or cols, depending on edge
    var offsetHeightWidth = this.offsetSizeAttr[edge];    // height or width, depending on edge

    // Having initialized the endPosition variable with the position of the bottom (right) 
    // of the block, relative to the shifted viewport, we decrement it with the 
    // height (width) of each row (column) until it becomes positive, at which point 
    // we've found the first row (column) in the shifted viewport.
    for (var sliceNum=slices.length-1; sliceNum>=1; --sliceNum) { // end at 1 because slice 0 is a sizing slice with size 0
        var slice = slices[sliceNum];
        var sliceSize = slice[offsetHeightWidth];
        endPosition += sliceSize; // now endPosition is the endPosition of the *previous* (i.e. closer-to-start-slice) slice
        if (endPosition > 0) {
            var startSliceOfBlock = this.getIntAttribute(endBlock, "_startSlice");
            return {slice: startSliceOfBlock + sliceNum - 1, // -1 b/c of sizing slice
                    // logic analogous to that of firstSliceInShiftedViewport, but easier to think of it this way:
                    pixelsInsideViewport: endPosition, // (positive) number of pixels of the slice that are inside the viewport
                    pixelsOutsideViewport: sliceSize - endPosition}; // (non-negative) number of pixels of the slice that are outside the viewport
        }
    }
    
    AdfAssert.assert(AdfPivotTableUtils.assertsOff || false, "lastSliceInShiftedViewport " +
        "completed without finding a last slice, which shouldn't happen"); 
}

AdfPivotTableUtils.prototype.scrollDistanceToSlice = function(edge, pane, desiredSlice)
{
    var blockIndex = this._pivotTablePeer.getBlockNumOfSlice(edge, desiredSlice, pane);
    if (blockIndex != -1)
    {
        var buffer = this._buffers2[edge][pane];
        
        var block = buffer[blockIndex];
        var table = block.firstChild;
        var rows = table.rows;
        
        var position = block[this._posAttr[edge]];              // position of block relative to shifted viewport
        var slices = (edge) ? rows : rows[0].cells;             // rows or cols, depending on edge
        var offsetHeightWidth = this.offsetSizeAttr[edge];      // height or width, depending on edge
        
        var startSliceOfBlock = this.getIntAttribute(block, "_startSlice");
        
        for (var sliceNum = 1; sliceNum < slices.length; sliceNum++) // start at 1 because slice 0 is a sizing slice with size 0
        {
            var overallSliceNum = startSliceOfBlock + sliceNum - 1;
            
            if (overallSliceNum == desiredSlice)
                return position;
            
            var slice = slices[sliceNum];
            var sliceSize = slice[offsetHeightWidth];
            position += sliceSize; // now position is the position of the *next* slice
        }
    }
}

//////////////////////////////////////////////////////////////////////////////////// DONE
AdfPivotTableUtils.prototype.firstLayerInViewport = function(edge) { 
    var scrollPosAttr = this.scrollPosAttr[1-edge]; // "scrollTop" or "scrollLeft".  //// TODO: convert to "scrollRight" in RTL
    var position = -this._headers[edge][1][scrollPosAttr]; // colHeader.scrollTop or rowHeader.scrollLeft.  Position of block relative to shifted viewport

    // Avoid getting any sizes, which is expensive, in the common case where the header doesn't have a scrollbar, and when 
    // it has a scrollbar but is scrolled to the default position, the beginning.
    if (position==0) 
        return {layer: 0, pixel: 0}; 

    var buffer = this._buffers2[edge][1];
    var startBlock = buffer[0];
    var table = startBlock.firstChild;
    var rows = table.rows;
    
    var layers = (edge == AdfPivotTableUtils.ROW_EDGE) ? rows[0].cells : rows; // rows of column edge or columns of row edge
    var offsetHeightWidth = this.offsetSizeAttr[1-edge]; // "offsetHeight" or "offsetWidth", depending on edge
    
    // Having initialized the position variable with the position of the top (left) of the block, relative to the viewport, 
    // we increment it with the height (width) of each layer until it becomes positive, at which point we've found the first 
    // layer in the viewport.
    for (var layerNum=1; layerNum<layers.length; ++layerNum) { // start at 1 because layer 0 is a sizing layer with size 0
        var layer = layers[layerNum];
        var layerSize = layer[offsetHeightWidth];
        position += layerSize; // now position is the position of the *next* layer
        if (position > 0) {
            return {layer: layerNum - 1, // -1 b/c of sizing layer
                    // (position - layerSize) is the position of *this* layer, which is non-positive, and the 
                    // negation of that is the (non-negative) pixel of the layer at which the viewport begins:
                    pixel: layerSize - position}; 
        }
    }
    
    AdfAssert.assert(AdfPivotTableUtils.assertsOff || false, "firstLayerInViewport " +
        "completed without finding a first layer, which shouldn't happen"); 
}

/**
 * The four start/end args are inclusive, forming a closed interval.  They refer to rows and 
 * columns of blocks in the databody pane specified by the 2 pane args, not rows and columns of the pivot table.  
 */
////////////////////// DONE: THIS METHOD, CALLEES, AND THE 1 CALLER
/**
 * @export
 */
AdfPivotTableUtils.prototype.findBlocksInRangeNeedingFetch = function(blocks, colHeaderSizeMap, rowHeaderSizeMap, colPane, startCol, endCol, rowPane, startRow, endRow) {
    // Hack to handle the no-data case.  TODO: Put this if-no-data logic much earlier 
    // in the path, e.g. beginning of DomReplaceNotify, and have it render a no-data 
    // message rather than an empty pivotTable.  TODO: is that done?  Can we remove this code then?
    if (this._rowExtent==0 || this._colExtent==0) {
        return false;
    }
    
    var rowHeaderBuffer = this._rowHeaderBuffers[rowPane];
    var colHeaderBuffer = this._colHeaderBuffers[colPane];
    var databodyBuffer  = this._databodyBuffers[rowPane][colPane];
        
    var rowCount = rowHeaderBuffer.length;
    var colCount = colHeaderBuffer.length;
    
    var rowHeaderSizeNeeded = {};
    var colHeaderSizeNeeded = {};
    
    // whether we need to fetch an extra row/col of blocks to the left/right/top/bottom of the current buffer.
    // Note that in RTL mode, needLeft and needRight are misnomers.  For generality they should really be 
    // called needPrevHoriz and needNextHoriz.  
    var needLeft   = false;
    var needRight  = false;
    var needTop    = false;
    var needBottom = false;
    
    if (startCol == -1) {
        startCol = 0;
        needLeft = true;
    }
    
    if (startRow == -1) {
        startRow = 0;
        needTop = true;
    }
    
    if (endCol == colCount) {
        endCol -= 1;
        needRight = true;
    }
    
    if (endRow == rowCount) {
        endRow -= 1;
        needBottom = true;
    }
    
    var colHeaderData = new Array(colCount);
    
    // Cache column header data:
    for (var c=startCol; c <= endCol; ++c) { // <= because inclusive
        var colHeaderBlock = colHeaderBuffer[c];
        var data = {};
        data.needColHeaderBlock = (colHeaderBlock['_placeholder']) ? true : false;
        data.startCol = this.getIntAttribute(colHeaderBlock, "_startSlice");
        data.numCols  = this.getIntAttribute(colHeaderBlock, "_numSlices");
        colHeaderData[c] = data;
    }
    
    // If we'll be fetching an extra col of blocks on either side of the buffer, cache data for that column:
    if (needLeft) {
        var leftData = {}; // like needLeft, in RTL mode this is a misnomer and should really be called prevData
        leftData.needColHeaderBlock = true;
        leftData.numCols  = Math.min(this._colBlockSize, colHeaderData[0].startCol - this._canvasStartSlice[0][colPane]);
        leftData.startCol = colHeaderData[0].startCol - leftData.numCols;  
    }
    
    if (needRight) {
        var rightData = {}; // like needRight, in RTL mode this is a misnomer and should really be called nextData
        rightData.needColHeaderBlock = true;
        rightData.startCol = (colHeaderData[endCol].startCol ? colHeaderData[endCol].startCol : 0) + (colHeaderData[endCol].numCols ? colHeaderData[endCol].numCols : 0);
        rightData.numCols  = Math.min(this._colBlockSize, this._canvasEndSlice[0][colPane] - rightData.startCol);
    }
    
    // main loop, in which we add to our request all needed blocks in the current buffer, 
    // including the blocks in the extra adjacent columns if relevant:
    for (var r=startRow; r <= endRow; ++r) { // <= because inclusive
        var rowHeaderBlock = rowHeaderBuffer[r];
        var needRowHeaderBlock = (rowHeaderBlock['_placeholder']) ? true : false;
        var startSlice = this.getIntAttribute(rowHeaderBlock, "_startSlice");
        var numRows  = this.getIntAttribute(rowHeaderBlock, "_numSlices");

        var databodyRow = databodyBuffer[r];
        for (var c=startCol; c <= endCol; ++c) { // <= because inclusive
            if (databodyRow[c]==null) {
                var colHeaderInfo = colHeaderData[c];
                var block = AdfPivotTableDataFetchEvent.createBlockParameters(
                                colHeaderInfo.startCol, startSlice, 
                                colHeaderInfo.needColHeaderBlock, needRowHeaderBlock, 
                                colHeaderInfo.numCols, numRows);
                blocks.push(block);
                if(!needRowHeaderBlock)
                    rowHeaderSizeNeeded[startSlice] = rowHeaderBlock;
                if(!colHeaderInfo.needColHeaderBlock)
                    colHeaderSizeNeeded[colHeaderInfo.startCol] = colHeaderBuffer[c];
            }
        }
        
        if (needLeft) {
            var block = AdfPivotTableDataFetchEvent.createBlockParameters(
                            leftData.startCol, startSlice, 
                            leftData.needColHeaderBlock, needRowHeaderBlock, 
                            leftData.numCols, numRows);
            blocks.push(block);
            if(!needRowHeaderBlock)
              rowHeaderSizeNeeded[startSlice] = rowHeaderBlock;
        }
        
        if (needRight) {
            var block = AdfPivotTableDataFetchEvent.createBlockParameters(
                            rightData.startCol, startSlice, 
                            rightData.needColHeaderBlock, needRowHeaderBlock, 
                            rightData.numCols, numRows);
            blocks.push(block);
            if(!needRowHeaderBlock)
                rowHeaderSizeNeeded[startSlice] = rowHeaderBlock;
        }
    }
    
    // now a separate loop for the extra row at the top, if relevant, in which we 
    // add to our request all blocks in that row:
    if (needTop) {
        var needRowHeaderBlock = true;
        var nextRowHeaderBlock = rowHeaderBuffer[0];
        var nextStartRow = this.getIntAttribute(nextRowHeaderBlock, "_startSlice");
        var numRows  = Math.min(this._rowBlockSize, nextStartRow - this._canvasStartSlice[1][rowPane]);
        var startSlice = nextStartRow - numRows;  

        for (var c=startCol; c <= endCol; ++c) { // <= because inclusive
            var colHeaderInfo = colHeaderData[c];
            var block = AdfPivotTableDataFetchEvent.createBlockParameters(
                            colHeaderInfo.startCol, startSlice, 
                            colHeaderInfo.needColHeaderBlock, needRowHeaderBlock, 
                            colHeaderInfo.numCols, numRows);
            blocks.push(block);
            if(!colHeaderInfo.needColHeaderBlock)
                colHeaderSizeNeeded[colHeaderInfo.startCol] = colHeaderBuffer[c];
        }
        
        // get corners if needed:
        if (needLeft) {
            var block = AdfPivotTableDataFetchEvent.createBlockParameters(
                            leftData.startCol, startSlice, 
                            false, false,
                            leftData.numCols, numRows);
            blocks.push(block);
        }
            
        if (needRight) {
            var block = AdfPivotTableDataFetchEvent.createBlockParameters(
                            rightData.startCol, startSlice, 
                            false, false,
                            rightData.numCols, numRows);
            blocks.push(block);
        }
    }
    
    // now a separate loop for the extra row at the bottom, if relevant, in which we 
    // add to our request all blocks in that row:
    if (needBottom) {
        var needRowHeaderBlock = true;
        var prevRowHeaderBlock = rowHeaderBuffer[endRow];
        var prevStartRow = this.getIntAttribute(prevRowHeaderBlock, "_startSlice");
        var prevNumRows  = this.getIntAttribute(prevRowHeaderBlock, "_numSlices");
        var startSlice = prevStartRow + prevNumRows;  
        var numRows  = Math.min(this._rowBlockSize, this._canvasEndSlice[1][rowPane] - startSlice);

        for (var c=startCol; c <= endCol; ++c) { // <= because inclusive
            var colHeaderInfo = colHeaderData[c];
            var block = AdfPivotTableDataFetchEvent.createBlockParameters(
                            colHeaderInfo.startCol, startSlice, 
                            colHeaderInfo.needColHeaderBlock, needRowHeaderBlock, 
                            colHeaderInfo.numCols, numRows);
            blocks.push(block);
            if(!colHeaderInfo.needColHeaderBlock)
                colHeaderSizeNeeded[colHeaderInfo.startCol] = colHeaderBuffer[c];
        }
        
        // get corners if needed:
        if (needLeft) {
            var block = AdfPivotTableDataFetchEvent.createBlockParameters(
                            leftData.startCol, startSlice, 
                            false, false,
                            leftData.numCols, numRows);
            blocks.push(block);
        }
            
        if (needRight) {
            var block = AdfPivotTableDataFetchEvent.createBlockParameters(
                            rightData.startCol, startSlice, 
                            false, false,
                            rightData.numCols, numRows);
            blocks.push(block);
        }
    }

    
    // -----------  figure out what sizes we need to send to the server!  -------------------------
    
    for (var startSlice in rowHeaderSizeNeeded) {
        if (rowHeaderSizeNeeded.hasOwnProperty(startSlice)) { // in case someone added stuff like convenience methods to Object prototype
            var sizeString = this.getSizeString(false, rowHeaderSizeNeeded[startSlice]);
            var databodySizes = AdfPivotTableDataFetchEvent.createSizingParameter(true, false, startSlice, sizeString);
            rowHeaderSizeMap[startSlice] = databodySizes;
        }
    }

    for (var startSlice in colHeaderSizeNeeded) {
        if (colHeaderSizeNeeded.hasOwnProperty(startSlice)) { // in case someone added stuff like convenience methods to Object prototype
            sizeString = this.getSizeString(true, colHeaderSizeNeeded[startSlice]);
            databodySizes = AdfPivotTableDataFetchEvent.createSizingParameter(false, true, startSlice, sizeString);
            colHeaderSizeMap[startSlice] = databodySizes;
        }
    }
    
    // ---------------------------  end sizing code  ------------------------------------------
}



// ========================================================================== //
//              END BUFFER METHODS, BEGIN UTILITY METHODS                     //
// ========================================================================== //


AdfPivotTableUtils.prototype.getArrivingBlocks = function(pprTarget, id, edge) {
    var substring;
    switch (edge) {
        case AdfPivotTableUtils.DATABODY:
            substring = id + "::" + "DatabodyBlock";
            break;
        case AdfPivotTableUtils.COLUMN_EDGE:
            substring = id + "::" + "ColHeaderBlock";
            break;
        case AdfPivotTableUtils.ROW_EDGE:
            substring = id + "::" + "RowHeaderBlock";
            break;
        default: 
            throw new Error("bad edge passed to AdfPivotTableUtils.getArrivingBlocks");
    }
    
    return this.getArrivingElements(pprTarget, substring);
}

AdfPivotTableUtils.prototype.getArrivingElements = function(pprTarget, idPrefix) {
    var elements = [];
    var nodes = pprTarget.childNodes;
    for (var i=0; i<nodes.length; i++) {
        var node = nodes[i];
        var nodeId = node.id;
        if( nodeId && (nodeId.indexOf(idPrefix)==0) )
            elements.push(node);
    }
    return elements;
}


// ========================================================================== //
//                              BIDI METHODS                                  //
// ========================================================================== //

/////////////// TODO: UPDATE COMMENTS FOR SPLIT PANES

/*
Summary of formulas useful for bidi. (#1-2 transcend bidi and are useful everywhere.)
See the below methods' Javadoc for explanation.  

Notes:

- Pos (with or without a "scroll" or "canvas" prefix) is a generic term for "left," 
  "right," or "top."  (Or bottom, but that is never useful.)
- For concreteness, horizontal formulas are shown, but are just as valid vertically.
  (Only (1) and (2a) are useful vertically.)
- The terms in the formulas are defined below.

Formulas:

1) canvasPos = scrollPos + pos  
2a) maxScrollPos = max(canvasWidth - viewportWidth, 0)   // viewportWidth is scrollerWidth
2b)              = max(scrollLeft + scrollRight, 0)  [derived from (2a) and (5)]
2c)              = mainScrollPos + browserOtherScrollPos  [derived by cases]
3) left + blockWidth + right = viewportWidth   // viewportWidth is usableWidth of databody or colHeader
4) canvasLeft + blockWidth + canvasRight = canvasWidth  
5) scrollLeft + viewportWidth + scrollRight = canvasWidth   // viewportWidth is scrollerWidth
6) browserScrollPos = max(scrollPos, 0)

Scrolling lag time:

Note that (1) is temporarily violated when a scroll has occurred but the pos of the 
blocks has not yet been updated accordingly.  The lag time is significant since, to 
minimize whitespace in the viewport, we don't move the existing blocks until the new 
blocks have arrived to (hopefully) fill all the space in the viewport.

ViewportWidth/Height:

ViewportWidth is always the usable width of the relevant div, where "usable width" 
is net of padding, borders, margin, and scrollbars.  In (3), viewportWidth is the 
usable width of either the databody or colHeader, depending on where the block 
lives.  In (2) and (5), viewportWidth is the usable width of the databodyScroller.  
Thus there are 3 notions of "viewportWidth", and we must be careful to use the right 
one in the right context.  (The same goes for heights.)

Note that there is a 4th definition of viewportWidth that is relevant to applications 
like sizing=="auto", but not to RTL.  In this definition, the viewportWidth is net of 
both scrollbars and scrollbar spacers, so that the portion of the div that is obscured 
by the spacer is not counted.  

Definitions and Ranges:

- left, right (generically, pos):  Position of block relative to viewport (i.e. left is block['style']['left']).  
  (See setHorizPos javadoc for details.) Completely unbounded: can be neg, 0, or pos.  
- canvasLeft, canvasRight (generically, canvasPos):  Position of block relative to canvas.  Always 
  have 0 <= canvasPos <= (canvasWidth - blockWidth) (i.e. block never hangs off either end of canvas).
- scrollLeft, scrollRight (generically, scrollPos): scrollbar position, i.e. position of viewport relative 
  to canvas.  scrollLeft is related to scroller.scrollLeft, but see below distinction between scrollPos 
  and browserScrollPos.  (For range, see mainScrollPos and otherScrollPos.)
- mainScrollPos: refers to scrollLeft in LTR mode, and scrollRight in RTL mode.  Always have 
  0 <= mainScrollPos <= maxScrollPos.  
- otherScrollPos: refers to the scrollPos other than mainScrollPos, i.e. scrollRight in LTR mode, 
  and scrollLeft in RTL mode.  Can be neg, 0, or pos.  If (canvasWidth >= scrollerWidth), have 
  0 <= otherScrollPos <= maxScrollPos.  If (canvasWidth < scrollerWidth), have 
  otherScrollPos == canvasWidth - scrollerWidth < 0.
- browserScrollPos:  Defined above.  Always >=0
- all widths (incl. all 3 definitions of viewportWidth):  Always >=0, but can be arbitrarily large.  

Expandos:

- _horizPos and _canvasHorizPos always set on a block unless it's a row header block
- _top      and _canvasTop      always set on a block unless it's a col header block 
- _top  = style['top']
- _horizPos consistent with style['left'] (= in LTR mode)
- a brand new block that hasn't yet been positioned will not yet have these expandos

ScrollPos:

- scrollPos refers to "logical scrollPos", which can be negative per above.  
- browserScrollLeft refers to the scrollLeft value reported by the browser, via 
  scroller.scrollLeft, which is always >=0.  Likewise, browserScrollRight refers 
  to the nonnegative portion of scrollRight.  See eqn (6).  
- When canvas is smaller than viewport (precisely, when canvasWidth < scrollerWidth):
  - Per (5), in this case scrollLeft + scrollRight < 0; i.e. at least one of scrollLeft 
    and scrollRight must be negative.  In fact, mainScrollPos == 0, and 
    otherScrollPos = canvasWidth - scrollerWidth < 0. 
  - When the canvas is smaller than the viewport, the beginning of the canvas is 
    always aligned with the beginning, not the end, of the viewport, so it is the 
    mainScrollPos that is 0, and the otherScrollPos that is negative.  
  - Since otherScrollPos is defined as the distance from the end of the viewport 
    to the end of the canvas, geometry dictates that it is negative iff the canvas 
    ends before (is smaller than) the viewport.
- (browserScrollLeft != scrollLeft) iff (scrollLeft < 0) iff (RTL mode && canvasWidth < viewportWidth)
- The get/setScrollPos methods get/set the mainScrollPos, which is always >=0, so
  there's no need to distinguish between whether they get/set the logical or browser 
  version.  They do both.

CanvasWidth and scrollerWidth (viewportWidth):

- (canvasWidth > scrollerWidth) indicates the non-borderline has-scrollbar case, where 
  - the canvas is strictly larger than the viewport, so that 
  - there's a scrollbar and 
  - mainScrollPos >= 0, otherScrollPos >= 0, so that 
  - neither formula 2a nor 2b nor 6 need a Math.max to keep things in bounds.
- (canvasWidth == scrollerWidth) indicates the borderline case, where 
  - the canvas and the viewport are exactly the same size, so that
  - there's no scrollbar and 
  - mainScrollPos == otherScrollPos == 0, so that
  - neither formula 2a nor 2b nor 6 need a Math.max to keep things in bounds.
- (canvasWidth < scrollerWidth) indicates the non-borderline no-scrollbar case, where 
  - the canvas is strictly smaller than the viewport, so that 
  - there's no scrollbar and 
  - mainScrollPos == 0, otherScrollPos < 0, so that 
  - formulas 2a, 2b, and 6 need a Math.max to keep things in bounds.
*/

/**
 * Sets the horizontal or vertical position of the element relative to the viewport.
 * In the vertical case, pos is the desired value for style['top'].  The horizontal 
 * case is complicated by Bidi--see setHorizPos for details.  
 * 
 * Precond: Element must be a valid element, not a placeholder.  When edge is column edge and left is false, 
 * all setHorizPos preconditions apply.
 * 
 * @param left boolean true indicates it should set left and _left, not horizPos and _horizPos
 */
AdfPivotTableUtils.prototype.setPos = function(edge, pos, element, left) {
    if (edge == AdfPivotTableUtils.COLUMN_EDGE) {
        if (left) {
            element['_left'] = pos;
            element['style']['left'] = pos + "px";
        } else {
            this.setHorizPos(pos, element);
        }
    } else {
        element['_top']      = pos;
        element['style']['top'] = pos + "px";
    }
}

/**
 * Sets the horizontal position of the element relative to the viewport, i.e., 
 * the horizontal distance from the beginning of the viewport to the beginning 
 * of the element.  Thus, in left-to-right mode, the horizPos arg is the desired 
 * value for style['left'].  In right-to-left mode, the horizPos arg is the desired 
 * value for an imaginary style['right'] property, i.e. the distance from the right 
 * edge of the viewport to the right edge of the element.  
 * 
 * In both cases, the element's style['left'] property and the _left expando are set 
 * to the value that achieves these semantics, and _horizPos is set to horizPos.  
 * Only style['left'] has the "px" on the end.  
 * 
 * By definition, elemLeft + elemWidth + elemRight = viewportWidth, even if left 
 * and/or right happen to be negative.  
 * 
 * Precond: viewport width and element width must have already been set, where 
 * viewport is (a pane of) either the column header or databody, whichever is relevant.  Element 
 * must be a valid element, not a placeholder.  Element must be a column header block 
 * or databody block, or other direct childNode of the column header or databody, 
 * since this is assumed by the code that determines the viewport width.  
 */
AdfPivotTableUtils.prototype.setHorizPos = function(horizPos, element) {  ////////////// DONE: THIS METHOD AND CALLEE.
    var left;
    if (this._rightToLeft) {
        // Per the precondition, parentNode is (a pane of) either the databody or column header, 
        // whichever is the appropriate viewport whose width we retrieve.  (They 
        // are different widths.)
        var viewport = element.parentNode;
        var viewportWidth = viewport['_width'];
        if (viewport == this._colHeaders[1])
            viewportWidth -= this.getColHeaderScrollbarWidth(); // viewport is narrower if there is a vertical scrollbar.  Relevant only to last pane of colHeader, since databody divs and first colHeader pane never have scrollbars. 
        var elemWidth = element['_width'];
        left = viewportWidth - elemWidth - horizPos; 
    } else {
        left = horizPos;
    }
    element['_horizPos']  = horizPos;
    element['_left']      = left;
    element['style']['left'] = left + "px";
}

/**
 * Sets the horizontal or vertical position of the element relative to the canvas.
 * In the vertical case, pos is the desired value for _canvasTop.  The horizontal 
 * case is complicated by Bidi--see setCanvasHorizPos for details.  
 */
AdfPivotTableUtils.prototype.setCanvasPos = function(edge, canvasPos, element) {
    if (edge == AdfPivotTableUtils.COLUMN_EDGE) {
        this.setCanvasHorizPos(canvasPos, element);
    } else {
        element['_canvasTop'] = canvasPos;
    }
}

/**
 * This method is like setHorizPos, except it sets the horizontal position of the 
 * element relative to the canvas, i.e., the horizontal distance from the beginning 
 * of the canvas to the beginning of the element.  
 */ 
AdfPivotTableUtils.prototype.setCanvasHorizPos = function(canvasHorizPos, element) {
    element['_canvasHorizPos'] = canvasHorizPos;
}

/**
 * Gets the horizontal or vertical scroll position.  In the vertical case, this is 
 * just scroller.scrollTop.  The horizontal case is complicated by Bidi--see getScrollHorizPos 
 * for details.  
 * 
 * Precond: maxScrollPos must have already been set.  
 */
AdfPivotTableUtils.prototype.getScrollPos = function(edge, pane) {
    if (edge == AdfPivotTableUtils.COLUMN_EDGE) {
        return this.getScrollHorizPos(pane);
    } else {
        return this.getScrollerByEdgeAndPane(edge, pane).scrollTop;
    }
}

/**
 * Gets scrollLeft in left-to-right mode, and the analogous value in right-to-left mode.   
 * 
 * Precond: maxScrollPos must have already been set.  
 */
AdfPivotTableUtils.prototype.getScrollHorizPos = function(pane) {
    var edge = AdfPivotTableUtils.COLUMN_EDGE;
    var scroller = this.getScrollerByEdgeAndPane(edge, pane);
    var scrollLeft = scroller.scrollLeft; 
    
    // If it's IE7 RTL, then we must flip the horizontal scrollbar coordinates.  For IE8+, scrollLeft is scrollStart in all cases as desired (it's scrollLeft in LTR, scrollRight in RTL), so don't need to flip coordinates.
    if (this._rightToLeft && (AdfAgent.AGENT.getPlatform()==AdfAgent.IE_PLATFORM) && !(AdfAgent.AGENT.getVersion() >= 8)) {
        var maxScrollPos = this._maxScrollPos[edge][pane]; 
        
        // TODO: delete the stuff between "begin test" and "end test" 
        // BEGIN TEST-----------------------------------------------------------
        // here and in the setter, we need to use (databodyWidth+1) since 
        // the scroller is sized 1px bigger than the databody.  
        if (!AdfPivotTableUtils.assertsOff) {
            var canvasSize = this.getCanvasSizeByEdgeAndPane(edge, pane);
            var databody = this._databodiesByEdgeAndPane[edge][pane];
            var viewportWidth = databody['_width']+1;
            AdfAssert.assert(AdfPivotTableUtils.assertsOff || 
                maxScrollPos - scrollLeft // calc that is used
                == 
                ((canvasSize < viewportWidth) 
                 ? 0
                 : canvasSize - viewportWidth - scrollLeft)); // equivalent calc
        }
        // END TEST-------------------------------------------------------------
             
        return maxScrollPos - scrollLeft; // from formula (2c) above
        
        // Equivalent formulas for the RTL case: (note that scroller.scrollLeft returns 
        // browserScrollLeft, not logicalScrollLeft):
        // Math.max(canvasWidth - scrollerWidth, 0) - getScroller().scrollLeft;
        // Math.max(canvasWidth - scrollerWidth - getScroller().scrollLeft, 0);
        // maxScrollPos - getScroller().scrollLeft  // calc that is used, from formula (2c)
        // (noScrollbar) ? 0 : canvasWidth - scrollerWidth - getScroller().scrollLeft
        // (noScrollbar) ? 0 : maxScrollPos - getScroller().scrollLeft
    }
    else if (this._rightToLeft){
	//fix for bug 32193742
	//If other than IE7 and if rightToLeft is True then we need to flip the horizontal scrollbar coordinates.
	return  -scrollLeft;
    }
    else {
        return scrollLeft;
    }
}

/**
 * Sets the scroll position.  
 * 
 * It is not required that there be a scrollbar.  If there is not, this method returns 
 * 0 (the only possible scrollPos) without incurring the expense of trying to set 
 * the scrollPos (since it already is set to the only possible value).  
 * 
 * As when setting scrollTop or scrollLeft directly, if newPosition < 0, then it
 * is treated as 0, and if it is > the max possible value, it is treated as the max 
 * value.  Thus, the caller need not ensure that the arg is in-bounds.  For the 
 * convenience of the caller, this method returns the in-bounds value that was actually set.  
 * 
 * Precond: maxScrollPos must have already been set.
 */
// This method is needed for two reasons.  First, programmatically setting the 
// scroll position triggers a scroll event in IE.  Thus this method sets 
// _lastScrollPos and _ignoreScrollPosChange first so that HandleDomScroll knows to 
// ignore the event.  There is still the possibility of a race condition if this 
// method is called twice in rapid succession, so that should be avoided at all costs.
// 
// If allowHandling is true, this method doesn't set those ivars, 
// so that HandleDomScroll will still run.  
// 
// Second, for horizontal scrolling only, this method is needed so that a generic 
// scroll position can be passed in that works for either Bidi mode.    
// 
// Note that unlike the other bidi setters, this method doesn't set an expando for 
// scrollHorizPos.  This is because the user can change the scroll 
// position directly, which wouldn't update these expandos unless we made the scroll 
// handler do so.  It's simpler to just use a getter for horizontal scroll position.  
// (The other bidi properties are accessed via the expando in lieu of a getter.)
// 
// TODO: revisit whether the <0 check is needed, and for that matter the overlarge check, vs. whether we should throw an exception (or at least log) in that case.  Perhaps this method should take optional boolean params bNegativeOK and bOverlargeOK, indicating whether it should bring such a number back in-bounds vs. throwing an exception.
AdfPivotTableUtils.prototype.setScrollPos = function(edge, pane, newPosition, allowHandling) {
//    if (this._pivotTablePeer.__logEnabled) {
//        this._pivotTablePeer.putInTestLog("setScrollPos(" + edge + ", " + pane + ", " + newPosition + ", " + allowHandling + ")");
//    }
    
    if (!this.scrollbarExists(edge, pane)) 
        return 0;
    
    var maxScrollPos = this._maxScrollPos[edge][pane];

    newPosition = Math.min(newPosition, maxScrollPos); // map overlarge values to the max value
    newPosition = Math.max(newPosition, 0); // map negative values to 0
    
    // If it's IE7 RTL, then we must flip the horizontal scrollbar coordinates.  For IE8+, scrollLeft is scrollStart in all cases as desired (it's scrollLeft in LTR, scrollRight in RTL), so don't need to flip coordinates.
    if (this._rightToLeft && (edge == AdfPivotTableUtils.COLUMN_EDGE) 
        && (AdfAgent.AGENT.getPlatform()==AdfAgent.IE_PLATFORM) && !(AdfAgent.AGENT.getVersion() >= 8)) 
    {
        var scrollPos = maxScrollPos - newPosition; // from formula (2c)

        // TODO: delete the stuff between "begin test" and "end test" 
        // BEGIN TEST-------------------------------------------------------
        // here and in the getter, we need to use (databodyWidth+1) since 
        // the scroller is sized 1px bigger than the databody.  
        AdfAssert.assert(AdfPivotTableUtils.assertsOff || 
                         scrollPos == Math.max(0, this.getCanvasSizeByEdgeAndPane(edge, pane) - (this._databodiesByEdgeAndPane[edge][pane]['_width']+1) - newPosition), 
                         "expected Math.max(0, canvasWidth - (databodyWidth+1) - newPosition) " +
                         "== (maxScrollPos - newPosition), but not equal");
        // END TEST---------------------------------------------------------
            
    }else if (this._rightToLeft && (edge == AdfPivotTableUtils.COLUMN_EDGE) ){
	//fix for bug 32193742
	//If other than IE7 and if rightToLeft is True then we just need to flip the horizontal scrollbar coordinates.
	scrollPos = - newPosition;
    }else { 
        scrollPos = newPosition;
    }
    
    var scroller = this.getScrollerByEdgeAndPane(edge, pane);
    var scrollPosAttr = this.scrollPosAttr[edge];
    
    if (!allowHandling) {
        // fix for bug 10279127
        // this flag is required to ensure that HandleDomScroll is not going to do anything when it is triggered by programmatically setting the scroll position
        // in IE under certain environment, we have seen cases where the dom scroll event is invoked not immediately after setting scroll position, but rather,
        // it happened only after we access the scroller.  This delay breaks the _lastScrollPos mechanism we used to prevent HandleDomScroll from 
        // doing unneccessary fetching.
        this._ignoreScrollPosChange = true;
        try
        {
            this._lastScrollPos[edge][pane] = newPosition;
            scroller[scrollPosAttr] = scrollPos;
        }
        finally 
        {
            this._ignoreScrollPosChange = false;    
        }
    } else {
        scroller[scrollPosAttr] = scrollPos;
    }

    AdfAssert.assert(AdfPivotTableUtils.domAssertsOff || scroller[scrollPosAttr] == scrollPos, 
                     "setScrollPos() tried to set an out-of-bounds scrollPos, which " +
                     "shouldn't be possible due to the preceding bounds checks.  Perhaps " +
                     "maxScrollPos is set too high."); 
    AdfAssert.assert(AdfPivotTableUtils.domAssertsOff || this.getScrollPos(edge, pane) == newPosition, 
                     "setScrollPos() tried to set the scroll pos, but the resulting scrollPos != newPosition"); 

    return newPosition;
}

/**
 * Indicates whether a databody scrollbar exists for the specified dimension (horiz for column edge, vert for row edge) and pane.  
 * E.g. scrollbarExists(ROW_EDGE, 0) refers to the vertical scrollbar of the top pane.
 * E.g. scrollbarExists(COLUMN_EDGE, 1) refers to the horizontal scrollbar of the right pane (left in RTL).
 * Note that the existence of a scrollbar is independent of whether that scrollbar is virtualized (i.e. whether 
 * all header blocks corresponding to that scrollbar are already present on the client).  All 4 combinations of "scrollbar 
 * exists?" and "scrollbar virtualized?" are possible.  (The "no scrollbar, but corresponding header blocks absent" scenario 
 * can occur after the arrival of a block that does not fill the viewport and before the adjacent blocks are fetched.)
 * 
 * Precond: maxScrollPos must have already been set.  
 */
AdfPivotTableUtils.prototype.scrollbarExists = function(edge, pane) {
    return (this._maxScrollPos[edge][pane]>0);  // equivalent to (canvasSize > viewportSize)
}

AdfPivotTableUtils.prototype.cloneArray = function(arr) 
{
    var i, copy;

    // we don't support IE8 so it's okay to use Array.isArray 
    if( Array.isArray( arr ) ) {
        copy = arr.slice( 0 );
        for( i = 0; i < copy.length; i++ ) {
            copy[ i ] = this.cloneArray( copy[ i ] );
        }
        return copy;
    } else {
        return arr;
    }
}
