/*
 *  Copyright 2020 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 tool shows the difference between two versions of the same content.
  */
Ext.define('Ametys.plugins.cms.content.compare.CompareContentVersionTool', {
    extend: 'Ametys.tool.Tool',
    
    toolCls: ['contenttool', 'contenttoolcomparator'],

    /**
     * @cfg {String} showEmptyDataButtonIconCls The separated CSS classes to apply to button to show empty data fields
     */
    showAllDataButtonIconCls: 'ametysicon-text70 decorator-ametysicon-body-part-eye',
    
    /**
     * @cfg {String} showEmptyDataButtonTooltip Tooltip of the button to show empty data fields
     */
    showAllDataButtonTooltip: "{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_DATA_ALL}}",
    
    /**
     * @cfg {String} hideEmptyDataButtonIconCls The separated CSS classes to apply to button to hide empty data fields
     */
    showDiffDataButtonIconCls: 'ametysicon-text70 decorator-ametysicon-body-part-eye-no',
    /**
     * @cfg {String} showEmptyDataButtonTooltip Tooltip of the button to hide empty data fields
     */
    showDiffDataButtonTooltip: "{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_DATA_ONLYDIFF}}",
    
    /**
     * @property {Boolean} _showAllData Show all fields? or only modified fields...
     * @private
     */
    _showAllData: true,
        
    /**
     * @private
     * @property {String} _contentId The content id
     */
    
    /**
     * @private
     * @property {String} _baseVersion The base content version
     */
    
    /**
     * @private
     * @property {String} _version The content version to be compared
     */
    
    /**
     * @private
     * @property {Ametys.plugins.cms.content.compare.CompareContentVersionPanel} _diffPanel The panel showing the diff between the two versions of the content
     */
    
    constructor: function(config)
    {
        this.callParent(arguments);
        
        Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onMessageModified, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.WORKFLOW_CHANGED, this._onMessageModified, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onDeleted, this);
    },
    
    setParams: function(params)
    {
        this._openedAtStartup = !Ametys.tool.ToolsManager.isInitialized();

        this._contentId = params.contentId;
        
        this._baseVersion = params.baseVersion;
        this._version = params.version;
        this._onLoadHistory();
        
        this._viewName = params['view-name'] || 'default-edition';
        this._fallbackViewName= params['fallback-view-name'] || 'main';
        this._onLoadViews();
        
        if (params.allData !== undefined)
        {
            this._showAllData = !params.allData; // Will be reverted by the toggle under
            this.getWrapper().down("#alldatabtn").toggle(!params.allData);
        }
        
        this.callParent(arguments);
        
        this.showOutOfDate();
        
        this._updateInfos();
    },
    
    /**
     * Update tool information
     * @private
     */
    _updateInfos: function()
    {
        Ametys.data.ServerComm.callMethod({
            role: "org.ametys.cms.repository.ContentDAO", 
            methodName: 'getContentDescription', 
            parameters: [ this.getContentId(), this._workspaceName],
            waitMessage: false,
            cancelCode: 'CompareContentVersionTool$updateInfo$' + this.getContentId(),
            callback: {
                handler: this._getContentDescriptionCb,
                scope: this,
                ignoreOnError: false
            }
        });
    },
    
    /**
     * Callback function called after #_updateInfos is processed
     * @param {Object} data The server response
     * @param {String} data.title The title
     * @param {Object} data.lastContributor The last contributor object
     * @param {String} data.lastContributor.fullname The fullname of the last contributor
     * @param {String} data.lastModified The last modified date at 'd/m/Y, H:i' format
     * @param {String} data.iconGlyph A css class to set a glyph
     * @param {String} data.iconDecorator A css class to set a glyph decorator
     * @param {String} data.smallIcon The path to the small (16x16) icon. iconGlyph win if specified
     * @param {String} data.mediumIcon The path to the medium (32x32) icon. iconGlyph win if specified
     * @param {String} data.largeIcon The path to the large (48x48) icon. iconGlyph win if specified
     * @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
     * @private
     */
    _getContentDescriptionCb: function (data, args)
    {
        if (!this.isNotDestroyed())
        {
            return;
        }
        
        if (!data)
        {
            // Alert user then close tool
            var details = '';
            var params = this.getParams();
            for (var name in params)
            {
                details += name + " : " + params[name] + '\n';
            }
            
            if (!this._openedAtStartup)
            {
                Ametys.log.ErrorDialog.display({
                    title: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_TITLE}}",
                    text: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_CLOSE_MSG}}",
                    details: details,
                    category: this.self.getName()
                });
            }
            else
            {
                Ametys.notify({
                    title: "{{i18n PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_TITLE}}",
                    description: "{{i18n PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_CLOSE_MSG}}",
                    type: "warn"
                });
                this.getLogger().error("Cannot open unexisting content");
            }
            
            this.close();
            return;
        }
        
        this._openedAtStartup = false;

        
        this._updateTitleAndTooltip(data.title);
        /*if (data.iconGlyph)
        {
            this.setGlyphIcon(data.iconGlyph);
            this.setIconDecorator(data.iconDecorator);
        }
        else
        {
            this.setGlyphIcon(null);
            this.setSmallIcon(data.smallIcon);
            this.setMediumIcon(data.mediumIcon);
            this.setLargeIcon(data.largeIcon);
        }*/
    },
    
    /**
     * @private
     * Update the title/tooltip with versions
     * @param {String} [newTitle] If the title has changed
     */
    _updateTitleAndTooltip: function(newTitle)
    {
        newTitle = newTitle || this._rawTitle || "";
        this._rawTitle = newTitle;
        
        var toolTitle = this.getInitialConfig("dynamic-title");
        var toolDescription = this.getInitialConfig("dynamic-description");
        
        var versionIndex = this._versionsStore.find('value', this._version);
        var version = versionIndex == -1 ? '?' : this._versionsStore.getAt(versionIndex).get('label');
        var baseVersionIndex = this._versionsStore.find('value', this._baseVersion);
        var baseVersion = baseVersionIndex == -1 ? '?' : this._versionsStore.getAt(baseVersionIndex).get('label');

        this.setTitle(Ext.String.format(toolTitle, newTitle, baseVersion, version));
        this.setDescription(Ext.String.format(toolDescription, newTitle, baseVersion, version));
    },
        
    /**
     * @protected
     * Get the top panel for hint and actions
     * @return {Ext.Panel} the top panel
     */
    _getTopPanel: function ()
    {
        let rightPartWidth = 230;
        
        return Ext.create({
            dock: 'top',
            xtype: 'container',
            layout: {
                type: 'hbox',
                align: 'middle'
            },
            cls: 'top',
            items: [
                {
                    // hint
                    xtype: 'component',
                    itemId: 'hint',
                    cls: 'hint',
                    html: '<div style="text-align: right">{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_HINT_1}}&#160;</div>',
                    width: Ametys.form.ConfigurableFormPanel.LABEL_WIDTH + rightPartWidth - 80 /* right margin */
                },
                this._createHistoriesComboBox(),
                {
                    xtype: 'container',
                    layout: {
                        type: 'hbox',
                        align: 'middle'
                    },
                    width: rightPartWidth,
                    items: [
                        this._createViewsComboBox(),
                        {
                            // show/hide empty data
                            xtype: 'button',
                            itemId: 'alldatabtn',
                            iconCls: this._allDataIconCls(),
                            tooltip: this._allDataTooltip(),
                            scope: this,
                            enableToggle: true,
                            toggleHandler: this.toggleAllData,
                            cls: 'a-btn-light'
                        }
                    ]
                },
            ]
        });
    },
    
    _allDataIconCls: function()
    {
        return !this._showAllData ? this.showAllDataButtonIconCls : this.showDiffDataButtonIconCls;
    },
    _allDataTooltip: function()
    {
        return !this._showAllData ? this.showAllDataButtonTooltip : this.showDiffDataButtonTooltip;
    },
    
    /**
     * Show/hide all data
     * @param {Ext.button.Button} button The button which triggered this method. Parameter not used.
     * @param {Boolean} state If true, show empty data
     * @private
     */
    toggleAllData: function(btn, state)
    {
        this._showAllData = !this._showAllData;
        
        btn.setIconCls(this._allDataIconCls());
        btn.setTooltip(this._allDataTooltip())
        
        this.showOutOfDate();
    },
    
        /**
     * Create a combo box for the views
     * @private
     */
    _createHistoriesComboBox: function()
    {
        this._versionsStore = Ext.create('Ext.data.Store', {
            model: 'Ametys.plugins.cms.content.compare.CompareContentVersionTool.CompareContentVersion',
            proxy: {
                type: 'ametys',
                plugin: 'cms',
                url: 'version-history.json',
                reader: {
                    type: 'json'
                }
            },
            autoLoad: true,
            
            listeners: {
                load: {fn: this._onLoadHistory, scope: this},
                beforeload: {fn: this._onBeforeLoadHistory, scope: this}
            }
        });
        
        var cfg = { 
            cls: 'ametys',
            forceSelection: true,
            editable: false,
            queryMode: 'local',
            allowBlank: false,
            
            store: this._versionsStore,
            
            hidden: false,
            valueField: 'value',
            displayField: 'fullLabel',
            
            flex: 1,
            listeners: {
                change: {fn: this._onChangeVersion, scope: this}
            },
            
            tpl: new Ext.XTemplate([
                '{% this.currentGroup = null; %}',
                '<div class="x-list-plain contenttoolcomparator-version-list">',
                '<tpl for=".">',
                '<tpl for="day" if="this.shouldShowHeader(values.day)"><div class="contenttoolcomparator-version-list-category">{[this.showHeader(values.day)]}</div></tpl>',
                '<div class="x-boundlist-item contenttoolcomparator-version-list-item">{listLabel}</div>',
                '</tpl>',
                '</div>',                    
                {  
                    shouldShowHeader: function(group){
                        return this.currentGroup != group;
                    },
                    showHeader: function(group){
                        this.currentGroup = group;
                        return group;
                    }
                }
            ])
        }
        
        this._versions1combo = Ext.create('Ext.form.field.ComboBox', cfg);
        this._versions2combo = Ext.create('Ext.form.field.ComboBox', cfg);
            
        return {
            xtype: 'container',
            layout: {
                type: 'hbox',
                align: 'middle'
            },
            flex: 1,
            items: [
                this._versions1combo, 
                {
                    xtype: 'component',
                    html: "&#160;{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_HINT_2}}&#160;"
                },
                this._versions2combo 
            ]  
        };
    },
    
    /**
     * Create a combo box for the views
     * @private
     */
    _createViewsComboBox: function()
    {
        this._viewStore = Ext.create('Ext.data.Store', {
            proxy: {
                type: 'ametys',
                role: 'org.ametys.cms.repository.ContentDAO',
                methodName: 'getContentViews',
                methodArguments: ['contentId','includeInternal'],
                reader: {
                    type: 'json'
                }
            },
            
            autoLoad: true,
            sortOnLoad: true,
            sorters: [{property: 'label', direction:'ASC'}],
            
            listeners: {
                load: {fn: this._onLoadViews, scope: this},
                beforeload: {fn: this._onBeforeLoadViews, scope: this}
            }
        });
        
        this._combo = Ext.create('Ext.form.field.ComboBox', {
                cls: 'ametys',
                forceSelection: true,
                editable: false,
                queryMode: 'local',
                allowBlank: false,
                
                store: this._viewStore,
                
                hidden: false,
                valueField: 'name',
                displayField: 'label',
                labelSeparator: '',
                fieldLabel:"{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_HINT_3}}&#160;",
                width: 200,
                labelWidth: 50,
                labelAlign: "right",
                listeners: {
                    change: {fn: this._onChangeView, scope: this}
                }
            });
            
        return this._combo;
    },
        
    /**
     * Listener called before load form display view combo box
     * @param {Object} store The store
     * @param {Object[]} operation The operation
     * @private
     */
    _onBeforeLoadHistory: function(store, operation)
    {
        operation.setParams( Ext.apply(operation.getParams() || {}, {
            contentId: this._contentId
        }));
    },
    
    /**
     * Listener called before load form display view combo box
     * @param {Object} store The store
     * @param {Object[]} operation The operation
     * @private
     */
    _onBeforeLoadViews: function(store, operation)
    {
        operation.setParams( Ext.apply(operation.getParams() || {}, {
            contentId: this._contentId,
            includeInternal: true
        }));
    },
    
    /**
     * Listener called after load form display view combo box
     * @param {Object} store The store
     * @param {Object[]} data The data
     * @private
     */
    _onLoadViews: function(store, data)
    {
        if (this._combo.getStore().find('name', this._viewName) != -1)
        {
            this._combo.setValue(this._viewName);
        }
        else
        {
            this._combo.setValue(this._fallbackViewName);
        }
    },
    
    
    /**
     * Listener called after load form display view combo box
     * @param {Object} store The store
     * @param {Object[]} data The data
     * @private
     */
    _onLoadHistory: function(store, data)
    {
        let version = this._version; // Remember value, since changing the baseVersion will replace the _version variable
        if (this._versions1combo.getStore().find('value', this._baseVersion) != -1)
        {
            this._versions1combo.setValue(this._baseVersion);
        }
        if (this._versions2combo.getStore().find('value', version) != -1)
        {
            this._versions2combo.setValue(version);
        }
        
    },    
        
    /**
     * Listener called on change of the view combo box
     * @param {Object} combo The combo box
     * @param {String} newValue The new value
     * @param {String} oldValue The old value
     * @private
     */
    _onChangeView: function(combo, newValue, oldValue)
    {
        if (this._viewName != newValue)
        {
            this._viewName = newValue;
            this._updateParams();
            this.showOutOfDate();
        }
    },

    /**
     * Listener called on change of the view combo box
     * @param {Object} combo The combo box
     * @param {String} newValue The new value
     * @param {String} oldValue The old value
     * @private
     */
    _onChangeVersion: function(combo, newValue, oldValue)
    {
        var oldBase = this._baseVersion;
        var oldVersion = this._version;
         
        this._baseVersion = this._versions1combo.getValue() || this._baseVersion;
        this._version = this._versions2combo.getValue() || this._version;
        
        if (this._baseVersion != oldBase || this._version != oldVersion)
        {
            this._updateParams();
            this._updateTitleAndTooltip();
            this.showOutOfDate();
        }
    },
    
    /**
     * @private
     */
    _updateParams: function()
    {
        var newParams = Ext.apply(this.getParams(), {
            "baseVersion": this._baseVersion,
            "version": this._version,
            "view-name": this._viewName,
        });
        Ametys.plugins.cms.content.compare.CompareContentVersionTool.superclass.setParams.call(this, newParams);
    },
    
    createPanel: function()
    {
        this.form = this._createFormPanel();
        
        Ext.override(this.form, {
            // override _addRepeater to force readOnly mode
            _addRepeater: function (ct, config, initialSize)
            {
                config.readOnly = true;
                this.callParent(arguments);
            }
        });
            
        return Ext.create('Ext.Panel', { 
            cls: this.toolCls,
            layout: 'fit',
            
            dockedItems: this._getTopPanel(),
            
            items: [ 
                this.form
            ]
        });
    },
    
    _createFormPanel: function()
    {
        return Ext.create('Ametys.form.ConfigurableFormPanel', {
            itemId: 'form-panel',
            cls: 'content-form-inner',
            scrollable: true,
            
            additionalWidgetsConfFromParams: {
                contentType: 'contentType' // some widgets require the contentType configuration
            },
            
            fieldNamePrefix: '',
            displayGroupsDescriptions: false,
            
            baseMode: null // FIXME Hack to be able to set values for reference (base values), then for values to be compared (use by Ametys.form.widget.Comparator)
        });
    },
    
    refresh: function()
    {
        this.showRefreshing();
        
        this.form.destroyComponents();
        this.form.baseMode = null;
        
        // Get the comparison result
        this.serverCall("getDiffValues", [this._contentId, this._viewName, this._showAllData, this._version, this._baseVersion], this._drawForm, {refreshing: true, cancelCode: 'CompareContentVersionTool$refresh$' + this.getContentId(),});
        
        this._updateInfos();
    },
    
    /**
     * Callback function called after retrieving different values from the content.
     * This draws the form for content edition
     * @param {Object} response The XML response provided by the {@link Ametys.data.ServerComm}
     * @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
     * @private
     */
    _drawForm: function (response, args)
    {
        // Create form from view
        var viewItems = response.view.elements;
        
        if (Object.keys(viewItems).length == 0)
        {
            this.form.mask("{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_NODATA}}", "ametys-mask-unloading")
        }
        else
        {
            this.form.unmask();
        }
        
        // Could not be set during the createPanel since, the _contentId is not set yet 
        this.form.setAdditionalWidgetsConf("contentInfo", { contentId: this._contentId });
                    
        this.form.configure(viewItems);
        
        // Fill form fields
        this._fillForm(response);
        
        this.sendCurrentSelection();
    },
    
    _fillForm: function(response)
    {
        var baseValues = this._stringToXml(response.baseValues);
        var values = this._stringToXml(response.values);
        
        // FIXME REPOSITORY-454 For now we cannot have values to JSON format
        // If that is the case, we can joined the JSON values (reference and current) and call this.form.setValues once.
        // Here we have to #setValues twice : for reference values and for current values, which is really bad
        // When it will be fixed, baseMode boolean could be removed.
        
        // Switch to reference mode to set reference values
        this.form.baseMode = true;
        this.form.setValues(baseValues, "metadata");
        this._overrideFormAfterBaseValuesAreSet();
        
        // Switch to current mode to set current values
        this.form.baseMode = false;
        this.form.setValues(values, "metadata");
        
        // Set the comparison state
        this._setDiffState(response.changedAttributeDataPaths);
        
        // Expand repeaters
        this._expandRepeaterEntries();
        
        this.showRefreshed();
    },
    
    /**
     * @private
     */
    _overrideFormAfterBaseValuesAreSet: function()
    {
        var allRepeaters = this.form.query("panel[isRepeater]");
        Ext.Array.forEach(allRepeaters, function(repeater) {
            Ext.override(repeater, {
                // the #setValues > ... > #_setValuesXMLMetadata > repeaterPanel#removeItem
                // can be called
                // so we override here to prevent any removing of any repeater entry
                removeItem: function(itemPanel)
                {
                    // Do not call parent, prevent any removing !
                    // there is a target repeater entry which is not present on current values => it was removed
                }
            });
        });
    },
    
    /**
     * @private
     * Set the diff state for each comparison fields
     * @param {Object} changedAttributeDataPaths The path of attributes with changes
     */
    _setDiffState: function(changedAttributeDataPaths)
    {
        var me = this;
        
        function _hasDiff(name)
        {
            return Ext.Array.contains(changedAttributeDataPaths, name) /* trivial case => the current data path is in the changes */
                    || _parentRepeaterOrCompositeHasChanged(name);
        }
        
        function _parentRepeaterOrCompositeHasChanged(name)
        {
            // case where changedAttributeDataPaths contains 'rep[2]' for instance, and the current data path is 'rep[2]/someStringAttribute'
            // => it should be marked with a diff too
            return changedAttributeDataPaths
                    .filter(function(dataPath) {
                        return Ext.String.startsWith(name, dataPath + "/")
                            || Ext.String.startsWith(name, dataPath + "[");
                    })
                    .length > 0;
        }
        
        var comparisonFields = this._getComparisonFields();
        Ext.Array.each(comparisonFields, function(field){
        
            var fieldName = field.getName();
            if (_hasDiff(fieldName))
            {
                field.setInDiffState(true);
            }
            else if (!me._showAllData)
            {
                field.hide(); // can happen in repeaters
                _removeIfNotAnyVisibleChild(field.ownerCt);
                {
                    
                }
            }
        });
        
        function _removeIfNotAnyVisibleChild(panel)
        {
            let oneChildIsVisible = false;
            panel.items.each(function(item) {
                if (!item.isHidden())
                {
                    oneChildIsVisible = true;
                    return false;
                }
            });
            
            if (!oneChildIsVisible)
            {
                let parent = panel.ownerCt;
                panel.ownerCt.remove(panel);
                _removeIfNotAnyVisibleChild(parent);
            }
        }
    },
    
    /**
     * @private
     * Expands all entries of all repeaters
     */
    _expandRepeaterEntries: function()
    {
        this.form.query('panel[isRepeater]').forEach(function(repeater) {
            repeater.expandAll();
        });
    },
    
    /**
     * Get the comparison fields
     * @return {Ext.form.Field[]} the comparison fields
     */
    _getComparisonFields: function()
    {
        return this.form.getFieldNames()
            .map(function(fieldName) {
                return this.form.getField(fieldName);
            }, this)
            .filter(function(field) { 
                return field.isComparatorField; 
            });
    },
    
    /**
     * Get the unique identifier of the content.
     * @returns {String} The identifier of the content
     */
    getContentId: function()
    {
        return this._contentId;
    },
    
    
    /**
     * Listener on {@link Ametys.message.Message#MODIFIED} message. If the current content is concerned, the tool will be out-of-date.
     * @param {Ametys.message.Message} message The message.
     * @protected
     */
    _onMessageModified: function(message)
    {
        var targets = message.getTargets(Ametys.message.MessageTarget.CONTENT);
        var contentIds = targets.map(function(target) {
            return target.getParameters().id;
        });
        
        if (Ext.Array.contains(contentIds, this._contentId))
        {
            this._versions1combo.getStore().reload();
            this._versions2combo.getStore().reload();
        }
    },
    
    /**
     * Listener on {@link Ametys.message.Message#DELETED} message. If the current content is concerned, the tool will be closed.
     * @param {Ametys.message.Message}  message The deleted message.
     * @protected
     */
    _onDeleted: function (message)
    {
        var me = this;
        var contentTargets = message.getTargets(Ametys.message.MessageTarget.CONTENT);
        
        for (var i=0; i < contentTargets.length; i++)
        {
            if (this._contentId == contentTargets[i].getParameters().id)
            {
                this.close();
            }
        }
    },
    
    getMBSelectionInteraction: function() 
    {
        return Ametys.tool.Tool.MB_TYPE_ACTIVE;
    },
    
    getType: function()
    {
        return Ametys.tool.Tool.TYPE_CONTENT;
    },
    
    sendCurrentSelection: function()
    {
        return Ext.create('Ametys.message.Message', {
            type: Ametys.message.Message.SELECTION_CHANGED,
            targets: {
                id: Ametys.message.MessageTarget.CONTENT,
                parameters: {
                    ids: [this._contentId]
                }
            }
        });
    },
    
    /**
     * @private
     * Converts given string into XMLDocument
     * @param {String} xmlAsString The XML/HTML as string
     * @return {XMLDocument} The XML representation
     */
    _stringToXml: function(xmlAsString)
    {
        var xmlDoc = new DOMParser().parseFromString(xmlAsString, 'text/xml');
        return xmlDoc;
    }
});