/**
 * @fileoverview Selection Model class
 * @author NHN Ent. FE Development Lab
 */

'use strict';

var $ = require('jquery');
var _ = require('underscore');

var Model = require('../base/model');
var GridEvent = require('../event/gridEvent');

var util = require('../common/util');
var typeConst = require('../common/constMap').selectionType;

/**
 * Selection Model class
 * @module model/selection
 * @extends module:base/view
 * @param {Object} attr - Attributes
 * @param {Object} options - Options
 * @ignore
 */
var Selection = Model.extend(/** @lends module:model/selection.prototype */{
    initialize: function(attr, options) {
        var domEventBus;

        Model.prototype.initialize.apply(this, arguments);

        _.assign(this, {
            dataModel: options.dataModel,
            columnModel: options.columnModel,
            dimensionModel: options.dimensionModel,
            focusModel: options.focusModel,
            renderModel: options.renderModel,
            coordRowModel: options.coordRowModel,
            coordConverterModel: options.coordConverterModel,
            domEventBus: options.domEventBus,

            inputRange: null,
            minimumColumnRange: null,
            intervalIdForAutoScroll: null,
            scrollPixelScale: 40,
            enabled: true,
            selectionType: typeConst.CELL,
            selectionUnit: attr.selectionUnit
        });

        this.listenTo(this.dataModel, 'add remove sort reset', this.end);
        this.listenTo(this.dataModel, 'paste', this._onPasteData);

        if (this.isEnabled() && options.domEventBus) {
            domEventBus = options.domEventBus;
            this.listenTo(domEventBus, 'dragstart:header', this._onDragStartHeader);
            this.listenTo(domEventBus, 'dragmove:header', this._onDragMoveHeader);
            this.listenTo(domEventBus, 'dragmove:body', this._onDragMoveBody);
            this.listenTo(domEventBus, 'dragend:body', this._onDragEndBody);
            this.listenTo(domEventBus, 'mousedown:body', this._onMouseDownBody);
            this.listenTo(domEventBus, 'key:move key:edit', this._onKeyMoveOrEdit);
            this.listenTo(domEventBus, 'key:select', this._onKeySelect);
            this.listenTo(domEventBus, 'key:delete', this._onKeyDelete);
        }

        this.on('change:range', this._triggerSelectionEvent);
    },

    defaults: {
        /**
         * Selection range
         * ex) {row: [0, 1], column: [1, 2]}
         * @type {{row: array, column: array}}
         */
        range: null
    },

    /**
     * Event handler for 'dragstart:header' event on domEventBus
     * @param {module:event/gridEvent} gridEvent - GridEvent
     * @private
     */
    _onDragStartHeader: function(gridEvent) {
        var columnModel = this.columnModel;
        var columnNames = columnModel.getUnitColumnNamesIfMerged(gridEvent.columnName);
        var columnRange;

        if (_.some(columnNames, util.isMetaColumn)) {
            gridEvent.stop();

            return;
        }

        columnRange = this._getColumnRangeWithNames(columnNames);

        if (gridEvent.shiftKey) {
            this.update(0, columnRange[1], typeConst.COLUMN);
            this._extendColumnSelection(columnRange, gridEvent.pageX, gridEvent.pageY);
        } else {
            this.minimumColumnWidth = columnRange;
            this.selectColumn(columnRange[0]);
            this.update(0, columnRange[1]);
        }
    },

    /**
     * Event handler for 'dragmove:header' event on domEventBus
     * @param {module:event/gridEvent} gridEvent - GridEvent
     * @private
     */
    _onDragMoveHeader: function(gridEvent) {
        var columnModel = this.columnModel;
        var columnNames, columnRange;

        if (gridEvent.isOnHeaderArea && !gridEvent.columnName) {
            return;
        }

        columnNames = columnModel.getUnitColumnNamesIfMerged(gridEvent.columnName);
        if (columnNames.length) {
            columnRange = this._getColumnRangeWithNames(columnNames);
        }
        this._extendColumnSelection(columnRange, gridEvent.pageX, gridEvent.pageY);
    },

    /**
     * Event handler for key:move/key:edit fevent on domEventBus
     * @private
     */
    _onKeyMoveOrEdit: function() {
        this.end();
    },

    /**
     * Event handler for key:select event on domEventBus
     * @param {module:event/gridEvent} ev - GridEvent
     * @private
     */
    _onKeySelect: function(ev) { // eslint-disable-line complexity
        var address = this._getRecentAddress();
        var lastRowIndex = this.dataModel.length - 1;
        var lastColummnIndex = this.columnModel.getVisibleColumns().length - 1;

        switch (ev.command) {
            case 'up':
                address.row -= 1;
                break;
            case 'down':
                address.row += 1;
                break;
            case 'left':
                address.column -= 1;
                break;
            case 'right':
                address.column += 1;
                break;
            case 'pageUp':
                address.row = this.coordRowModel.getPageMovedIndex(address.row, false);
                break;
            case 'pageDown':
                address.row = this.coordRowModel.getPageMovedIndex(address.row, true);
                break;
            case 'firstColumn':
                address.column = 0;
                break;
            case 'lastColumn':
                address.column = lastColummnIndex;
                break;
            case 'firstCell':
                address.row = 0;
                address.column = 0;
                break;
            case 'lastCell':
                address.row = lastRowIndex;
                address.column = lastColummnIndex;
                break;
            case 'all':
                this.selectAll();
                address = null;
                break;
            default:
                address = null;
        }

        if (address) {
            this.update(address.row, address.column, this.getSelectionUnit());
            this._scrollTo(address.row, address.column);
        }
    },

    /**
     * Event handler for key:delete event on domEventBus
     * @private
     */
    _onKeyDelete: function() {
        var dataModel = this.dataModel;
        var focused;

        if (this.hasSelection()) {
            dataModel.delRange(this.get('range'));
        } else {
            focused = this.focusModel.which();
            dataModel.del(focused.rowKey, focused.columnName);
        }
    },

    /**
     * Return an address of recently extended cell
     * @returns {{row: number, column:number}} index
     * @private
     */
    _getRecentAddress: function() {
        var focusedIndex = this.focusModel.indexOf();
        var selectionRange = this.get('range');
        var index = _.assign({}, focusedIndex);
        var selectionRowRange, selectionColumnRange;

        if (selectionRange) {
            selectionRowRange = selectionRange.row;
            selectionColumnRange = selectionRange.column;

            index.row = selectionRowRange[0];
            index.column = selectionColumnRange[0];

            if (selectionRowRange[1] > focusedIndex.row) {
                index.row = selectionRowRange[1];
            }
            if (selectionColumnRange[1] > focusedIndex.column) {
                index.column = selectionColumnRange[1];
            }
        }

        return index;
    },

    /**
     * Returns whether the given address is valid
     * @param {{row: number, column: number}} address - address
     * @returns {boolean}
     * @private
     */
    _isValidAddress: function(address) {
        return !!this.dataModel.at(address.row) && !!this.columnModel.at(address.colummn);
    },

    /**
     * Scrolls to the position of given address
     * @param {number} rowIndex - row index
     * @param {number} columnIndex - column index
     * @private
     */
    _scrollTo: function(rowIndex, columnIndex) {
        var row = this.dataModel.at(rowIndex);
        var column = this.columnModel.at(columnIndex);
        var rowKey, columnName, selectionType, scrollPosition;

        if (!row || !column) {
            return;
        }

        rowKey = row.get('rowKey');
        columnName = column.name;
        scrollPosition = this.coordConverterModel.getScrollPosition(rowKey, columnName);
        if (scrollPosition) {
            selectionType = this.getType();
            if (selectionType === typeConst.COLUMN) {
                delete scrollPosition.scrollTop;
            } else if (selectionType === typeConst.ROW) {
                delete scrollPosition.scrollLeft;
            }
            this.renderModel.set(scrollPosition);
        }
    },

    /**
     * Examine the type of selection with given column index
     * @param {Number} columnIndex - columnIndex
     * @returns {String}
     * @private
     */
    _getTypeByColumnIndex: function(columnIndex) {
        var visibleColumns = this.columnModel.getVisibleColumns(null, true);
        var columnName = visibleColumns[columnIndex].name;

        switch (columnName) {
            case '_button':
                return null;
            case '_number':
                return typeConst.ROW;
            default:
                return typeConst.CELL;
        }
    },

    /**
     * Event handler for 'mousedown:body' event on domEventBus
     * @param {module:event/gridEvent} gridEvent - GridEvent
     * @private
     */
    _onMouseDownBody: function(gridEvent) {
        var address = this.coordConverterModel.getIndexFromMousePosition(gridEvent.pageX, gridEvent.pageY, true);
        var selType = this._getTypeByColumnIndex(address.column);
        var rowIndex, columnIndex;

        if (!selType) {
            return;
        }

        rowIndex = address.row;
        columnIndex = address.column - this.columnModel.getVisibleMetaColumnCount();

        if (gridEvent.shiftKey) {
            this.update(rowIndex, Math.max(columnIndex, 0));
        } else if (selType === typeConst.ROW) {
            this.selectRow(rowIndex);
        } else {
            this.focusModel.focusAt(rowIndex, columnIndex);
            this.end();
        }
    },

    /**
     * Event handler for 'dragmove:body' event on domEventBus
     * @param {module:event/gridEvent} gridEvent - GridEvent
     * @private
     */
    _onDragMoveBody: function(gridEvent) {
        var address = this.coordConverterModel.getIndexFromMousePosition(gridEvent.pageX, gridEvent.pageY);

        this.update(address.row, address.column, this.getSelectionUnit());
        this._setScrolling(gridEvent.pageX, gridEvent.pageY);
    },

    /**
     * Event handler for 'dragend:body' event on domEventBus
     * @private
     */
    _onDragEndBody: function() {
        this.stopAutoScroll();
    },

    /**
     * Event handler for 'paste' event on DataModel
     * @param {Object} range - Range
     */
    _onPasteData: function(range) {
        this.start(range.startIdx.row, range.startIdx.column);
        this.update(range.endIdx.row, range.endIdx.column);
    },

    /**
     * Returns the range of column index of given column names
     * @param {Array.<string>} columnNames - column names
     * @returns {Array.<number>}
     * @private
     */
    _getColumnRangeWithNames: function(columnNames) {
        var columnModel = this.columnModel;
        var columnIndexes = _.map(columnNames, function(name) {
            return columnModel.indexOfColumnName(name, true);
        });
        var minMax = util.getMinMax(columnIndexes);

        return [minMax.min, minMax.max];
    },

    /**
     * Set selection type
     * @param {string} type - Selection type (CELL, ROW, COLUMN)
     */
    setType: function(type) {
        this.selectionType = typeConst[type] || this.selectionType;
    },

    /**
     * Returns the selection type (using internal state)
     * @returns {string} type - Selection type (CELL, ROW, COLUMN)
     */
    getType: function() {
        return this.selectionType;
    },

    /**
     * Returns the selection unit (by options)
     * @returns {string} unit - Selection unit (CELL, ROW)
     */
    getSelectionUnit: function() {
        return this.get('selectionUnit').toUpperCase();
    },

    /**
     * Enables the selection.
     */
    enable: function() {
        this.enabled = true;
    },

    /**
     * Disables the selection.
     */
    disable: function() {
        this.end();
        this.enabled = false;
    },

    /**
     * Returns whether the selection is enabled.
     * @returns {boolean} True if the selection is enabled.
     */
    isEnabled: function() {
        return this.enabled;
    },

    /**
     * Starts the selection.
     * @param {Number} rowIndex - Row index
     * @param {Number} columnIndex - Column index
     * @param {string} type - Selection type
     */
    start: function(rowIndex, columnIndex, type) {
        if (!this.isEnabled()) {
            return;
        }

        this.setType(type);
        this.inputRange = {
            row: [rowIndex, rowIndex],
            column: [columnIndex, columnIndex]
        };
        this._resetRangeAttribute();
    },

    /**
     * Updates the selection range.
     * @param {number} rowIndex - Row index
     * @param {number} columnIndex - Column index
     * @param {string} [type] - Selection type
     */
    update: function(rowIndex, columnIndex, type) { // eslint-disable-line complexity
        var focusedIndex;

        if (!this.enabled ||
            (type !== typeConst.COLUMN && rowIndex < 0) ||
            (type !== typeConst.ROW && columnIndex < 0)) {
            return;
        }

        if (!this.hasSelection()) {
            focusedIndex = this.focusModel.indexOf();
            if (type === typeConst.ROW) {
                this.start(focusedIndex.row, 0, typeConst.ROW);
            } else {
                this.start(focusedIndex.row, focusedIndex.column, typeConst.CELL);
            }
        } else {
            this.setType(type);
        }

        this._updateInputRange(rowIndex, columnIndex);
        this._resetRangeAttribute();
    },

    /**
     * Update input range (end range, not start range)
     * @param {number} rowIndex - Row index
     * @param {number} columnIndex - Column index
     * @private
     */
    _updateInputRange: function(rowIndex, columnIndex) {
        var inputRange = this.inputRange;

        if (this.selectionType === typeConst.ROW) {
            columnIndex = this.columnModel.getVisibleColumns().length - 1;
        } else if (this.selectionType === typeConst.COLUMN) {
            rowIndex = this.dataModel.length - 1;
        }

        inputRange.row[1] = rowIndex;
        inputRange.column[1] = columnIndex;
    },

    /**
     * Extend column selection
     * @param {undefined|Array} columnIndexes - Column indexes
     * @param {number} pageX - Mouse position X
     * @param {number} pageY - Mouse positino Y
     * @private
     */
    _extendColumnSelection: function(columnIndexes, pageX, pageY) {
        var minimumColumnRange = this.minimumColumnRange;
        var index = this.coordConverterModel.getIndexFromMousePosition(pageX, pageY);
        var range = {
            row: [0, this.dataModel.length - 1],
            column: []
        };
        var minMax;

        if (!columnIndexes || !columnIndexes.length) {
            columnIndexes = [index.column];
        }

        this._setScrolling(pageX, pageY);
        if (minimumColumnRange) {
            minMax = util.getMinMax(columnIndexes.concat(minimumColumnRange));
        } else {
            columnIndexes.push(this.inputRange.column[0]);
            minMax = util.getMinMax(columnIndexes);
        }
        range.column.push(minMax.min, minMax.max);
        this._resetRangeAttribute(range);
    },

    /**
     * Set auto scrolling for selection
     * @param {number} pageX - Mouse position X
     * @param {number} pageY - Mouse positino Y
     * @private
     */
    _setScrolling: function(pageX, pageY) {
        var overflow = this.dimensionModel.getOverflowFromMousePosition(pageX, pageY);

        this.stopAutoScroll();
        if (this._isAutoScrollable(overflow.x, overflow.y)) {
            this.intervalIdForAutoScroll = setInterval(
                _.bind(this._adjustScroll, this, overflow.x, overflow.y)
            );
        }
    },

    /**
     * selection 영역 선택을 종료하고 selection 데이터를 초기화한다.
     */
    end: function() {
        this.inputRange = null;
        this.unset('range');
        this.minimumColumnRange = null;
    },

    /**
     * Stops the auto-scroll interval.
     */
    stopAutoScroll: function() {
        if (!_.isNull(this.intervalIdForAutoScroll)) {
            clearInterval(this.intervalIdForAutoScroll);
            this.intervalIdForAutoScroll = null;
        }
    },

    /**
     * Select all data in a row
     * @param {Number} rowIndex - Row idnex
     */
    selectRow: function(rowIndex) {
        if (this.isEnabled()) {
            this.focusModel.focusAt(rowIndex, 0);
            this.start(rowIndex, 0, typeConst.ROW);
            this.update(rowIndex, this.columnModel.getVisibleColumns().length - 1);
        }
    },

    /**
     * Select all data in a column
     * @param {Number} columnIdx - Column index
     */
    selectColumn: function(columnIdx) {
        if (this.isEnabled()) {
            this.focusModel.focusAt(0, columnIdx);
            this.start(0, columnIdx, typeConst.COLUMN);
            this.update(this.dataModel.length - 1, columnIdx);
        }
    },

    /**
     * Selects all data range.
     */
    selectAll: function() {
        if (this.isEnabled()) {
            this.start(0, 0, typeConst.CELL);
            this.update(this.dataModel.length - 1, this.columnModel.getVisibleColumns().length - 1);
        }
    },

    /**
     * Returns the row and column indexes of the starting position.
     * @returns {{row: number, column: number}} Objects containing indexes
     */
    getStartIndex: function() {
        var range = this.get('range');

        return {
            row: range.row[0],
            column: range.column[0]
        };
    },

    /**
     * Returns the row and column indexes of the ending position.
     * @returns {{row: number, column: number}} Objects containing indexes
     */
    getEndIndex: function() {
        var range = this.get('range');

        return {
            row: range.row[1],
            column: range.column[1]
        };
    },

    /**
     * selection 데이터가 존재하는지 확인한다.
     * @returns {boolean} selection 데이터 존재여부
     */
    hasSelection: function() {
        return !!this.get('range');
    },

    /**
     * Returns whether given range is a single cell. (include merged cell)
     * @param {Array.<String>} columnNames - columnNames
     * @param {Array.<Object>} rowList - rowList
     * @returns {Boolean}
     */
    _isSingleCell: function(columnNames, rowList) {
        var isSingleColumn = columnNames.length === 1;
        var isSingleRow = rowList.length === 1;
        var isSingleMergedCell = isSingleColumn && !isSingleRow &&
            (rowList[0].getRowSpanData(columnNames[0]).count === rowList.length);

        return (isSingleColumn && isSingleRow) || isSingleMergedCell;
    },

    /**
     * Returns the string value of all cells in the selection range as a single string.
     * @returns {String}
     */
    getValuesToString: function() {
        var self = this;
        var rowList = this._getRangeRowList();
        var columnNames = this._getRangeColumnNames();
        var rowValues = _.map(rowList, function(row) {
            return _.map(columnNames, function(columnName) {
                return self.getValueToString(row.get('rowKey'), columnName);
            }).join('\t');
        });

        if (this._isSingleCell(columnNames, rowList)) {
            return rowValues[0];
        }

        return rowValues.join('\n');
    },

    /**
     * Returns the string value of a single cell by copy options.
     * @param {Nubmer} rowKey - Row key
     * @param {Number} columnName - Column name
     * @returns {String}
     */
    getValueToString: function(rowKey, columnName) {
        var columnModel = this.columnModel;
        var cellData = this.renderModel.getCellData(rowKey, columnName);
        var copyOptions = columnModel.getCopyOptions(columnName);
        var column = columnModel.getColumnModel(columnName);
        var row = this.dataModel.get(rowKey);
        var value = row.getValueString(columnName);
        var text;

        if (copyOptions.customValue) {
            text = this._getCustomValue(
                copyOptions.customValue,
                value,
                row.toJSON(),
                column
            );
        } else if (copyOptions.useListItemText) {
            text = value;
        } else if (copyOptions.useFormattedValue) {
            text = cellData.formattedValue;
        } else {
            text = value;
        }

        return text;
    },

    /**
     * If the column has a 'copyOptions.customValue' function, exeucute it and returns the result.
     * @param {String} customValue - value to display
     * @param {String} value - value to display
     * @param {Object} rowAttrs - All attributes of the row
     * @param {Object} column - Column info
     * @returns {String}
     * @private
     */
    _getCustomValue: function(customValue, value, rowAttrs, column) {
        var result;

        if (_.isFunction(customValue)) {
            result = customValue(value, rowAttrs, column);
        } else {
            result = customValue;
        }

        return result;
    },

    /**
     * Returns an array of selected row list
     * @returns {Array.<module:model/data/row>}
     * @private
     */
    _getRangeRowList: function() {
        var rowRange = this.get('range').row;

        return this.dataModel.slice(rowRange[0], rowRange[1] + 1);
    },

    /**
     * Returns an array of selected column names
     * @returns {Array.<string>}
     * @private
     */
    _getRangeColumnNames: function() {
        var columnRange = this.get('range').column;
        var columns = this.columnModel.getVisibleColumns().slice(columnRange[0], columnRange[1] + 1);

        return _.pluck(columns, 'name');
    },

    /**
     * 마우스 드래그로 selection 선택 시 auto scroll 조건에 해당하는지 반환한다.
     * @param {Number} overflowX    가로축 기준 영역 overflow 값
     * @param {Number} overflowY    세로축 기준 영역 overflow 값
     * @returns {boolean} overflow 되었는지 여부
     * @private
     */
    _isAutoScrollable: function(overflowX, overflowY) {
        return !(overflowX === 0 && overflowY === 0);
    },

    /**
     * Adjusts scrollTop and scrollLeft value.
     * @param {Number} overflowX    가로축 기준 영역 overflow 값
     * @param {Number} overflowY    세로축 기준 영역 overflow 값
     * @private
     */
    _adjustScroll: function(overflowX, overflowY) {
        var renderModel = this.renderModel;

        if (overflowX) {
            this._adjustScrollLeft(overflowX, renderModel.get('scrollLeft'), renderModel.get('maxScrollLeft'));
        }
        if (overflowY) {
            this._adjustScrollTop(overflowY, renderModel.get('scrollTop'), renderModel.get('maxScrollTop'));
        }
    },

    /**
     * Adjusts scrollLeft value.
     * @param  {number} overflowX - 1 | 0 | -1
     * @param  {number} scrollLeft - Current scrollLeft value
     * @param  {number} maxScrollLeft - Max scrollLeft value
     * @private
     */
    _adjustScrollLeft: function(overflowX, scrollLeft, maxScrollLeft) {
        var adjusted = scrollLeft;
        var pixelScale = this.scrollPixelScale;

        if (overflowX < 0) {
            adjusted = Math.max(0, scrollLeft - pixelScale);
        } else if (overflowX > 0) {
            adjusted = Math.min(maxScrollLeft, scrollLeft + pixelScale);
        }
        this.renderModel.set('scrollLeft', adjusted);
    },

    /**
     * Adjusts scrollTop value.
     * @param  {number} overflowY - 1 | 0 | -1
     * @param  {number} scrollTop - Current scrollTop value
     * @param  {number} maxScrollTop - Max scrollTop value
     * @private
     */
    _adjustScrollTop: function(overflowY, scrollTop, maxScrollTop) {
        var adjusted = scrollTop;
        var pixelScale = this.scrollPixelScale;

        if (overflowY < 0) {
            adjusted = Math.max(0, scrollTop - pixelScale);
        } else if (overflowY > 0) {
            adjusted = Math.min(maxScrollTop, scrollTop + pixelScale);
        }
        this.renderModel.set('scrollTop', adjusted);
    },

    /**
     * Expands the 'this.inputRange' if rowspan data exists, and resets the 'range' attributes to the value.
     * @param {{column: number[], row: number[]}} [inputRange] - Input range. Default is this.inputRange
     * @private
     */
    _resetRangeAttribute: function(inputRange) { // eslint-disable-line complexity
        var dataModel = this.dataModel;
        var hasSpannedRange, spannedRange, tmpRowRange;

        inputRange = inputRange || this.inputRange;
        if (!inputRange) {
            this.set('range', null);

            return;
        }

        spannedRange = {
            row: _.sortBy(inputRange.row),
            column: _.sortBy(inputRange.column)
        };

        if (dataModel.isRowSpanEnable() && this.selectionType === typeConst.CELL) {
            do {
                tmpRowRange = _.assign([], spannedRange.row);
                spannedRange = this._getRowSpannedIndex(spannedRange);

                hasSpannedRange = (
                    spannedRange.row[0] !== tmpRowRange[0] ||
                    spannedRange.row[1] !== tmpRowRange[1]
                );
            } while (hasSpannedRange);
            this._setRangeMinMax(spannedRange.row, spannedRange.column);
        }

        this.set('range', spannedRange);
    },

    /**
     * Trigger 'selection' event
     * @private
     */
    _triggerSelectionEvent: function() {
        var range = this.get('range');
        var dataModel = this.dataModel;
        var columnModel = this.columnModel;
        var rowRange, columnRange, gridEvent;
        var startRow, endRow, startColumn, endColumn;

        if (!range) {
            return;
        }

        rowRange = range.row;
        columnRange = range.column;

        startRow = dataModel.getRowDataAt(rowRange[0]);
        startColumn = columnModel.at(columnRange[0]);
        endRow = dataModel.getRowDataAt(rowRange[1]);
        endColumn = columnModel.at(columnRange[1]);

        if (!startRow || !endRow || !startColumn || !endColumn) {
            return;
        }

        gridEvent = new GridEvent(null, {
            range: {
                start: [startRow.rowKey, startColumn.name],
                end: [endRow.rowKey, endColumn.name]
            }
        });

        /**
         * Occurs when selecting cells
         * @event Grid#selection
         * @type {module:event/gridEvent}
         * @property {Object} range - Range of selection
         * @property {Array} range.start - Info of start cell (ex: [rowKey, columName])
         * @property {Array} range.end - Info of end cell (ex: [rowKey, columnName])
         * @property {Grid} instance - Current grid instance
         */
        this.trigger('selection', gridEvent);
    },

    /**
     * Set min, max value of range(row, column)
     * @param {Array} rowRange - Row range
     * @param {Array} columnRange - Column range
     * @private
     */
    _setRangeMinMax: function(rowRange, columnRange) {
        if (rowRange) {
            rowRange[0] = Math.max(0, rowRange[0]);
            rowRange[1] = Math.min(this.dataModel.length - 1, rowRange[1]);
        }

        if (columnRange) {
            columnRange[0] = Math.max(0, columnRange[0]);
            columnRange[1] = Math.min(this.columnModel.getVisibleColumns().length - 1, columnRange[1]);
        }
    },

    /**
     * row start index 기준으로 rowspan 을 확인하며 startRangeList 업데이트 하는 함수
     * @param {object} param - parameters
     * @private
     */
    _concatRowSpanIndexFromStart: function(param) {
        var startIndex = param.startIndex;
        var endIndex = param.endIndex;
        var columnName = param.columnName;
        var rowSpanData = param.startRowSpanDataMap && param.startRowSpanDataMap[columnName];
        var startIndexList = param.startIndexList;
        var endIndexList = param.endIndexList;
        var spannedIndex;

        if (!rowSpanData) {
            return;
        }

        if (!rowSpanData.isMainRow) {
            spannedIndex = startIndex + rowSpanData.count;
            startIndexList.push(spannedIndex);
        } else {
            spannedIndex = startIndex + rowSpanData.count - 1;
            if (spannedIndex > endIndex) {
                endIndexList.push(spannedIndex);
            }
        }
    },

    /**
     * row end index 기준으로 rowspan 을 확인하며 endRangeList 를 업데이트 하는 함수
     * @param {object} param - parameters
     * @private
     */
    _concatRowSpanIndexFromEnd: function(param) {
        var endIndex = param.endIndex;
        var columnName = param.columnName;
        var rowSpanData = param.endRowSpanDataMap && param.endRowSpanDataMap[columnName];
        var endIndexList = param.endIndexList;
        var dataModel = param.dataModel;
        var spannedIndex, tmpRowSpanData;

        if (!rowSpanData) {
            return;
        }

        if (!rowSpanData.isMainRow) {
            spannedIndex = endIndex + rowSpanData.count;
            tmpRowSpanData = dataModel.at(spannedIndex).getRowSpanData(columnName);
            spannedIndex += tmpRowSpanData.count - 1;
            if (spannedIndex > endIndex) {
                endIndexList.push(spannedIndex);
            }
        } else {
            spannedIndex = endIndex + rowSpanData.count - 1;
            endIndexList.push(spannedIndex);
        }
    },

    /**
     * rowSpan 된 Index range 를 반환한다.
     * @param {{row: Array, column: Array}} spannedRange 인덱스 정보
     * @returns {{row: Array, column: Array}} New Range
     * @private
     */
    _getRowSpannedIndex: function(spannedRange) {
        var columns = this.columnModel.getVisibleColumns()
            .slice(spannedRange.column[0], spannedRange.column[1] + 1);
        var dataModel = this.dataModel;
        var startIndexList = [spannedRange.row[0]];
        var endIndexList = [spannedRange.row[1]];
        var startRow = dataModel.at(spannedRange.row[0]);
        var endRow = dataModel.at(spannedRange.row[1]);
        var newSpannedRange = $.extend({}, spannedRange);
        var startRowSpanDataMap, endRowSpanDataMap, param;

        if (!startRow || !endRow) {
            return newSpannedRange;
        }

        startRowSpanDataMap = dataModel.at(spannedRange.row[0]).getRowSpanData();
        endRowSpanDataMap = dataModel.at(spannedRange.row[1]).getRowSpanData();

        // 모든 열을 순회하며 각 열마다 설정된 rowSpan 정보에 따라 인덱스를 업데이트 한다.
        _.each(columns, function(columnModel) {
            param = {
                columnName: columnModel.name,
                startIndex: spannedRange.row[0],
                endIndex: spannedRange.row[1],
                endRowSpanDataMap: endRowSpanDataMap,
                startRowSpanDataMap: startRowSpanDataMap,
                startIndexList: startIndexList,
                endIndexList: endIndexList,
                dataModel: dataModel
            };
            this._concatRowSpanIndexFromStart(param);
            this._concatRowSpanIndexFromEnd(param);
        }, this);

        newSpannedRange.row = [Math.min.apply(null, startIndexList), Math.max.apply(null, endIndexList)];

        return newSpannedRange;
    }
});

module.exports = Selection;