/**
 * This class provides an abstract grid editing plugin on selected
 * {@link Ext.grid.column.Column columns}. The editable columns are specified by providing an
 * {@link Ext.grid.column.Column#editor editor} in the
 * {@link Ext.grid.column.Column column configuration}.
 *
 * **Note:** This class should not be used directly. See {@link Ext.grid.plugin.CellEditing} and
 * {@link Ext.grid.plugin.RowEditing}.
 */
Ext.define('Ext.grid.plugin.Editing', {
    extend: 'Ext.plugin.Abstract',
    alias: 'editing.editing',
 
    requires: [
        'Ext.grid.column.Column',
        'Ext.util.KeyNav',
        // Requiring Ext.form.field.Base and Ext.view.Table ensures that grid editor sass
        // variables can derive from both form field vars and grid vars in the neutral theme
        'Ext.form.field.Base',
        'Ext.view.Table'
    ],
 
    mixins: [
        'Ext.mixin.Observable'
    ],
 
    /**
     * @cfg {Number} clicksToEdit
     * The number of clicks on a grid required to display the editor.
     * The only accepted values are **1** and **2**.
     */
    clicksToEdit: 2,
 
    /**
     * @cfg {String} triggerEvent
     * The event which triggers editing. Supersedes the {@link #clicksToEdit} configuration.
     * May be one of:
     *
     *  * cellclick
     *  * celldblclick
     *  * cellfocus
     *  * rowfocus
     */
    triggerEvent: undefined,
 
    /**
     * @property {Boolean} editing
     * Set to `true` while the editing plugin is active and an Editor is visible.
     */
 
    relayedEvents: [
        'beforeedit',
        'edit',
        'validateedit',
        'canceledit'
    ],
 
    /**
     * @cfg {String} default UI for editor fields
     */
    defaultFieldUI: 'default',
 
    defaultFieldXType: 'textfield',
 
    // cell, row, form
    editStyle: '',
 
    /**
     * @event beforeedit
     * Fires before editing is triggered. Return false from event handler to stop the editing.
     *
     * @param {Ext.grid.plugin.Editing} editor 
     * @param {Object} context The editing context with the following properties:
     * @param {Ext.grid.Panel} context.grid The owning grid Panel.
     * @param {Ext.data.Model} context.record The record being edited.
     * @param {String} context.field The name of the field being edited.
     * @param {Mixed} context.value The field's current value.
     * @param {HTMLElement} context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The Column being edited.
     * @param {Number} context.rowIdx The index of the row being edited.
     * @param {Number} context.colIdx The index of the column being edited.
     * @param {Boolean} context.cancel Set this to `true` to cancel the edit or return false
     * from your handler.
     * @param {Mixed} context.originalValue Alias for value (only when using
     * {@link Ext.grid.plugin.CellEditing CellEditing}).
     */
 
    /**
     * @event edit
     * Fires after editing. Usage example:
     *
     *     grid.on('edit', function(editor, e) {
     *         // commit the changes right after editing finished
     *         e.record.commit();
     *     });
     *
     * @param {Ext.grid.plugin.Editing} editor 
     * @param {Object} context The editing context with the following properties:
     * @param {Ext.grid.Panel} context.grid The owning grid Panel.
     * @param {Ext.data.Model} context.record The record being edited.
     * @param {String} context.field The name of the field being edited.
     * @param {Mixed} context.value The field's current value.
     * @param {HTMLElement} context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The Column being edited.
     * @param {Number} context.rowIdx The index of the row being edited.
     * @param {Number} context.colIdx The index of the column being edited.
     */
 
    /**
     * @event validateedit
     * Fires after editing, but before the value is set in the record. Return false
     * from event handler to cancel the change.
     *
     * Usage example showing how to remove the red triangle (dirty record indicator)
     * from some records (not all). By observing the grid's validateedit event, it can be cancelled
     * if the edit occurs on a targeted row (for example) and then setting the field's new value
     * in the Record directly:
     *
     *     grid.on('validateedit', function (editor, context) {
     *         var myTargetRow = 6;
     *
     *         if (context.rowIdx === myTargetRow) {
     *             context.record.data[context.field] = context.value;
     *         }
     *     });
     *
     * @param {Ext.grid.plugin.Editing} editor 
     * @param {Object} context The editing context with the following properties:
     * @param {Ext.grid.Panel} context.grid The owning grid Panel.
     * @param {Ext.data.Model} context.record The record being edited.
     * @param {String} context.field The name of the field being edited.
     * @param {Mixed} context.value The field's current value.
     * @param {HTMLElement} context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The Column being edited.
     * @param {Number} context.rowIdx The index of the row being edited.
     * @param {Number} context.colIdx The index of the column being edited.
     */
 
    /**
     * @event canceledit
     * Fires when the user started editing but then cancelled the edit.
     * @param {Ext.grid.plugin.Editing} editor 
     * @param {Object} context The editing context with the following properties:
     * @param {Ext.grid.Panel} context.grid The owning grid Panel.
     * @param {Ext.data.Model} context.record The record being edited.
     * @param {String} context.field The name of the field being edited.
     * @param {Mixed} context.value The field's current value.
     * @param {HTMLElement} context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The Column being edited.
     * @param {Number} context.rowIdx The index of the row being edited.
     * @param {Number} context.colIdx The index of the column being edited.
     */
 
    constructor: function(config) {
        var me = this;
 
        me.callParent([config]);
        me.mixins.observable.constructor.call(me);
        // TODO: Deprecated, remove in 5.0
        me.on("edit", function(editor, e) {
            me.fireEvent("afteredit", editor, e);
        });
    },
 
    init: function(grid) {
        var me = this,
            ownerLockable = grid.ownerLockable;
 
        me.grid = grid;
        me.view = grid.view;
        me.initEvents();
 
        // Set up fields at render and reconfigure time
        if (grid.rendered) {
            me.setup();
        }
        else {
            me.mon(grid, {
                beforereconfigure: me.onBeforeReconfigure,
                reconfigure: me.onReconfigure,
                scope: me,
                beforerender: {
                    fn: me.onBeforeRender,
                    single: true,
                    scope: me
                }
            });
        }
 
        grid.editorEventRelayers = grid.relayEvents(me, me.relayedEvents);
 
        // If the editable grid is owned by a lockable, relay up another level.
        if (ownerLockable) {
            ownerLockable.editorEventRelayers = ownerLockable.relayEvents(me, me.relayedEvents);
        }
 
        // Marks the grid as editable, so that the SelectionModel
        // can make appropriate decisions during navigation
        grid.isEditable = true;
        grid.editingPlugin = grid.view.editingPlugin = me;
    },
 
    onBeforeReconfigure: function() {
        this.reconfiguring = true;
    },
 
    /**
     * Fires after the grid is reconfigured
     * @protected
     */
    onReconfigure: function() {
        this.setup();
        delete this.reconfiguring;
    },
 
    onBeforeRender: function() {
        this.setup();
    },
 
    setup: function() {
        // In a Lockable assembly, the owner's view aggregates all grid columns across both sides.
        // We grab all columns here.
        this.initFieldAccessors(this.grid.getTopLevelColumnManager().getColumns());
    },
 
    destroy: function() {
        var me = this,
            grid = me.grid;
 
        Ext.destroy(me.keyNav);
        
        // Clear all listeners from all our events, clear all managed listeners we added
        // to other Observables
        me.clearListeners();
 
        if (grid) {
            if (grid.ownerLockable) {
                Ext.destroy(grid.ownerLockable.editorEventRelayers);
                grid.ownerLockable.editorEventRelayers = null;
            }
            
            Ext.destroy(grid.editorEventRelayers);
            grid.editorEventRelayers = null;
            
            grid.editingPlugin = grid.view.editingPlugin = null;
        }
 
        me.callParent();
    },
 
    getEditStyle: function() {
        return this.editStyle;
    },
 
    initFieldAccessors: function(columns) {
        // If we have been passed a group header, process its leaf headers
        if (columns.isGroupHeader) {
            columns = columns.getGridColumns();
        }
 
        // Ensure we are processing an array
        else if (!Ext.isArray(columns)) {
            columns = [columns];
        }
 
        // eslint-disable-next-line vars-on-top
        var me = this,
            c,
            cLen = columns.length,
            getEditor = function(record, defaultField) {
                return me.getColumnField(this, defaultField);
            },
            hasEditor = function() {
                return me.hasColumnField(this);
            },
            setEditor = function(field) {
                me.setColumnField(this, field);
            },
            column;
 
        for (= 0; c < cLen; c++) {
            column = columns[c];
 
            if (!column.getEditor) {
                column.getEditor = getEditor;
            }
 
            if (!column.hasEditor) {
                column.hasEditor = hasEditor;
            }
 
            if (!column.setEditor) {
                column.setEditor = setEditor;
            }
        }
    },
 
    removeFieldAccessors: function(columns) {
        // If we have been passed a group header, process its leaf headers
        if (columns.isGroupHeader) {
            columns = columns.getGridColumns();
        }
 
        // Ensure we are processing an array
        else if (!Ext.isArray(columns)) {
            columns = [columns];
        }
 
        // eslint-disable-next-line vars-on-top
        var c,
            cLen = columns.length,
            column;
 
        for (= 0; c < cLen; c++) {
            column = columns[c];
            column.getEditor = column.hasEditor = column.setEditor = column.field =
                column.editor = null;
        }
    },
 
    getColumnField: function(columnHeader, defaultField) {
        // remaps to the public API of Ext.grid.column.Column.getEditor
        var me = this,
            field = columnHeader.field;
 
        if (!(field && field.isFormField)) {
            field = columnHeader.field = me.createColumnField(columnHeader, defaultField);
        }
 
        if (field && field.ui === 'default' && !field.hasOwnProperty('ui')) {
            field.ui = me.defaultFieldUI;
        }
 
        return field;
    },
 
    hasColumnField: function(columnHeader) {
        // remaps to the public API of Ext.grid.column.Column.hasEditor
        return !!(columnHeader.field && columnHeader.field.isComponent);
    },
 
    setColumnField: function(columnHeader, field) {
        // remaps to the public API of Ext.grid.column.Column.setEditor
        columnHeader.field = field;
        columnHeader.field = this.createColumnField(columnHeader);
    },
 
    createColumnField: function(column, defaultField) {
        var field = column.field,
            dataIndex;
 
        if (!field && column.editor) {
            // Protect the column's editor propwerty from the mutation we are going
            // to be doing here.
            field = column.editor = Ext.clone(column.editor);
 
            // Allow for this kind of setup when CellEditing is being used, and the field
            // is wrapped in a CellEditor. They might need to configure the CellEditor.
            //    editor: {
            //        completeOnEnter: false,
            //        field: {
            //            xtype: 'combobox'
            //        }
            //    }
            if (field.field) {
                field = field.field;
                field.editorCfg = column.editor;
                delete field.editorCfg.field;
            }
 
            column.editor = null;
        }
 
        if (!field && defaultField) {
            field = defaultField;
        }
 
        if (field) {
            dataIndex = column.dataIndex;
 
            if (field.isComponent) {
                field.column = column;
            }
            else {
                if (Ext.isString(field)) {
                    field = {
                        name: dataIndex,
                        xtype: field,
                        column: column
                    };
                }
                else {
                    field = Ext.apply({
                        name: dataIndex,
                        column: column
                    }, field);
                }
 
                field = Ext.ComponentManager.create(field, this.defaultFieldXType);
            }
 
            // Stamp on the dataIndex which will serve as a reliable lookup regardless
            // of how the editor was defined (as a config or as an existing component).
            // See EXTJSIV-11650.
            field.dataIndex = dataIndex;
 
            field.isEditorComponent = true;
            column.field = field;
        }
 
        return field;
    },
 
    initEvents: function() {
        var me = this;
 
        me.initEditTriggers();
        me.initCancelTriggers();
    },
 
    initCancelTriggers: Ext.emptyFn,
 
    initEditTriggers: function() {
        var me = this,
            view = me.view;
 
        // Listen for the edit trigger event.
        if (me.triggerEvent === 'cellfocus') {
            me.mon(view, 'cellfocus', me.onCellFocus, me);
        }
        else if (me.triggerEvent === 'rowfocus') {
            me.mon(view, 'rowfocus', me.onRowFocus, me);
        }
        else {
 
            // Prevent the View from processing when the SelectionModel focuses.
            // This is because the SelectionModel processes the mousedown event, and
            // focusing causes a scroll which means that the subsequent mouseup might
            // take place at a different document XY position, and will therefore
            // not trigger a click.
            // This Editor must call the View's focusCell method directly when we
            // receive a request to edit
            if (view.getSelectionModel().isCellModel) {
                view.onCellFocus = me.beforeViewCellFocus.bind(me);
            }
 
            // Listen for whichever click event we are configured to use
            me.mon(
                view,
                me.triggerEvent || ('cell' + (me.clicksToEdit === 1 ? 'click' : 'dblclick')),
                me.onCellClick, me
            );
        }
 
        // add/remove header event listeners need to be added immediately because
        // columns can be added/removed before render
        me.initAddRemoveHeaderEvents();
 
        // Attach new bindings to the View's NavigationModel which processes cellkeydown events.
        me.view.getNavigationModel().addKeyBindings({
            esc: me.onEscKey,
            defaultEventAction: false,
            scope: me
        });
    },
 
    // Override of View's method so that we can pre-empt the View's processing if the view
    // is being triggered by a mousedown
    beforeViewCellFocus: function(position) {
        // Pass call on to view if the navigation is from the keyboard,
        // or we are not going to edit this cell.
        if (this.view.selModel.keyNavigation || !this.editing || !this.isCellEditable ||
            !this.isCellEditable(position.row, position.columnHeader)) {
            this.view.focusCell.apply(this.view, arguments);
        }
    },
 
    onRowFocus: function(record, row, rowIdx) {
        // Used if we are triggered by the rowfocus event
        this.startEdit(row, 0);
    },
 
    onCellFocus: function(record, cell, position) {
        // Used if we are triggered by the cellfocus event
        this.startEdit(position.row, position.column);
    },
 
    onCellClick: function(view, cell, colIdx, record, row, rowIdx, e) {
        // Used if we are triggered by a cellclick event
        // *IMPORTANT* Due to V4.0.0 history, the colIdx here is the index within ALL columns,
        // including hidden.
        //
        // Make sure that the column has an editor.  In the case of CheckboxModel,
        // calling startEdit doesn't make sense when the checkbox is clicked.
        // Also, cancel editing if the element that was clicked was a tree expander.
        var ownerGrid = view.ownerGrid,
            expanderSelector = view.expanderSelector,
            // Use getColumnManager() in this context because colIdx includes hidden columns.
            columnHeader = view.ownerCt.getColumnManager().getHeaderAtIndex(colIdx),
            editor = columnHeader.getEditor(record),
            targetCmp;
 
        if (this.shouldStartEdit(editor) && (!expanderSelector || !e.getTarget(expanderSelector))) {
            ownerGrid.setActionableMode(true, e.position);
        }
        // Clicking on a component in a widget column
        else if (ownerGrid.actionableMode && view.owns(e.target) &&
                 (targetCmp = Ext.Component.from(e, cell)) && targetCmp.focusable) {
            return;
        }
        // The cell is not actionable, we we must exit actionable mode
        else if (ownerGrid.actionableMode) {
            ownerGrid.setActionableMode(false);
        }
    },
 
    initAddRemoveHeaderEvents: function() {
        var me = this,
            headerCt = me.grid.headerCt;
 
        me.mon(headerCt, {
            scope: me,
            add: me.onColumnAdd,
            columnmove: me.onColumnMove,
            beforedestroy: me.beforeGridHeaderDestroy
        });
    },
 
    onColumnAdd: function(ct, column) {
        this.initFieldAccessors(column);
    },
 
    // Template method which may be implemented in subclasses (RowEditing and CellEditing)
    onColumnMove: Ext.emptyFn,
 
    onEscKey: function(e) {
        var targetComponent;
 
        if (this.editing) {
            targetComponent = Ext.getCmp(e.getTarget().getAttribute('componentId'));
 
            // ESCAPE when a picker is expanded does not cancel the edit
            if (!(targetComponent && targetComponent.isPickerField && targetComponent.isExpanded)) {
                e.stopEvent();
 
                return this.cancelEdit();
            }
        }
    },
 
    /**
     * @method
     * @private
     * @template
     * Template method called before editing begins.
     * @param {Object} context The current editing context
     * @return {Boolean} Return false to cancel the editing process
     */
    beforeEdit: Ext.emptyFn,
 
    shouldStartEdit: function(editor) {
        return !!editor;
    },
 
    /**
     * @private
     * Collects all information necessary for any subclasses to perform their editing functions.
     * @param {Ext.data.Model/Number} record The record or record index to edit.
     * @param {Ext.grid.column.Column/Number} columnHeader The column of column index to edit.
     * @param {Boolean} horizontalScroll True to scroll horizontally and display the Cell
     * in the editing context
     * @param {Ext.view.Table} view The view to get the context from (only useful
     * with lockable grids).
     * @return {Ext.grid.CellContext/undefined} The editing context based upon the passed record
     * and column
     */
    getEditingContext: function(record, columnHeader, horizontalScroll, view) {
        var me = this,
            grid = me.grid,
            colMgr = ((view && view.grid) || grid).visibleColumnManager,
            layoutView = me.grid.lockable ? me.grid : me.view,
            gridRow, rowIdx, colIdx, result;
 
        // The view must have had a layout to show the editor correctly, defer until that time.
        // In case a grid's startup code invokes editing immediately.
        if (!layoutView.componentLayoutCounter) {
            layoutView.on({
                boxready: Ext.Function.bind(me.startEdit, me, [record, columnHeader]),
                single: true
            });
 
            return;
        }
 
        // If disabled or grid collapsed, or view not truly visible, don't calculate a context -
        // we cannot edit
        if (me.disabled || me.grid.collapsed || !me.grid.view.isVisible(true)) {
            return;
        }
 
        // They've asked to edit by column number.
        // Note that in a locked grid, the columns are enumerated in a unified set for this purpose.
        if (Ext.isNumber(columnHeader)) {
            columnHeader =
                colMgr.getHeaderAtIndex(Math.min(columnHeader, colMgr.getColumns().length));
        }
 
        // No corresponding column. Possible if all columns have been moved to the other side
        // of a lockable grid pair
        if (!columnHeader) {
            return;
        }
 
        // Coerce the column to the closest visible column
        if (columnHeader.hidden) {
            columnHeader = columnHeader.next(':not([hidden])') ||
                           columnHeader.prev(':not([hidden])');
        }
 
        // Navigate to the view and grid which the column header relates to.
        if (!view) {
            view = columnHeader.getView();
        }
 
        grid = view.ownerCt;
 
        if (Ext.isNumber(record)) {
            rowIdx = Math.min(record, view.dataSource.getCount() - 1);
            record = view.dataSource.getAt(rowIdx);
        }
        else {
            rowIdx = view.dataSource.indexOf(record);
        }
 
        // Ensure the row we want to edit is in the rendered range if the view is buffer rendered
        grid.ensureVisible(record, {
            column: horizontalScroll ? columnHeader : null
        });
        
        gridRow = view.getRow(record);
 
        // An intervening listener may have deleted the Record.
        if (!gridRow) {
            return;
        }
 
        // Column index must be relative to the View the Context is using.
        // It must be the real owning View, NOT the lockable pseudo view.
        colIdx = view.getVisibleColumnManager().indexOf(columnHeader);
 
        // The record may be removed from the store but the view
        // not yet updated, so check it exists
        if (!record) {
            return;
        }
 
        // Create a new CellContext
        result = new Ext.grid.CellContext(view).setAll(view, rowIdx, colIdx, record, columnHeader);
 
        // Add extra Editing information
        result.grid = grid;
        result.store = view.dataSource;
        result.field = columnHeader.dataIndex;
        result.value = result.originalValue = record.get(columnHeader.dataIndex);
        result.row = gridRow;
        result.node = view.getNode(record);
        result.cell = result.getCell(true);
 
        return result;
    },
 
    /**
     * Cancels any active edit that is in progress.
     */
    cancelEdit: function() {
        var me = this;
 
        me.editing = false;
        me.fireEvent('canceledit', me, me.context);
    },
 
    /**
     * Completes the edit if there is an active edit in progress.
     */
    completeEdit: function() {
        var me = this;
 
        if (me.editing && me.validateEdit()) {
            me.fireEvent('edit', me, me.context);
        }
 
        me.context = null;
        me.editing = false;
    },
 
    validateEdit: function(context) {
        var me = this;
 
        return me.fireEvent('validateedit', me, context) !== false && !context.cancel;
    }
});