/*
 *  Copyright 2010 Anyware Services
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * This panel displays editable properties in a table
 * 
 * Creates your own panel class by inheriting this one and define at least the following methods: 
 * #saveProperty, #deleteProperty, #downloadBinaryProperty
 * @private
 */
Ext.define('Ametys.repository.tool.NodePropertiesTool.TablePropertiesPanel',
{
    extend: 'Ext.panel.Panel',
    
    /**
     * @cfg {Boolean} [showPendingChanges=true] Set to false to disable pending changes ui
     */
    showPendingChanges: true,
    /**
     * @cfg {Boolean} [enableEdition=true] Set to false to disable properties' edition
     */
    enableEdition: true,
    /**
     * @cfg {Boolean} [enableDeletion=true] Set to false to disable properties' deletion
     */
    enableDeletion: true,
    
    /**
     * @cfg {String[]} binaryTypes The list of types considered as binary
     */
    binaryTypes: ['binary'],
    
    collapsible: true,
    titleCollapse: true,
    animCollapse: true,
    header: {
        titlePosition: 1
    },
    
    ui: 'light',
    cls: 'table-properties-panel',
    border: false,
    
    defaults: {
        // applied to each contained panel
        bodyStyle: 'padding:10px'
    },
    bodyPadding: '10',
    
    layout: {
        type: 'table',
        columns: 5
    },
    
    /**
     * (Re-)Draw table's rows for each properties from XML nodes
     * @param {Object[]} properties the XML node properties
     */
    updatePropertiesFromXml: function (properties)
    {
        var me = this;
        this.removeAll();
        
        Ext.Array.each(properties, function (property) {
            var name = property.getAttribute('name');
            var type = property.getAttribute('type');
            var multiple = property.getAttribute('multiple') == 'true';
            
            var editable = property.getAttribute('editable') == 'true';
            var isNew = property.getAttribute('new') == 'true';
            var isModified = property.getAttribute('modified') == 'true';
            
            var values = [];
            
            if (multiple)
            {
                var nodeValues = Ext.dom.Query.select('> value', property);
                for (var j = 0; j < nodeValues.length; j++)
                {
                    values[j] = nodeValues[j].firstChild ? nodeValues[j].firstChild.nodeValue : '';
                }
            }
            else
            {
                values[0] = Ext.dom.Query.selectValue('> value', property, '');
            }
            
            me._insertRow(name, type, values, multiple, editable, isNew, isModified);
        });
    },
    
    /**
     * @private
     * Updates the HTML value and hide form
     * @param {String} name The property name
     * @param {Object} value The raw value
     */
    _updateReadableValue: function (name, value)
    {
        var form =  this._getCmp(name + '_form');
        
        if (this.showPendingChanges)
        {
            this._getCmp(name + '_pendingchanges').show();
            this._getCmp(name + '_label').addCls('modified');
        }
        
        var valueCmp = this._getCmp(name + '_value');
        var inputFd = this._getCmp(name + '_input');
        
        var html = '';
        
        var type = inputFd.type.toLowerCase();
        if (Ext.Array.contains(this.binaryTypes, type))
        {
            html = Ametys.repository.utils.Utils.formatBinaryValue([value], inputFd.multiple);
        }
        else
        {
            switch (type)
            {
	            case 'date':
	                html = Ametys.repository.utils.Utils.formatDateValue([value]);
	                break;
	            case 'reference':
                case 'weakreference':
	                html = Ametys.repository.utils.Utils.formatReferenceValue(typeof (value) == 'string' ? value.split(/\r?\n/g) : ['' + value], inputFd.multiple);
	                break;
                case 'string':
                    var values = inputFd.multiple ? value : ['' + value];
                    html = Ametys.repository.utils.Utils.formatStringValue(typeof (value) == 'string' ? value.split(/\r?\n/g) : values, inputFd.multiple, true);
                    break;
                case 'path':
                case 'name':
                case 'double':
                case 'long':
                case 'boolean':
                    var values = inputFd.multiple ? value : ['' + value];
                    html = Ametys.repository.utils.Utils.formatStringValue(typeof (value) == 'string' ? value.split(/\r?\n/g) : values, inputFd.multiple);
                    break;
	            default:
	                html = '<span class="not-supported">The #' + type + ' type is not supported for display</span>'; 
	                break;
            }
        }
        
        valueCmp.update(html);
        valueCmp.value = typeof (value) == 'string' ? value.split(/\r?\n/g) : value;
        
        form.hide();
        valueCmp.show();
    },
    
    /**
     * @protected
     * @template
     * Do save the property. 
     * You have to implement this method.
     * @param {Ext.form.Panel} form The form panel to submit the value
     * @param {String} type The property type
     * @param {String} id The id of form field.
     * @param {String} name The property name.
     * @param {String} value The property new value as string
     * @param {Function} [successCb] The callback function to call in case of success
     * @param {Function} [failureCb] The callback function to call in case of failure
     */
    saveProperty: function (form, type, id, name, value, successCb, failureCb)
    {
        throw new Error("Method #saveProperty is not implemented");
    },
    
    /**
     * @protected
     * @template
     * Do delete the property. 
     * You have to implement this method.
     * @param {String} name The property's name
     * @param {Function} [successCb] The callback function to call in case of success
     * @param {Function} [failureCb] The callback function to call in case of failure
     */
    deleteProperty: function (name, successCb, failureCb)
    {
        throw new Error("Method #deleteProperty is not implemented");
    },
    
    /**
     * @protected
     * @template
     * Download a binary property. You have to implement this method.
     * @param {String} name The property's name
     * @param {String} type The property's type
     */
    downloadBinaryProperty: function (name, type)
    {
        throw new Error("Method #downloadBinaryProperty is not implemented");
    },
    
    /**
     * @protected
     * @template
     * Function invoked when cliking on a 'reference'/'weakreference' value
     * @param {String} value the value 
     */
    onClickReferenceProperty: function (value)
    {
        throw new Error("Method #onClickReferenceProperty is not implemented");
    },
    
    /**
     * @private
     * Insert the new row
     * @param {String} name The property's name.
     * @param {String} type The property's type.
     * @param {String[]} values The property's values as a array of string.
     * @param {Boolean} multiple true if multiple
     * @param {Boolean} editable true if editable
     * @param {Boolean} isNew true if new
     * @param {Boolean} isModified true if has been modified
     */
    _insertRow: function(name, type, values, multiple, editable, isNew, isModified)
    {
        var components = [];
        
        if (this.showPendingChanges)
        {
            // Pending changes icon
            components.push({
	            xtype: 'container',
	            items: [{
	                cls: 'property-btn-unactive',
	                itemId: this._escapeId(name + '_pendingchanges'),
	                hidden: !(isNew || isModified),
	                border: false,
	                xtype: 'button',
	                width: 30,
	                iconCls: 'ametysicon-clock169',
	                tooltip: "{{i18n PLUGINS_REPOSITORYAPP_PROPERTY_PENDING_CHANGES}}"
	            }]
	        });
        }
        else
        {
            // Empty components
            components.push({
                xtype: 'component'
            });
        }
        
        
        if (this.enableDeletion && editable)
        {
            // Delete property button
            components.push({
                cls: 'property-btn',
                border: false,
                xtype: 'button',
                width: 30,
                iconCls: 'ametysicon-delete30',
                tooltip: "{{i18n PLUGINS_REPOSITORYAPP_PROPERTY_REMOVE_BUTTON_TEXT}}",
                handler: Ext.bind(this.deleteProperty, this, [name], false),
                scope: this
            });
        }
        else
        {
            // Empty components
            components.push({
                xtype: 'component'
            });
        }
        
        if (this.enableEdition && editable)
        {   
            // Edit property button
            components.push({
                cls: 'property-btn',
                xtype: 'button',
                border: false,
                iconCls: 'ametysicon-edit5',
                width: 30,
                tooltip: "{{i18n PLUGINS_REPOSITORYAPP_PROPERTY_EDIT_BUTTON_TEXT}}",
                handler: Ext.bind(this._editProperty, this, [name], false),
                scope: this
            });
        }
        else
        {
            components.push({
                xtype: 'component'
            });
        }
        
        // Property label
        components.push({
            xtype: 'component',
            itemId: this._escapeId(name + '_label'),
            flex: 0.4,
            cls: 'property-label' + (isNew || isModified ? ' modified' : ''),
            html: "<div><b>" + name + "</b> (" + type + ")</div>"
        });
        
        // Property value
        var valueCmp = {
            itemId: this._escapeId(name + '_value'),
            xtype: 'component',
            cls: 'property-value' + (!editable ? '-disabled': ''),
            flex: 1,
            html: this._formatValue(type, values, multiple),
            value: Ametys.repository.utils.Utils.getMultipleValueDisplay(values, '\n'),
            listeners: {
                render: {
                    fn: function(comp, eOpts) {
                        type = type.toLowerCase();
                        if (type == 'reference' || type == 'weakreference')
                        {
                            comp.mon(comp.getEl(), 'click', function () { this.onClickReferenceProperty(values[0]) }, this);
                        }
                        else if (Ext.Array.contains(this.binaryTypes, type))
                        {
                            comp.mon(comp.getEl(), 'click', function () { this.downloadBinaryProperty(name, type) }, this);
                        }
                        
                        if (editable && type != 'reference' && type != 'weakreference')
                        {
                            comp.mon(comp.getEl(), 'dblclick', function () { this._editProperty(name) }, this);
                        }
                    },
                    scope: this
                }
            }
        };
        
        var form = Ext.create('Ext.form.Panel', {
            itemId: this._escapeId(name + '_form'),
            defaults: {
                width: 400
            },
            border: false,
            hidden: true,
            listeners: {
                'show': function(form) {
                    // FIXME File-field workaround
                    var fileField = form.down('filefield');
                    if (fileField != null)
                    {
                        fileField.ametysSaved = false;
                        fileField.ametysChanged = false;
                    }
                },
                'hide': function(form) {
//                    var fileField = form.down('filefield');
//                    if (fileField != null)
//                    {
//                        fileField.ametysChanged = false;
//                    }
                }
            }
        });
        
        components.push({
            xtype: 'panel',
            itemId: this._escapeId(name + '_valuePanel'),
            border: false,
            items: [
                valueCmp,
                form
            ]
        });
        
        if (this.enableEdition && editable)
        {
            var editField = this._getEditionField(type, name, values, multiple);
            if (editField != null)
            {
                form.add(editField);
            }
        }
        
        this.add(components);
    },
    
    /**
     * Get a string representation of a property value.
     * @param {String} type The property type.
     * @param {Array} values The property values.
     * @param {Boolean} multiple true if the property is multi-valued, false otherwise.
     * @return {String} the HTML property representation.
     * @private
     */
    _formatValue: function(type, values, multiple)
    {
        type = type.toLowerCase();
        
        if (Ext.Array.contains(this.binaryTypes, type))
        {
            return Ametys.repository.utils.Utils.formatBinaryValue(values, multiple);
        }
        else
        {
            switch (type.toLowerCase())
	        {
	            case 'date':
	                return Ametys.repository.utils.Utils.formatDateValue(values, multiple);
	            case 'reference':
                case 'weakreference':
	                return Ametys.repository.utils.Utils.formatReferenceValue(values, multiple);
	            case 'string':
                    return Ametys.repository.utils.Utils.formatStringValue(values, multiple, true);
                case 'name':
                case 'path':
                case 'double':
                case 'long':
                case 'boolean':
	                return Ametys.repository.utils.Utils.formatStringValue(values, multiple);
                default:
                    return '<span class="not-supported">The #' + type + ' type is not supported for display</span>';    
	        }
        }
    },
    
    /**
     * @protected
     * Get the field for editing property
     * @param {String} type The type
     * @param {String} name The property name
     * @param {Object} values The property's values
     * @param {Boolean} multiple true if the property is multiple
     * @return {Ext.form.field.Base} The form field
     */
    _getEditionField: function (type, name, values, multiple)
    {
        type = type.toLowerCase();
        
        if (Ext.Array.contains(this.binaryTypes, type))
        {
             return this._getBinaryField(name, type, values);
        }
        else
        {
            switch (type)
	        {
	            case 'string':
	            case 'name':
	            case 'path':
	            case 'reference':
                case 'weakreference':
	                return this._getStringField(name, type, values, multiple);
	            case 'date':
	                return this._getDateField(name, type, values, multiple);
	            case 'boolean':
	                return this._getBooleanField(name, type, values, multiple);
	            case 'long':
	                return this._getLongField(name, type, values, multiple);
	            case 'double':
	                return this._getDoubleField(name, type, values, multiple);
	            default:
	                this.getLogger().info('[' + this.self.getName() + '] Type #' + type + ' is not supported for edition');
	                return null;
	        }
        }
        
    },
    
    /**
     * The field configuration for a string property
     * @param {String} name The property name.
     * @param {String} type The property type.
     * @param {Array} values The property values.
     * @param {Boolean} multiple true if the property is multi-valued, false otherwise.
     * @return {Ext.form.field.Base} the input field.
     * @private
     */
    _getStringField: function(name, type, values, multiple)
    {
        var cfg = {
            xtype: multiple ? 'textarea' : 'textfield',
            hideLabel: true,
            itemId: this._escapeId(name + '_input'),
            name: name,
            type: type,
            enableKeyEvents: true,
            multiple: multiple,
            listeners: {
                'specialkey': {fn: this._onKeyPress, scope: this},
                'blur': {fn: this._validEdit, scope: this}
            }
        }
        
        if (multiple)
        {
            cfg.value = Ametys.repository.utils.Utils.getMultipleValueDisplay(values, '\n', true);
        }
        else
        {
            cfg.value = values[0];
        }
        
        return cfg;
    },
    
    /**
     * The field configuration for a string property
     * @param {String} name The property name.
     * @param {String} type The property type.
     * @param {Array} values The property values.
     * @return {Ext.form.FieldContainer} the input field.
     * @private
     */
    _getDateField: function(name, type, values)
    {
        var value = values[0];
        if (!Ext.isDate(values[0]))
        {
            value = Ext.Date.parse(values[0], Ext.Date.patterns.ISO8601DateTime);
        }
        
        return {
            xtype: 'datetimefield',
            hideLabel: true,
            itemId: this._escapeId(name + '_input'),
            name: name,
            type: type,
            enableKeyEvents: true,
            allowBlank: false,
            value: value,
            listeners: {
                'specialkey': {fn: this._onKeyPress, scope: this},
                'blur': {fn: this._validEdit, scope: this}
            }
        }
    },
    
    /**
     * Draw an input field for a Long property.
     * @param {String} name The property name.
     * @param {String} type The property type.
     * @param {Array} values The property values.
     * @param {Boolean} multiple true if the property is multi-valued, false otherwise.
     * @return {Ext.form.field.Base} the input field.
     * @private
     */
    _getLongField: function(name, type, values, multiple)
    {
        var cfg = {
            hideLabel: true,
            itemId: this._escapeId(name + '_input'),
            name: name,
            type: type,
            enableKeyEvents: true,
            allowBlank: false,
            listeners: {
                'specialkey': {fn: this._onKeyPress, scope: this},
                'blur': {fn: this._validEdit, scope: this}
            }
        }
        
        if (!multiple)
        {
            return Ext.apply(cfg, {
                xtype: 'numberfield',
                allowDecimals: false,
                value: values[0]
            });
        }
        else
        {
            return Ext.apply(cfg, {
                xtype: 'textarea',
                multiple: true,
                value: Ametys.repository.utils.Utils.getMultipleValueDisplay(values, '\n', true)
            });
        }
    },
    
    /**
     * Draw an input field for a double property.
     * @param {String} name The property name.
     * @param {String} type The property type.
     * @param {Array} values The property values.
     * @param {Boolean} multiple true if the property is multi-valued, false otherwise.
     * @return {Ext.form.field.Base} the input field.
     * @private
     */
    _getDoubleField: function(name, type, values, multiple)
    {
        var cfg = {
            hideLabel: true,
            itemId: this._escapeId(name + '_input'),
            name: name,
            type: type,
            enableKeyEvents: true,
            allowBlank: false,
            listeners: {
                'specialkey': {fn: this._onKeyPress, scope: this},
                'blur': {fn: this._validEdit, scope: this}
            }
        }
        
        if (!multiple)
        {
            return Ext.apply(cfg, {
                xtype: 'numberfield',
                allowDecimals: true,
                value: values[0]
            });
        }
        else
        {
            return Ext.apply(cfg, {
                xtype: 'textarea',
                multiple: true,
                value: Ametys.repository.utils.Utils.getMultipleValueDisplay(values, '\n', true)
            });
        }
    },
    
    /**
     * Draw an input field for a Boolean property.
     * @param {String} name The property name.
     * @param {String} type The property type.
     * @param {Array} values The property values.
     * @return {Ext.form.field.Base} the input field.
     * @private
     */
    _getBooleanField: function(name, type, values)
    {
        return {
            xtype: 'combobox',
            hideLabel: true,
            itemId: this._escapeId(name + '_input'),
            name: name,
            triggerAction: 'all',
            type: type,
            width: 70,
            store: ['true', 'false'],
            enableKeyEvents: true,
            value: values[0],
            listeners: {
                'specialkey': {fn: this._onKeyPress, scope: this},
                'blur': {fn: this._validEdit, scope: this}
            } 
        }
    },
    
    /**
     * Draw an input field for a Binary property.
     * @param {String} name The property name.
     * @param {String} type The property type.
     * @param {Array} values The property values.
     * @return {Ext.form.field.Base} the input field.
     * @private
     */
    _getBinaryField: function(name, type, values)
    {
        return {
            xtype: 'filefield',
            hideLabel: true,
            itemId: this._escapeId(name + '_input'),
            name: name,
            value: values[0],
            type: type,
            enableKeyEvents: true,
            listeners: {
                'specialkey': {fn: this._onKeyPress, scope: this},
                // FIXME File-field workaround
                'blur': {fn: this._onBlurBinary, scope: this},
                'change': {fn: this._onChangeBinary, scope: this}
            }
        }
    },
    
    /**
     * Listens for enter and escape keys to save the property or cancel edition mode.
     * @param {Ext.form.field.Base} field The field on which the key was pressed.
     * @param {Ext.event.Event} event The keypress event.
     * @param {Object} eOpts Options added to the addListener
     * @private
     */
    _onKeyPress: function(field, event, eOpts)
    {
        if (event.getKey() == event.ENTER && !field.multiple)
        {
            this._validEdit(field);
        }
        else if (event.getKey() == event.ESC)
        {
            this._cancelEdit(field);
        }
    },

    /**
     * @private
     * Listener when the binary field is blured
     * @param {Ext.form.field.File} field The blured field
     * @param {Ext.event.Event} event The blur event
     */
    _onBlurBinary: function(field, event)
    {
        // FIXME File-field workaround
        if (field.ametysChanged === true && field.getValue() != '')
        {
            this._validEdit(field);
        }
    },
    
    /**
     * @private
     * Listener when the binary field value has changed
     * @param {Ext.form.field.File} field The field
     * @param {String} newValue The new value of the field
     */
    _onChangeBinary: function(field, newValue)
    {
        // FIXME File-field workaround
        field.ametysChanged = true;
    },
    
    /**
     * Validate and save the new property value.
     * @param {Ext.form.field.Base} field The property input field.
     * @private
     */
    _validEdit: function(field)
    {
        var name = field.getName();
        var value = field.fileInput != null ? field.fileInput.getValue() : field.getValue();
        
        var form = this._getCmp(name + '_form');
        if (!form.hidden)
        {
            var valueCt = this._getCmp(name + '_value');
            if (valueCt.value == value)
            {
                form.hide();
                valueCt.show();
            }
            else
            {
                if (field.multiple && !Ext.isArray(value))
                {
                    value = value.split("\n");
                }
                
                if (Ext.isDate(value))
                {
                    value = Ext.Date.format (value, Ext.Date.patterns.ISO8601DateTime);
                }
                this.saveProperty(form, field.type, field.getId(), name, value, field.multiple, Ext.bind( this._updateReadableValue, this, [name, value]));
                
//                if (!field.isXType('filefield') || field.saved === false)
//                if (!field.isXType('filefield') || field.saved === false)
//                {
                    
//                    field.saved = true;
//                }
            }
        }
    },
    
    /**
     * Inline-edit a property: hide the value field and show the input field instead.
     * @param {String} name The property name.
     * @private
     */
    _editProperty: function(name)
    {
        var form = this._getCmp(name + '_form');
        
        if (form != null)
        {
            form.show();
            
            var valueCt = this._getCmp(name + '_value');
            valueCt.hide();
            
            var inputFd = this._getCmp(name + '_input');
            inputFd.focus();
        }
    },
    
    /**
     * Cancel edition mode: hide the input field and show the value field.
     * @param {Ext.form.field.Base} field The property input field.
     * @private
     */
    _cancelEdit: function(field)
    {
        var name = field.getName();
        var form = this._getCmp(name + '_form');
        var valueCt = this._getCmp(name + '_value');
        field.setValue(valueCt.value);
        
        form.hide();
        valueCt.show();
    },
    
    /**
     * Get a component under the properties panel (at any level) by its ID.
     * @param {String} id the component ID (unescaped).
     * @return {Ext.Component} the component.
     */
    _getCmp: function(id)
    {
        return this.down('#' + this._escapeId(id));
    },
    
    /**
     * Escape a string to use as a component ID.
     * @param {String} id The string to use as ID.
     * @return {String} The escaped ID.
     */
    _escapeId: function(id)
    {
        // prefix with id to ensure item id always starts with a letter
        return 'id-' + id.replace(/[^a-z0-9\-_]/ig, '_');
    }
});