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),
            handle = me.handle,
            shouldDisplay;

        // Cell corresponding to the position might not be rendered.
        // This will be called upon scroll
        if (firstCell && lastCell) {
            me.enable();
            handle.alignTo(lastCell, 'c-br');

            shouldDisplay = me.isHandleWithinView(Ext.fly(lastCell).up('.x-grid-view'));
            handle.setVisible(shouldDisplay);
        }
        else {
            me.disable();
        }
    },

    isHandleWithinView: function(view) {
        var me = this,
            viewBox = view.getBox(),
            handleBox = me.handle.getBox(),
            withinX;

        withinX = viewBox.left <= handleBox.left &&
                  viewBox.right >= (handleBox.right - handleBox.width);

        return withinX;
    },

    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();
        }

        // eslint-disable-next-line vars-on-top
        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(),
            preventReduce = !me.allowReduceSelection;

        // 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;
        }
        // reducing Y selection dragged from the bottom
        else if (!preventReduce && curPos.rowIdx < me.lastPos.rowIdx && me.extendY &&
                 curPos.colIdx === me.lastPos.colIdx) {
            me.extensionDescriptor = {
                type: 'rows',
                start: extensionStart.setRow(me.firstPos.rowIdx),
                end: extensionEnd.setRow(curPos.rowIdx),
                rows: -1,
                mousePosition: me.lastXY,
                reduce: true
            };

            me.mask.dom.style.borderTopWidth = '0';
            maskBox.x = selRegion.x;
            maskBox.y = selRegion.top;
            maskBox.width = selRegion.right - selRegion.left;
            maskBox.height = overCell.getRegion().bottom - selRegion.top;
        }

        // 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.getRegion().left;
                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;
            }
            // reducing X selection dragged from the right
            else if (!preventReduce && curPos.colIdx < me.lastPos.colIdx && me.extendX) {
                me.extensionDescriptor = {
                    type: 'columns',
                    start: extensionStart.setColumn(me.firstPos.colIdx),
                    end: extensionEnd.setColumn(curPos.colIdx),
                    columns: -1,
                    mousePosition: me.lastXY,
                    reduce: true
                };
                me.mask.dom.style.borderLeftWidth = '0';
                maskBox.x = selRegion.left;
                maskBox.y = selRegion.top;
                maskBox.width = overCell.getRegion().right - selRegion.left;
                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();
    }
});