/**
 * @fileoverview Grid 의 Data Source 에 해당하는 Collection 정의
 * @author NHN Ent. FE Development Team
 */

'use strict';

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

var Collection = require('../../base/collection');
var Row = require('./row');
var GridEvent = require('../../event/gridEvent');

/**
 * Raw 데이터 RowList 콜렉션. (DataSource)
 * Grid.setData 를 사용하여 콜렉션을 설정한다.
 * @module model/data/rowList
 * @extends module:base/collection
 * @param {Array} models - 콜랙션에 추가할 model 리스트
 * @param {Object} options - 생성자의 option 객체
 * @ignore
 */
var RowList = Collection.extend(/** @lends module:model/data/rowList.prototype */{
    initialize: function(models, options) {
        Collection.prototype.initialize.apply(this, arguments);
        _.assign(this, {
            columnModel: options.columnModel,
            domState: options.domState,
            gridId: options.gridId,
            lastRowKey: -1,
            originalRows: [],
            originalRowMap: {},
            startIndex: options.startIndex || 1,
            sortOptions: {
                columnName: 'rowKey',
                ascending: true,
                useClient: (_.isBoolean(options.useClientSort) ? options.useClientSort : true)
            },

            /**
             * Whether the all rows are disabled.
             * This state is not related to individual state of each rows.
             * @type {Boolean}
             */
            disabled: false,
            publicObject: options.publicObject
        });

        if (!this.sortOptions.useClient) {
            this.comparator = null;
        }

        if (options.domEventBus) {
            this.listenTo(options.domEventBus, 'click:headerCheck', this._onClickHeaderCheck);
            this.listenTo(options.domEventBus, 'click:headerSort', this._onClickHeaderSort);
        }
    },

    model: Row,

    /**
     * Backbone 이 collection 생성 시 내부적으로 parse 를 호출하여 데이터를 포멧에 맞게 파싱한다.
     * @param {Array} data  원본 데이터
     * @returns {Array}  파싱하여 가공된 데이터
     */
    parse: function(data) {
        data = (data && data.contents) || data;

        return this._formatData(data);
    },

    /**
     * Event handler for 'click:headerCheck' event on domEventBus
     * @param {module:event/gridEvent} ev - GridEvent
     * @private
     */
    _onClickHeaderCheck: function(ev) {
        if (ev.checked) {
            this.checkAll();
        } else {
            this.uncheckAll();
        }
    },

    /**
     * Event handler for 'click:headerSort' event on domEventBus
     * @param {module:event/gridEvent} ev - GridEvent
     * @private
     */
    _onClickHeaderSort: function(ev) {
        this.sortByField(ev.columnName);
    },

    /**
     * 데이터의 _extraData 를 분석하여, Model 에서 사용할 수 있도록 가공한다.
     * _extraData 필드에 rowSpanData 를 추가한다.
     * @param {Array} data  가공할 데이터
     * @returns {Array} 가공된 데이터
     * @private
     */
    _formatData: function(data) {
        var rowList = _.filter(data, _.isObject);

        _.each(rowList, function(row, i) {
            rowList[i] = this._baseFormat(rowList[i]);
            if (this.isRowSpanEnable()) {
                this._setExtraRowSpanData(rowList, i);
            }
        }, this);

        return rowList;
    },

    /**
     * row 를 기본 포멧으로 wrapping 한다.
     * 추가적으로 rowKey 를 할당하고, rowState 에 따라 checkbox 의 값을 할당한다.
     *
     * @param {object} row  대상 row 데이터
     * @param {number} index    해당 row 의 인덱스 정보. rowKey 를 자동 생성할 경우 사용된다.
     * @returns {object} 가공된 row 데이터
     * @private
     */
    _baseFormat: function(row) {
        var defaultExtraData = {
                rowSpan: null,
                rowSpanData: null,
                rowState: null
            },
            keyColumnName = this.columnModel.get('keyColumnName'),
            rowKey = (keyColumnName === null) ? this._createRowKey() : row[keyColumnName];

        row._extraData = $.extend(defaultExtraData, row._extraData);
        row._button = row._extraData.rowState === 'CHECKED';
        row.rowKey = rowKey;

        return row;
    },

    /**
     * 새로운 rowKey를 생성해서 반환한다.
     * @returns {number} 생성된 rowKey
     * @private
     */
    _createRowKey: function() {
        this.lastRowKey += 1;

        return this.lastRowKey;
    },

    /**
     * 랜더링시 사용될 extraData 필드에 rowSpanData 값을 세팅한다.
     * @param {Array} rowList - 전체 rowList 배열. rowSpan 된 경우 자식 row 의 데이터도 가공해야 하기 때문에 전체 list 를 인자로 넘긴다.
     * @param {number} index - 해당 배열에서 extraData 를 설정할 배열
     * @returns {Array} rowList - 가공된 rowList
     * @private
     */
    _setExtraRowSpanData: function(rowList, index) {
        var row = rowList[index],
            rowSpan = row && row._extraData && row._extraData.rowSpan,
            rowKey = row && row.rowKey,
            subCount, childRow, i;

        function hasRowSpanData(row, columnName) { // eslint-disable-line no-shadow, require-jsdoc
            var extraData = row._extraData;

            return !!(extraData.rowSpanData && extraData.rowSpanData[columnName]);
        }
        function setRowSpanData(row, columnName, rowSpanData) { // eslint-disable-line no-shadow, require-jsdoc
            var extraData = row._extraData;
            extraData.rowSpanData = (extraData && extraData.rowSpanData) || {};
            extraData.rowSpanData[columnName] = rowSpanData;

            return extraData;
        }

        if (rowSpan) {
            _.each(rowSpan, function(count, columnName) {
                if (!hasRowSpanData(row, columnName)) {
                    setRowSpanData(row, columnName, {
                        count: count,
                        isMainRow: true,
                        mainRowKey: rowKey
                    });
                    // rowSpan 된 row 의 자식 rowSpanData 를 가공한다.
                    subCount = -1;
                    for (i = index + 1; i < index + count; i += 1) {
                        childRow = rowList[i];
                        childRow[columnName] = row[columnName];
                        childRow._extraData = childRow._extraData || {};
                        setRowSpanData(childRow, columnName, {
                            count: subCount,
                            isMainRow: false,
                            mainRowKey: rowKey
                        });
                        subCount -= 1;
                    }
                }
            });
        }

        return rowList;
    },

    /**
     * originalRows 와 originalRowMap 을 생성한다.
     * @param {Array} [rowList] rowList 가 없을 시 현재 collection 데이터를 originalRows 로 저장한다.
     * @returns {Array} format 을 거친 데이터 리스트.
     */
    setOriginalRowList: function(rowList) {
        this.originalRows = rowList ? this._formatData(rowList) : this.toJSON();
        this.originalRowMap = _.indexBy(this.originalRows, 'rowKey');

        return this.originalRows;
    },

    /**
     * 원본 데이터 리스트를 반환한다.
     * @param {boolean} [isClone=true]  데이터 복제 여부.
     * @returns {Array}  원본 데이터 리스트 배열.
     */
    getOriginalRowList: function(isClone) {
        isClone = _.isUndefined(isClone) ? true : isClone;

        return isClone ? _.clone(this.originalRows) : this.originalRows;
    },

    /**
     * 원본 row 데이터를 반환한다.
     * @param {(Number|String)} rowKey  데이터의 키값
     * @returns {Object} 해당 행의 원본 데이터값
     */
    getOriginalRow: function(rowKey) {
        return _.clone(this.originalRowMap[rowKey]);
    },

    /**
     * rowKey 와 columnName 에 해당하는 원본 데이터를 반환한다.
     * @param {(Number|String)} rowKey  데이터의 키값
     * @param {String} columnName   컬럼명
     * @returns {(Number|String)}    rowKey 와 컬럼명에 해당하는 셀의 원본 데이터값
     */
    getOriginal: function(rowKey, columnName) {
        return _.clone(this.originalRowMap[rowKey][columnName]);
    },

    /**
     * mainRowKey 를 반환한다.
     * @param {(Number|String)} rowKey  데이터의 키값
     * @param {String} columnName   컬럼명
     * @returns {(Number|String)}    rowKey 와 컬럼명에 해당하는 셀의 main row 키값
     */
    getMainRowKey: function(rowKey, columnName) {
        var row = this.get(rowKey),
            rowSpanData;
        if (this.isRowSpanEnable()) {
            rowSpanData = row && row.getRowSpanData(columnName);
            rowKey = rowSpanData ? rowSpanData.mainRowKey : rowKey;
        }

        return rowKey;
    },

    /**
     * rowKey 에 해당하는 index를 반환한다.
     * @param {(Number|String)} rowKey 데이터의 키값
     * @returns {Number} 키값에 해당하는 row의 인덱스
     */
    indexOfRowKey: function(rowKey) {
        return this.indexOf(this.get(rowKey));
    },

    /**
     * rowSpan 이 적용되어야 하는지 여부를 반환한다.
     * 랜더링시 사용된다.
     * - sorted, 혹은 filterd 된 경우 false 를 리턴한다.
     * @returns {boolean}    랜더링 시 rowSpan 을 해야하는지 여부
     */
    isRowSpanEnable: function() {
        return !this.isSortedByField();
    },

    /**
     * 현재 RowKey가 아닌 다른 컬럼에 의해 정렬된 상태인지 여부를 반환한다.
     * @returns {Boolean}    정렬된 상태인지 여부
     */
    isSortedByField: function() {
        return this.sortOptions.columnName !== 'rowKey';
    },

    /**
     * 정렬옵션 객체의 값을 변경하고, 변경된 값이 있을 경우 sortChanged 이벤트를 발생시킨다.
     * @param {string} columnName 정렬할 컬럼명
     * @param {boolean} ascending 오름차순 여부
     * @param {boolean} requireFetch 서버 데이타의 갱신이 필요한지 여부
     */
    setSortOptionValues: function(columnName, ascending, requireFetch) {
        var options = this.sortOptions,
            isChanged = false;

        if (_.isUndefined(columnName)) {
            columnName = 'rowKey';
        }
        if (_.isUndefined(ascending)) {
            ascending = true;
        }

        if (options.columnName !== columnName || options.ascending !== ascending) {
            isChanged = true;
        }
        options.columnName = columnName;
        options.ascending = ascending;

        if (isChanged) {
            this.trigger('sortChanged', {
                columnName: columnName,
                ascending: ascending,
                requireFetch: requireFetch
            });
        }
    },

    /**
     * 주어진 컬럼명을 기준으로 오름/내림차순 정렬한다.
     * @param {string} columnName 정렬할 컬럼명
     * @param {boolean} ascending 오름차순 여부
     */
    sortByField: function(columnName, ascending) {
        var options = this.sortOptions;

        if (_.isUndefined(ascending)) {
            ascending = (options.columnName === columnName) ? !options.ascending : true;
        }
        this.setSortOptionValues(columnName, ascending, !options.useClient);

        if (options.useClient) {
            this.sort();
        }
    },

    /**
     * rowList 를 반환한다.
     * @param {boolean} [checkedOnly=false] true 로 설정된 경우 checked 된 데이터 대상으로 비교 후 반환한다.
     * @param {boolean} [withRawData=false] true 로 설정된 경우 내부 연산용 데이터 제거 필터링을 거치지 않는다.
     * @returns {Array} Row List
     */
    getRows: function(checkedOnly, withRawData) {
        var rows, checkedRows;

        if (checkedOnly) {
            checkedRows = this.where({
                '_button': true
            });
            rows = [];
            _.each(checkedRows, function(checkedRow) {
                rows.push(checkedRow.attributes);
            }, this);
        } else {
            rows = this.toJSON();
        }

        return withRawData ? rows : this._removePrivateProp(rows);
    },

    /**
     * row Data 값에 변경이 발생했을 경우, sorting 되지 않은 경우에만
     * rowSpan 된 데이터들도 함께 update 한다.
     *
     * @param {object} row row 모델
     * @param {String} columnName   변경이 발생한 컬럼명
     * @param {(String|Number)} value 변경된 값
     */
    syncRowSpannedData: function(row, columnName, value) {
        var index, rowSpanData, i;

        // 정렬 되지 않았을 때만 rowSpan 된 데이터들도 함께 update 한다.
        if (this.isRowSpanEnable()) {
            rowSpanData = row.getRowSpanData(columnName);
            if (!rowSpanData.isMainRow) {
                this.get(rowSpanData.mainRowKey).set(columnName, value);
            } else {
                index = this.indexOfRowKey(row.get('rowKey'));
                for (i = 0; i < rowSpanData.count - 1; i += 1) {
                    this.at(i + 1 + index).set(columnName, value);
                }
            }
        }
    },

    /* eslint-disable complexity */
    /**
     * Backbone 에서 sort() 실행시 내부적으로 사용되는 메소드.
     * @param {Row} a 비교할 앞의 모델
     * @param {Row} b 비교할 뒤의 모델
     * @returns {number} a가 b보다 작으면 -1, 같으면 0, 크면 1. 내림차순이면 반대.
     */
    comparator: function(a, b) {
        var columnName = this.sortOptions.columnName;
        var ascending = this.sortOptions.ascending;
        var valueA = a.get(columnName);
        var valueB = b.get(columnName);

        var isEmptyA = _.isNull(valueA) || _.isUndefined(valueA) || valueA === '';
        var isEmptyB = _.isNull(valueB) || _.isUndefined(valueB) || valueB === '';
        var result = 0;

        if (isEmptyA && !isEmptyB) {
            result = -1;
        } else if (!isEmptyA && isEmptyB) {
            result = 1;
        } else if (valueA < valueB) {
            result = -1;
        } else if (valueA > valueB) {
            result = 1;
        }

        if (!ascending) {
            result = -result;
        }

        return result;
    },
    /* eslint-enable complexity */

    /**
     * rowList 에서 내부에서만 사용하는 property 를 제거하고 반환한다.
     * @param {Array} rowList   내부에 설정된 rowList 배열
     * @returns {Array}  private 프로퍼티를 제거한 결과값
     * @private
     */
    _removePrivateProp: function(rowList) {
        return _.map(rowList, function(row) {
            return _.omit(row, Row.privateProperties);
        });
    },

    /**
     * rowKey 에 해당하는 그리드 데이터를 삭제한다.
     * @param {(Number|String)} rowKey - 행 데이터의 고유 키
     * @param {object} options - 삭제 옵션
     * @param {boolean} options.removeOriginalData - 원본 데이터도 함께 삭제할 지 여부
     * @param {boolean} options.keepRowSpanData - rowSpan이 mainRow를 삭제하는 경우 데이터를 유지할지 여부
     */
    removeRow: function(rowKey, options) {
        var row = this.get(rowKey);
        var rowSpanData, nextRow, removedData, currentIndex;

        if (!row) {
            return;
        }

        if (options && options.keepRowSpanData) {
            removedData = _.clone(row.attributes);
        }

        currentIndex = this.indexOf(row);
        rowSpanData = _.clone(row.getRowSpanData());
        nextRow = this.at(currentIndex + 1);

        this.remove(row, {
            silent: true
        });
        this._syncRowSpanDataForRemove(rowSpanData, nextRow, removedData);

        if (options && options.removeOriginalData) {
            this.setOriginalRowList();
        }
        this.trigger('remove', rowKey, currentIndex);
    },

    /**
     * 삭제된 행에 rowSpan이 적용되어 있었을 때, 관련된 행들의 rowSpan데이터를 갱신한다.
     * @param {object} rowSpanData - 삭제된 행의 rowSpanData
     * @param {Row} nextRow - 삭제된 다음 행의 모델
     * @param {object} [removedData] - 삭제된 행의 데이터 (삭제옵션의 keepRowSpanData가 true인 경우에만 넘겨짐)
     * @private
     */
    _syncRowSpanDataForRemove: function(rowSpanData, nextRow, removedData) {
        if (!rowSpanData) {
            return;
        }
        _.each(rowSpanData, function(data, columnName) {
            var mainRowSpanData = {},
                mainRow, startOffset, spanCount;

            if (data.isMainRow) {
                if (data.count === 1) {
                    // if isMainRow is true and count is 1, rowSpanData is meaningless
                    return;
                }
                mainRow = nextRow;
                spanCount = data.count - 1;
                startOffset = 1;
                if (spanCount > 1) {
                    mainRowSpanData.mainRowKey = mainRow.get('rowKey');
                    mainRowSpanData.isMainRow = true;
                }
                mainRow.set(columnName, (removedData ? removedData[columnName] : ''), {
                    silent: true
                });
            } else {
                mainRow = this.get(data.mainRowKey);
                spanCount = mainRow.getRowSpanData(columnName).count - 1;
                startOffset = -data.count;
            }

            if (spanCount > 1) {
                mainRowSpanData.count = spanCount;
                mainRow.setRowSpanData(columnName, mainRowSpanData);
                this._updateSubRowSpanData(mainRow, columnName, startOffset, spanCount);
            } else {
                mainRow.setRowSpanData(columnName, null);
            }
        }, this);
    },

    /**
     * append, prepend 시 사용할 dummy row를 생성한다.
     * @returns {Object} 값이 비어있는 더미 row 데이터
     * @private
     */
    _createDummyRow: function() {
        var columns = this.columnModel.get('dataColumns');
        var data = {};

        _.each(columns, function(columnModel) {
            data[columnModel.name] = '';
        }, this);

        return data;
    },

    /**
     * Insert the new row with specified data to the end of table.
     * @param {(Array|Object)} [rowData] - The data for the new row
     * @param {Object} [options] - Options
     * @param {Number} [options.at] - The index at which new row will be inserted
     * @param {Boolean} [options.extendPrevRowSpan] - If set to true and the previous row at target index
     *        has a rowspan data, the new row will extend the existing rowspan data.
     * @param {Boolean} [options.focus] - If set to true, move focus to the new row after appending
     * @returns {Array.<module:model/data/row>} Row model list
     */
    append: function(rowData, options) {
        var modelList = this._createModelList(rowData),
            addOptions;

        options = _.extend({at: this.length}, options);
        addOptions = {
            at: options.at,
            add: true,
            silent: true
        };

        this.add(modelList, addOptions);

        this._syncRowSpanDataForAppend(options.at, modelList.length, options.extendPrevRowSpan);
        this.trigger('add', modelList, options);

        return modelList;
    },

    /**
     * 현재 rowList 에 최상단에 데이터를 append 한다.
     * @param {Object} rowData  prepend 할 행 데이터
     * @param {object} [options] - Options
     * @param {boolean} [options.focus] - If set to true, move focus to the new row after appending
     * @returns {Array.<module:model/data/row>} Row model list
     */
    prepend: function(rowData, options) {
        options = options || {};
        options.at = 0;

        return this.append(rowData, options);
    },

    /**
     * rowKey에 해당하는 행의 데이터를 리턴한다. isJsonString을 true로 설정하면 결과를 json객체로 변환하여 리턴한다.
     * @param {(Number|String)} rowKey  행 데이터의 고유 키
     * @param {Boolean} [isJsonString=false]  true 일 경우 JSON String 으로 반환한다.
     * @returns {Object} 행 데이터
     */
    getRowData: function(rowKey, isJsonString) {
        var row = this.get(rowKey),
            rowData = row ? row.toJSON() : null;

        return isJsonString ? JSON.stringify(rowData) : rowData;
    },

    /**
     * 그리드 전체 데이터 중에서 index에 해당하는 순서의 데이터 객체를 리턴한다.
     * @param {Number} index 행의 인덱스
     * @param {Boolean} [isJsonString=false]  true 일 경우 JSON String 으로 반환한다.
     * @returns {Object} 행 데이터
     */
    getRowDataAt: function(index, isJsonString) {
        var row = this.at(index),
            rowData = row ? row.toJSON() : null;

        return isJsonString ? JSON.stringify(row) : rowData;
    },

    /**
     * rowKey 와 columnName 에 해당하는 값을 반환한다.
     * @param {(Number|String)} rowKey    행 데이터의 고유 키
     * @param {String} columnName   컬럼 이름
     * @param {boolean} [isOriginal]  원본 데이터 리턴 여부
     * @returns {(Number|String|undefined)}    조회한 셀의 값.
     */
    getValue: function(rowKey, columnName, isOriginal) {
        var value, row;

        if (isOriginal) {
            value = this.getOriginal(rowKey, columnName);
        } else {
            row = this.get(rowKey);
            value = row && row.get(columnName);
        }

        return value;
    },

    /**
     * Sets the vlaue of the cell identified by the specified rowKey and columnName.
     * @param {(Number|String)} rowKey - rowKey
     * @param {String} columnName - columnName
     * @param {(Number|String)} value - value
     * @param {Boolean} [silent=false] - whether set silently
     * @returns {Boolean} True if affected row exists
     */
    setValue: function(rowKey, columnName, value, silent) {
        var row = this.get(rowKey);

        if (row) {
            row.set(columnName, value, {
                silent: silent
            });

            return true;
        }

        return false;
    },

    /**
     * columnName에 해당하는 column data list를 리턴한다.
     * @param {String} columnName   컬럼명
     * @param {boolean} [isJsonString=false]  true 일 경우 JSON String 으로 반환한다.
     * @returns {Array} 컬럼명에 해당하는 셀들의 데이터 리스트
     */
    getColumnValues: function(columnName, isJsonString) {
        var valueList = this.pluck(columnName);

        return isJsonString ? JSON.stringify(valueList) : valueList;
    },

    /**
     * columnName 에 해당하는 값을 전부 변경한다.
     * @param {String} columnName 컬럼명
     * @param {(Number|String)} columnValue 변경할 컬럼 값
     * @param {Boolean} [isCheckCellState=true] 셀의 편집 가능 여부 와 disabled 상태를 체크할지 여부
     * @param {Boolean} [silent=false] change 이벤트 trigger 할지 여부.
     */
    setColumnValues: function(columnName, columnValue, isCheckCellState, silent) {
        var obj = {},
            cellState = {
                disabled: false,
                editable: true
            };

        obj[columnName] = columnValue;
        isCheckCellState = _.isUndefined(isCheckCellState) ? true : isCheckCellState;

        this.forEach(function(row) {
            if (isCheckCellState) {
                cellState = row.getCellState(columnName);
            }
            if (!cellState.disabled && cellState.editable) {
                row.set(obj, {
                    silent: silent
                });
            }
        }, this);
    },

    /**
     * rowKey 와 columnName 에 해당하는 Cell 의 rowSpanData 를 반환한다.
     * @param {(Number|String)} rowKey 행 데이터의 고유 rowKey
     * @param {String} columnName 컬럼 이름
     * @returns {object} rowSpanData
     */
    getRowSpanData: function(rowKey, columnName) {
        var row = this.get(rowKey);

        return row ? row.getRowSpanData(columnName) : null;
    },

    /**
     * Returns true if there are at least one row modified.
     * @returns {boolean} - True if there are at least one row modified.
     */
    isModified: function() {
        var modifiedRowsArr = _.values(this.getModifiedRows());

        return _.some(modifiedRowsArr, function(modifiedRows) {
            return modifiedRows.length > 0;
        });
    },

    /**
     * Enables or Disables all rows.
     * @param  {Boolean} disabled - Whether disabled or not
     */
    setDisabled: function(disabled) {
        if (this.disabled !== disabled) {
            this.disabled = disabled;
            this.trigger('disabledChanged');
        }
    },

    /**
     * rowKey에 해당하는 행을 활성화시킨다.
     * @param {(Number|String)} rowKey 행 데이터의 고유 키
     */
    enableRow: function(rowKey) {
        this.get(rowKey).setRowState('');
    },

    /**
     * rowKey에 해당하는 행을 비활성화 시킨다.
     * @param {(Number|String)} rowKey    행 데이터의 고유 키
     */
    disableRow: function(rowKey) {
        this.get(rowKey).setRowState('DISABLED');
    },

    /**
     * rowKey에 해당하는 행의 메인 체크박스를 체크할 수 있도록 활성화 시킨다.
     * @param {(Number|String)} rowKey 행 데이터의 고유 키
     */
    enableCheck: function(rowKey) {
        this.get(rowKey).setRowState('');
    },

    /**
     * rowKey에 해당하는 행의 메인 체크박스를 체크하지 못하도록 비활성화 시킨다.
     * @param {(Number|String)} rowKey 행 데이터의 고유 키
     */
    disableCheck: function(rowKey) {
        this.get(rowKey).setRowState('DISABLED_CHECK');
    },

    /**
     * rowKey에 해당하는 행의 체크박스 및 라디오박스를 선택한다.
     * @param {(Number|String)} rowKey    행 데이터의 고유 키
     * @param {Boolean} [silent] 이벤트 발생 여부
     */
    check: function(rowKey, silent) {
        var isDisabledCheck = this.get(rowKey).getRowState().isDisabledCheck;
        var selectType = this.columnModel.get('selectType');

        if (!isDisabledCheck && selectType) {
            if (selectType === 'radio') {
                this.uncheckAll();
            }
            this.setValue(rowKey, '_button', true, silent);
        }
    },

    /**
     * rowKey 에 해당하는 행의 체크박스 및 라디오박스를 선택한다.
     * @param {(Number|String)} rowKey    행 데이터의 고유 키
     * @param {Boolean} [silent] 이벤트 발생 여부
     */
    uncheck: function(rowKey, silent) {
        this.setValue(rowKey, '_button', false, silent);
    },

    /**
     * 전체 행을 선택한다.
     * TODO: disableCheck 행 처리
     */
    checkAll: function() {
        this.setColumnValues('_button', true);
    },

    /**
     * 모든 행을 선택 해제 한다.
     */
    uncheckAll: function() {
        this.setColumnValues('_button', false);
    },

    /**
     * 주어진 데이터로 모델 목록을 생성하여 반환한다.
     * @param {object|array} rowData - 모델을 생성할 데이터. Array일 경우 여러개를 동시에 생성한다.
     * @returns {Row[]} 생성된 모델 목록
     */
    _createModelList: function(rowData) {
        var modelList = [],
            rowList;

        rowData = rowData || this._createDummyRow();
        if (!_.isArray(rowData)) {
            rowData = [rowData];
        }
        rowList = this._formatData(rowData);

        _.each(rowList, function(row) {
            var model = new Row(row, {
                collection: this,
                parse: true
            });
            modelList.push(model);
        }, this);

        return modelList;
    },

    /**
     * 새로운 행이 추가되었을 때, 관련된 주변 행들의 rowSpan 데이터를 갱신한다.
     * @param {number} index - 추가된 행의 인덱스
     * @param {number} length - 추가된 행의 개수
     * @param {boolean} extendPrevRowSpan - 이전 행의 rowSpan 데이터가 있는 경우 합칠지 여부
     */
    _syncRowSpanDataForAppend: function(index, length, extendPrevRowSpan) {
        var prevRow = this.at(index - 1);

        if (!prevRow) {
            return;
        }
        _.each(prevRow.getRowSpanData(), function(data, columnName) {
            var mainRow, mainRowData, startOffset, spanCount;

            // count 값은 mainRow인 경우 '전체 rowSpan 개수', 아닌 경우는 'mainRow까지의 거리 (음수)'를 의미한다.
            // 0이면 rowSpan 되어 있지 않다는 의미이다.
            if (data.count === 0) {
                return;
            }
            if (data.isMainRow) {
                mainRow = prevRow;
                mainRowData = data;
                startOffset = 1;
            } else {
                mainRow = this.get(data.mainRowKey);
                mainRowData = mainRow.getRowSpanData()[columnName];
                // 루프를 순회할 때 의미를 좀더 명확하게 하기 위해 양수값으로 변경해서 offset 처럼 사용한다.
                startOffset = -data.count + 1;
            }

            if (mainRowData.count > startOffset || extendPrevRowSpan) {
                mainRowData.count += length;
                spanCount = mainRowData.count;

                this._updateSubRowSpanData(mainRow, columnName, startOffset, spanCount);
            }
        }, this);
    },

    /**
     * 특정 컬럼의 rowSpan 데이터를 주어진 범위만큼 갱신한다.
     * @param {Row} mainRow - rowSpan의 첫번째 행
     * @param {string} columnName - 컬럼명
     * @param {number} startOffset - mainRow로부터 몇번째 떨어진 행부터 갱신할지를 지정하는 값
     * @param {number} spanCount - span이 적용될 행의 개수
     */
    _updateSubRowSpanData: function(mainRow, columnName, startOffset, spanCount) {
        var mainRowIdx = this.indexOf(mainRow),
            mainRowKey = mainRow.get('rowKey'),
            row, offset;

        for (offset = startOffset; offset < spanCount; offset += 1) {
            row = this.at(mainRowIdx + offset);
            row.set(columnName, mainRow.get(columnName), {
                silent: true
            });
            row.setRowSpanData(columnName, {
                count: -offset,
                mainRowKey: mainRowKey,
                isMainRow: false
            });
        }
    },

    /**
     * 해당 row가 수정된 Row인지 여부를 반환한다.
     * @param {Object} row - row 데이터
     * @param {Object} originalRow - 원본 row 데이터
     * @param {Array} ignoredColumns - 비교에서 제외할 컬럼명
     * @returns {boolean} - 수정여부
     */
    _isModifiedRow: function(row, originalRow, ignoredColumns) {
        var filtered = _.omit(row, ignoredColumns);
        var result = _.some(filtered, function(value, columnName) {
            if (typeof value === 'object') {
                return (JSON.stringify(value) !== JSON.stringify(originalRow[columnName]));
            }

            return value !== originalRow[columnName];
        }, this);

        return result;
    },

    /**
     * 수정된 rowList 를 반환한다.
     * @param {Object} options 옵션 객체
     *      @param {boolean} [options.checkedOnly=false] true 로 설정된 경우 checked 된 데이터 대상으로 비교 후 반환한다.
     *      @param {boolean} [options.withRawData=false] true 로 설정된 경우 내부 연산용 데이터 제거 필터링을 거치지 않는다.
     *      @param {boolean} [options.rowKeyOnly=false] true 로 설정된 경우 키값만 저장하여 리턴한다.
     *      @param {Array} [options.ignoredColumns]   행 데이터 중에서 데이터 변경으로 간주하지 않을 컬럼 이름을 배열로 설정한다.
     * @returns {{createdRows: Array, updatedRows: Array, deletedRows: Array}} options 조건에 해당하는 수정된 rowList 정보
     */
    getModifiedRows: function(options) {
        var withRawData = options && options.withRawData;
        var checkedOnly = options && options.checkedOnly;
        var rowKeyOnly = options && options.rowKeyOnly;
        var original = withRawData ? this.originalRows : this._removePrivateProp(this.originalRows);
        var current = withRawData ? this.toJSON() : this._removePrivateProp(this.toJSON());
        var ignoredColumns = options && options.ignoredColumns;
        var result = {
            createdRows: [],
            updatedRows: [],
            deletedRows: []
        };

        original = _.indexBy(original, 'rowKey');
        current = _.indexBy(current, 'rowKey');
        ignoredColumns = _.union(ignoredColumns, this.columnModel.getIgnoredColumnNames());

        // 추가/ 수정된 행 추출
        _.each(current, function(row, rowKey) {
            var originalRow = original[rowKey],
                item = rowKeyOnly ? row.rowKey : _.omit(row, ignoredColumns);

            if (!checkedOnly || (checkedOnly && this.get(rowKey).get('_button'))) {
                if (!originalRow) {
                    result.createdRows.push(item);
                } else if (this._isModifiedRow(row, originalRow, ignoredColumns)) {
                    result.updatedRows.push(item);
                }
            }
        }, this);

        // 삭제된 행 추출
        _.each(original, function(obj, rowKey) {
            var item = rowKeyOnly ? obj.rowKey : _.omit(obj, ignoredColumns);
            if (!current[rowKey]) {
                result.deletedRows.push(item);
            }
        }, this);

        return result;
    },

    /**
     * data 를 설정한다. setData 와 다르게 setOriginalRowList 를 호출하여 원본데이터를 갱신하지 않는다.
     * @param {Array} data - 설정할 데이터 배열 값
     * @param {boolean} [parse=true]  backbone 의 parse 로직을 수행할지 여부
     * @param {Function} [callback] callback function
     */
    resetData: function(data, parse, callback) {
        if (!data) {
            data = [];
        }
        if (_.isUndefined(parse)) {
            parse = true;
        }
        this.trigger('beforeReset', data.length);

        this.lastRowKey = -1;
        this.reset(data, {
            parse: parse
        });

        if (_.isFunction(callback)) {
            callback();
        }
    },

    /**
     * data 를 설정하고, setOriginalRowList 를 호출하여 원본데이터를 갱신한다.
     * @param {Array} data - 설정할 데이터 배열 값
     * @param {boolean} [parse=true]  backbone 의 parse 로직을 수행할지 여부
     * @param {function} [callback] 완료시 호출될 함수
     */
    setData: function(data, parse, callback) {
        var wrappedCallback = _.bind(function() {
            this.setOriginalRowList();
            if (_.isFunction(callback)) {
                callback();
            }
        }, this);

        this.resetData(data, parse, wrappedCallback);
    },

    /**
     * setData()를 통해 그리드에 설정된 초기 데이터 상태로 복원한다.
     * 그리드에서 수정되었던 내용을 초기화하는 용도로 사용한다.
     */
    restore: function() {
        var originalRows = this.getOriginalRowList();
        this.resetData(originalRows, true);
    },

    /**
     * rowKey 와 columnName 에 해당하는 text 형태의 셀의 값을 삭제한다.
     * @param {(Number|String)} rowKey 행 데이터의 고유 키
     * @param {String} columnName 컬럼 이름
     * @param {Boolean} [silent=false] 이벤트 발생 여부. true 로 변경할 상황은 거의 없다.
     */
    del: function(rowKey, columnName, silent) {
        var mainRowKey = this.getMainRowKey(rowKey, columnName),
            cellState = this.get(mainRowKey).getCellState(columnName),
            editType = this.columnModel.getEditType(columnName),
            isDeletableType = _.contains(['text', 'password'], editType);

        if (isDeletableType && cellState.editable && !cellState.disabled) {
            this.setValue(mainRowKey, columnName, '', silent);
        }
    },

    /**
     * Calls del() method for multiple cells silently, and trigger 'deleteRange' event
     * @param {{row: Array.<number>, column: Array.<number>}} range - visible indexes
     */
    delRange: function(range) {
        var columnModels = this.columnModel.getVisibleColumns();
        var rowIdxes = _.range(range.row[0], range.row[1] + 1);
        var columnIdxes = _.range(range.column[0], range.column[1] + 1);
        var rowKeys, columnNames;

        rowKeys = _.map(rowIdxes, function(idx) {
            return this.at(idx).get('rowKey');
        }, this);

        columnNames = _.map(columnIdxes, function(idx) {
            return columnModels[idx].name;
        });

        _.each(rowKeys, function(rowKey) {
            _.each(columnNames, function(columnName) {
                this.del(rowKey, columnName, true);
                this.get(rowKey).validateCell(columnName, true);
            }, this);
        }, this);

        /**
         * Occurs when cells are deleted by 'del' key
         * @event Grid#deleteRange
         * @type {module:event/gridEvent}
         * @property {Array} columnNames - columName list of deleted cell
         * @property {Array} rowKeys - rowKey list of deleted cell
         * @property {Grid} instance - Current grid instance
         */
        this.trigger('deleteRange', new GridEvent(null, {
            rowKeys: rowKeys,
            columnNames: columnNames
        }));
    },

    /**
     * 2차원 배열로 된 데이터를 받아 현재 Focus된 셀을 기준으로 하여 각각의 인덱스의 해당하는 만큼 우측 아래 방향으로
     * 이동하며 셀의 값을 변경한다. 완료한 후 적용된 셀 범위에 Selection을 지정한다.
     * @param {Array[]} data - 2차원 배열 데이터. 내부배열의 사이즈는 모두 동일해야 한다.
     * @param {{row: number, column: number}} startIdx - 시작점이 될 셀의 인덱스
     */
    paste: function(data, startIdx) {
        var endIdx = this._getEndIndexToPaste(data, startIdx);

        _.each(data, function(row, index) {
            this._setValueForPaste(row, startIdx.row + index, startIdx.column, endIdx.column);
        }, this);

        this.trigger('paste', {
            startIdx: startIdx,
            endIdx: endIdx
        });
    },

    /**
     * Validates all data and returns the result.
     * Return value is an array which contains only rows which have invalid cell data.
     * @returns {Array.<Object>} An array of error object
     * @example
        [
            {
                rowKey: 1,
                errors: [
                    {
                        columnName: 'c1',
                        errorCode: 'REQUIRED'
                    },
                    {
                        columnName: 'c2',
                        errorCode: 'REQUIRED'
                    }
                ]
            },
            {
                rowKey: 3,
                errors: [
                    {
                        columnName: 'c2',
                        errorCode: 'REQUIRED'
                    }
                ]
            }
        ]
     */
    validate: function() {
        var errorRows = [];
        var requiredColumnNames = _.chain(this.columnModel.getVisibleColumns())
            .filter(function(columnModel) {
                return columnModel.validation && columnModel.validation.required === true;
            })
            .pluck('name')
            .value();

        this.each(function(row) {
            var errorCells = [];
            _.each(requiredColumnNames, function(columnName) {
                var errorCode = row.validateCell(columnName);
                if (errorCode) {
                    errorCells.push({
                        columnName: columnName,
                        errorCode: errorCode
                    });
                }
            });
            if (errorCells.length) {
                errorRows.push({
                    rowKey: row.get('rowKey'),
                    errors: errorCells
                });
            }
        });

        return errorRows;
    },

    /**
     * 붙여넣기를 실행할 때 끝점이 될 셀의 인덱스를 반환한다.
     * @param  {Array[]} data - 붙여넣기할 데이터
     * @param  {{row: number, column: number}} startIdx - 시작점이 될 셀의 인덱스
     * @returns {{row: number, column: number}} 행과 열의 인덱스 정보를 가진 객체
     */
    _getEndIndexToPaste: function(data, startIdx) {
        var columns = this.columnModel.getVisibleColumns(),
            rowIdx = data.length + startIdx.row - 1,
            columnIdx = Math.min(data[0].length + startIdx.column, columns.length) - 1;

        return {
            row: rowIdx,
            column: columnIdx
        };
    },

    /**
     * 주어진 행 데이터를 지정된 인덱스의 컬럼에 반영한다.
     * 셀이 수정 가능한 상태일 때만 값을 변경하며, RowSpan이 적용된 셀인 경우 MainRow인 경우에만 값을 변경한다.
     * @param  {rowData} rowData - 붙여넣을 행 데이터
     * @param  {number} rowIdx - 행 인덱스
     * @param  {number} columnStartIdx - 열 시작 인덱스
     * @param  {number} columnEndIdx - 열 종료 인덱스
     */
    _setValueForPaste: function(rowData, rowIdx, columnStartIdx, columnEndIdx) {
        var row = this.at(rowIdx),
            columnModel = this.columnModel,
            attributes = {},
            columnIdx, columnName, cellState, rowSpanData;

        if (!row) {
            row = this.append({})[0];
        }
        for (columnIdx = columnStartIdx; columnIdx <= columnEndIdx; columnIdx += 1) {
            columnName = columnModel.at(columnIdx, true).name;
            cellState = row.getCellState(columnName);
            rowSpanData = row.getRowSpanData(columnName);

            if (cellState.editable && !cellState.disabled && (!rowSpanData || rowSpanData.count >= 0)) {
                attributes[columnName] = rowData[columnIdx - columnStartIdx];
            }
        }
        row.set(attributes);
    },

    /**
     * rowKey 와 columnName 에 해당하는 td element 를 반환한다.
     * 내부적으로 자동으로 mainRowKey 를 찾아 반환한다.
     * @param {(Number|String)} rowKey    행 데이터의 고유 키
     * @param {String} columnName   컬럼 이름
     * @returns {jQuery} 해당 jQuery Element
     */
    getElement: function(rowKey, columnName) {
        var mainRowKey = this.getMainRowKey(rowKey, columnName);

        return this.domState.getElement(mainRowKey, columnName);
    },

    /**
     * Returns the count of check-available rows and checked rows.
     * @returns {{available: number, checked: number}}
     */
    getCheckedState: function() {
        var available = 0;
        var checked = 0;

        this.forEach(function(row) {
            var buttonState = row.getCellState('_button');

            if (!buttonState.disabled && buttonState.editable) {
                available += 1;
                if (row.get('_button')) {
                    checked += 1;
                }
            }
        });

        return {
            available: available,
            checked: checked
        };
    }
});

module.exports = RowList;