/*
 *  Copyright 2015 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 class allow to create a message for the ametys server : it will use the dispatch generator to group requests. 
 * 
 * See #send for more information on sending request to an url:
 * 
 *      Ametys.data.ServerComm.send({
 *          plugin: 'cms',
 *          url: 'contents/get-info',
 *          parameters: {ids: ids}, 
 *          priority: Ametys.data.ServerComm.PRIORITY_MAJOR, 
 *          callback: {
 *              handler: this._getContentsCB,
 *              scope: this,
 *              arguments: [callback]
 *          },
 *          responseType: 'text'
 *      });
 *      
 * See #callMethod for more information on directly calling a java method:
 * 
 *      Ametys.data.ServerComm.callMethod({
 *          role: "org.ametys.core.ui.RibbonControlsManager",
 *          id: "org.ametys.cms.content.Delete",
 *          methodName: "getStatus",
 *          parameters: [
 *              ['content://12345678-1234-1234']
 *          ],
 *          callback: {
 *              handler: function(a) { alert(a) },
 *              ignoreOnError: false
 *          },
 *          waitMessage: true,
 *          errorMessage: { msg: 'An error occured' }
 *      });   
 * 
 * The mapping between Javascript types and Java types is the following:
 * 
 * <table>
 *  <tr><th><strong>JAVASCRIPT TYPE</strong></th>       <th><strong>JAVA CLASS</strong></th></tr>
 *  <tr><td>object</td>                     <td>java.util.LinkedHashMap&lt;String, Object&gt;</td></tr>
 *  <tr><td>array</td>                      <td>java.util.ArrayList&lt;Object&gt;</td></tr>
 *  <tr><td>string</td>                     <td>java.lang.String</td></tr>
 *  <tr><td>Number (without decimals)</td>  <td>java.lang.Integer, java.lang.Long or java.math.BigInteger<br/>(the smallest possible)</td></tr>
 *  <tr><td>Number (with decimal)</td>      <td>java.lang.Double</td></tr>
 *  <tr><td>boolean</td>                    <td>java.lang.Boolean</td></tr>
 *  <tr><td>null</td>                       <td>null</td></tr>
 * </table>
 */
Ext.define(
    "Ametys.data.ServerComm", 
    {
        singleton: true,
        
        /**
         * @private
         * @readonly
         * @type String
         * The full url to the server dispatch url
         */
        SERVERCOMM_URL: Ametys.getPluginDirectPrefix("core-ui") + "/servercomm/messages.xml",
        
        /**
         * The enumeration for message priority : The message will leave now regardless of the queue and of the suspend.
         * The request will have a 10x longer timeout.
         * The request will be alone.
         * Sample: creating a long search message.
         * @type {Number}
         * @readonly
         */
        PRIORITY_LONG_REQUEST: -2,
        /**
         * The enumeration for message priority : The message will leave now regardless of the queue and of the suspend.
         * The send method will become blocking and will return the response.
         * Callback is simply ignored.
         * Sample: creating a target message.
         * @type {Number}
         * @readonly
         */
        PRIORITY_SYNCHRONOUS: -1,
        /**
         * The enumeration for message priority : The message needs to leave as soon as possible.
         * Sample: saving any user modifications.
         * @type {Number}
         * @readonly  
         */
        PRIORITY_MAJOR: 0,
        /**
         * The enumeration for message priority : The message can be delayed.
         * Sample : updating a minor view.
         * @type {Number}
         * @readonly  
         */
        PRIORITY_NORMAL: 10,
        /**
         * The enumeration for message priority : The message is for background use.
         * Sample : ping, preferences save.
         * @type {Number}
         * @readonly  
         */
        PRIORITY_MINOR: 40,
        /**
         * The enumeration for message priority : The message is for beacon use. This is a message with no return value and low priority that can be sent when unloading the page.
         * Sample : preferences save.
         * @type {Number}
         * @readonly  
         */
        PRIORITY_BEACON: -3,
        /**
         * The enumeration for message priority : The message is to be sent when ServerComm is idle (not used for several seconds)
         * Sample : preferences save.
         * @type {Number}
         * @readonly  
         */
        PRIORITY_IDLE: -4,
        
        /**
         * @private
         * @property {Object[]} _messages The waiting messages. See {@link #send} method to see the message object
         */
        _messages: [],

        /**
         * @private
         * @property {Object[]} __idleMessages The waiting messages for idle mode. See {@link #send} method to see the message object
         */
        _idleMessages: [],
        /**
         * @private
         * @property {Number} _idleMessagesLastUpdate The time the last idle message was added
         */
        _idleMessagesLastUpdate: 0,

        /**
         * @private
         * @property {Object} _observer The ServerComm observer that will receive events on each action. There is no setter, you have to set it directly.
         */
        _observer: {},

        /**
         * @private
         * @property {Number} _suspended The number of times the communication was suspended. 0 means communication are not suspended. Cannot be negative.
         */
        _suspended: 0,

        /**
         * @private
         * @property {Number} _sendTask The time out value (return by setTimeout)
         */
        _sendTask: null,

        /**
         * @private
         * @property {Number} _nextTimer The date as long when the next timer should stop. Null if no timer.
         */
        _nextTimer: null,

        /**
         * @private
         * @property {Object} _runningRequests Association of id and send options ; to remember while timeout
         */
        _runningRequests: {},
        
        /**
         * @private
         * @property {Number} _runningRequestsIndex The index for the next running request.
         */
        _runningRequestsIndex: 0,
        
        /**
         * @private
         * @property {Object} _lastUniqueIdForCancelCode Contains an association {String} cancelCode / {String} the identifier of the last request called for this code.
         */
        _lastUniqueIdForCancelCode: {},
        
        /**
         * @private
         * @property {Number} _idleStatus 0 means working, otherwise contains the time that idle started 
         */
        _idleStatus: 0,
        
        /**
         * @private
         * @property {Element} _relogIframe An hidden iframe to try to silently relogin when connection is lost
         */
        _relogIframe: null,
        
        /**
         * @private
         * @property {Ext.window.Messagebox} _relogMsgBox The dialog that display the "connection back"" message. We keep a pointer to it, to avoid multiple display when connection is unstable
         */
        _relogMsgBox: null,
        
        /**
         * Suspend the communication with the server.
         * Use it when you know that several component will add messages with a major priority to do a single request
         * Do not forget to call the restart method to effectively send messages.
         * Be sure to finally call the {@link #restart} method.
         */
        suspend: function()
        {
            this._suspended++;
            this._updateIdleStatus();
        },
        
        /**
         * Restart suspended communications with server.
         * Do not call this method if you do not have call the {@link #suspend} one before.
         */
        restart: function()
        {
            if (this._suspended == 0)
            {
                throw new Error("Servercomm#restart method has been called but communications where not suspended");
            }

            this._suspended--;
            
            if (this._suspended == 0 && this._nextTimer != null && this._nextTimer < new Date().getTime())
            {
                this._sendMessages();
            }
            
            this._updateIdleStatus();
        },

        /**
         * @private
         * Is server comm currently idle?
         * @return {Boolean} true is the servercomm is idle
         */
        _isIdle: function()
        {
            var me = this;
            
            function isImportantRequestsAboutToRun()
            {
                return me._messages.filter(function(m) { return m.priority != Ametys.data.ServerComm.PRIORITY_MINOR }).length > 0
            }
            function isImportantRequestRunning()
            {
                var important = false;
                
                 Ext.Object.eachValue(me._runningRequests, function(runningRequest) {
                    if (runningRequest.messages.filter(function(m) { return m.priority != Ametys.data.ServerComm.PRIORITY_LONG_REQUEST }).length > 0)
                    {
                        important = true;
                        return false; // stop iteration
                    }
                 })
                 
                 return important;
            }
            
            return this._suspended == 0 
                && !isImportantRequestsAboutToRun()
                && !isImportantRequestRunning();
        },

        /**
         * @private
         * Update #property-_idleStatus 
         */
        _updateIdleStatus: function()
        {
            if (this._isIdle())
            {
                if (this._idleStatus == 0)
                {
                    // Server is newly idle
                    this._idleStatus = (new Date()).getTime();
                }
                // otherwise server was already idled
            }
            else
            {
                // Server is busy now
                this._idleStatus = 0;
            }
        },
        
        /**
         * @private
         * Determine if some idle messages needs to be sent now
         */
        _sendIdleMessages: function()
        {
            if (this._idleStatus != 0
                && this._idleMessages.length > 0)
            {
                var now = (new Date()).getTime();
                if (this._idleStatus + 5000 < now // The servercomm is idle for 5 seconds
                    && this._idleMessagesLastUpdate + 5000 < now) // Send request that are 5 seconds old
                {
                    this._sendMessages(null, true);
                }
            }
        },
        
        /**
         * Add a message to the 'to send' list of message. Depending on its priority, it will be send immediatly or later.
         * @param {Object} message The config object for the message to send.
         * @param {String} message.plugin The name of the server plugin targeted. Can be null. Do not use with workspace.
         * @param {String} message.workspace The name of the server workpace targeted. Can be null if plugin is specified or to send to current workspace.
         * @param {String} message.url The url on the server relative to the plugin or workspace
         * @param {Object} message.parameters The parameters to send to the server (Map<String, String>)
         * 
         * @param {Number} [message.priority=Ametys.data.ServerComm.PRIORITY_MAJOR] The priority of the message. Use ServerComm.PRIORITY_* constants.
         * 
         * @param {Object/Object[]} message.callback When using non synchronous messages, a callback configuration is required. Not available for #PRIORITY_SYNCHRONOUS requests. 
         * @param {Function} message.callback.handler The function to call when the message will come back. 
         * @param {Object} message.callback.handler.response Will be the xml parent node of the response. This node can be null or empty on fatal error (see errorMessage). An attribute 'code' is available on this node with the http code. This response has an extra method 'getText' that get the text from a node in parameter of the response.
         * @param {Object[]} message.callback.handler.callbackarguments Is the 'callback.arguments' array
         * @param {Object} [message.callback.scope] The scope of the function call. Optional.
         * @param {String[]} [message.callback.arguments] An array of string that will be given as arguments of the callback. Optional.
         * @param {Boolean} [message.callback.ignoreOnError=true] Is the callback called with a null or empty response?
         * 
         * @param {Object/String/Boolean} [message.progressMessage] The message and title to show on the dialog containing a progress bar, or only the message or true to use default message and title. This will be shown when downloading a file to show its progress with a progress bar.
         * @param {String} [message.progressMessage.title] The title of the dialog containing a progress bar, if not present, the default title will be used
         * @param {Object} [message.progressMessage.msg] The message of the dialog containing a progress bar, if not present, the default message will be used
         * 
         * @param {Object} [message.progressCallback] The callback to call to show progress
         * @param {Function} [message.progressCallback.handler] Called to show progress.
         * @param {Object} [config.progressCallback.handler.uploadPercent] The percentage of progress of the upload. 
         * @param {Object} [config.progressCallback.handler.serverPercent] The percentage of progress of the server process. null when not supported. 
         * @param {Object[]} [message.progressCallback.handler.arguments] Is the 'progressCallback.arguments'
         * 
         * @param {String} [message.responseType=xml] Can be "xml" (default) to have a xml response, "text" to have a single text node response or "xml2text" to have a single text node response where xml prologue as text is removed
         *
         * @param {Boolean/String/Object} [message.waitMessage=false] Display a Ext.LoadMask while the request is running. Set to true to display a default loading message. Set to a string to display your message. Set to a Ext.LoadMask configuration object to do more accurate stuffs (such as covering only a component - since by default all the ui is grayed), but if no target is specified, this will use the Ametys.mask.GlobalMask and so will ignore most properties. Not available for #PRIORITY_SYNCHRONOUS requests.
         * 
         * @param {Boolean/String/Object} [message.errorMessage=false] When the request is a failure display a message to the user (using #handleBadResponse). 
         * Set to false, the callback is called with a null or empty response (you should protected your code with #handleBadResponse). 
         * Set to true to display a default error message and your callback will not be called.
         * Set to a string to display a custom error message and your callback will not be called.
         * Set to an object with the following options: 
         * @param {String} [message.errorMessage.msg] The error message. There is a default message. 
         * @param {String} [message.errorMessage.category] The error message category for log purposes. 
         *  
         * @param {String} [message.cancelCode] This parameter allow to cancel a request or ignore its response if it is out-dated. Not available for #PRIORITY_SYNCHRONOUS requests.
         * A classic case it that the button wants more information on the last selected content: while asking server for information on content A, if the content B is selected by the user: this parameter allow to discard the information on A. 
         * Note that you will not be informed if the cancelled request was not sent or send but ignored by the client : so this is to use on read request only and should be an identifier for your kind of operation (such as 'MyClass$getContentInfo').
         * 
         * @param {Object} [message.cancellationCallback] Use this parameter to be informed and do some action when the message was cancelled or ignored by the client.
         * @param {Function} message.cancellationCallback.handler The function to call when the message was cancelled.
         * @param {Object[]} message.cancellationCallback.handler.callbackarguments Is the 'callback.arguments' array
         * @param {Object} [message.cancellationCallback.scope] The scope of the function call. Optional.
         * @param {String[]} [message.cancellationCallback.arguments] An array of string that will be given as arguments of the callback. Optional.
      
         * @return {Object} The XHR object containing the response data for #PRIORITY_SYNCHRONOUS requests. Null in other cases.
         */
        send: function(message)
        {
            // Generating a unique id for this message for cancelling purposes
            if (message.cancelCode)
            {
                this.cancel(message.cancelCode);
                message.uniqueId = Ext.id(null, 'serverinfo-');
                this._lastUniqueIdForCancelCode[message.cancelCode] = message.uniqueId;
            }

            Ext.applyIf(message, {
                pluginOrWorkspace: message.plugin ? message.plugin : (message.workspace ? '_' + message.workspace : null),
                toRequest: function() {
                    var m = {};
                    
                    m.pluginOrWorkspace = this.pluginOrWorkspace || '_' + Ametys.WORKSPACE_NAME;
                    m.responseType = this.responseType;
                    m.url = this.url;
                    m.parameters = this.parameters;
                    
                    return m;
                },
                addFiles: function(files, index) {
                    Ext.Object.each(this.files, function(key, value) {
                        files['request#' + index + '#' + key] = value;
                    });
                }
            });
            message.responseType = message.responseType || "xml";
            message.priority = message.priority || Ametys.data.ServerComm.PRIORITY_MAJOR;
            
            try
            {
                throw new Error("get trace");
            }
            catch (e)
            {
                message.callstack = e.stack;
            }
            
            if (message.priority == Ametys.data.ServerComm.PRIORITY_SYNCHRONOUS)
            {
                return this._sendSynchronousMessage(message.toRequest());
            }
            else if (message.priority == Ametys.data.ServerComm.PRIORITY_BEACON)
            {
                this._sendBeaconMessage(message.toRequest());
                return null;
            }
            
            message.callback = Ext.Array.from(message.callback);
            message.callback.forEach(function(cb) 
                {
                    if (!cb || !cb.handler)
                    { 
                        throw new Error("The message cannot be sent with a null callback or handler"); 
                    }
                }
            );
            
            message.progressCallback = Ext.Array.from(message.progressCallback);
            message.progressCallback.forEach(function(cb) 
                {
                    if (!cb || !cb.handler)
                    { 
                        throw new Error("The message cannot be sent with a null progressCallback or handler"); 
                    }
                }
            );
            
            // Load mask
            message.waitMessage = this._showWaitMessage(message.waitMessage);
            
            // Progress dialog
            message.progressMessage = this._showProgressDialog(message.progressMessage);
            
            if (message.priority == Ametys.data.ServerComm.PRIORITY_LONG_REQUEST)
            {
                this._sendMessages(message);
                this._updateIdleStatus();
                return null;
            }
            else if (message.priority == Ametys.data.ServerComm.PRIORITY_IDLE)
            {
                // add the message to the list
                // The message will be sent when the ServerComm will be idled
                this._idleMessages.push(message);
                this._idleMessagesLastUpdate = (new Date).getTime();
                this._updateIdleStatus();
                return null;
            }
            else
            {
                // add the message to the list
                this._messages.push(message);
                
                // compute delay wanted and ring time associated (add a 50 milliseconds delay to try to cumulate several messages)
                var delay = 1000 * message.priority + 50;
                var ringTime = new Date().getTime() + delay;
            
                // if the current timer rings after the wanted time (at 20 milliseconds near)
                if (this._nextTimer == null || ringTime < this._nextTimer - 20)
                {
                    this._nextTimer = ringTime;
                    if (this._sendTask)
                    {
                        window.clearTimeout(this._sendTask);
                    }
                    this._sendTask = window.setTimeout(function () { Ametys.data.ServerComm._sendMessages(); }, delay);
                }
                this._updateIdleStatus();
                return null;
            }
        },
        
        /**
         * Cancel the request corresponding to the given code
         * @param {String} cancelCode The cancel code
         */
        cancel: function(cancelCode)
        {
            var me = this;
            
            this._lastUniqueIdForCancelCode[cancelCode] = null;

            _cancel(this._messages);
            _cancel(this._idleMessages);
            this._updateIdleStatus();
            
            function _cancel(messages)
            {
                // removing any unsent message with the same cancelCode
                var messagesIndexToCancel = [];
                for (var i = 0; i < messages.length; i++)
                {
                    var oldMessage = messages[i];
                    if (oldMessage.cancelCode == cancelCode)
                    {
                        if (me.getLogger().isDebugEnabled())
                        {
                            me.getLogger().debug("Discarding message with cancel code '" + oldMessage.cancelCode + "'");
                        }
                        oldMessage.cancelled = true;
                        
                        messagesIndexToCancel.push(i);
                        
                        me._hideWaitMessage(oldMessage.waitMessage);
                        me._hideProgressDialog(oldMessage.progressMessage);
                        
                        if (oldMessage.cancellationCallback)
                        {
                            oldMessage.cancellationCallback.handler.apply(oldMessage.cancellationCallback.scope, [oldMessage.cancellationCallback.arguments]);
                        }
                    }
                }
                for (var i = messagesIndexToCancel.length - 1; i >= 0; i--)
                {
                    var index = messagesIndexToCancel[i];
                    Ext.Array.remove(messages, messages[index]);
                }
            }
        },
        
        /**
         * Directly calls Java code on server-side.
         * @param {Object} config The configuration object
         * @param {String} config.role The Java component id
         * @param {String} [config.id] If the role refers to an extension point, the id refers to an extension. If not, the id should be null.
         * @param {String} config.methodName The "callable" method to call in the Java component.
         * @param {Object[]} config.parameters The methods parameters. They will be converted to Java Objects keeping types as much as possible. File/File[] are also supported (Retrives the files to put them in the parameters with fileField.getFiles()).
         * 
         * @param {Object/Object[]} config.callback The callback configuration.
         * @param {Function} config.callback.handler Called after method execution.
         * @param {Object} config.callback.handler.response The server response. Can be null for a void response or undefined if an error occurred when ignoreOnError is false.
         * @param {Object[]} config.callback.handler.arguments Is the 'callback.arguments' array
         * @param {Object} [config.callback.scope] The scope of the function call. Optional.
         * @param {String[]} [config.callback.arguments] An array of Objects that will be passed to the callback as second argument. Optional.
         * @param {Boolean} [config.callback.ignoreOnError=true] Is the callback called with a null or empty response?
         * 
         * @param {Object/String/Boolean} [config.progressMessage] The message and title to show on the dialog containing a progress bar, or only the message or true to use default message and title. This will be shown when downloading a file to show its progress with a progress bar.
         * @param {String} [config.progressMessage.title] The title of the dialog containing a progress bar, if not present, the default title will be used
         * @param {Object} [config.progressMessage.msg] The message of the dialog containing a progress bar, if not present, the default message will be used
         *
         * @param {Object} [config.progressCallback] The callback to call to show progress
         * @param {Function} [config.progressCallback.handler] Called to show progress.
         * @param {Object} [config.progressCallback.handler.uploadPercent] The percentage of progress of the upload. 
         * @param {Object} [config.progressCallback.handler.serverPercent] The percentage of progress of the server process. null when not supported. 
         * @param {Object[]} [config.progressCallback.handler.arguments] Is the 'progressCallback.arguments'
         * 
         * @param {Object} [config.cancellationCallback] Use this parameter to be informed and do some action when the message was cancelled or ignored by the client. See #send for more information about this parameter.
         * 
         * @param {String} [config.cancelCode] This allow to cancel a previous unfinished request. See #send for more information on the cancelCode.
         * @param {Boolean/String/Object} [config.waitMessage] Display a waiting message while the request is running. See #send for more information on the waitingMessage.
         * @param {Boolean/String/Object} [config.errorMessage] An error message. See #send for more information on the errorMessage.
         * @param {Number} [config.priority] The message priority. See #send for more information on the priority. PRIORITY_SYNCHRONOUS cannot be used here.
         */
        callMethod: function(config)
        {
            // Handle file uploads
            var files = {};
            var parameters = config.parameters;
            if (parameters && parameters.forEach)
            {
                let index = 0;
                var totalSize = 1000; // Let's keep some size for request overhead

                parameters.forEach(function(param, index) 
                {
                    if (param instanceof File)
                    {
                        files["parameters." + index] = param;
                        parameters[index] = null;
                        totalSize += param.size;
                    }
                    else if (param instanceof Array && param.length > 0 && param[0] instanceof File)
                    {
                        for (let i = 0; i < param.length; i++)
                        {
                            let file = param[i];
                            if (file instanceof File)
                            {
                                files["parameters." + index + "." + i] = file;
                                parameters[index][i] = null;
                                totalSize += file.size;
                            }
                        }
                    }
                    
                    index++;
                });
                
                if (totalSize > Ametys.MAX_UPLOAD_SIZE)
                {
                    throw new Error("Cannot send files of " + totalSize + " bytes. The max size is " + Ametys.MAX_UPLOAD_SIZE + " bytes.");
                }
            }
            
            this.send({
                plugin: 'core-ui',
                url: 'client-call',
                parameters: {
                    role: config.role,
                    id: config.id,
                    methodName: config.methodName,
                    parameters: parameters
                },
                files: files,
                callback: {
                    handler: this._callProcessed,
                    scope: this,
                    ignoreOnError: false,
                    arguments: {cb: config.callback}
                },
                progressCallback: config.progressCallback,
                progressMessage: config.progressMessage,
                responseType: 'text',
                cancellationCallback: {
                    handler: this._cancellationProcessed,
                    scope: this,
                    arguments: {cb: config.cancellationCallback}
                },
                cancelCode: config.cancelCode,
                waitMessage: config.waitMessage,
                errorMessage: config.errorMessage,
                priority: (config.priority == Ametys.data.ServerComm.PRIORITY_SYNCHRONOUS) ? null : config.priority
            });
        },
        
        /**
         * @private
         * Internal callback after cancellation
         * @param {Object} arguments The arguments 
         * @param {Object} arguments.cb The callback 
         * @param {Function} arguments.cb.handler The callback function 
         * @param {Object} arguments.cb.scope The scope of the callback 
         * @param {Object[]} arguments.cb.arguments The arguments of the callback 
         */
        _cancellationProcessed: function (arguments)
        {
            var callback = arguments.cb || {};
            
            if (callback.handler)
            {
                callback.handler.apply(callback.scope || this, [callback.arguments]);
            }
        },
        
        /**
         * @private
         * Internal callback for the #callMethod function.
         * @param {Object} response The server response
         * @param {Object} arguments The arguments 
         * @param {Function} arguments.cb The callback function 
         */
        _callProcessed: function(response, arguments)
        {
            var callback = arguments.cb;
            
            var responseAsObject = undefined;
            if (!this.isBadResponse(response))
            {
                responseAsObject = Ext.JSON.decode(response.textContent || response.text);
            }
            
            callback = Ext.Array.from(callback);
            Ext.Array.forEach(callback, function (cb) {
                if (responseAsObject !== undefined || cb.ignoreOnError === false)
                {
                     cb.handler.apply(cb.scope || this, [responseAsObject, cb.arguments]);
                }
            }, this);
        },

        /**
         * @private
         * Send a beacon message
         * @param {Object} messageRequest An object returned by ServerMessage.toRequest
         */
        _sendBeaconMessage: function(messageRequest)
        {
            console.info("Beacon request")
            // Sending even if suspended
            
            var data = new FormData();
            data.append("content", (Ext.JSON.encode({0: messageRequest})));
            data.append("context.parameters", (Ext.JSON.encode(Ametys.getAppParameters())));
            
            if (!navigator.sendBeacon(Ametys.data.ServerComm.SERVERCOMM_URL, data))
            {
                console.error("Unable to send beacon request");
            }
        },
        
        /**
         * @private
         * Send a synchronous message
         * @param {Object} messageRequest An object returned by ServerMessage.toRequest
         * @return {Object} The XHR object containing the response data, or null if the ServerComm is shut down, or if a error occured
         */
        _sendSynchronousMessage: function(messageRequest)
        {
            if (Ametys.isSuspended())
            {
                return null;
            }
            
            if (typeof this._observer.onSyncRequestDeparture == "function")
            {
                try
                { 
                    this._observer.onSyncRequestDeparture(messageRequest);
                }
                catch (e)
                {
                    alert("Exception in Ametys.data.ServerComm._observer.onSyncRequestDeparture: " + e);
                }
            }

            var conn = null;
            
            try
            {
                conn = Ext.Ajax.request({url: Ametys.data.ServerComm.SERVERCOMM_URL, params: "content=" + encodeURIComponent(Ext.JSON.encode({0: messageRequest})) + "&context.parameters=" + encodeURIComponent(Ext.JSON.encode(Ametys.getAppParameters())), async: false});
            }
            catch(e)
            {
                if (typeof this._observer.onSyncRequestArrival == "function")
                {
                    try
                    { 
                        this._observer.onSyncRequestArrival(messageRequest, 2, null);
                    }
                    catch (e)
                    {
                        alert("Exception in Ametys.data.ServerComm._observer.onSyncRequestArrival: " + e);
                    }
                }
                
                this._handleResponseFatalErrors(null, null, false);
                return null;
            }
            
            if (typeof this._observer.onSyncRequestArrival == "function")
            {
                try
                { 
                    this._observer.onSyncRequestArrival(messageRequest, 0, conn);
                }
                catch (e)
                {
                    alert("Exception in Ametys.data.ServerComm._observer.onSyncRequestArrival: " + e);
                }
            }
            
            if (this._handleResponseFatalErrors(conn, null, true) != null)
            {
                return Ext.dom.Query.selectNode("/responses/response[@id='0']", conn.responseXML);
            }
            else
            {
                return null;
            }
        },
        
        /**
         * Send the waiting messages to the server
         * @param {Object} m An optional message. If null the method will empty the queue, else it will send only this message.
         * @param {Boolean} idle Send message waiting for idle. m must be null.
         * @private
         */
        _sendMessages: function(m, idle)
        {
            var messageType = idle ? '_idleMessages' : '_messages'; 
            
            var timeout = Ametys.data.ServerComm.TimeoutDialog.TIMEOUT;
            
            var parameters = {};
            var files = {};
            
            if (m == null)
            {
                if (idle !== true)
                {
                    window.clearTimeout(this._sendTask);
                    this._sendTask = null;
                    this._nextTimer = null;
                }
                
                if (this._suspended > 0)
                {
                    // communication is suspended - messages will be sent as soon as possible
                    return;
                }
                
                if (this[messageType].length == 0)
                {
                    return;
                }
            
                // Effectively send messages
                for (var i = 0; i < this[messageType].length; i++)
                {
                    var message = this[messageType][i];
                    parameters[i] = message.toRequest();
                    message.addFiles(files, i);
                }
            }
            else
            {
                timeout *= 600;
                parameters[0] = m.toRequest();
                m.addFiles(files, 0);
            }
            
            var sendOptions = {};
            var index = Ametys.data.ServerComm._runningRequestsIndex ++;
            Ametys.data.ServerComm._runningRequests[index] = sendOptions;
            
            var data = new FormData();
            data.append("content", Ext.JSON.encode(parameters));
            data.append("context.parameters", Ext.JSON.encode(Ametys.getAppParameters()));
            Ext.Object.each(files, function(key, file) {
                data.append(key, file);
            });
            
            sendOptions.url = Ametys.data.ServerComm.SERVERCOMM_URL;
            sendOptions.success = this._onRequestComplete;
            sendOptions.failure = this._onRequestFailure;
            sendOptions.progress = this._onRequestProgress;
            sendOptions.scope = this;
            sendOptions.rawData = data;
            sendOptions.headers = {'Content-Type': null};
            sendOptions._timeoutIndex = index;
            sendOptions._timeout = window.setTimeout("Ametys.data.ServerComm._onRequestTimeout ('" + index + "', " + timeout + ");", timeout);
            
            sendOptions._transactionId = Ext.Ajax.request(sendOptions);
            sendOptions.messages = m != null ? [m] : this[messageType];

            if (m == null)
            {
                this[messageType] = [];
            }


            if (typeof this._observer.onRequestDeparture == "function")
            {
                try
                {
                    this._observer.onRequestDeparture(sendOptions);
                }
                catch (e)
                {
                    alert("Exception in Ametys.data.ServerComm._observer.onRequestDeparture: " + e);
                }
            }
        },
        
        /**
         * @private
         * Abort a request (note that the server will still execute it but the result will be discard)
         * @param {Object} options The send options
         * @param {boolean} silently Do not call any listeners. Default to false.
         */
        _abort: function(options, silently)
        {
            Ext.Ajax.abort(options._transactionId);

            this._cancelTimeout(options);
            
            if (silently !== false && typeof this._observer.onRequestArrival == "function")
            {
                try
                { 
                    this._observer.onRequestArrival(options, 1, null);
                }
                catch (e)
                {
                    alert("Exception in Ametys.data.ServerComm._observer.onRequestArrival (1): " + e);
                }
            }

            if (silently !== false)
            {
                this._dispatch({}, options)
            }
        },
        
        /**
         * @private
         * When a request times out
         * @param {Number} index The index of the request in the _runningRequest map.
         * @param {Number} timeout The timeout value
         */
        _onRequestTimeout: function(index, timeout)
        {
            this.getLogger().debug("Request timeout [n°" + index + "]");

            var sendOptions = Ametys.data.ServerComm._runningRequests[index];
            if (sendOptions != null) // Should never happens (since timeout are disarmed) but it does...
            {
                sendOptions._timeout = null;
                sendOptions._timeoutDialog = new Ametys.data.ServerComm.TimeoutDialog(sendOptions.messages, index, timeout);
            }
        },

        /**
         * @private
         * Cancel the timeout and kill the timeout dialogue
         * @param {Object} options The arguments passed
         */
        _cancelTimeout: function(options)
        {
            if (options._timeout != null)
            {
                this.getLogger().debug("Clear timeout [n°" + options._timeoutIndex + "]");
                
                window.clearTimeout(options._timeout);
            }
            else
            {
                this.getLogger().debug("No timeout [n°" + options._timeoutIndex + "]");
            }
            
            if (options._timeoutDialog != null)
            {
                this.getLogger().debug("Closing timeout dialog [n°" + options._timeoutIndex + "]");

                options._timeoutDialog.kill();
                options._timeoutDialog = null;
            }
            delete Ametys.data.ServerComm._runningRequests[options._timeoutIndex];
            
            for (var i = 0; i < options.messages.length; i++)
            {
                var message = options.messages[i];
                
                this._hideWaitMessage(message.waitMessage);
                this._hideProgressDialog(message.progressMessage);
            }
        },
        
        /**
         * @private
         * Display a wait message during request.
         * @param {Boolean/String/Object} waitMessage See waitMessage configuration on #send method.
         * @return {String/Ext.LoadMask} The wait message instance, or Ametys.mask.GlobalLoadMask identifier
         */
        _showWaitMessage: function(waitMessage)
        {
            if (waitMessage != null && waitMessage !== false)
            {
                if (waitMessage === true)
                {
                    waitMessage = { };
                }
                if (Ext.isString(waitMessage))
                {
                    waitMessage = { msg: waitMessage };
                }
                
                if (!waitMessage.target)
                {
                    waitMessage = Ametys.mask.GlobalLoadMask.mask(waitMessage.msg);
                }
                else
                {
                    var manualShow = !waitMessage.target || waitMessage.target.rendered;
                    
                    waitMessage.autoShow = true;
                    waitMessage = Ext.create("Ext.LoadMask", waitMessage);
                    if (manualShow)
                    {
                        waitMessage.show();
                    }
                }
            }
            
            return waitMessage;
        },
        
        /**
         * @private
         * Wait the wait message
         * @param {String/Ext.LoadMask} waitMessage The wait message instance, or Ametys.mask.GlobalLoadMask identifier
         */
        _hideWaitMessage: function(waitMessage)
        {
            if (Ext.isString(waitMessage))
                {
                Ametys.mask.GlobalLoadMask.unmask(waitMessage);
                }
            else if (Ext.isObject(waitMessage))
            {
                waitMessage.hide();
                Ext.destroy(waitMessage);
            }
        },
        
        /**
         * @private
         * Display a wait message during request.
         * @param {Boolean/String/Object} progressMessage See progressMessage configuration on #send method.
         * @return {String/Ext.LoadMask} The progress message instance
         */
        _showProgressDialog: function(progressMessage)
        {
            if (progressMessage != null && progressMessage !== false)
            {
                if (progressMessage === true)
                {
                    progressMessage = { };
                }
                
                if (Ext.isString(progressMessage))
                {
                    progressMessage = { msg: progressMessage };
                }
                
                return Ext.create('Ametys.window.DialogBox', {
                    layout: 'anchor',
                    title: progressMessage.title || "{{i18n plugin.core-ui:PLUGINS_CORE_UI_PROGRESS_DEFAULT_TITLE}}",
                    autoShow: true,
                    closable: false,
                    width: 400,
                    bodyPadding:15,
                    items: [
                        {
                            xtype: 'component',
                            html: progressMessage.msg || Ametys.mask.GlobalLoadMask.DEFAULT_MESSAGE,
                            anchor: '100%',
                            style: {
                                marginBottom: '5px'
                            }
                        },
                        {
                            id: 'progressbar',
                            xtype: 'progressbar',
                            anchor: '100%'
                        }
                    ]
                })
            }
            
            return null;
        },
        
        /**
         * @private
         * Wait the wait message
         * @param {Ametys.window.DialogBox} progressMessage The progress message instance
         */
        _hideProgressDialog: function(progressMessage)
        {
            if (progressMessage != null)
            {
                progressMessage.close();
            }
        },

        /**
         * @private
         * Listener on requests that succeed
         * @param {Object} response The XHR object containing the response data.
         * @param {Object} options The arguments of the request method call
         */
        _onRequestComplete: function(response, options)
        {
            this._cancelTimeout(options);
            
            if (typeof this._observer.onRequestArrival == "function")
            {
                try
                { 
                    this._observer.onRequestArrival(options, 0, response);
                }
                catch (e)
                {
                    alert("Exception in Ametys.data.ServerComm._observer.onRequestArrival (0): " + e);
                }
            }
            
            if (this._handleResponseFatalErrors(response, options, true))
            {
                this._dispatch(response, options);
            }
            
            this._updateIdleStatus();
        },
        
        /**
         * @private
         * Handle the response big errors in order to displatch it
         * @param {Object} response The XHR object containing the response data. Can be null if no success
         * @param {Object} options The arguments of the request method call
         * @param {Boolean} success Was the request a success?
         * @returns {Boolean} true if the message should be dispatched
         */
        _handleResponseFatalErrors: function(response, options, success)
        {
            if (Ametys.isUnloading())
            {
                // Request lost when unloading are silently ignored
                Ametys.suspend();
                return false;
            }
            else if (Ametys.isSuspended())
            {
                // ServerComm was shutted down => discard the answer
                return false;
            }
            else if (!success && response.status == 503)
            {
                // Server is in maintenance
                Ametys.suspend("<span class='ametysicon-object-hammer-wrench'></span>&#160;{{i18n PLUGINS_CORE_UI_SERVERCOMM_LISTENERREQUEST_MAINTENANCE_TITLE}}",
                                    "{{i18n PLUGINS_CORE_UI_SERVERCOMM_LISTENERREQUEST_MAINTENANCE_MESSAGE}}",
                                    null,
                                    true);
                return true;
            }
            else if (!success && options && options.messages[0].priority == this.PRIORITY_LONG_REQUEST)
            {
                // PRIORITY_LONG_REQUEST may timeout but the user connection can still be good 
                return true;
            }
            else if (!success && response.status != 0) // CORS leads to a 0 status code 
            {
                // Fail to contact server, but includes important messages
                Ametys.suspend("<span class='icon-serverunavailable'></span>&#160;{{i18n PLUGINS_CORE_UI_SERVERCOMM_LISTENERREQUEST_SERVERUNAVAILABLE_TITLE}}",
                                    "{{i18n PLUGINS_CORE_UI_SERVERCOMM_LISTENERREQUEST_SERVERUNAVAILABLE_MESSAGE}}",
                                    null,
                                    true);
                return true;
            }
            else if (response.getResponseHeader("Ametys-Dispatched") != "true") // Can happen whereas success is true (login form) or not (for ex, in case of a CAS redirect that lead to a CORS)
            {
                if (options && options.messages.filter(o => o.priority != this.PRIORITY_MINOR).length > 0)
                {
                    // Authentication was lost
                    this._suspendForAuthIssue();
                }
                else
                {
                    this._relog(true);
                }
                return true;
            }
            else
            {
                return true;
            }
        },
        
        /**
         * @private
         * Suspend for auth issue
         */
        _suspendForAuthIssue: function()
        {
            Ametys.suspend("<span class='icon-loadsession'></span>&#160;{{i18n PLUGINS_CORE_UI_SERVERCOMM_LISTENERREQUEST_LOSTSESSION_TITLE}}",
                "{{i18n PLUGINS_CORE_UI_SERVERCOMM_LISTENERREQUEST_LOSTSESSION_MESSAGE}}",
                null,
                true,
                true);
        },

        /**
         * @private
         * Try to relog
         * @param {Boolean} suspendOnFail If the relog fail, then suspend
         */
        _relog: function(suspendOnFail)
        {
            let me = this;
            if (this._relogIframe == null)
            {
                console.log("Authentication lost. Trying an automatic reconnection...")
                this._relogIframe = document.createElement("iframe");
                this._relogIframe.style = "position: absolute; top: -10000px; left: -10000px; width: 5px; height: 5px;"
                this._relogIframe.setAttribute("aria-hidden", "true");
                this._relogIframe.addEventListener("load", function(e) { me._relogLoaded(e, suspendOnFail); });
                this._relogIframe.src = Ametys.getPluginDirectPrefix("core-ui") + "/authentication/test.xml?NonBlocking=force&UserPopulation=" + Ametys.getAppParameter('user').population;
                document.body.append(this._relogIframe);
            }
        },
        
        /**
         * @private
         * Stop from reloggin
         */
        _stopRelog: function()
        {
            if (this._relogIframe != null)
            {
                this._relogIframe.remove();
                this._relogIframe = null;
            }
        },
                /**
         * @private
         * Relog iframe's load listner
         * @param {Object} e Event
         * @param {Boolean} suspendOnFail If the relog fail, then suspend
         */
        _relogLoaded: function(e, suspendOnFail)
        {
            if (this._relogIframe == null)
            {
                // Ametys was relogedin in the meantime
                return;
            }
            
            try
            {
                console.log("Iframe loaded");
                console.log(this._relogIframe.contentWindow.location.href)
                
                if (Ext.dom.Query.selectValue("/*/status", this._relogIframe.contentDocument) == 'ok')
                {
                    // OK - Finished
                    console.log("Automatic reconnection sucessful")
                    Ametys.notify({
                        type: "info",
                        title: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_AUTOMATICRELOG_TITLE}}",
                        description: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_AUTOMATICRELOG_DESCRIPTION}}",
                    });
                                        
                    return;
                }
            }
            catch (e)
            {
                // many exception can occures => not xml document (because on auth page for ex), not on the right domain (because on an extenal auth page such as cas)...
                console.error("Cannot load authentication test url", e)
            }
            finally
            {
                this._stopRelog();
            }
            
            console.log("Automatic reconnection failed")
            if (suspendOnFail)
            {
                this._suspendForAuthIssue();
            }
        },     
        
        /**
         * @private
         * Call the callbacks for the response (that can be null)
         * @param {Object} response The XHR object containing the response data
         * @param {Object} options The arguments of the request method call
         */
        _dispatch: function(response, options)
        {
            Ext.suspendLayouts();

            // Protection against buggy components
            var layoutSupensionSize = 200;
            var initialValue = Ext.Component.layoutSuspendCount += layoutSupensionSize; 
            
            try
            {
                // for each message call the handler
                for (var i = 0; i < options.messages.length; i++)
                {
                        var message = options.messages[i];
                        
                        var node = Ext.dom.Query.selectNode("/responses/response[@id='" + i + "']", response.responseXML);
                        if ((node == null && message.cancelled == true)
                            || (message.cancelCode && this._lastUniqueIdForCancelCode[message.cancelCode] != message.uniqueId))
                        {
                            // only discard a canceled request if there is no answer
                            // a cancel message with an answer means it has been canceled too late
                            if (!message.cancelled && this.getLogger().isDebugEnabled())
                            {
                                this.getLogger().debug("Discarding response for a message with cancel code '" + message.cancelCode + "'");
                            }
                            
                            if (message.cancellationCallback)
                            {
                                message.cancellationCallback.handler.apply(message.cancellationCallback.scope, [message.cancellationCallback.arguments]);
                            }
                            
                            continue;
                        }
                        
                        var badResponse = false;
                        if (message.errorMessage != null && message.errorMessage !== false)
                        {
                            var msg = "{{i18n PLUGINS_CORE_UI_SERVERCOMM_ERROR_DESC}}";
                            var category = this.self.getName(); 
                            
                            if (Ext.isString(message.errorMessage))
                            {
                                msg = message.errorMessage;
                            }
                            else if (Ext.isObject(message.errorMessage))
                            {
                                if (message.errorMessage.msg != null)
                                {
                                    msg = message.errorMessage.msg;
                                }
                                if (message.errorMessage.category != null)
                                {
                                    category = message.errorMessage.category;
                                }
                            }
                            
                            badResponse = this.handleBadResponse(msg, node, category);
                        }
                        
                        // Call message callbacks
                        Ext.Array.forEach(message.callback, function(callback) {
                            if (!badResponse || callback.ignoreOnError === false)  
                            {
                                try
                                {
                                    callback.handler.apply(callback.scope, [node, callback.arguments]);
                                }
                                catch (e)
                                {
                                    function throwException(e) 
                                    { 
                                        throw e; 
                                    }
                                    Ext.defer(throwException, 1, this, [e]);
                                    
                                    Ametys.log.ErrorDialog.display({
                                        title: "{{i18n PLUGINS_CORE_UI_SERVERCOMM_ERROR_TITLE}}",
                                        text: "{{i18n PLUGINS_CORE_UI_SERVERCOMM_ERROR_DESC}}",
                                        details: e,
                                        category: "Ametys.data.ServerComm"
                                    });
                                }
                            }
                        }, this);
                        
                        // Protection against buggy components
                        if (Ext.Component.layoutSuspendCount <= layoutSupensionSize)
                        {
                            this.getLogger().error("Their was a supend/resume layout issue");
                        }
                        Ext.Component.layoutSuspendCount = initialValue;
                }
            }
            finally
            {
                // Protection against buggy components
                Ext.Component.layoutSuspendCount = Math.max(1, Ext.Component.layoutSuspendCount - layoutSupensionSize); 
                Ext.resumeLayouts(true);
            }
        },      
        
        /**
         * @private
         * Listener on requests that failed
         * @param {Object} response The XHR object containing the response data
         * @param {Object} options The arguments of the request method call
         */
        _onRequestFailure: function(response, options)
        {
            this._cancelTimeout(options);
            
            if (typeof this._observer.onRequestArrival == "function")
            {
                try
                { 
                    this._observer.onRequestArrival(options, 2, null);
                }
                catch (e)
                {
                    alert("Exception in Ametys.data.ServerComm._observer.onRequestArrival (2): " + e);
                }
            }
            
            if (this._handleResponseFatalErrors(response, options, false))
            {
                this._dispatch(response, options);
            }
            
            this._updateIdleStatus();
        },
        
        /**
         * @private
         * Listener on requests that progresses
         * @param {Number} percent The percent of progress
         * @param {Object} options The arguments of the request method call
         */
        _onRequestProgress: function(percent, options)
        {
            // for each message call the handler
            for (var i = 0; i < options.messages.length; i++)
            {
                var message = options.messages[i];
                
                Ext.Array.forEach(message.progressCallback, function(progressCallback) {
                    try
                    {
                        progressCallback.handler.apply(progressCallback.scope, [percent, null, progressCallback.arguments]);
                    }
                    catch (e)
                    {
                        console.error("Error while processing progress callback", e);
                    }
                }, this);
                
                if (message.progressMessage)
                {
                    message.progressMessage.items.items[1].updateProgress(percent);
                }
            }
        },
        
        /**
         * Test for a null response or a 404 or a 500.
         * @param {Object} response The response received
         * @return {boolean} True if a bad request was found (and you should alert the user and abort your process)
         */
        isBadResponse: function(response)
        {
            return response == null || response.getAttribute("code") == "500" || response.getAttribute("code") == "404";
        },
        
        /**
         * Call this method to handle a bad response for you. Test response with #isBadResponse.
         * @param {String} message The error message to display if the response is bad
         * @param {Object} response The response received
         * @param {String} category The log category. Can be null to avoid logging.
         * @return {boolean} True if a bad request was found (and you should abort your process)
         */
        handleBadResponse: function(message, response, category)
        {
            if (this.isBadResponse(response))
            {
                if (response == null)
                {
                    Ametys.log.ErrorDialog.display({
                            title: "{{i18n PLUGINS_CORE_UI_SERVERCOMM_BADRESPONSE_TITLE}}", 
                            text: message,
                            details: "{{i18n PLUGINS_CORE_UI_SERVERCOMM_BADRESPONSE_DESC}}",
                            category: category
                    });
                }
                else
                {
                    var intMsg = Ext.dom.Query.selectValue("message", response);
                    var hasMsg = intMsg != null && intMsg != "";
                    var intStk = Ext.dom.Query.selectValue("stacktrace", response);
                    var hasStk = intStk != null && intStk != "";
                        
                    Ametys.log.ErrorDialog.display({
                            title: "{{i18n PLUGINS_CORE_UI_SERVERCOMM_SERVER_FAILED_DESC}}" + response.getAttribute("code") + ")", 
                            text: message,
                            details: (hasMsg ? intMsg : "")
                            + (hasMsg && hasStk ? "\n\n" : "")
                            + (hasStk ? intStk : ""),
                            category: category
                    });
                }
                return true;
            }
            else
            {
                return false;
            }
        }
    }
);

// Every second, let's see if there is any waiting "idle" message
Ext.interval(Ametys.data.ServerComm._sendIdleMessages, 1000, Ametys.data.ServerComm);