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

'use strict';

var _ = require('underscore');

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

/**
 * Focus model
 * @param {Object} attrs - Attributes
 * @param {Object} options - Options
 * @module model/focus
 * @extends module:base/model
 * @ignore
 */
var Focus = Model.extend(/** @lends module:model/focus.prototype */{
    initialize: function(attrs, options) {
        var editEventName = options.editingEvent + ':cell';
        var domEventBus;

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

        _.assign(this, {
            dataModel: options.dataModel,
            columnModel: options.columnModel,
            coordRowModel: options.coordRowModel,
            domEventBus: options.domEventBus,
            domState: options.domState
        });

        this.listenTo(this.dataModel, 'reset', this._onResetData);
        this.listenTo(this.dataModel, 'add', this._onAddDataModel);

        if (this.domEventBus) {
            domEventBus = this.domEventBus;
            this.listenTo(domEventBus, editEventName, this._onMouseClickEdit);
            this.listenTo(domEventBus, 'mousedown:focus', this._onMouseDownFocus);
            this.listenTo(domEventBus, 'key:move', this._onKeyMove);
            this.listenTo(domEventBus, 'key:edit', this._onKeyEdit);
        }
    },

    defaults: {
        /**
         * row key of the current cell
         * @type {String|Number}
         */
        rowKey: null,

        /**
         * column name of the current cell
         * @type {String}
         */
        columnName: null,

        /**
         * row key of the previously focused cell
         * @type {String|Number}
         */
        prevRowKey: null,

        /**
         * column name of the previously focused cell
         * @type {String}
         */
        prevColumnName: '',

        /**
         * address of the editing cell
         * @type {{rowKey:(String|Number), columnName:String}}
         */
        editingAddress: null,

        /**
         * Whether focus state is active or not
         * @type {Boolean}
         */
        active: false
    },

    /**
     * Event handler for 'reset' event on dataModel.
     * @private
     */
    _onResetData: function() {
        this.blur();
    },

    /**
     * Event handler for 'add' event on dataModel.
     * @param  {module:model/data/rowList} dataModel - data model
     * @param  {Object} options - options for appending. See {@link module:model/data/rowList#append}
     * @private
     */
    _onAddDataModel: function(dataModel, options) {
        if (options.focus) {
            this.focusAt(options.at, 0);
        }
    },

    /**
     * Event handler for 'click:cell' or 'dblclick:cell' event on domEventBus
     * @param {module:event/gridEvent} ev - event data
     * @private
     */
    _onMouseClickEdit: function(ev) {
        this.focusIn(ev.rowKey, ev.columnName);
    },

    /* eslint-disable complexity */
    /**
     * Event handler for key:move event
     * @param {module:event/gridEvent} ev - GridEvent
     * @private
     */
    _onKeyMove: function(ev) {
        var rowKey, columnName;

        switch (ev.command) {
            case 'up':
                rowKey = this.prevRowKey();
                break;
            case 'down':
                rowKey = this.nextRowKey();
                break;
            case 'left':
                columnName = this.prevColumnName();
                break;
            case 'right':
                columnName = this.nextColumnName();
                break;
            case 'pageUp':
                rowKey = this._getPageMovedRowKey(false);
                break;
            case 'pageDown':
                rowKey = this._getPageMovedRowKey(true);
                break;
            case 'firstColumn':
                columnName = this.firstColumnName();
                break;
            case 'lastColumn':
                columnName = this.lastColumnName();
                break;
            case 'firstCell':
                rowKey = this.firstRowKey();
                columnName = this.firstColumnName();
                break;
            case 'lastCell':
                rowKey = this.lastRowKey();
                columnName = this.lastColumnName();
                break;
            default:
        }

        rowKey = _.isUndefined(rowKey) ? this.get('rowKey') : rowKey;
        columnName = columnName || this.get('columnName');

        this.focus(rowKey, columnName, true);
    },
    /* eslint-enable complexity */

    /**
     * Event handler for key:edit event
     * @param {module:event/gridEvent} ev - GridEvent
     * @private
     */
    _onKeyEdit: function(ev) {
        var address;

        switch (ev.command) {
            case 'currentCell':
                address = this.which();
                break;
            case 'nextCell':
                address = this.nextAddress();
                break;
            case 'prevCell':
                address = this.prevAddress();
                break;
            default:
        }

        if (address) {
            this.focusIn(address.rowKey, address.columnName, true);
        }
    },

    /**
     * Returns the moved rowKey by page unit from current position
     * @param {boolean} isDownDir - true: down, false: up
     * @returns {number}
     * @private
     */
    _getPageMovedRowKey: function(isDownDir) {
        var rowIndex = this.dataModel.indexOfRowKey(this.get('rowKey'));
        var prevPageRowIndex = this.coordRowModel.getPageMovedIndex(rowIndex, isDownDir);
        var rowKey;

        if (isDownDir) {
            rowKey = this.nextRowKey(prevPageRowIndex - rowIndex);
        } else {
            rowKey = this.prevRowKey(rowIndex - prevPageRowIndex);
        }

        return rowKey;
    },

    /**
     * Event handler for 'mousedown' event on domEventBus
     * @private
     */
    _onMouseDownFocus: function() {
        this.focusClipboard();
    },

    /**
     * Saves previous data.
     * @private
     */
    _savePrevious: function() {
        if (this.get('rowKey') !== null) {
            this.set('prevRowKey', this.get('rowKey'));
        }
        if (this.get('columnName')) {
            this.set('prevColumnName', this.get('columnName'));
        }
    },

    /**
     * Returns whether given rowKey and columnName is equal to current value
     * @param {(Number|String)} rowKey - row key
     * @param {String} columnName - column name
     * @param {Boolean} isMainRowKey - true if the target row key is main row
     * @returns {Boolean} - True if equal
     */
    isCurrentCell: function(rowKey, columnName, isMainRowKey) {
        var curColumnName = this.get('columnName');
        var curRowKey = this.get('rowKey');

        if (isMainRowKey) {
            curRowKey = this.dataModel.getMainRowKey(curRowKey, curColumnName);
        }

        return String(curRowKey) === String(rowKey) && curColumnName === columnName;
    },

    /* eslint-disable complexity */
    /**
     * Focus to the cell identified by given rowKey and columnName.
     * @param {Number|String} rowKey - rowKey
     * @param {String} columnName - columnName
     * @param {Boolean} isScrollable - if set to true, move scroll position to focused position
     * @returns {Boolean} true if focused cell is changed
     */
    focus: function(rowKey, columnName, isScrollable) {
        if (!this.get('active')) {
            this.set('active', true);
        }

        if (!this._isValidCell(rowKey, columnName) ||
            util.isMetaColumn(columnName) ||
            this.isCurrentCell(rowKey, columnName)) {
            return true;
        }

        if (!this._triggerFocusChangeEvent(rowKey, columnName)) {
            return false;
        }

        this.blur();
        this.set({
            rowKey: rowKey,
            columnName: columnName
        });

        this.trigger('focus', rowKey, columnName, isScrollable);

        if (this.columnModel.get('selectType') === 'radio') {
            this.dataModel.check(rowKey);
        }

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

    /**
     * Trigger 'focusChange' event and returns the result
     * @param {(number|string)} rowKey - rowKey
     * @param {stringd} columnName - columnName
     * @returns {boolean}
     * @private
     */
    _triggerFocusChangeEvent: function(rowKey, columnName) {
        var gridEvent = new GridEvent(null, {
            rowKey: rowKey,
            prevRowKey: this.get('rowKey'),
            columnName: columnName,
            prevColumnName: this.get('columnName')
        });

        /**
         * Occurs when focused cell is about to change
         * @api
         * @event Grid#focusChange
         * @type {module:event/gridEvent}
         * @property {number} rowKey - rowKey of the target cell
         * @property {number} columnName - columnName of the target cell
         * @property {number} prevRowKey - rowKey of the currently focused cell
         * @property {number} prevColumnName - columnName of the currently focused cell
         * @property {Grid} instance - Current grid instance
         */
        this.trigger('focusChange', gridEvent);

        return !gridEvent.isStopped();
    },

    /**
     * Focus to the cell identified by given rowIndex and columnIndex.
     * @param {(Number|String)} rowIndex - rowIndex
     * @param {String} columnIndex - columnIndex
     * @param {boolean} [isScrollable=false] - if set to true, scroll to focused cell
     * @returns {Boolean} true if success
     */
    focusAt: function(rowIndex, columnIndex, isScrollable) {
        var row = this.dataModel.at(rowIndex);
        var column = this.columnModel.at(columnIndex, true);
        var result = false;

        if (row && column) {
            result = this.focus(row.get('rowKey'), column.name, isScrollable);
        }

        return result;
    },

    /**
     * Focus to the cell identified by given rowKey and columnName and change it to edit-mode if editable.
     * @param {(Number|String)} rowKey - rowKey
     * @param {String} columnName - columnName
     * @param {boolean} [isScrollable=false] - if set to true, scroll to focused cell
     * @returns {Boolean} true if success
     */
    focusIn: function(rowKey, columnName, isScrollable) {
        var result = this.focus(rowKey, columnName, isScrollable);

        if (result) {
            rowKey = this.dataModel.getMainRowKey(rowKey, columnName);

            if (this.dataModel.get(rowKey).isEditable(columnName)) {
                this.finishEditing();
                this.startEditing(rowKey, columnName);
            } else {
                this.focusClipboard();
            }
        }

        return result;
    },

    /**
     * Focus to the cell identified by given rowIndex and columnIndex and change it to edit-mode if editable.
     * @param {(Number|String)} rowIndex - rowIndex
     * @param {String} columnIndex - columnIndex
     * @param {Boolean} [isScrollable=false] - if set to true, scroll to focused cell
     * @returns {Boolean} true if success
     */
    focusInAt: function(rowIndex, columnIndex, isScrollable) {
        var row = this.dataModel.at(rowIndex);
        var column = this.columnModel.at(columnIndex, true);
        var result = false;

        if (row && column) {
            result = this.focusIn(row.get('rowKey'), column.name, isScrollable);
        }

        return result;
    },

    /**
     * clipboard 에 focus 한다.
     */
    focusClipboard: function() {
        this.trigger('focusClipboard');
    },

    /**
     * If the grid has an element which has a focus, make sure that focusModel has a valid data,
     * Otherwise change the focus state.
     */
    refreshState: function() {
        var restored;

        if (!this.domState.hasFocusedElement()) {
            this.set('active', false);
        } else if (!this.has()) {
            restored = this.restore();
            if (!restored) {
                this.focusAt(0, 0);
            }
        }
    },

    /**
     * Apply blur state on cell
     * @returns {Model.Focus} This object
     */
    blur: function() {
        if (!this.has()) {
            return this;
        }

        if (this.has(true)) {
            this._savePrevious();
        }

        this.trigger('blur', this.get('rowKey'), this.get('columnName'));

        this.set({
            rowKey: null,
            columnName: null
        });

        return this;
    },

    /**
     * 현재 focus 정보를 반환한다.
     * @returns {{rowKey: (number|string), columnName: string}} 현재 focus 정보에 해당하는 rowKey, columnName
     */
    which: function() {
        return {
            rowKey: this.get('rowKey'),
            columnName: this.get('columnName')
        };
    },

    /**
     * 현재 focus 정보를 index 기준으로 반환한다.
     * @param {boolean} isPrevious 이전 focus 정보를 반환할지 여부
     * @returns {{row: number, column: number}} The object that contains index info
     */
    indexOf: function(isPrevious) {
        var rowKey = isPrevious ? this.get('prevRowKey') : this.get('rowKey');
        var columnName = isPrevious ? this.get('prevColumnName') : this.get('columnName');

        return {
            row: this.dataModel.indexOfRowKey(rowKey),
            column: this.columnModel.indexOfColumnName(columnName, true)
        };
    },

    /**
     * Returns whether has focus.
     * @param {boolean} checkValid - if set to true, check whether the current cell is valid.
     * @returns {boolean} True if has focus.
     */
    has: function(checkValid) {
        var rowKey = this.get('rowKey');
        var columnName = this.get('columnName');

        if (checkValid) {
            return this._isValidCell(rowKey, columnName);
        }

        return !util.isBlank(rowKey) && !util.isBlank(columnName);
    },

    /**
     * Restore previous focus data.
     * @returns {boolean} True if restored
     */
    restore: function() {
        var prevRowKey = this.get('prevRowKey');
        var prevColumnName = this.get('prevColumnName');
        var restored = false;

        if (this._isValidCell(prevRowKey, prevColumnName)) {
            this.focus(prevRowKey, prevColumnName);
            this.set({
                prevRowKey: null,
                prevColumnName: null
            });
            restored = true;
        }

        return restored;
    },

    /**
     * Returns whether the cell identified by given rowKey and columnName is editing now.
     * @param {Number} rowKey - row key
     * @param {String} columnName - column name
     * @returns {Boolean}
     */
    isEditingCell: function(rowKey, columnName) {
        var address = this.get('editingAddress');

        return address &&
            (String(address.rowKey) === String(rowKey)) &&
            (address.columnName === columnName);
    },

    /**
     * Starts editing a cell identified by given rowKey and columnName, and returns the result.
     * @param {(String|Number)} rowKey - row key
     * @param {String} columnName - column name
     * @returns {Boolean} true if succeeded, false otherwise.
     */
    startEditing: function(rowKey, columnName) {
        if (this.get('editingAddress')) {
            return false;
        }

        if (_.isUndefined(rowKey) && _.isUndefined(columnName)) {
            rowKey = this.get('rowKey');
            columnName = this.get('columnName');
        } else if (!this.isCurrentCell(rowKey, columnName, true)) {
            return false;
        }

        rowKey = this.dataModel.getMainRowKey(rowKey, columnName);
        if (!this.dataModel.get(rowKey).isEditable(columnName)) {
            return false;
        }
        this.set('editingAddress', {
            rowKey: rowKey,
            columnName: columnName
        });

        return true;
    },

    /**
     * Finishes editing the current cell, and returns the result.
     * @returns {Boolean} - true if an editing cell exist, false otherwise.
     */
    finishEditing: function() {
        if (!this.get('editingAddress')) {
            return false;
        }

        this.set('editingAddress', null);

        return true;
    },

    /**
     * Returns whether the specified cell is exist
     * @param {String|Number} rowKey - Rowkey
     * @param {String} columnName - ColumnName
     * @returns {boolean} True if exist
     * @private
     */
    _isValidCell: function(rowKey, columnName) {
        var isValidRowKey = !util.isBlank(rowKey) && !!this.dataModel.get(rowKey);
        var isValidColumnName = !util.isBlank(columnName) && !!this.columnModel.getColumnModel(columnName);

        return isValidRowKey && isValidColumnName;
    },

    /**
     * 현재 focus 된 row 기준으로 offset 만큼 이동한 rowKey 를 반환한다.
     * @param {Number} offset   이동할 offset
     * @returns {?Number|String} rowKey   offset 만큼 이동한 위치의 rowKey
     * @private
     */
    _findRowKey: function(offset) {
        var dataModel = this.dataModel;
        var rowKey = null;
        var index, row;

        if (this.has(true)) {
            index = Math.max(
                Math.min(
                    dataModel.indexOfRowKey(this.get('rowKey')) + offset,
                    this.dataModel.length - 1
                ), 0
            );
            row = dataModel.at(index);
            if (row) {
                rowKey = row.get('rowKey');
            }
        }

        return rowKey;
    },

    /**
     * 현재 focus 된 column 기준으로 offset 만큼 이동한 columnName 을 반환한다.
     * @param {Number} offset   이동할 offset
     * @returns {?String} columnName  offset 만큼 이동한 위치의 columnName
     * @private
     */
    _findColumnName: function(offset) {
        var columnModel = this.columnModel;
        var columns = columnModel.getVisibleColumns();
        var columnIndex = columnModel.indexOfColumnName(this.get('columnName'), true);
        var columnName = null;
        var index;

        if (this.has(true)) {
            index = Math.max(Math.min(columnIndex + offset, columns.length - 1), 0);
            columnName = columns[index] && columns[index].name;
        }

        return columnName;
    },

    /**
     * Returns data of rowSpan
     * @param {Number|String} rowKey - Row key
     * @param {String} columnName - Column name
     * @returns {boolean|{count: number, isMainRow: boolean, mainRowKey: *}} rowSpanData - Data of rowSpan
     * @private
     */
    _getRowSpanData: function(rowKey, columnName) {
        if (rowKey && columnName) {
            return this.dataModel.get(rowKey).getRowSpanData(columnName);
        }

        return false;
    },

    /**
     * offset 만큼 뒤로 이동한 row 의 index 를 반환한다.
     * @param {number} offset   이동할 offset
     * @returns {Number} 이동한 위치의 row index
     */
    nextRowIndex: function(offset) {
        var rowKey = this.nextRowKey(offset);

        return this.dataModel.indexOfRowKey(rowKey);
    },

    /**
     * offset 만큼 앞으로 이동한 row의 index를 반환한다.
     * @param {number} offset 이동할 offset
     * @returns {Number} 이동한 위치의 row index
     */
    prevRowIndex: function(offset) {
        var rowKey = this.prevRowKey(offset);

        return this.dataModel.indexOfRowKey(rowKey);
    },

    /**
     * 다음 컬럼의 인덱스를 반환한다.
     * @returns {Number} 다음 컬럼의 index
     */
    nextColumnIndex: function() {
        var columnName = this.nextColumnName();

        return this.columnModel.indexOfColumnName(columnName, true);
    },

    /**
     * 이전 컬럼의 인덱스를 반환한다.
     * @returns {Number} 이전 컬럼의 인덱스
     */
    prevColumnIndex: function() {
        var columnName = this.prevColumnName();

        return this.columnModel.indexOfColumnName(columnName, true);
    },

    /**
     * keyEvent 발생 시 호출될 메서드로,
     * rowSpan 정보 까지 계산된 다음 rowKey 를 반환한다.
     * @param {number}  offset 이동할 offset
     * @returns {Number|String} offset 만큼 이동한 위치의 rowKey
     */
    nextRowKey: function(offset) {
        var focused = this.which();
        var rowKey = focused.rowKey;
        var count, rowSpanData;

        offset = (typeof offset === 'number') ? offset : 1;
        if (offset > 1) {
            rowKey = this._findRowKey(offset);
            rowSpanData = this._getRowSpanData(rowKey, focused.columnName);
            if (rowSpanData && !rowSpanData.isMainRow) {
                rowKey = this._findRowKey(rowSpanData.count + offset);
            }
        } else {
            rowSpanData = this._getRowSpanData(rowKey, focused.columnName);
            if (rowSpanData.isMainRow && rowSpanData.count > 0) {
                rowKey = this._findRowKey(rowSpanData.count);
            } else if (rowSpanData && !rowSpanData.isMainRow) {
                count = rowSpanData.count;
                rowSpanData = this._getRowSpanData(rowSpanData.mainRowKey, focused.columnName);
                rowKey = this._findRowKey(rowSpanData.count + count);
            } else {
                rowKey = this._findRowKey(1);
            }
        }

        return rowKey;
    },

    /**
     * keyEvent 발생 시 호출될 메서드로,
     * rowSpan 정보 까지 계산된 이전 rowKey 를 반환한다.
     * @param {number}  offset 이동할 offset
     * @returns {Number|String} offset 만큼 이동한 위치의 rowKey
     */
    prevRowKey: function(offset) {
        var focused = this.which();
        var rowKey = focused.rowKey;
        var rowSpanData;

        offset = typeof offset === 'number' ? offset : 1;
        offset *= -1;

        if (offset < -1) {
            rowKey = this._findRowKey(offset);
            rowSpanData = this._getRowSpanData(rowKey, focused.columnName);
            if (rowSpanData && !rowSpanData.isMainRow) {
                rowKey = this._findRowKey(rowSpanData.count + offset);
            }
        } else {
            rowSpanData = this._getRowSpanData(rowKey, focused.columnName);
            if (rowSpanData && !rowSpanData.isMainRow) {
                rowKey = this._findRowKey(rowSpanData.count - 1);
            } else {
                rowKey = this._findRowKey(-1);
            }
        }

        return rowKey;
    },

    /**
     * keyEvent 발생 시 호출될 메서드로, 다음 columnName 을 반환한다.
     * @returns {String} 다음 컬럼명
     */
    nextColumnName: function() {
        return this._findColumnName(1);
    },

    /**
     * keyEvent 발생 시 호출될 메서드로, 이전 columnName 을 반환한다.
     * @returns {String} 이전 컬럼명
     */
    prevColumnName: function() {
        return this._findColumnName(-1);
    },

    /**
     * 첫번째 row 의 key 를 반환한다.
     * @returns {(string|number)} 첫번째 row 의 키값
     */
    firstRowKey: function() {
        return this.dataModel.at(0).get('rowKey');
    },

    /**
     * 마지막 row의 key 를 반환한다.
     * @returns {(string|number)} 마지막 row 의 키값
     */
    lastRowKey: function() {
        return this.dataModel.at(this.dataModel.length - 1).get('rowKey');
    },

    /**
     * 첫번째 columnName 을 반환한다.
     * @returns {string} 첫번째 컬럼명
     */
    firstColumnName: function() {
        var columns = this.columnModel.getVisibleColumns();

        return columns[0].name;
    },

    /**
     * 마지막 columnName 을 반환한다.
     * @returns {string} 마지막 컬럼명
     */
    lastColumnName: function() {
        var columns = this.columnModel.getVisibleColumns();
        var lastIndex = columns.length - 1;

        return columns[lastIndex].name;
    },

    /**
     * Returns the address of previous cell.
     * @returns {{rowKey: number, columnName: string}}
     */
    prevAddress: function() {
        var rowKey = this.get('rowKey');
        var columnName = this.get('columnName');
        var isFirstColumn = columnName === this.firstColumnName();
        var isFirstRow = rowKey === this.firstRowKey();
        var prevRowKey, prevColumnName;

        if (isFirstRow && isFirstColumn) {
            prevRowKey = rowKey;
            prevColumnName = columnName;
        } else if (isFirstColumn) {
            prevRowKey = this.prevRowKey();
            prevColumnName = this.lastColumnName();
        } else {
            prevRowKey = rowKey;
            prevColumnName = this.prevColumnName();
        }

        return {
            rowKey: prevRowKey,
            columnName: prevColumnName
        };
    },

    /**
     * Returns the address of next cell.
     * @returns {{rowKey: number, columnName: string}}
     */
    nextAddress: function() {
        var rowKey = this.get('rowKey');
        var columnName = this.get('columnName');
        var isLastColumn = columnName === this.lastColumnName();
        var isLastRow = rowKey === this.lastRowKey();
        var nextRowKey, nextColumnName;

        if (isLastRow && isLastColumn) {
            nextRowKey = rowKey;
            nextColumnName = columnName;
        } else if (isLastColumn) {
            nextRowKey = this.nextRowKey();
            nextColumnName = this.firstColumnName();
        } else {
            nextRowKey = rowKey;
            nextColumnName = this.nextColumnName();
        }

        return {
            rowKey: nextRowKey,
            columnName: nextColumnName
        };
    }
});

module.exports = Focus;