/**
 * 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',

    // @private
    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);
        });
    },

    // @private
    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 = me.grid = me.view = me.editor = me.keyNav = null;
        }

        me.callParent();
    },

    // @private
    getEditStyle: function() {
        return this.editStyle;
    },

    // @private
    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];
        }

        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 (c = 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;
            }
        }
    },

    // @private
    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];
        }

        var c,
            cLen = columns.length,
            column;

        for (c = 0; c < cLen; c++) {
            column = columns[c];
            column.getEditor = column.hasEditor = column.setEditor = column.field = column.editor = null;
        }
    },

    // @private
    // remaps to the public API of Ext.grid.column.Column.getEditor
    getColumnField: function(columnHeader, defaultField) {
        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;
    },

    // @private
    // remaps to the public API of Ext.grid.column.Column.hasEditor
    hasColumnField: function(columnHeader) {
        return !!(columnHeader.field && columnHeader.field.isComponent);
    },

    // @private
    // remaps to the public API of Ext.grid.column.Column.setEditor
    setColumnField: function(columnHeader, field) {
        columnHeader.field = field;
        columnHeader.field = this.createColumnField(columnHeader);
    },

    createColumnField: function (column, defaultField) {
        var field = column.field,
            dataIndex;

        if (!field && column.editor) {
            field = column.editor;
            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;
    },

    // @private
    initEvents: function() {
        var me = this;
        me.initEditTriggers();
        me.initCancelTriggers();
    },

    // @abstract
    initCancelTriggers: Ext.emptyFn,

    // @private
    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 recieve 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,
            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);
        }
    },

    // @private Used if we are triggered by the rowfocus event
    onRowFocus: function(record, row, rowIdx) {
        this.startEdit(row, 0);
    },

    // @private Used if we are triggered by the cellfocus event
    onCellFocus: function(record, cell, position) {
        this.startEdit(position.row, position.column);
    },

    // @private 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.
    onCellClick: function(view, cell, colIdx, record, row, rowIdx, e) {
        // 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 expanderSelector = view.expanderSelector,
        // Use getColumnManager() in this context because colIdx includes hidden columns.
            columnHeader = view.ownerCt.getColumnManager().getHeaderAtIndex(colIdx),
            editor = columnHeader.getEditor(record);

        if (this.shouldStartEdit(editor) && (!expanderSelector || !e.getTarget(expanderSelector))) {
            view.ownerGrid.setActionableMode(true, e.position);
        }
    },

    initAddRemoveHeaderEvents: function(){
        var me = this,
            headerCt = me.grid.headerCt;

        me.mon(headerCt, {
            scope: me,
            add: me.onColumnAdd,
            columnmove: me.onColumnMove,
            beforedestroy: me.beforeGridHeaderDestroy
        });
    },

    // @private
    onColumnAdd: function(ct, column) {
        this.initFieldAccessors(column);
    },

    // Template method which may be implemented in subclasses (RowEditing and CellEditing)
    onColumnMove: Ext.emptyFn,

    // @private
    onEscKey: function(e) {
        if (this.editing) {
            var targetComponent = Ext.getCmp(e.getTarget().getAttribute('componentId'));

            // ESCAPE when a picker is expanded does not cancel the edit
            if (!(targetComponent && targetComponent.isPickerField && targetComponent.isExpanded)) {
                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.
     * @return {Ext.grid.CellContext/undefined} The editing context based upon the passed record and column
     */
    getEditingContext: function(record, columnHeader) {
        var me = this,
            grid = me.grid,
            colMgr = grid.visibleColumnManager,
            view,
            gridRow,
            rowIdx, colIdx,
            result,
            layoutView = me.grid.lockable ? me.grid : me.view;

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

        // 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.
        view = columnHeader.getView();
        grid = view.ownerCt;

        // Ensure the row we want to edit is in the rendered range if the view is buffer rendered
        grid.ensureVisible(record, {
            column : columnHeader
        });
        
        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);

        if (Ext.isNumber(record)) {
            // look up record if numeric row index was passed
            rowIdx = record;
            record = view.getRecord(gridRow);
        } else {
            rowIdx = view.indexOf(gridRow);
        }

        // 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 = view.getCellByPosition(result, 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;
    },

    // @abstract
    validateEdit: function(context) {
        var me = this;

        return me.fireEvent('validateedit', me, context) !== false && !context.cancel;
    }
});