/**
 * The `Ext.grid.rowedit.Plugin` provides inline row editing for a `grid` or `lockedgrid`.
 * When editing begins, a small floating dialog will be shown for the appropriate row. Each
 * editable column will show a field for editing. There are configurable buttons to save
 * or cancel the edit.
 *
 * The {@link Ext.grid.column.Column#editor editors} specified for each column are used to
 * edit the record. The editor can be a field instance or a field configuration. See also
 * the {@link Ext.grid.column.Column#cfg!editable editable} config.
 *
 * The cell content will be displayed for non-editable columns.
 *
 * An appropriate field type should be chosen to match the data structure that it will be
 * editing. For example, to edit a date, a {@link Ext.field.Date datefield} would be the
 * appropriate editor. The `dataIndex` of the column and the corresponding `Ext.data.Model`
 * definition for the grid's store are consulted for the appropriate default editor type,
 * therefore in most cases, only `editable: true` is required for a column.
 *
 * @since 7.0
 */
Ext.define('Ext.grid.rowedit.Plugin', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.rowedit',
 
    requires: [
        'Ext.grid.rowedit.Editor'
    ],
 
    /**
     * @member Ext.grid.Grid
     * @event beforeedit
     * Fires before row editing is triggered. Return `false` from event handler to prevent
     * the editing.
     *
     * This event is only fired if the {@link Ext.grid.rowedit.Plugin rowedit} plugin is
     * configured on the grid.
     *
     * @param {Ext.grid.Grid} sender 
     * @param {Ext.grid.Location} location The editing location.
     * @param {Ext.grid.rowedit.RowEditor} location.editor The editor component.
     */
 
    /**
     * @member Ext.grid.Grid
     * @event edit
     * Fires after editing.
     *
     * This event is only fired if the {@link Ext.grid.rowedit.Plugin rowedit} plugin is
     * configured on the grid.
     *
     * Usage example:
     *
     *      {
     *          xtype: 'grid',
     *          plugins: 'rowedit',
     *
     *          listeners: {
     *              edit: function(grid, location) {
     *                  // commit the changes right after editing finished
     *                  location.record.commit();
     *              }
     *          }
     *      }
     *
     * @param {Ext.grid.Grid} sender 
     * @param {Ext.grid.Location} location The editing location.
     * @param {Ext.grid.rowedit.RowEditor} location.editor The editor component.
     */
 
    /**
     * @member Ext.grid.Grid
     * @event validateedit
     * Fires after editing, but before the value is set in the record. Return `false`
     * from the event handler to prevent the change.
     *
     * This event is only fired if the {@link Ext.grid.rowedit.Plugin rowedit} plugin is
     * configured on the grid.
     *
     *      {
     *          xtype: 'grid',
     *          plugins: 'rowedit',
     *
     *          listeners: {
     *              edit: function(grid, location) {
     *                  var changes = location.editor.getChanges();
     *
     *                  // validate the fields affected in changes...
     *              }
     *          }
     *      }
     *
     * @param {Ext.grid.Grid} sender 
     * @param {Ext.grid.Location} location The editing location.
     * @param {Ext.grid.rowedit.RowEditor} location.editor The editor component.
     */
 
    /**
     * @member Ext.grid.Grid
     * @event canceledit
     * Fires when the user started editing but then cancelled the edit.
     *
     * This event is only fired if the {@link Ext.grid.rowedit.Plugin rowedit} plugin is
     * configured on the grid.
     *
     * @param {Ext.grid.Grid} sender 
     * @param {Ext.grid.Location} location The editing location.
     * @param {Ext.grid.rowedit.RowEditor} location.editor The editor component.
     */
 
    config: {
        /**
         * @cfg {String} dirtyText
         * The message to display when dirty data prevents closing the row editor.
         * @locale
         */
        dirtyText: 'You need to commit or cancel your changes',
 
        /**
         * @cfg {Object/Ext.grid.plugin.RowEditor} editor
         * The config object for the row editor component.
         */
        editor: {
            lazy: true,
 
            $value: {
                xtype: 'roweditor'
            }
        },
 
        /**
         * @cfg {String} invalidToastMessage
         * A message displayed using `Ext.toast` if the user attempts to save invalid
         * data.
         *
         * Set to `null` to disable this message.
         */
        invalidToastMessage: 'Cannot save invalid data',
 
        /**
         * @cfg {String} triggerEvent
         * The pointer event to trigger editing.
         */
        triggerEvent: 'doubletap',
 
        //-------------------------------------------------------------------------
        // Internals
 
        /**
         * @cfg {Ext.grid.Grid/Ext.grid.locked.Grid} grid
         * @private
         */
        grid: null
    },
 
    cachedConfig: {
        /**
         * @cfg {Boolean/Object/"discard"} autoConfirm
         * By default, this config is set to `'discard'` which will automatically cancel
         * pending edits when the row editor {@link #cfg!repositionEvent repositions} to a
         * different row. If the record was newly added, it will be removed as the editor
         * moves to the new row.
         *
         * Set this config to `true` to automatically update the current record before
         * editing a different record.
         *
         * Set to `false` to force the user to select Discard or Update in order to leave
         * a new or modified row.
         *
         * Since newly added rows are by definition modified as soon as the row editor
         * appears, there are additional possibilities that can be controlled by using
         * an object.
         *
         *      {
         *          xtype: 'grid',
         *          plugins: {
         *              rowedit: {
         *                  autoConfirm: {
         *                      // Discard new records w/no data entered:
         *                      new: 'discard',
         *
         *                      // Require Save/Cancel for new records w/data
         *                      // entered:
         *                      populated: false,
         *
         *                      // Auto confirm updates to existing records.
         *                      updated: true
         *                  }
         *              }
         *          }
         *      }
         *
         * @cfg {Boolean/"discard"} autoConfirm.new This key determines what is done for
         * new records that have had no data entered into them.
         *
         * @cfg {Boolean/"discard"} autoConfirm.populated This key determines what is done
         * for new records that have data entered into them. If this key is not defined,
         * new records will use the value of the `new` property.
         *
         * @cfg {Boolean/"discard"} autoConfirm.updated This key determines what is done
         * with existing records that have been edited.
         */
        autoConfirm: 'discard',
 
        /**
         * @cfg {Object/Ext.Button[]} buttons
         * The buttons to be displayed below the row editor as a keyed object (or array)
         * of button configuration objects.
         *
         *      Ext.create({
         *          xtype: 'grid',
         *          ...
         *
         *          plugins: {
         *              rowedit: {
         *                  buttons: {
         *                      ok: { text: 'OK', handler: 'onOK' }
         *                  }
         *              }
         *          }
         *      });
         *
         * The {@link #minButtonWidth} is used as the default
         * {@link Ext.Button#minWidth minWidth} for the buttons in the buttons toolbar.
         */
        buttons: {
            // Standard buttons:
            ok: {
                iconCls: 'fi-check',
                text: null,
                handler: 'up.saveAndClose',
                tooltip: 'Save changes and close editor'
            },
            cancel: {
                iconCls: 'fi-times',
                text: null,
                handler: 'up.cancel',
                tooltip: 'Close editor, discarding any changes'
            },
 
            // Custom buttons:
            reset: {
                iconCls: 'fi-refresh',
                text: null,
                handler: 'up.resetChanges',
                margin: '0 0 0 8',
                tooltip: 'Reset editor to initially displayed values',
                weight: 200
            },
            revert: {
                iconCls: 'fi-undo',
                text: null,
                handler: 'up.revertChanges',
                margin: '0 0 0 8',
                tooltip: 'Reset editor to record\'s original values',
                weight: 210
            }
        },
 
        // TODO add "delete" to match features of Editable plugin:
        // delete: {
        //     iconCls: 'fi-trash',
        //     text: null,
        //     handler: 'up.onDropRecord',
        //     margin: '0 0 0 8',
        //     tooltip: 'Delete this record',
        //     weight: 300
        // }
 
        confirmation: {
            reset: 'Are you sure you want to discard the current edits?',
            revert: 'Are you sure you want to revert all edits to this record?'
        },
 
        //-----------------------------
        // Private
 
        adapters: {
            default: {
                xtype: 'roweditorcell'
            },
 
            checkcell: {
                xtype: 'checkbox',
                isRowEditorCell: true,
                bodyAlign: 'center',
                label: null,
                $hasValue: true
            },
 
            rownumberercell: {
                driver: 'rownumber'
            },
 
            expandercell: null,
            widgetcell: null
        },
 
        drivers: {
            default: {
                prop: 'value',
                commit: function(item) {
                    item.resetOriginalValue();
                },
                convert: Ext.identityFn,
                get: function(item) {
                    return item.getValue();
                },
                reset: function(item) {
                    item.reset();
                },
                set: function(item, value) {
                    if (item.forceSetValue) {
                        item.forceSetValue(value);
                    }
                    else {
                        item.setValue(value);
                    }
                }
            },
 
            checkbox: {
                prop: 'checked',
                convert: function(value) {
                    return !!value;
                },
                get: function(item) {
                    return item.getChecked();
                },
                set: function(item, value) {
                    item.setChecked(value);
                }
            },
 
            rownumber: {
                read: function(record, col, editBar) {
                    return col.printValue(editBar.parent.recordIndex + 1);
                }
            },
 
            roweditorcell: {
                prop: 'html',
                commit: Ext.emptyFn,
                convert: function(value, col) {
                    return Ext.htmlEncode(col.printValue(value));
                },
                get: function(item) {
                    return Ext.htmlDecode(item.getHtml());
                },
                reset: Ext.emptyFn,
                set: function(item, value) {
                    item.setHtml(this.convert(value, item.getColumn()));
                }
            }
        }
    },
 
    /**
     * @property {Boolean} editing
     * This property is `true` when the row editor is currently editing a row.
     * @readonly
     */
    editing: false,
 
    // /*
    //  * @cfg {String} formAriaLabel
    //  * The ARIA label template for screen readers to announce when row editing starts.
    //  * This label can be a {@link Ext.String#format} template, with the only parameter
    //  * being the row number. Note that row numbers start at base {@link #formAriaLabelRowBase}.
    //  * @locale
    //  */
    // formAriaLabel: 'Editing row {0}',
    //
    // /*
    //  * @cfg {Number} [formAriaLabelRowBase=2]
    //  * Screen readers will announce grid column header as first row of the ARIA table,
    //  * so the first actual data row is #2 for screen reader users. If your grid has
    //  * more than one column header row, you might want to increase this number.
    //  * If the column header is not visible, the base will be decreased automatically.
    //  */
    // formAriaLabelRowBase: 2,
 
    constructor: function(config) {
        this.callParent([ config || {} ]);  // ensure we call initConfig()
    },
 
    init: function(grid) {
        this.callParent([grid]);
 
        this.setGrid(grid);
    },
 
    doDestroy: function() {
        this.editing = false;
 
        this.setEditor(null);
        this.setGrid(null);
 
        this.callParent();
    },
 
    cancelEdit: function() {
        if (this.editing) {
            this.getEditor().cancel();
        }
    },
 
    completeEdit: function() {
        if (this.editing) {
            this.getEditor().saveAndClose();
        }
    },
 
    /**
     * Starts editing the specified record, using the specified Column definition to define
     * which field is being edited.
     * @param {Ext.data.Model/Ext.grid.Location} record The Store data record which backs
     * the row to be edited.
     * @param {Ext.grid.column.Column/Number} [column] The column to be focused, or index
     * of the column. If not specified, it will default to the first visible column.
     * @return {Boolean} `true` if editing was started, `false` otherwise.
     */
    startEdit: function(record, column) {
        var me = this,
            editor = me.getEditor(),
            location = record;
 
        if (!location.isLocation) {
            // TODO locked grid?
            location = me.grid.createLocation({
                record: record,
                column: column
            });
        }
 
        me.grid.ensureVisible(location.record);
 
        if (location.record && editor.beforeEdit(location) !== false) {
            editor.startEdit(location);
 
            return true;
        }
 
        return false;
    },
 
    //------------------------------------------------------------
    // Configs
 
    // autoConfirm
 
    updateAutoConfirm: function(value) {
        var full;
 
        if (!Ext.isObject(value)) {
            full = {
                new: value,
                populated: value,
                updated: value
            };
        }
        else {
            full = Ext.apply({}, value);
 
            if (!('updated' in full)) {
                full.updated = 'discard';
            }
 
            if (!('new' in full)) {
                full.new = full.updated;
            }
 
            if (!('populated' in full)) {
                full.populated = full.new;
            }
        }
 
        //<debug>
        Ext.each(['new', 'populated', 'updated'], function(key) {
            var v = full[key];
 
            if (typeof v !== 'boolean' && v !== 'discard') {
                Ext.raise('Invalid autoConfirm' + (Ext.isObject(value) ? '.' + key : '') +
                    ' value "' + v + '"');
            }
        });
        //</debug>
 
        this.$autoConfirm = full;
    },
 
    // editor
 
    applyEditor: function(config, existing) {
        return Ext.updateWidget(existing, config, this, 'createEditor');
    },
 
    createEditor: function(config) {
        var grid = this.getGrid();
 
        return Ext.apply({
            $initParent: grid,
            owner: grid,
            parent: grid,
            plugin: this,
            hidden: true,
            buttons: this.getButtons(),
            left: 0,
            right: 0,
            top: 0
        }, config);
    },
 
    updateEditor: function(editor) {
        this.editor = editor;
 
        if (editor) {
            this.getGrid().add(editor);
        }
    },
 
    // grid
 
    updateGrid: function(grid, oldGrid) {
        var me = this;
 
        me.grid = grid;
 
        if (oldGrid && !oldGrid.destroying) {
            oldGrid.unregisterActionable(this);
        }
 
        if (grid) {
            // This plugin has an interest in processing a request for actionable mode.
            // It does not actually enter actionable mode, it just calls startEdit
            grid.registerActionable(this);
        }
    },
 
    //------------------------------------------------------------
    // Internals
 
    activateCell: function(location) {
        this.startEdit(location);
    },
 
    pickGrid: function() {
        var grid = this.grid;
 
        if (grid.isLockedGrid) {
            grid = grid.regionMap.center.getGrid();
        }
 
        return grid;
    }
});