Ext.define('Ext.grid.selection.SelectionExtender', {
    extend: 'Ext.dd.DragTracker',
    
    maskBox: {},
    
    constructor: function(config) {
        var me = this;
 
        // We can only initialize properly if there are elements to work with 
        if (config.view.rendered) {
            me.initSelectionExtender(config);
        } else {
            me.view = config.view;
            config.view.on({
                render: me.initSelectionExtender,
                args: [config],
                scope: me
            });
        }
    },
 
    initSelectionExtender: function(config) {
        var me = this,
            displayMode = Ext.dom.Element.DISPLAY;
 
        me.el = config.view.el;
 
        me.handle = config.view.ownerGrid.body.createChild({
            cls: Ext.baseCSSPrefix + 'ssm-extender-drag-handle',
            style: 'display:none'
        }).setVisibilityMode(displayMode);
        me.handle.on({
            contextmenu: function(e) {
                e.stopEvent();
            }
        });
        
        me.mask = me.el.createChild({
            cls: Ext.baseCSSPrefix + 'ssm-extender-mask',
            style: 'display:none'
        }).setVisibilityMode(displayMode);
 
        me.superclass.constructor.call(me, config);
 
        // Mask and andle must survive being orphaned 
        me.mask.skipGarbageCollection = me.handle.skipGarbageCollection = true;
 
        me.viewListeners = me.view.on({
            scroll: me.onViewScroll,
            scope: me,
            destroyable: true
        });
        me.gridListeners = me.view.ownerGrid.on({
            columnResize: me.alignHandle,
            scope: me,
            destroyable: true
        });
 
        me.extendX = !!(me.axes & 1);
        me.extendY = !!(me.axes & 2);
    },
 
    setHandle: function(firstPos, lastPos) {
        var me = this;
 
        if (!me.view.rendered) {
            me.view.on({
                render: me.initSelectionExtender,
                args: [firstPos, lastPos],
                scope: me
            });
            return;
        }
 
        me.firstPos = firstPos;
        me.lastPos = lastPos;
 
        // If we've done a "select all rows" and there is buffered rendering, then 
        // the cells might not be rendered, so we can't activate the replicator. 
        if (firstPos && lastPos && firstPos.getCell(true) && lastPos.getCell(true)) {
            if (me.curPos) {
                me.curPos.setPosition(lastPos);
            } else {
                me.curPos = lastPos.clone();
            }
 
            // Align centre of handle with bottom-right corner of last cell if possible. 
            me.alignHandle();
        } else {
            me.disable();
        }
    },
 
    alignHandle: function() {
        var me = this,
            firstCell = me.firstPos && me.firstPos.getCell(true),
            lastCell = me.lastPos && me.lastPos.getCell(true);
 
        // Cell corresponding to the position might not be rendered. 
        // This will be called upon scroll 
        if (firstCell && lastCell) {
            me.enable();
            me.handle.alignTo(lastCell, 'c-br');
        } else {
            me.disable();
        }
    },
 
    enable: function() {
        this.handle.show();
        this.callParent();
    },
 
    disable: function() {
        this.handle.hide();
        this.mask.hide();
        this.callParent();
    },
 
    onDrag: function(e) {
        // pointer-events-none is not supported on IE10m. 
        // So if shrinking the extension zone, the mousemove target may be the mask. 
        // We have to retarget on the cell *below* that. 
        if (e.target === this.mask.dom) {
            this.mask.hide();
            e.target = document.elementFromPoint.apply(document, e.getXY());
            this.mask.show();
        }
 
        var me = this,
            view = me.view,
            viewTop = view.el.getY(),
            viewLeft = view.el.getX(),
            overCell = e.getTarget(me.view.getCellSelector()),
            scrollTask = me.scrollTask || (me.scrollTask = Ext.util.TaskManager.newTask({
                run: me.doAutoScroll,
                scope: me,
                interval: 10
            })),
            scrollBy = me.scrollBy || (me.scrollBy = []);
 
        // Dragged outside the view; stop scrolling. 
        if (!me.el.contains(e.target)) {
            scrollBy[0] = scrollBy[1] = 0;
            return scrollTask.stop();
        }
 
        // Neart bottom of view 
        if (me.lastXY[1] > viewTop + view.el.getHeight(true) - 15) {
            if (me.extendY) {
                scrollBy[1] = 3;
                scrollTask.start();
            }
        }
        
        // Near top of view 
        else if (me.lastXY[1] < viewTop + 10) {
            if (me.extendY) {
                scrollBy[1] = -3;
                scrollTask.start();
            }
        }
 
        // Near right edge of view 
        else if (me.lastXY[0] > viewLeft + view.el.getWidth(true) - 15) {
            if (me.extendX) {
                scrollBy[0] = 3;
                scrollTask.start();
            }
        }
        
        // Near left edge of view 
        else if (me.lastXY[0] < viewLeft + 10) {
            if (me.extendX) {
                scrollBy[0] = -3;
                scrollTask.start();
            }
        }
        
        // Not near an edge, cancel autoscrolling 
        else {
            scrollBy[0] = scrollBy[1] = 0;
            scrollTask.stop();
        }
 
        if (overCell && overCell !== me.lastOverCell) {
            me.lastOverCell = overCell;
            me.syncMaskOnCell(overCell);
        }
    },
 
    doAutoScroll: function() {
        var me = this,
            view = me.view,
            scrollOverCell;
 
        // Bump the view in whatever direction was decided in the onDrag method. 
        view.scrollBy.apply(view, me.scrollBy);
 
        // Mouseover does not fire on autoscroll so see where the mouse is over on each scroll 
        scrollOverCell = document.elementFromPoint.apply(document, me.lastXY);
        if (scrollOverCell) {
            scrollOverCell = Ext.fly(scrollOverCell).up(view.cellSelector);
            if (scrollOverCell && scrollOverCell !== me.lastOverCell) {
                me.lastOverCell = scrollOverCell;
                me.syncMaskOnCell(scrollOverCell);
            }
        }
    },
 
    onEnd: function(e) {
        var me = this;
 
        if (me.scrollTask) {
            me.scrollTask.stop();
        }
        if (me.extensionDescriptor) {
            me.disable();
            me.view.getSelectionModel().extendSelection(me.extensionDescriptor);
        }
    },
    
    onViewScroll: function() {
        var me = this;
 
        // If being dragged 
        if (me.active && me.lastOverCell) {
            me.syncMaskOnCell(me.lastOverCell);
        }
 
        // We have been applied to a selection block 
        if (me.firstPos) {
 
            // Align centre of handle with bottom-right corner of last cell if possible. 
            me.alignHandle();
        }
    },
 
    syncMaskOnCell: function(overCell) {
        var me = this,
            view = me.view,
            rows = view.all,
            curPos = me.curPos,
            maskBox = me.maskBox,
            selRegion,
            firstPos = me.firstPos.clone(),
            lastPos = me.lastPos.clone(),
            extensionStart = me.firstPos.clone(),
            extensionEnd = me.lastPos.clone();
 
        // Constrain cell positions to be within rendered range. 
        firstPos.setRow(Math.min(Math.max(firstPos.rowIdx, rows.startIndex), rows.endIndex));
        lastPos.setRow( Math.min(Math.max(lastPos.rowIdx,  rows.startIndex), rows.endIndex));
 
        me.selectionRegion = selRegion = firstPos.getCell().getRegion().union(lastPos.getCell().getRegion());
 
        curPos.setPosition(view.getRecord(overCell), view.getHeaderByCell(overCell));
 
        // The above calls require the cell to be a DOM reference 
        overCell = Ext.fly(overCell);
 
        // Reset border to default, which is the overall border setting from SASS 
        // We disable the border which is contiguous to the selection. 
        me.mask.dom.style.borderTopWidth = me.mask.dom.style.borderRightWidth = me.mask.dom.style.borderBottomWidth = me.mask.dom.style.borderLeftWidth = '';
 
        // Dragged above the selection 
        if (curPos.rowIdx < me.firstPos.rowIdx && me.extendY) {
            me.extensionDescriptor = {
                type: 'rows',
                start: extensionStart.setRow(curPos.rowIdx),
                end: extensionEnd.setRow(me.firstPos.rowIdx - 1),
                rows: curPos.rowIdx - me.firstPos.rowIdx,
                mousePosition: me.lastXY
            };
            me.mask.dom.style.borderBottomWidth = '0';
            maskBox.x = selRegion.x;
            maskBox.y = overCell.getY();
            maskBox.width = selRegion.right - selRegion.left;
            maskBox.height = selRegion.top - overCell.getY();
        }
 
        // Dragged below selection 
        else if (curPos.rowIdx > me.lastPos.rowIdx && me.extendY) {
            me.extensionDescriptor = {
                type: 'rows',
                start: extensionStart.setRow(me.lastPos.rowIdx + 1),
                end: extensionEnd.setRow(curPos.rowIdx),
                rows: curPos.rowIdx - me.lastPos.rowIdx,
                mousePosition: me.lastXY
            };
            me.mask.dom.style.borderTopWidth = '0';
            maskBox.x = selRegion.x;
            maskBox.y = selRegion.bottom;
            maskBox.width = selRegion.right - selRegion.left;
            maskBox.height = overCell.getRegion().bottom - selRegion.bottom;
        }
 
        // row position is within selected row range 
        else {
 
            // Dragged to left of selection 
            if (curPos.colIdx < me.firstPos.colIdx && me.extendX) {
                me.extensionDescriptor = {
                    type: 'columns',
                    start: extensionStart.setColumn(curPos.colIdx),
                    end: extensionEnd.setColumn(me.firstPos.colIdx - 1),
                    columns: curPos.colIdx - me.firstPos.colIdx,
                    mousePosition: me.lastXY
                };
                me.mask.dom.style.borderRightWidth = '0';
                maskBox.x = overCell.getX();
                maskBox.y = selRegion.top;
                maskBox.width = selRegion.left - overCell.getX();
                maskBox.height = selRegion.bottom - selRegion.top;
            }
 
            // Dragged to right of selection 
            else if (curPos.colIdx > me.lastPos.colIdx && me.extendX) {
                me.extensionDescriptor = {
                    type: 'columns',
                    start: extensionStart.setColumn(me.lastPos.colIdx + 1),
                    end: extensionEnd.setColumn(curPos.colIdx),
                    columns: curPos.colIdx - me.lastPos.colIdx,
                    mousePosition: me.lastXY
                };
                me.mask.dom.style.borderLeftWidth = '0';
                maskBox.x = selRegion.right;
                maskBox.y = selRegion.top;
                maskBox.width = overCell.getRegion().right - selRegion.right;
                maskBox.height = selRegion.bottom - selRegion.top;
            } else {
                me.extensionDescriptor = null;
            }
        }
 
        if (view.ownerGrid.hasListeners.selectionextenderdrag) {
            view.ownerGrid.fireEvent('selectionextenderdrag', view.ownerGrid, view.getSelectionModel().getSelected(), me.extensionDescriptor);
        }
        if (me.extensionDescriptor) {
            me.mask.show();
            me.mask.setBox(maskBox);
        } else {
            me.mask.hide();
        }
    },
 
    destroy: function() {
        var me = this;
        Ext.destroy(me.gridListeners, me.viewListeners, me.mask, me.handle);
        me.callParent();
    }
});