/**
 * A specialized grid implementation intended to mimic the traditional property grid as
 * typically seen in development IDEs.  Each row in the grid represents a property of some object,
 * and the data is stored as a set of name/value pairs in
 * {@link Ext.grid.property.Property Properties}. By default, the editors shown are inferred
 * from the data in the cell. More control over this can be specified by using the
 * {@link #sourceConfig} option. Example usage:
 *
 *     @example
 *     Ext.create('Ext.grid.property.Grid', {
 *         title: 'Properties Grid',
 *         width: 300,
 *         renderTo: Ext.getBody(),
 *         source: {
 *             "(name)": "My Object",
 *             "Created": Ext.Date.parse('10/15/2006', 'm/d/Y'),
 *             "Available": false,
 *             "Version": 0.01,
 *             "Description": "A test object"
 *         }
 *     });
 */
Ext.define('Ext.grid.property.Grid', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.propertygrid',
    alternateClassName: 'Ext.grid.PropertyGrid',
 
    uses: [
        'Ext.grid.plugin.CellEditing',
        'Ext.grid.property.Store',
        'Ext.grid.property.HeaderContainer',
        'Ext.XTemplate',
        'Ext.grid.CellEditor',
        'Ext.form.field.Date',
        'Ext.form.field.Text',
        'Ext.form.field.Number',
        'Ext.form.field.ComboBox'
    ],
 
    /**
     * @cfg {Object} sourceConfig
     * This option allows various configurations to be set for each field in the property grid.
     * None of these configurations are required
     *
     * ####displayName
     * A custom name to appear as label for this field. If specified, the display name will be shown
     * in the name column instead of the property name. Example usage:
     *
     *     new Ext.grid.property.Grid({
     *         source: {
     *             clientIsAvailable: true
     *         },
     *         sourceConfig: {
     *             clientIsAvailable: {
     *                 // Custom name different to the field
     *                 displayName: 'Available'
     *             }
     *         }
     *     });
     *
     * ####renderer
     * A function used to transform the underlying value before it is displayed in the grid.
     * By default, the grid supports strongly-typed rendering of strings, dates, numbers and
     * booleans using built-in form editors, but any custom type can be supported and associated
     * with the type of the value. Example usage:
     *
     *     new Ext.grid.property.Grid({
     *         source: {
     *             clientIsAvailable: true
     *         },
     *         sourceConfig: {
     *             clientIsAvailable: {
     *                 // Custom renderer to change the color based on the value
     *                 renderer: function(v){
     *                     var color = v ? 'green' : 'red';
     *                     return '<span style="color: ' + color + ';">' + v + '</span>';
     *                 }
     *             }
     *         }
     *     });
     *
     * ####type
     * Used to explicitly specify the editor type for a particular value. By default, the type is
     * automatically inferred from the value. See {@link #inferTypes}. Accepted values are:
     *
     * - 'date'
     * - 'boolean'
     * - 'number'
     * - 'string'
     *
     * For more advanced control an editor configuration can be passed (see the next section).
     * Example usage:
     *
     *     new Ext.grid.property.Grid({
     *         source: {
     *             attending: null
     *         },
     *         sourceConfig: {
     *             attending: {
     *                 // Force the type to be a numberfield, a null value would otherwise default
     *                 // to a textfield
     *                 type: 'number'
     *             }
     *         }
     *     });
     *
     * ####editor
     * Allows the grid to support additional types of editable fields.  By default, the grid
     * supports strongly-typed editing of strings, dates, numbers and booleans using built-in
     * form editors, but any custom type can be supported and associated with a custom input control
     * by specifying a custom editor. Example usage
     *
     *     new Ext.grid.property.Grid({
     *         // Data object containing properties to edit
     *         source: {
     *             evtStart: '10:00 AM'
     *         },
     *
     *         sourceConfig: {
     *             evtStart: {
     *                 editor: Ext.create('Ext.form.field.Time', {selectOnFocus: true}),
     *                 displayName: 'Start Time'
     *             }
     *         }
     *     });
     */
 
    /**
    * @cfg {Object} propertyNames
    * An object containing custom property name/display name pairs.
    * If specified, the display name will be shown in the name column instead of the property name.
    * @deprecated 6.5.0 See {@link #sourceConfig} displayName
    */
 
    /**
     * @cfg {Object} source
     * A data object to use as the data source of the grid (see {@link #setSource} for details).
     */
 
    /**
     * @cfg {Object} customEditors
     * An object containing name/value pairs of custom editor type definitions that allow
     * the grid to support additional types of editable fields.  By default, the grid supports
     * strongly-typed editing of strings, dates, numbers and booleans using built-in form editors,
     * but any custom type can be supported and associated with a custom input control
     * by specifying a custom editor.  The name of the editor type should correspond
     * with the name of the property that will use the editor.  Example usage:
     *
     *     var grid = new Ext.grid.property.Grid({
     *
     *         // Custom editors for certain property names
     *         customEditors: {
     *             evtStart: Ext.create('Ext.form.TimeField', {selectOnFocus: true})
     *         },
     *
     *         // Displayed name for property names in the source
     *         propertyNames: {
     *             evtStart: 'Start Time'
     *         },
     *
     *         // Data object containing properties to edit
     *         source: {
     *             evtStart: '10:00 AM'
     *         }
     *     });
     * @deprecated 6.5.0 See {@link #sourceConfig} editor
     */
 
    /**
     * @cfg {Object} customRenderers
     * An object containing name/value pairs of custom renderer type definitions that allow
     * the grid to support custom rendering of fields. By default, the grid supports strongly-typed
     * rendering of strings, dates, numbers and booleans using built-in form editors,
     * but any custom type can be supported and associated with the type of the value.
     * The name of the renderer type should correspond with the name of the property
     * that it will render.  Example usage:
     *
     *     var grid = Ext.create('Ext.grid.property.Grid', {
     *         customRenderers: {
     *             Available: function(v){
     *                 if (v) {
     *                     return '<span style="color: green;">Yes</span>';
     *                 } else {
     *                     return '<span style="color: red;">No</span>';
     *                 }
     *             }
     *         },
     *         source: {
     *             Available: true
     *         }
     *     });
     * @deprecated 6.5.0 See {@link #sourceConfig} renderer
     */
 
    /**
     * @cfg {String} valueField
     * The name of the field from the property store to use as the value field name.
     * This may be useful if you do not configure the property Grid from an object,
     * but use your own store configuration.
     */
    valueField: 'value',
 
    /**
     * @cfg {String} nameField
     * The name of the field from the property store to use as the property field name.
     * This may be useful if you do not configure the property Grid from an object,
     * but use your own store configuration.
     */
    nameField: 'name',
 
    /**
     * @cfg {Boolean} inferTypes
     * True to automatically infer the {@link #sourceConfig type} based on the initial value passed
     * for each field. This ensures the editor remains the correct type even if the value is blanked
     * and becomes empty.
     */
    inferTypes: true,
 
    /**
     * @cfg {Number/String} [nameColumnWidth=115]
     * Specify the width for the name column. The value column will take any remaining space.
     */
 
    // private config overrides
    enableColumnMove: false,
    columnLines: true,
    stripeRows: false,
    trackMouseOver: false,
    clicksToEdit: 1,
    enableHdMenu: false,
 
    gridCls: Ext.baseCSSPrefix + 'property-grid',
 
    /**
     * @event beforepropertychange
     * Fires before a property value changes.  Handlers can return false to cancel the property
     * change (this will internally call {@link Ext.data.Model#reject} on the property's record).
     * @param {Object} source The source data object for the grid (corresponds to the same object
     * passed in as the {@link #source} config property).
     * @param {String} recordId The record's id in the data store
     * @param {Object} value The current edited property value
     * @param {Object} oldValue The original property value prior to editing
     */
 
    /**
     * @event propertychange
     * Fires after a property value has changed.
     * @param {Object} source The source data object for the grid (corresponds to the same object
     * passed in as the {@link #source} config property).
     * @param {String} recordId The record's id in the data store
     * @param {Object} value The current edited property value
     * @param {Object} oldValue The original property value prior to editing
     */
 
    initComponent: function() {
        var me = this,
            // selectOnFocus: true results in weird exceptions thrown when tabbing
            // between cell editors in IE and there's no known cure at the moment
            selectOnFocus = !Ext.isIE,
            view;
 
        me.source = me.source || {};
        me.addCls(me.gridCls);
        me.plugins = me.plugins || [];
 
        // Enable cell editing. Inject a custom startEdit which always edits column 1
        // regardless of which column was clicked.
        me.plugins.push(new Ext.grid.plugin.CellEditing({
            clicksToEdit: me.clicksToEdit,
 
            // Inject a startEdit which always edits the value column
            startEdit: function(record, column) {
                // Maintainer: Do not change this 'this' to 'me'! It is the CellEditing object's
                // own scope.
                return this.self.prototype.startEdit.call(this, record, me.valueColumn);
            },
 
            // Gets an editor based on the property name and not column itemId
            getEditor: function(record, column) {
                return this.getCachedEditor(record.get(me.nameField), record, column);
            }
        }));
 
        me.selModel = {
            type: 'cellmodel',
            onCellSelect: function(position) {
                // We are only allowed to select the value column.
                position.column = me.valueColumn;
                position.colIdx = me.valueColumn.getVisibleIndex();
 
                return this.self.prototype.onCellSelect.call(this, position);
            }
        };
 
        me.sourceConfig = Ext.apply({}, me.sourceConfig);
 
        // Create a property.Store from the source object unless configured with a store
        if (!me.store) {
            me.propStore = me.store = new Ext.grid.property.Store(me, me.source);
        }
 
        me.configure(me.sourceConfig);
 
        if (me.sortableColumns) {
            me.store.sort('name', 'ASC');
        }
 
        me.columns = new Ext.grid.property.HeaderContainer(me, me.store);
 
        me.callParent();
 
        view = me.getView();
 
        // Inject a custom implementation of walkCells which only goes up or down
        view.walkCells = me.walkCells;
 
        // Inject a custom implementation that only allows focusing value column
        view.getDefaultFocusPosition = me.getDefaultFocusPosition;
 
        // Set up our default editor set for the 4 atomic data types
        me.editors = {
            'date': new Ext.grid.CellEditor({
                field: new Ext.form.field.Date({
                    selectOnFocus: selectOnFocus
                })
            }),
            'string': new Ext.grid.CellEditor({
                field: new Ext.form.field.Text({
                    selectOnFocus: selectOnFocus
                })
            }),
            'number': new Ext.grid.CellEditor({
                field: new Ext.form.field.Number({
                    selectOnFocus: selectOnFocus
                })
            }),
            'boolean': new Ext.grid.CellEditor({
                field: new Ext.form.field.ComboBox({
                    editable: false,
                    store: [[ true, me.headerCt.trueText ], [false, me.headerCt.falseText ]]
                })
            })
        };
 
        // Track changes to the data so we can fire our events.
        me.store.on('update', me.onUpdate, me);
    },
 
    configure: function(config) {
        var me = this,
            store = me.store,
            i = 0,
            len = me.store.getCount(),
            nameField = me.nameField,
            valueField = me.valueField,
            name, value, rec, type;
 
        me.configureLegacy(config);
 
        if (me.inferTypes) {
            for (; i < len; ++i) {
                rec = store.getAt(i);
                name = rec.get(nameField);
 
                if (!me.getConfigProp(name, 'type')) {
                    value = rec.get(valueField);
 
                    if (Ext.isDate(value)) {
                        type = 'date';
                    }
                    else if (Ext.isNumber(value)) {
                        type = 'number';
                    }
                    else if (Ext.isBoolean(value)) {
                        type = 'boolean';
                    }
                    else {
                        type = 'string';
                    }
 
                    me.setConfigProp(name, 'type', type);
                }
            }
        }
    },
 
    getConfigProp: function(fieldName, key, defaultValue) {
        var config = this.sourceConfig[fieldName],
            out;
 
        if (config) {
            out = config[key];
        }
 
        return out || defaultValue;
    },
 
    setConfigProp: function(fieldName, key, value) {
        var sourceCfg = this.sourceConfig,
            o = sourceCfg[fieldName];
 
        if (!o) {
            o = sourceCfg[fieldName] = {
                __copied: true
            };
        }
        else if (!o.__copied) {
            o = Ext.apply({
                __copied: true
            }, o);
            sourceCfg[fieldName] = o;
        }
 
        o[key] = value;
 
        return value;
    },
 
    // to be deprecated in 4.2
    configureLegacy: function(config) {
        var me = this;
 
        me.copyLegacyObject(config, me.customRenderers, 'renderer');
        me.copyLegacyObject(config, me.customEditors, 'editor');
        me.copyLegacyObject(config, me.propertyNames, 'displayName');
 
        //<debug>
        // exclude types since it's new
        if (me.customRenderers || me.customEditors || me.propertyNames) {
            if (Ext.global.console && Ext.global.console.warn) {
                Ext.global.console.warn(
                    this.$className,
                    'customRenderers, customEditors & propertyNames have been consolidated ' +
                    'into a new config, see "sourceConfig". ' +
                    'These configurations will be deprecated.'
                );
            }
        }
        //</debug>
    },
 
    copyLegacyObject: function(config, o, keyName) {
        var key;
 
        for (key in o) {
            if (o.hasOwnProperty(key)) {
                if (!config[key]) {
                    config[key] = {};
                }
 
                config[key][keyName] = o[key];
            }
        }
    },
 
    /**
     * @private
     */
    onUpdate: function(store, record, operation) {
        var me = this,
            v, oldValue;
 
        if (me.rendered && operation === Ext.data.Model.EDIT) {
            v = record.get(me.valueField);
            oldValue = record.modified.value;
 
            // eslint-disable-next-line max-len
            if (me.fireEvent('beforepropertychange', me.source, record.getId(), v, oldValue) !== false) {
                if (me.source) {
                    me.source[record.getId()] = v;
                }
 
                record.commit();
                me.fireEvent('propertychange', me.source, record.getId(), v, oldValue);
            }
            else {
                record.reject();
            }
        }
    },
 
    // Custom implementation of walkCells which only goes up and down.
    // Runs in the scope of the TableView
    walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {
        var me = this,
            valueColumn = me.ownerCt.valueColumn;
 
        if (direction === 'left') {
            direction = 'up';
        }
        else if (direction === 'right') {
            direction = 'down';
        }
 
        pos = Ext.view.Table.prototype.walkCells.call(me, pos, direction, e, preventWrap,
                                                      verifierFn, scope);
 
        // We are only allowed to navigate to the value column.
        pos.column = valueColumn;
        pos.colIdx = valueColumn.getVisibleIndex();
 
        return pos;
    },
 
    getDefaultFocusPosition: function() {
        var view = this, // NOT grid!
            focusPosition;
 
        focusPosition = new Ext.grid.CellContext(view).setColumn(1);
 
        return focusPosition;
    },
 
    /**
     * @private
     * Returns the correct editor type for the property type, or a custom one keyed
     * by the property name
     */
    getCellEditor: function(record, column) {
        var me = this,
            propName = record.get(me.nameField),
            val = record.get(me.valueField),
            editor = me.getConfigProp(propName, 'editor'),
            type = me.getConfigProp(propName, 'type'),
            editors = me.editors,
            field;
 
        // A custom editor was found. If not already wrapped with a CellEditor, wrap it,
        // and stash it back
        // If it's not even a Field, just a config object, instantiate it before wrapping it.
        if (editor) {
            if (!(editor instanceof Ext.grid.CellEditor)) {
                if (!(editor instanceof Ext.form.field.Base)) {
                    editor = Ext.ComponentManager.create(editor, 'textfield');
                }
 
                editor = me.setConfigProp(
                    propName, 'editor', new Ext.grid.CellEditor({ field: editor })
                );
            }
        }
        else if (type) {
            switch (type) {
                case 'date':
                    editor = editors.date;
                    break;
 
                case 'number':
                    editor = editors.number;
                    break;
 
                case 'boolean':
                    editor = editors[type];
                    break;
 
                default:
                    editor = editors.string;
            }
        }
        else if (Ext.isDate(val)) {
            editor = editors.date;
        }
        else if (Ext.isNumber(val)) {
            editor = editors.number;
        }
        else if (Ext.isBoolean(val)) {
            editor = editors.boolean;
        }
        else {
            editor = editors.string;
        }
 
        field = editor.field;
 
        if (field && field.ui === 'default' && !field.hasOwnProperty('ui')) {
            field.ui = me.editingPlugin.defaultFieldUI;
        }
 
        // Give the editor a unique ID because the CellEditing plugin caches them
        editor.editorId = propName;
        editor.field.column = me.valueColumn;
 
        if (propName) {
            propName = Ext.String.htmlEncode(propName);
 
            if (field.rendered) {
                field.inputEl.dom.setAttribute('aria-label', propName);
            }
            else {
                field.ariaLabel = propName;
            }
        }
 
        return editor;
    },
 
    doDestroy: function() {
        var me = this;
 
        me.destroyEditors(me.editors);
        me.destroyEditors(me.customEditors);
 
        me.callParent();
    },
 
    destroyEditors: function(editors) {
        var ed;
 
        for (ed in editors) {
            if (editors.hasOwnProperty(ed)) {
                Ext.destroy(editors[ed]);
            }
        }
    },
 
    /**
     * Sets the source data object containing the property data.  The data object can contain
     * one or more name/value pairs representing all of the properties of an object to display
     * in the grid, and this data will automatically be loaded into the grid's {@link #store}.
     * The values should be supplied in the proper data type if needed, otherwise string type
     * will be assumed.  If the grid already contains data, this method will replace any
     * existing data.  See also the {@link #source} config value.  Example usage:
     *
     *     grid.setSource({
     *         "(name)": "My Object",
     *         "Created": Ext.Date.parse('10/15/2006', 'm/d/Y'),  // date type
     *         "Available": false,  // boolean type
     *         "Version": .01,      // decimal type
     *         "Description": "A test object"
     *     });
     *
     * @param {Object} source The data object.
     * @param {Object} [sourceConfig] A new {@link #sourceConfig object}. If this argument
     * is not passed the current configuration will be re-used. To reset the config,
     * pass `null` or an empty object literal.
     */
    setSource: function(source, sourceConfig) {
        var me = this;
 
        me.source = source;
 
        if (sourceConfig !== undefined) {
            me.sourceConfig = Ext.apply({}, sourceConfig);
            me.configure(me.sourceConfig);
        }
 
        me.propStore.setSource(source);
    },
 
    /**
     * Gets the source data object containing the property data.  See {@link #setSource}
     * for details regarding the format of the data object.
     * @return {Object} The data object.
     */
    getSource: function() {
        return this.propStore.getSource();
    },
 
    /**
     * Gets the value of a property.
     * @param {String} prop The name of the property.
     * @return {Object} The property value. `null` if there is no value.
     *
     * @since 5.1.1
     */
    getProperty: function(prop) {
        return this.propStore.getProperty(prop);
    },
 
    /**
     * Sets the value of a property.
     * @param {String} prop The name of the property to set.
     * @param {Object} value The value to test.
     * @param {Boolean} [create=false] `true` to create the property if it doesn't already exist.
     */
    setProperty: function(prop, value, create) {
        this.propStore.setValue(prop, value, create);
    },
 
    /**
     * Removes a property from the grid.
     * @param {String} prop The name of the property to remove.
     */
    removeProperty: function(prop) {
        this.propStore.remove(prop);
    }
 
    /**
     * @cfg {Object} store
     * @private
     */
 
    /**
     * @cfg {Object} columns
     * @private
     */
});