/*
 *  Copyright 2014 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 is a tool for viewing or editing a content
 * @private
 */
Ext.define('Ametys.plugins.cms.content.tool.ContentTool', {
    extend: "Ametys.tool.Tool",
    
    statics: {
        /**
         * @private
         * @readonly
         * @property {RegExp} __URL_REGEXP The regexp to match url
         */
        __URL_REGEXP: new RegExp("^(https?:\/\/[^\/]+)(\/.*)?")
    },
    
    /**
     * @property {String} _contentId The unique identifier of the content.
     * @private
     */
    /**
     * @property {Object} _widgetInfo Informations transmitted to the widget
     * @private
     */
    
    /**
     * @cfg {String} [mode="view"] The content view mode : 'view' for viewing content, 'edit' for editing content
     */
    /**
     * @property {String} _mode The view mode. See {@link #cfg-mode}
     * @private
     */
    
    /**
     * @cfg {String} [view-name="main"] The name of view of content to use for edition.
     */
    /**
     * @property {String} _viewName The view for edition. See #cfg-view-name.
     */
    
    /**
     * @cfg {String} [fallback-view-name="main"] The fallback view for view or edition. Used (for legacy purpose) when #cfg-view-name is not found.
     */
    /**
     * @property {String} _fallbackViewName The fallback view for view or edition. See #cfg-fallback-view-name.
     */
    
    /**
     * @cfg {Number} [content-width=0] The width of content to fix width of rich text fields
     */
    /**
     * @property {String} _contentWidth The fixed width of rich text fields. See #cfg-content-width.
     */
    
    /**
     * @cfg {String} [jcr-workspace-name] The workspace name
     */
    /**
     * @property {String} _workspaceName The JCR workspace name. See #cfg-jcr-workspace-name.
     */
    
    /**
     * @cfg {Boolean/String} [ignore-workflow=false] Set to true  or 'true' to ignore workflow
     */
    /**
     * @property {Boolean} _ignoreWorkflow  True to ignore workflow. See #cfg-ignore-workflow.
     */
    
    /**
     * @cfg {Number} [edit-workflow-action=2] The workflow action to execute for editing content
     */
    /**
     * @property {Number} _workflowAction The workflow action for edition. See #cfg-workflow-action
     */
    
    /**
     * @cfg {String} [content-message-type="content"] The message type to send. Defaults to 'content'.
     * A MessageTargetFactory handling the message type and accepting an 'ids' array parameter must exist.
     */
    
    /**
     * @property {String} _contentMessageType The message type to send. See #cfg-content-message-type
     */
    
    /**
     * @private
     * @property {Ext.ux.IFrame} _iframe The iframe object
     */
    
    /**
     * @private
     * @property {String} _formId The unique id for the form edition
     */
    
    /**
     * @private
     * @property {Ext.Template} _tooltipDescriptionTpl The template used for tooltip description
     */
    
    /**
     * @private
     * @property {Ametys.relation.dd.AmetysDropZone} _dropZone The drop zone (initializated on render)
     */
    
    /**
     * @private
     * @property {Boolean} _paramsHasChanged If set params was called but not applied. Will be applied on next focus.
     */
    
    /**
     * @private
     * @property {String} _baseUrl The url that the iframe should stay on
     */
    
    /**
     * @property {Number} _autoSaveFrequency When auto-save is enabled, represents the save frequency in minutes. `0` if disabled.
     * @private
     */
    _autoSaveFrequency: 0,

    /**
     * @private
     * @property {Boolean} _openedAtStartup Was the tool opened during the UI startup
     */
    
    /**
     * @cfg {Boolean/String} [close-after-edition=false] Set to true or 'true' to close the tool when leaving the edition mode
     */
    /**
     * @property {Number} _closeAfterEdition true to close the tool when leaving the edition mode. See #cfg-close-after-edition
     */
    
    constructor: function(config)
    {
        this._widgetInfo = {};
        
        this.callParent(arguments);
        
        this._tooltipDescriptionTpl = new Ext.Template(
                "<u>{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_TOOLTIP_TEXT_LASTCONTRIBUTOR}}</u> : ",
                "<b>{author}</b><br/>",
                "<u>{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_TOOLTIP_TEXT_LASTMODIFIED}}</u> : ",
                "<b>{lastModified}</b>"
        );
        
        Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onEdited, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onDeleted, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.ARCHIVED, this._onDeleted, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.UNARCHIVED, this._onDeleted, this);
        Ametys.message.MessageBus.on('*', this._onAnyMessageBus, this);
    },
    
    createPanel: function ()
    {
        this.form = this._createFormEditionPanel();
        this.view =  this._createViewPanel();
        
        return new Ext.Panel ({
            layout: { 
                type: 'card', 
                deferredRender: false 
            },
            
            cls: 'contenttool',
            
            items: [ 
                    this.view, 
                    this.form
            ]
        });
    },
    
    /**
     * @inheritdoc
     * @param {Object} params The new parameter. Depends on the tool implementation.
     * @param {String} [params.mode="view"] The content view mode : 'view' for viewing content, 'edit' for editing content
     * @param {Boolean} [params.forceMode=false] True to bypass verification on mode when opening tool
     * @param {String} [params.view-name="main"] The name of view of content to use for edition.
     * @param {Boolean} [params.ignore-workflow=false] True to ignore workflow
     * @param {Number} [params.edit-workflow-action=2] The workflow action to execute for editing content
     * @param {String} [params.content-message-type="content"] The message type to send.
     * @param {String} [params.jcr-workspace-name] The workspace name
     * @param {String} [params.content-width] the width of content to fix the width of rich text fields in 'edit' mode.
     */
    setParams: function (params)
    {
        this._openedAtStartup = !Ametys.tool.ToolsManager.isInitialized();
        
        var requiredViewName = params['view-name'] || 'main'; 
        
        if (!params.mode && this._mode == 'edit' && this._viewName == requiredViewName)
        {
            // No explicit mode is required, stay in edition mode without refreshing
            return;
        }
        
        var requiredMode = params['mode'] || "view"; 
        if (this._mode == requiredMode && this._viewName == requiredViewName)
        {
            // The tool is already opened in required mode
            return;
        }
        
        // Clear the auto save timeout, if it exists.
        this.clearAutosaveTimeout();
        
        this.callParent(arguments);
        
        this._paramsHasChanged = true;
        
        // Was not already opened ?
        if (!this._mode)
        {
            // First render will be done by onActivate... for the moment just get minimal informations to have a tab title and tooltip
            this._setParams2();
            
            this._updateInfos();
        }
        else
        {
            this._setParams();
        }
    },

    /**
     * @private
     * This method effectively change params IF params was changed since last call.
     */
    _setParams: function()
    {
        if (!this._paramsHasChanged)
        {
            return;
        }
        
        this._paramsHasChanged = false;
        
        var params = this.getParams();
        var requiredMode = params['mode'] || "view"; 
        var requiredViewName = params['view-name'] || 'main'; 

        if (!params.forceMode && this._mode != requiredMode && this._mode == 'edit')
        {
            // Quit the edition mode ?
            if (this.isDirty())
            {
                var me = this;
                Ametys.Msg.confirm("{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_LABEL}}", 
                        "{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_CONFIRM}}", 
                        function (answer) {
                            if (answer == 'yes')
                            {
                                me._setParamsCB();
                                me.setDirty(false);
                                this.sendCurrentSelection();
                            }
                        },
                        this
                );
            }
            else
            {
                this._setParamsCB();
            }
            return;
        }
        
        if (!params.forceMode && this._mode == requiredMode && this._mode == 'edit' && this._viewName != requiredViewName)
        {
            if (this.isDirty())
            {
                var me = this;
                Ametys.Msg.confirm("{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_LABEL}}", 
                        "{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_CONFIRM}}", 
                        function (answer) {
                            if (answer == 'yes')
                            {
                                me._destroyEditionComponents();
                                me._setParamsCB();
                                me.setDirty(false);
                                this.sendCurrentSelection();
                            }
                        },
                        this
                );
            }
            else
            {
                this._destroyEditionComponents();
                this._setParamsCB();
            }
            return;
        }
        
        this._setParamsCB();
    },
    
    /**
     * Internal set parameters and refresh the tool
     * @private
     */
    _setParamsCB: function ()
    {
        this._setParams2();
        
        Ametys.cms.content.ContentDAO.getContent(this._contentId, Ext.bind(this._getContentCb, this), this._workspaceName, false);
    },

    /**
     * Internal set parameters
     * @private
     */
    _setParams2: function ()
    {
        var params = this.getParams();
        
        this._contentId = params['contentId'] || params['id'];
        this._widgetInfo.contentId = this._contentId; 
        
        this._mode = params['mode'] || 'view'; 
        this._viewName = params['view-name'] || 'main';
        this._fallbackViewName = params['fallback-view-name'] || 'main';
        this._workflowAction = params['edit-workflow-action'] || 2;
        
        this._ignoreWorkflow = Ext.isBoolean(params["ignore-workflow"]) ? params["ignore-workflow"] : params["ignore-workflow"] == "true";
        this._workspaceName = params['jcr-workspace-name'];
        this._contentMessageType = params['content-message-type'] || Ametys.message.MessageTarget.CONTENT;
        
        this._contentWidth = params['content-width'] || 0;
        
        this._closeAfterEdition = Ext.isBoolean(params["close-after-edition"]) ? params["close-after-edition"] : params["close-after-edition"] == "true";
    },
    
    /**
     * @private
     * Callback of #_setParams for the Ametys.cms.content.ContentDAO#getContent call
     * @param {Ametys.cms.content.Content} content The content retrieved
     */
    _getContentCb: function (content)
    {
        this._content = content;
        this.refresh();
    },
    
    getMBSelectionInteraction: function() 
    {
        return Ametys.tool.Tool.MB_TYPE_ACTIVE;
    },
    
    getType: function()
    {
        return Ametys.tool.Tool.TYPE_CONTENT;
    },
    
    /**
     * Function called when leaving edition mode
     */
    quitEditionMode: function ()
    {
        this.clearAutosaveTimeout();
        
        if (this._closeAfterEdition)
        {
            this.close();
        }
        else
        {
            Ametys.tool.ToolsManager.openTool("uitool-content", {id: this._contentId, mode: 'view', forceMode: true});
    
            if (this.isDirty())
            {
            	this.setDirty(false);
            }
            this.sendCurrentSelection();
        }
    },
    
    /**
     * @private Get the panel used for view content
     * @return {Ext.Panel} The panel used for view
     */
    _createViewPanel: function ()
    {
        this._iframe = Ext.create("Ext.ux.IFrame", {}); 
        this._iframe.on ('render', this._onIframeRender, this);
        this._iframe.on ('load', this._onIframeLoad, this);
        return this._iframe;
    },
    
    /**
     * @private Get the panel used for edit content
     * @return {Ext.Panel} The form panel
     */
    _createFormEditionPanel: function ()
    {
        return Ext.create('Ametys.form.ConfigurableFormPanel', {
                cls: 'content-form-inner',
                
                listeners: {
                	'fieldchange': Ext.bind(this.setDirty, this, [true], false),
                    'inputfocus':  Ext.bind(this.sendCurrentSelection, this),
                    'htmlnodeselected': Ext.bind(this.sendCurrentSelection, this),
                    'testresultschange': Ext.bind(this.sendCurrentSelection, this),
                    'formready': Ext.bind(this.onFormReady, this)
                },

                additionalWidgetsConf: {
                    contentInfo: this._widgetInfo
                },
                additionalWidgetsConfFromParams: {
                    contentType: 'contentType', // some widgets require the contentType configuration
                    editableSource: 'editableSource' // for richtext widgets we want to check the right on content
                },
                
                fieldNamePrefix: 'content.input.',
                displayGroupsDescriptions: false
        });
    },
    
    /**
	 * @private
	 * Focuses the form panel
	 */
	_focusForm: function()
	{
		this.form.focus();
	},
    
    refresh: function()
    {
        this.showRefreshing();
        
        if (this.getMode() == 'view')
        {
            this._destroyEditionComponents();
            
            this.getContentPanel().getLayout().setActiveItem(0);
            
            var wrappedUrl = this.getWrappedContentUrl ();
            
            this._iframe.load(Ametys.CONTEXT_PATH + wrappedUrl)
            this._baseUrl = Ametys.CONTEXT_PATH + wrappedUrl;
            
            this.showRefreshed();
        }
        else
        {
            this.getContentPanel().getLayout().setActiveItem(1);
            this._content.lock(Ext.bind(this._drawEdition, this), false);
        }
        
        this._updateInfos();
    },
    
    /**
     * @private
     * Listener called when the iframe is rendered.
     * Protects the iframe by handling links of the internal frame by setting a onclick on every one
     * @param {Ext.ux.IFrame} iframe The iframe
     */
    _onIframeRender: function (iframe)
    {
        this._dropZone = new Ametys.relation.dd.AmetysDropZone(iframe.getEl(), {setAmetysDropZoneInfos: Ext.bind(this.getDropInfo, this)});
    },
    
    /**
     * @private
     * This event is thrown before the beforeDrop event and create the target of the drop operation relation.
     * @param {Object} target The target records of the drop operation.
     * @param {Object} item The default drag data that will be transmitted. You have to add a 'target' item in it: 
     * @param {Object} item.target The target (in the relation way) of the drop operation. A Ametys.relation.RelationPoint config. 
     */ 
    getDropInfo: function(target, item)
    {
        item.target = {
            relationTypes: [Ametys.relation.Relation.MOVE, Ametys.relation.Relation.REFERENCE, Ametys.relation.Relation.COPY], 
            targets: {
                id: this._contentMessageType,
                parameters: { ids: [this._contentId] }
            }
        };
    },
    
    /**
     * @private
     * Listener called when the iframe is loaded.
     * Protects the iframe by handling links of the internal frame by setting a onclick on every one
     * @param {Ext.ux.IFrame} iframe The iframe
     */
    _onIframeLoad: function (iframe)
    {
        if (window.event && window.event.srcElement.readyState && !/loaded|complete/.test(window.event.srcElement.readyState))
        {
            return;
        }
        
        Ametys.plugins.cms.content.tool.ContentTool.__URL_REGEXP.test(window.location.href);
        
        var win = this._iframe.getWin();
        
        var outside = false;
        try
        {
            win.location.href
        }
        catch (e)
        {
            outside = true;
        }
        
        if (outside || (win.location.href != 'about:blank' && win.location.href.indexOf(RegExp.$1 + this._baseUrl) != 0))
        {
            var outsideUrl = win.location.href;
            // Back to iframe base url
            win.location.href = this._baseUrl;
            // Open ouside url in a new window
            window.open(outsideUrl);
        }
        else
        {
            var links = win.document.getElementsByTagName("a");
            for (var a = 0; a < links.length; a++)
            {
                if (links[a].getAttribute("internal") == null && !links[a].onclick)
                {
                    links[a].onclick = Ext.bind(this._handleLink, this, [links[a]], false);
                }
            }
            
            this._iframe.getWin().onfocus = Ext.bind(this._onIframeFocus, this);
        }
    },
    
    /**
     * @private
     * Listener when iframe takes the focus.<br/>
     * Force the focus on tool.
     */
    _onIframeFocus: function ()
    {
    	// Force focus on tool
    	this.focus();
    },
    
    /**
     * @private
     * Lazy handling of links. Each times a link is clicked, check if we should open it in a window, open another tool...
     * @param {HTMLElement} link The clicked link
     */
    _handleLink: function(link)
    {
        var currentURI = this._iframe.getWin().location.href;
        
        var absHref = link.href;
        var relHref = null;
        if (/^(https?:\/\/[^\/]+)(\/.*)?/.test(absHref) && absHref.indexOf(RegExp.$1 + Ametys.CONTEXT_PATH) == 0)
        {
            relHref = absHref.substring(absHref.indexOf(RegExp.$1 + Ametys.CONTEXT_PATH) + (RegExp.$1 + Ametys.CONTEXT_PATH).length);
        }

        // Internal link
        if (absHref.indexOf(currentURI) == 0) return true;
        
        // JS Link
        if (absHref.indexOf("javascript:") == 0) return true;
        
        // Download link
        if (relHref != null && relHref.indexOf("/plugins/explorer/download/") == 0) return true;

        // Unknown link : open in a new window
        window.open(absHref);
        return false;
    },
    
    /**
     * 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,
            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.setTitle(data.title);
    	
    	var values = {author: data.lastContributor.fullname, lastModified: Ext.util.Format.date(data.lastModified, 'd/m/Y, H:i')};
        var description = this._tooltipDescriptionTpl.applyTemplate (values);
        
        this.setDescription(description);
        
        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);
        }
        
        // Register the accessed content in navigation history
        this._saveToNavhistory();   
    },
    
    /**
     * Get the mode of the tool
     * @returns {String} The mode of the tool. Cannot be null.
     */
    getMode : function()
    {
        return this._mode;
    },
    
    /**
     * Get the unique identifier of the content.
     * @returns {String} The identifier of the content
     */
    getContentId: function()
    {
        return this._contentId;
    },
    
    /**
     * Get the name of the view for edition.
     * @return {String} The name of the view
     */
    getViewName: function()
    {
        return this._viewName;
    },
    
    /**
     * Get the name of the fallback view for edition.
     * @return {String} The name of the fallback view
     */
    getFallbackViewName: function()
    {
        return this._fallbackViewName;
    },
    
    /**
     * Get the wrapped url for content
     * @returns {String} The wrapped url
     */
    getWrappedContentUrl: function ()
    {
        var additionParams = '';
        
        // FIXME CMS-3057
        if (this._ignoreWorkflow)
        {
            additionParams += '&ignore-workflow=true';
        }
        
        additionParams += '&viewName=' + this._viewName;
        additionParams += '&lang=' + Ametys.cms.language.LanguageDAO.getCurrentLanguage(); // default rendering language for multilingual content
        
        // FIXME CMS-2316
        var appParameters = Ametys.getAppParameters();
        
        Ext.Object.each(appParameters, function(key, value) {
            additionParams += '&' + key + '=' + encodeURIComponent(value);
        });
        
        return (this._workspaceName ? '/' + this._workspaceName : '') + '/_wrapped-content.html?contentId=' + this.getContentId() + additionParams;
    },
    
    /**
     * Draw edition form
     * @private
     */
    _drawEdition: function (success)
    {
        if (!success)
        {
            Ametys.plugins.cms.content.actions.SaveAction.backToContent(this._contentId);
            return;
        }
        
        // Reset automatic backup.
        this._backupDate = null;
        this._backupCreator = null;
        this._backupData = null;
        this._hasIndexingReferences = false;
        
        Ametys.data.ServerComm.suspend();
        
        // 1 Ask server for view definition
        Ametys.data.ServerComm.callMethod({
            role: 'org.ametys.cms.content.ContentHelper',
            methodName: 'getContentViewAsJSON',
            parameters: [this._contentId, this._viewName, this._fallbackViewName, true],
            callback: {
                scope: this,
                handler: this._drawEditionForm,
                ignoreOnError: false
            },
            waitMessage: false, // The tool is refreshing
            cancelCode: this.getId() + "$1"
        });
        
        // 2 Ask server if an automatic backup exists for the content.
        Ametys.data.ServerComm.callMethod({
            role: "org.ametys.cms.content.autosave.ContentBackupClientInteraction",
            methodName: "getContentBackup",
            parameters: [this._contentId],
            callback: {
                scope: this,
                handler: this._handleAutoBackup
            },
            waitMessage: false, // The tool is refreshing
            errorMessage: {
                msg: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORMDEFINITION_ERROR}} '" + this._contentId + "'",
                category: Ext.getClassName(this)
            },
            cancelCode: this.getId() + "$2"
        });
        
        // 3 Ask server for view values
        this._getData(this._fillEditionForm);
        
        // 4 Test if the content has "indexing references".
        Ametys.data.ServerComm.callMethod({
            role: 'org.ametys.cms.content.ContentHelper',
            methodName: 'getContentEditionInformation',
            parameters: [this._contentId],
            callback: {
                scope: this,
                handler: this._getContentInformation
            },
            waitMessage: false, // The tool is refreshing
            errorMessage: {
                msg: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORMDEFINITION_ERROR}} '" + this._contentId + "'",
                category: Ext.getClassName(this)
            },
            cancelCode: this.getId() + "$4"
        });
        
        Ametys.data.ServerComm.restart();
    },
    
    _extractDataRegExp: /(<[^>\/\s]*\s)[^>]*(typeId="content"\s)[^>\/]*>(.*?)(<\/[^>\/\s]*>)/gm,
    _extractDataRegExp2: /"id":"([^"]*)"/gm,
    /**
     * @private
     */
    _extractDataFromResponse(response)
    {
        let me = this;
        
        if (me.isNotDestroyed() && me._editionFormReady && !Ametys.data.ServerComm.isBadResponse(response))
        {
            let rawData = Ext.dom.Query.selectNode("> content > metadata", response).innerHTML;
            
            // remove linked content titles and other meta that are not part of the current content
            rawData = rawData.replace(me._extractDataRegExp, function(match, g1, g2, g3, g4, index, fulltext) {
                let ids = "";
                g3.replace(me._extractDataRegExp2, function(match, id) {
                    ids += (ids ? ", ":"") + id;
                });
                return g1 + "\"id\"=\"" + ids + "\"/>";
            });
            
            return {
                version: Ext.dom.Query.selectValue("> content > @version", response),
                data: rawData
                
            };
        }
        else
        {
            return null;
        }
    },
    
    /**
     * @private
     * Download the content data
     */
    _getData: function(callback)
    {
        Ametys.data.ServerComm.send({
            url: '_content.xml',
            parameters:  {contentId: this._contentId, isEdition: "true", viewName: this._viewName, fallbackViewName: this._fallbackViewName}, 
            priority: Ametys.data.ServerComm.PRIORITY_MAJOR, 
            callback: {
                handler: callback,
                scope: this
            },
            cancelCode: this.getId() + "$3"
        });
    },
    
    /**
     * Callback function called after retrieving view 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
     */
    _drawEditionForm: function (response, args)
    {
        this._destroyEditionComponents(); // Ensure form has no components
        
        if (response === undefined)
        {
            Ametys.notify({
                title: Ext.String.format("{{i18n PLUGINS_CMS_TOOL_CONTENT_FORM_VIEW_ERROR_TITLE}}", this._content.getTitle()),
                description: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORM_VIEW_ERROR_MESSAGE}}",
                type: "warn"
            });
            
            this.getLogger().error("Cannot modify content '" + this._contentId + "' because the view is not compatible with edition");
            this.close();
        }
        else
        {
            if (this._contentWidth > 0)
            {
            	this.form.setAdditionalWidgetsConf ('wysiwygWidth', this._contentWidth);
            }
            
            // Add the hidden field that store the content version to avoid mid-air collisions at save time
            response.view.elements['_version'] = {
                "widget": "edition.hidden",
                "type": "string",
                "multiple": false,
                "label": "Version",
                "description": "Version",
                "help": null
            };

            this.form.configure(response.view.elements);
            
            this._editionFormReady = true;
            this.sendCurrentSelection();
        }
    },
    
    /**
     * Handle automatic backup: store if automatic save is enabled, the frequency, and retrieve the backup data if it exists.
     * @param {Object} result The server response as JSON object
     * @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
     * @private
     */
    _handleAutoBackup: function(result, args)
    {
        var freq = result.frequency;
        
        if (this.isNotDestroyed() && result.enabled && freq > 0)
        {
            // Store the frequency.
            this._autoSaveFrequency = freq;
            
            // Test if an auto backup exists.
            var autoBackup = result['auto-backup'];
            if (autoBackup)
            {
                // There is an auto save: store all its data for later use.
                this._backupDate = autoBackup.date;
                this._backupCreator = autoBackup.creator;
                
                this._backupData = this._getBackupData(autoBackup.data);
            }
        }
    },
    
    /**
     * Extract backup data from the server response.
     * @param {Object} data the XML data node.
     * @return {Object} The backup data object.
     * @private
     */
    _getBackupData: function(data)
    {
        var backupData = {
            values: {},
            invalid: {},
            repeaters: []
        };
        
        // Get the repeater data.
        var repeaters = data.repeaters || [];
        for (var i = 0; i < repeaters.length; i++)
        {
            var name = repeaters[i].name;
            var prefix = repeaters[i].prefix;
            var count = parseInt(repeaters[i].count);
            
            backupData.repeaters.push({
                name: name,
                prefix: prefix,
                count: count
            });
        }
        
        // Get the valid attributes values.
        var attributes = data.attributes || [];
        for (var i = 0; i < attributes.length; i++)
        {
            var name = attributes[i].name;
            var value = attributes[i].value;
            if (attributes[i].json)
            {
                value = Ext.decode(value);
            }
            
            backupData.values[name] = value;
        }
        
        // Get the invalid attributes values.
        var invalidAttributes = data['invalid-attributes'] || [];
        for (var i = 0; i < invalidAttributes.length; i++)
        {
            var name = invalidAttributes[i].name;
            var value = invalidAttributes[i].value;
            
            backupData.invalid[name] = value;
        }
        
        return backupData;
    },
    
    /**
     * Callback function called after retrieving metadata values from the
     * content. This sets the the form field values
     * 
     * @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
     */
    _fillEditionForm: function (response, args)
    {
        if (!this.isNotDestroyed())
        {
            return;
        }
        
        if (Ametys.data.ServerComm.handleBadResponse("{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORMDEFINITION_ERROR}} '" + this._contentId + "'", response, Ext.getClassName(this)))
        {
            this.showRefreshed();
            return;
        }
        
        if (!this._editionFormReady)
        {
            if (this.getLogger().isWarnEnabled())
            {
                this.getLogger().warn("#_fillEditionForm has been called but the edition form is not drawn");
            }
            this.showRefreshed();
        }
        
        this._baseData = this._extractDataFromResponse(response);
            
        // Test if there is an automatic backup for the content.
        if (this._backupDate != null && this._backupData != null)
        {
            var lastModification = Ext.dom.Query.selectValue('> content > @lastModifiedAt', response);
            
            var backupDate = Ext.Date.parse(this._backupDate, 'c');
            var lastModifDate = Ext.Date.parse(lastModification, 'c');
            

                        if (backupDate > lastModifDate)
            {
                var contentTitle = Ext.DomQuery.selectValue('content/@title', response);
                
                // The backup is more recent: ask the user if he wants to use this data instead of the regular content.
                Ametys.Msg.confirm(
                    "{{i18n plugin.cms:CONTENT_EDITION_USE_BACKUP_LABEL}}", 
                    "{{i18n plugin.cms:CONTENT_EDITION_USE_BACKUP_MESSAGE_1}}" + '"' + contentTitle + '"' + "{{i18n plugin.cms:CONTENT_EDITION_USE_BACKUP_MESSAGE_2}}", 
                    function (answer) {
                        if (answer == 'yes')
                        {
                            // Initialize the form from the backup.
                            this.form.setValues(this._backupData);
                            this.form.getForm().clearInvalid();
                            this.showRefreshed();
                        }
                        else
                        {
                            // Delete the automatic save.
                            Ametys.data.ServerComm.callMethod({
                                role: "org.ametys.cms.content.autosave.ContentBackupClientInteraction",
                                methodName: "deleteContentBackup",
                                parameters: [this._contentId],
                                waitMessage: false,
                                callback: {
                                    scope: this,
                                    handler: this._deleteAutoBackupCb
                                },
                                errorMessage: false
                            });
                            
                            // Initialize the form from the regular content.
                            this.form.setValues(response, "metadata");
                            this.showRefreshed();
                        }
                    },
                    this
                );
                return;
            }
        }
        
        this.form.setValues(response, "metadata");
        this.showRefreshed();
        
        this.form.on({
        	afterlayout: {fn: this._focusForm, scope: this, single: true}
        });
    },
    
    /**
     * Get additional edition information on the content.
     * @param {Object} result The server response as JSON object
     * @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
     * @private
     */
    _getContentInformation: function(result, args)
    {
        this._hasIndexingReferences = result.hasIndexingReferences;
    },
    
    /**
     * Destroy the all form items
     * @private
     */
    _destroyEditionComponents: function()
    {
        this.form.destroyComponents();
    },
    
    /**
     * Launch an automatic save of the current form.
     * @private
     */
    _autoSaveContent: function()
    {
        var me = this;
        function removePrefix(rawValues)
        {
            var values = {};
            Ext.Object.each(rawValues, function(key, value) {
                if (Ext.String.startsWith(key, me.form.getFieldNamePrefix()))
                {
                    key = key.substring(me.form.getFieldNamePrefix().length);
                }
                else if (Ext.String.startsWith(key, "_" + me.form.getFieldNamePrefix()))
                {
                    key = "_" + key.substring(me.form.getFieldNamePrefix().length + 1);
                }
                
                values[key] = value;
            });
            return values;            
        }
        
        if (this.getMode() == 'edit')
        {
            // Schedule the next automatic save.
            this.armAutosaveTimeout();
            
            // Get form values from the form and backup them.
            var values = removePrefix(this.form.getJsonValues());
            var invalidValues = removePrefix(this.form.getInvalidFieldValues());
            
            var repeaters = [];
            var fRepeaters = this.form.getRepeaters();
            for (var i = 0; i < fRepeaters.length; i++)
            {
                repeaters.push({
                    name: fRepeaters[i].name,
                    prefix: fRepeaters[i].prefix.substring(this.form.getFieldNamePrefix().length),
                    count: fRepeaters[i].getItemCount().toString()
                });
            }
            
            // Store the values
            Ametys.data.ServerComm.callMethod({
                role: "org.ametys.cms.content.autosave.ContentBackupClientInteraction",
                methodName: "setContentBackup",
                parameters: [this._contentId, values, invalidValues, repeaters],
                callback: {
                    scope: this,
                    handler: this._autoSaveContentCb
                },
                errorMessage: false
            });
        }
    },
    
    /**
     * Callback invoked when the automatic save is finished.
     * @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
     */
    _autoSaveContentCb: function(response, args)
    {
        // Ignore.
    },
    
    /**
     * Test if the content currently being edited has indexing references. 
     */
    hasIndexingReferences: function()
    {
        return this._hasIndexingReferences;
    },
    
    /**
     * Disarm the autosave function timer. 
     */
    clearAutosaveTimeout: function()
    {
        // Clear the auto save timeout, if it exists.
        if (this._autoSaveTimeoutId != null)
        {
            window.clearTimeout(this._autoSaveTimeoutId);
            this._autoSaveTimeoutId = null;
        }
    },
    
    /**
     * Arm the autosave function timer. 
     */
    armAutosaveTimeout: function()
    {
        // Launch the auto save timer.
        if (this._autoSaveFrequency > 0)
        {
            var millis = this._autoSaveFrequency * 60 * 1000;
            this._autoSaveTimeoutId = Ext.defer(this._autoSaveContent, millis, this);
        }
    },
    
    /**
     * @private
     * When closing edition
     */
    _closeEdit: function(manual)
    {
        // Delete the automatic save.
        Ametys.data.ServerComm.callMethod({
            role: "org.ametys.cms.content.autosave.ContentBackupClientInteraction",
            methodName: "deleteContentBackup",
            parameters: [this._contentId],
            callback: {
                scope: this,
                handler: this._deleteAutoBackupCb
            },
            errorMessage: false
        });
        
        // Unlock the content
        Ametys.cms.content.ContentDAO.unlockContent(this._contentId, this.messageTarget);
        
        Ametys.plugins.cms.content.tool.ContentTool.superclass.close.call(this, [manual]);
    },
    
    close: function (manual)
    {
        if (manual && this.getMode() == 'edit')
        {
            if (this.isDirty())
            {
                Ametys.Msg.confirm("{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_LABEL}}", 
                        "{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_CONFIRM}}", 
                        function (answer) {
                            if (answer == 'yes')
                            {
                                this.setDirty(false);
                                this._closeEdit(manual);
                            }
                        },
                        this
                );
            }
            else
            {
                this._closeEdit(manual);
            }
            return;
        }
        
        if (this._dropZone)
        {
            this._dropZone.destroy();
        }
        
        this.callParent (arguments);
    },
    
    /**
     * Callback invoked when an automatic save is deleted.
     * @param {Object} response The XML response provided by the {@link Ametys.data.ServerComm}
     * @param {Object} params The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
     * @private
     */
    _deleteAutoBackupCb: function(response, params)
    {
        // Ignore the callback.
    },
    
    onClose: function ()
    {
        // Clear the auto-save timeout.
        this.clearAutosaveTimeout();
        
        this.callParent(arguments);
    },
    
    sendCurrentSelection: function()
    {
        if (this._contentMessageType == Ametys.message.MessageTarget.CONTENT)
        {
            // For traditional 'content' messages, the content object is stored in the _content variable.
            if (this._content == null)
            {
                Ametys.cms.content.ContentDAO.getContent(this.getContentId(), Ext.bind(this._createContentTarget, this, [], 1), this._workspaceName, this._openedAtStartup ? false : undefined);
            }
            else
            {
                this._createContentTarget(this._content);
            }
        }
        else
        {
            // 'Custom' content message.
            this._createSimpleSelectionEvent();
        }
    },
    
    /**
     * Listener called when field receives the focus.
     * Fires a event of selection on message bus.
     * @param {Ext.form.Panel} form The form that is ready
     */
    onFormReady: function(form)
    {
        this.armAutosaveTimeout();
    },
    
    /**
     * Create and fire a selection event on the message bus. 
     * @private
     */
    _createSimpleSelectionEvent: function()
    {
        Ext.create('Ametys.message.Message', {
            type: Ametys.message.Message.SELECTION_CHANGED,
            
            targets: {
                id: this._contentMessageType,
                parameters: { ids: [this.getContentId()] }
            }
        });
    },
    
    /**
     * Creates and fires a event of selection on the message bus 
     * @param {Ametys.cms.content.Content} content The concerned content
     * @private
     */
    _createContentTarget: function (content)
    {
        this._content = content;
        
        if (content)
        {
            var msgParams = {};
            
            // Targets need to be prepared for that SELECTION_CHANGING says the message is ready 
            function prep(targets)
            {
                Ext.create("Ametys.message.Message", {
                    type: Ametys.message.Message.SELECTION_CHANGED,
                    parameters: msgParams,
                    targets: targets
                });
            }
            Ametys.message.MessageTargetFactory.createTargets({
                id: this._contentMessageType,
                parameters: { contents: [this._content] },
                
                subtargets: this._mode == 'edit' ? this.form.getMessageTargetConf() : null
            }, prep);
        }
    },
    
    /**
     * 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 edited message.
     * @protected
     */
    _onEdited: function (message)
    {
        var me = this;
        var contentTargets = message.getTargets(function (target) {return me._contentMessageType == target.getId();});
        
        for (var i=0; i < contentTargets.length; i++)
        {
            if (this._contentId == contentTargets[i].getParameters().id && this._mode != 'edit')
            {
                this.showOutOfDate();
            }
        }
    },
    
    /**
     * 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(function (target) {return me._contentMessageType == target.getId();});
        
        for (var i=0; i < contentTargets.length; i++)
        {
            if (this._contentId == contentTargets[i].getParameters().id)
            {
                this.close();
            }
        }
    },
    
    onActivate: function()
    {
        this.callParent(arguments);
        
        this._setParams();
    },
    
    /**
     * Saves the accessed content in navigation history
     * @private
     */
    _saveToNavhistory: function ()
    {
    	var i18nDesc = "{{i18n PLUGINS_CMS_TOOL_CONTENT_HISTORY_ENTRY_VIEW}}";
    	if (this._mode == 'edit')
    	{
    		i18nDesc = "{{i18n PLUGINS_CMS_TOOL_CONTENT_HISTORY_ENTRY_EDIT}}";
    	}
        
        var toolId = this.getFactory().getId();
        var currentToolParams = Ext.clone(this.getParams());
        delete currentToolParams.mode; // Force mode to view
        
        Ametys.navhistory.HistoryDAO.addEntry({
            id: this.getId(), 
            objectId: this._contentId,
            label: this.getTitle(),
            description: Ext.String.format(i18nDesc, Ext.String.escapeHtml(this.getTitle())),
            iconGlyph: this._mode == 'edit' ? 'ametysicon-edit5' : this.getGlyphIcon(),
            iconSmall: this.getSmallIcon(),
            iconMedium: this.getMediumIcon(),
            iconLarge: this.getLargeIcon(),
            type: Ametys.navhistory.HistoryDAO.CONTENT_TYPE,            
            action: Ext.bind(Ametys.tool.ToolsManager.openTool, Ametys.tool.ToolsManager, [toolId, currentToolParams], false)
        });
    },
    
    /**
     * Fire message before saving content
     */
    fireMessageBeforeSaving: function ()
    {
        // Nothing
    },
    
    /**
     * Fire messages on bus event when the content edition succeeded.
     */
    fireMessagesOnSuccessEdition: function ()
    {
        // Nothing
    },
    
    /**
     * Fire message on bus event when the content edition failed
     */
    fireMessageOnFailEdition: function ()
    {
        // Nothing
    },

    /**
     * Listener on any message. If the current content is concerned, the tool will update it's internal reference.
     * @param {Ametys.message.Message}  message The message.
     * @protected
     */
    _onAnyMessageBus: function(message)
    {
        var me = this;
        var contentTargets = message.getTargets(function (target) {return me._contentMessageType == target.getId();});

        for (var i=0; i < contentTargets.length; i++)
        {
            if (this._contentId == contentTargets[i].getParameters().id && contentTargets[i].getParameters().content)
            {
                //this.close();
                this._content = contentTargets[i].getParameters().content;
            }
        }
    }
    
});