/*
 *  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 tool does display the messages sent to the server.
 * @private
 */
Ext.define("Ametys.plugins.coreui.system.requesttracker.RequestTrackerTool", {
	extend: "Ametys.tool.Tool",
	
	statics: {
		/**
		 * This action find the unique instance of the request tracker tool, and removes all the entries
		 */
		removeAll: function()
		{
			var tool = Ametys.tool.ToolsManager.getTool("uitool-requeststracker");
			if (tool != null)
			{
				tool.tree.getRootNode().removeAll();
			}
			else
			{
				this.getLogger().error("Cannot remove entries from unexisting tool 'uitool-requeststracker'");
			}
		}
	},
	
	/**
	 * @property {Number} _currentId The current identifier. Each request displayed will show an identifier
	 * @private
	 */
	
	/**
	 * @property {Ext.data.TreeStore} store The store with the requests sent
	 * @private
	 */
	
	/**
	 * @property {Ext.tree.Panel} tree The tree panel displaying the requests
	 * @private
	 */
	
	constructor: function()
	{
		this.callParent(arguments);
		this._currentId = 1;
		
		Ametys.data.ServerComm._observer = this;
	},
	
	createPanel: function()
	{
		this.store = Ext.create("Ext.data.TreeStore",{
			model: "Ametys.plugins.coreui.system.requesttracker.RequestTrackerTool.RequestEntry",
			autoDestroy: true,
			autoSync: true,
			proxy: { type: 'memory' }
	    });
	    
	    this.store.setRootNode({
            type: 'root',
            text: "Root",
            label: "Root",
            expanded: true
        });
		
		this.tree = Ext.create("Ext.tree.Panel", { 
            rootVisible: false,
            minHeight: 60,
            minWidth: 100,
            flex: 0.5,
			stateful: true,
			stateId: this.self.getName() + "$treegrid",
			store: this.store,
			scrollable: true,
            border: true,
            defaults: {
                width: 1000,
            },
            
            dockedItems: [
                {
                    xtype: 'toolbar',
                    dock: 'top',
                    itemId: "toolbar",
                    layout: { 
                        type: 'hbox',
                        align: 'stretch'
                    },
                    items: [
                        {
                            xtype: 'textfield',
                            cls: 'ametys',
                            itemId: 'textfilter',
                            flex: 1,
                            maxWidth: 30,
                            maxWidth: 300,
                            emptyText: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_TOOLBAR_FILTER}}",
                            listeners: {change: Ext.Function.createBuffered(this._filter, 500, this)},
                            style: {
                                marginRight: '0px'
                            }
                        }, 
                        {
                            // Clear filter
                            tooltip: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_TOOLBAR_CLEARFILTER}}",
                            handler: Ext.bind (this._clearFilter, this),
                            iconCls: 'a-btn-glyph ametysicon-eraser11 size-16',
                            cls: 'a-btn-light'
                        },
                        {
                            xtype: 'tbspacer',
                            width: 20
                        },
                        "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_TOOLBAR_PRIORITY}}",
                        {
                            tooltip: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_SYNCHRONOUS}}",
                            handler: Ext.bind (this._filter, this),
                            itemId: 'priorityfiltersync',
                            iconCls: 'a-btn-glyph ametysicon-arrow-circle-right-double size-16',
                            cls: 'a-btn-light',
                            enableToggle: true,
                            pressed: true,
                            style: {
                                marginRight: '2px'
                            }
                        },
                        {
                            tooltip: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_LONG_REQUEST}}",
                            handler: Ext.bind (this._filter, this),
                            itemId: 'priorityfilterlong',
                            iconCls: 'a-btn-glyph ametysicon-datetime-clock size-16',
                            cls: 'a-btn-light',
                            enableToggle: true,
                            pressed: true,
                            style: {
                                marginRight: '2px'
                            }
                        },                        
                        {
                            tooltip: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_MAJOR}}",
                            handler: Ext.bind (this._filter, this),
                            itemId: 'priorityfiltermajor',
                            iconCls: 'a-btn-glyph ametysicon-arrow-up-double size-16',
                            cls: 'a-btn-light',
                            enableToggle: true,
                            pressed: true,
                            style: {
                                marginRight: '2px'
                            }
                        },
                        {
                            tooltip: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_NORMAL}}",
                            handler: Ext.bind (this._filter, this),
                            itemId: 'priorityfilternormal',
                            iconCls: 'a-btn-glyph ametysicon-arrow-up-simple size-16',
                            cls: 'a-btn-light',
                            enableToggle: true,
                            pressed: true,
                            style: {
                                marginRight: '2px'
                            }
                        },
                        {
                            tooltip: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_MINOR}}",
                            handler: Ext.bind (this._filter, this),
                            itemId: 'priorityfilterminor',
                            iconCls: 'a-btn-glyph ametysicon-arrow-down-simple size-16',
                            cls: 'a-btn-light',
                            enableToggle: true,
                            style: {
                                marginRight: '2px'
                            }
                        },
                        {
                            tooltip: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_IDLE}}",
                            handler: Ext.bind (this._filter, this),
                            itemId: 'priorityfilteridle',
                            iconCls: 'a-btn-glyph ametysicon-arrow-down-double size-16',
                            cls: 'a-btn-light',
                            enableToggle: true,
                            style: {
                                marginRight: '2px'
                            }
                        },                        
                        {
                            tooltip: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_BEACON}}",
                            handler: Ext.bind (this._filter, this),
                            itemId: 'priorityfilterbeacon',
                            iconCls: 'a-btn-glyph ametysicon-system-connection-wireless size-16',
                            cls: 'a-btn-light',
                            enableToggle: true
                        },                        {
                            xtype: 'tbspacer',
                            flex: 0.0001
                        },
                        {
                            tooltip: "{{i18n PLUGINS_CORE_UI_TOOLS_REQUESTS_TRACKER_CLEAR_DESCRIPTION}}",
                            handler: function() { Ametys.plugins.coreui.system.requesttracker.RequestTrackerTool.removeAll(); },
                            iconCls: 'ametysicon-garbage11 size-16',
                            cls: 'a-btn-light'
                        }
                    ]
                }
            ],
            
		    columns: [
		        {stateId: 'treegrid-id', xtype: 'treecolumn', header: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_ID}}", minWidth: 102, width: 102, sortable: false, hideable: false, dataIndex: 'name', renderer:  Ext.bind(this._renderId, this)},
		        {stateId: 'treegrid-date', header: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_TIME}}", minWidth: 65, width: 65, sortable: false, hideable: false, dataIndex: 'date', renderer: Ext.bind(this._renderDateOrPriority, this)},
                {stateId: 'treegrid-duration', header: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_DURATION}}", minWidth: 115, width: 115, sortable: false, hideable: false, dataIndex: 'serverSideDuration', renderer: Ext.bind(this._renderDuration, this) },
                {stateId: 'treegrid-status', header: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_STATUS}}", minWidth: 70, width: 70, sortable: false, dataIndex: 'status', hideable: false, renderer: Ext.bind(this._renderStatus, this)},
                {stateId: 'treegrid-call', header: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_CALL}}", minWidth: 200, flex: 1000, sortable: false, hideable: false, dataIndex: 'type', renderer: Ext.bind(this._renderCall, this)},
		    ],
            
            viewConfig: { 
                getRowClass: function(record) { 
                    if (record.get('type') == null)
                    {
                        return record.get('status') && record.get('status') != '200' ? 'request-errors' : '';
                    }
                    else if (record.get('errors') == 0 || record.get('status') == null)
                    {
                        return ''
                    }
                    else
                    {
                        return record.get('children').length == record.get('errors') ? 'request-errors' : 'request-warnings';
                    }
                } 
            }, 
		    
		    listeners: {
                'selectionchange': this._onSelectionChange, 
                scope : this
            }
		});
		
        this.rightRequestPanel = Ext.create("Ext.Component", {
            scrollable: true,
            title: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_DISPLAYMESSAGE_REQUEST}}",
            cls: 'a-panel-text',
            defaultHtml: "{{i18n PLUGINS_CORE_UI_TOOLS_REQUESTS_TRACKER_MESSAGE}}",
            html: "{{i18n PLUGINS_CORE_UI_TOOLS_REQUESTS_TRACKER_MESSAGE}}"
        });
        
        this.rightResponsePanel = Ext.create("Ext.Component", {
            scrollable: true,
            title: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_DISPLAYMESSAGE_RESPONSE}}",
            cls: 'a-panel-text',
            defaultHtml: "{{i18n PLUGINS_CORE_UI_TOOLS_REQUESTS_TRACKER_MESSAGE}}",
            html: "{{i18n PLUGINS_CORE_UI_TOOLS_REQUESTS_TRACKER_MESSAGE}}"
        });
        
        this.rightCallstackPanel = Ext.create("Ext.Component", {
            scrollable: true,
            title: "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_DISPLAYMESSAGE_CALLSTACK}}",
            cls: 'a-panel-text',
            defaultHtml: "{{i18n PLUGINS_CORE_UI_TOOLS_REQUESTS_TRACKER_MESSAGE}}",
            html: "{{i18n PLUGINS_CORE_UI_TOOLS_REQUESTS_TRACKER_MESSAGE}}",
            listeners: {
                'show': function() {
                    this.openTrace();
                },
                scope: this
            }
        });
        
		this.rightPanel = Ext.create("Ext.tab.Panel", {
            stateId: this.self.getName() + "$rightPanel",
            stateful: true,
            minWidth: 100,
            split: true,
            border: true,
            flex: 0.5,
            items: [ this.rightRequestPanel, this.rightResponsePanel, this.rightCallstackPanel ]
        });

        this._messageRequestTpl = new Ext.Template(
            "<b>{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_DISPLAYMESSAGE_URL}}</b> : ",
            "{url}<br/><br/>",
            "<b>{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_DISPLAYMESSAGE_PARAMETERS}}</b> : ",
            "<code class='request-tracker'>{parameters}</code>",
        );
        this._messageResponseTpl = new Ext.Template(
            "<code class='request-tracker'>{response}</code><br/>",
        );
        this._messageCallstackTpl = new Ext.XTemplate(
            "<tpl if='unminified == false'><div class='request-tracker-unminifying'>{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_DISPLAYMESSAGE_CALLSTACK_LOADING}}</div></tpl>", 
            "<div>{callstack}</div>"
        );
        
		return Ext.create("Ext.container.Container", {
			layout: { 
                type: 'hbox',
                align: 'stretch'
            },
            cls: 'uitool-requesttracker',
			items: [ this.tree, this.rightPanel ]
		});
	},
	
	_onSelectionChange: function(view, records)
    {
        if(records.length > 0 && this._isMsg(records[0]))
        {
            this._onSelectMessage(view, records[0])
        }
        else
        {
            this.rightRequestPanel.update(this.rightRequestPanel.defaultHtml); 
            this.rightResponsePanel.update(this.rightResponsePanel.defaultHtml); 
            this.rightCallstackPanel.update(this.rightCallstackPanel.defaultHtml);
        }
    },
	
	/**
     * @private
     * Is a message record (or a group message)?
     */
	_isMsg: function(record)
	{
        return record.get('name').includes(".");
    },

    _renderId: function(value, metadata, record)
    {
        if (this._isMsg(record))
        {
            let i = value.indexOf(".");
            return value.substring(i+1);
        }
        else
        {
            return value + " (" + record.get('children').length + ")";
        }
    },
    
    _renderDateOrPriority: function(value, metadata, record)
    {
        if (this._isMsg(record))
        {
            let priority;
            switch (record.get('message').priority)
            {
                case Ametys.data.ServerComm.PRIORITY_MAJOR: priority = "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_MAJOR}}"; break;
                case Ametys.data.ServerComm.PRIORITY_NORMAL: priority = "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_NORMAL}}"; break;
                case Ametys.data.ServerComm.PRIORITY_MINOR: priority = "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_MINOR}}"; break;
                case Ametys.data.ServerComm.PRIORITY_SYNCHRONOUS: priority = "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_SYNCHRONOUS}}"; break;
                case Ametys.data.ServerComm.PRIORITY_LONG_REQUEST: priority = "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_LONG_REQUEST}}"; break;
                case Ametys.data.ServerComm.PRIORITY_BEACON: priority = "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_BEACON}}"; break;
                case Ametys.data.ServerComm.PRIORITY_IDLE: priority = "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_PRIORITY_IDLE}}"; break;
                default: priority = record.get('message').priority;
            }
            return priority;
        }
        else
        {
            return Ext.util.Format.dateRenderer(Ext.Date.patterns.ShortTime).apply(this, arguments);
        }
    },
         
    _renderDuration: function(value, metadata, record)
    { 
        if (isNaN(parseFloat(value)))
        {
            return "<div style='padding-top: 16px' class='x-mask-msg-text'></div>";
        }
        else if (!this._isMsg(record))
        {
            return "<div style='text-align: right'>" + Ext.util.Format.duration(record.get('globalDuration')*1000.0) + " / " + Ext.util.Format.duration(record.get('serverSideDuration')*1000.0) + "<div>"; 
        }
        else
        {
            return "<div style='text-align: right'>" + Ext.util.Format.duration(record.get('serverSideDuration')*1000.0) + "<div>"; 
        }
    },
    
    _renderStatus: function(value, metadata, record)
    {
        if (this._isMsg(record))
        {
            return value;
        }
        else
        {
            switch (value)
            {
                case 0:
                    if (record.get('errors') > 0)
                    {
                        return Ext.String.format("{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_STATUS_ERRORS}}", record.get('errors'), record.get('children').length); 
                    } 
                    else
                    {
                        return "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_STATUS_OK}}"; 
                    }
                case 1: return "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_STATUS_CANCELED}}";
                case 2: return "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_STATUS_FAILURE}}";
                default: return value;
            }
        }
    },
	
	/**
	 * @private
	 * The renderer for the call column
     * @param {Object} value The data value for the current cell
     * @param {Object} metaData A collection of metadata about the current cell; can be used or modified by the renderer. Recognized properties are: tdCls, tdAttr, and style.
     * @param {Ext.data.Model} record The record for the current row
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The data store
     * @param {Ext.view.View} view The current view
     * @return {String} The HTML string to be rendered.
     */
	_renderCall: function(value, metaData, record, rowIndex, colIndex, store, view)
	{
        if (this._isMsg(record))
        {
            return this._clientCallToString(record);
        }
        else
        {
            switch (value)
            {
                case "async": return "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_CALL_ASYNC}}";
                case "sync": return "{{i18n PLUGINS_CORE_UI_REQUESTSTRACKER_TOOL_COL_CALL_SYNC}}";
            }
        }
	},
	
	/**
     * @private
     */
	_clientCallToString: function(record)
	{
        let cc = record.get('clientCall');
        if (cc == null)
        {
            return (record.get('workspace') ? ("/_" + record.get('workspace')) : record.get('plugin') ? ('/plugins/' + record.get('plugin')) : '') + "/" + 
                record.get('url') + " (" + record.get('returnType') + ")";
        }
        else
        {
            return (cc.id || cc.role) + "#" + cc.method + (cc.id ? " (" + cc.role + ")": "");
        }
    },
	
	getMBSelectionInteraction: function() 
	{
	    return Ametys.tool.Tool.MB_TYPE_NOSELECTION;
	},
	        
    getType: function()
    {
        return Ametys.tool.Tool.TYPE_DEVELOPER;
    },
	
	/**
	 * Listener on the south grid panel (details in a request), when selecting a record
	 * @param {Ext.selection.RowModel} selModel The selection mode
	 * @param {Ext.data.Model} record The record selected
	 * @param {Object} eOpts The options object passed to Ext.util.Observable.addListener.
	 * @private
	 */
	_onSelectMessage: function (selModel, record, eOpts)
	{
		var id = (record.get('name')).split(".")[1];
		var message = record.get("message");
		var response = record.get("response");
		
		var parametersAsString = Ext.JSON.prettyEncode( Ext.JSON.decode(Ext.JSON.encode(message.parameters || null)) ); // Encode/decode to ensure the json object reflect the sent one (e.g. eliminate "undefined" values)
		var responseAsString = this._responseToString(response, id);
		
		this.rightRequestPanel.update(this._messageRequestTpl.applyTemplate({url: message.url, parameters: parametersAsString})); 
        this.rightResponsePanel.update(this._messageResponseTpl.applyTemplate({response: responseAsString})); 
		this.rightCallstackPanel.update(this._messageCallstackTpl.applyTemplate({unminified: false, callstack: Ext.String.stacktraceToHTML(record.get("callstack"))}));
		console.info(record.get("callstack"));
		if (this.rightCallstackPanel.isVisible())
		{
            this.openTrace();
        }
	},
	
	/**
	 * Display a part of the response as a readable string
	 * @param {Object} response The XMLHTTPResponse
	 * @param {String} id The id of the part of the response to extract and display
	 * @private
	 */
	_responseToString: function(response, id)
	{
		if (response == null)
		{
			return "null";
		}
		else
		{
			function xmlstringToHTML(xmlstring)
			{
				function escape(htmlstring)
				{
					return htmlstring.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\r\n/g, '<br/>').replace(/&amp;#160;/g, '&#160;');
				}
				function escapeOpening(tagstring)
				{
					return "<span class='tag'>" + tagstring.replace(/( [^=]+)(=)(\"[^"]*\")/g, "<span class='attr-name'>$1</span><span class='attr-eq'>$2</span><span class='attr-value'>$3</span>") + "</span>";
				}
				function escapeText(textstring, pad)
				{
					var json = Ext.JSON.decode(textstring, true)
					if (json != null)
					{
						var ppad = parseInt(pad/ 2) + 1;
						textstring = Ext.JSON.prettyEncode(json, ppad);
						if (textstring.indexOf("<br/>") != -1)
						{
							var padding = "";
					        for (var i = 0; i < ppad; i++) 
					        {
					            padding += '&#160;&#160;&#160;&#160;';
					        }
							textstring = "<br/>" + padding + textstring + "<br/>";
						}
					}
					
					return "<span class='text'>" + textstring + "</span>";
				}
				function escapeClosing(tagstring)
				{
					return "<span class='tag'>" + tagstring + "</span>";
				}
				
			    var formatted = '';
			    xml = xmlstring.replace(/(>)(<)(\/*)/g, '$1\r\n$2$3');
			    var pad = 0;
			    Ext.each(xml.split('\r\n'), function(node, index) 
			    	{
				        var indent = 0;
				        if (node.match( /.+<\/\w[^>]*>$/ )) 
				        {
				        	// full tag <test>foo</test>
				        	node = escape(node);
				        	
				        	var i = node.indexOf('>');
				        	var j = node.indexOf('&lt;', i+1);
				        	node = escapeOpening(node.substring(0, i+1)) 
				        			+ escapeText(node.substring(i+1, j), pad) 
				        			+ escapeClosing(node.substring(j));
				            indent = 0;
				        } 
				        else if (node.match( /^<\/\w/ )) 
				        {
				        	// just a closing tag </test>
				        	node = escape(node);
				        	node = escapeClosing(node);
				            if (pad != 0) 
				            {
				                pad -= 1;
				            }
				        } 
				        else if (node.match( /^<\w[^>]*[^\/]>.*$/ )) 
				        {
				        	// just an opening tag <test attr="1">
				        	node = escape(node);
				        	node = escapeOpening(node);
				            indent = 1;
				        } 
				        else 
				        {
				        	// autoclosing tags <test/>
				        	node = escape(node);
				        	node = escapeOpening(node);
				            indent = 0;
				        }
				 
				        var padding = '';
				        for (var i = 0; i < pad; i++) 
				        {
				            padding += '&#160;&#160;';
				        }
				 
				        formatted += padding + node + '<br/>';
				        pad += indent;
			    	}
			    );
			 
			    return formatted;
			}
			
			var node = Ext.dom.Query.selectNode("/responses/response[@id='" + id + "']", response.responseXML);
			if (node.outerHTML)
			{
				return xmlstringToHTML(node.outerHTML);
			}
			else
			{
				try 
				{
					// Gecko- and Webkit-based browsers (Firefox, Chrome), Opera.
					return xmlstringToHTML((new XMLSerializer()).serializeToString(node));
				}
				catch (e) 
				{
					try 
					{
						// Internet Explorer.
						return xmlstringToHTML(xmlNode.xml);
					}
					catch (e) {  
						//Other browsers without XML Serializer
						return "..."
					}
				}
			}
		}
	},
	
	_createEntry: function(type, messages) 
	{
        var store = this.store;
        try
        {
            var id = this._currentId++;
            
            var record = Ext.create("Ametys.plugins.coreui.system.requesttracker.RequestTrackerTool.RequestEntry", {
                "id": id,
                "name": id,
                "response": null,
                
                "expanded": true,
                
                "type": type,
                "date": new Date(),
                "globalDuration": null,
                "serverSideDuration": null,
                "status": null,
                
                "iconCls": 'a-tree-glyph ametysicon-system-server-sync',
                "children": this._createBottomLevelModelEntry(id, messages)
            });
            
            
            // Scroll to the top of the panel if is was already on top...
            if (this.tree.getView().getEl().getScrollTop() == 0)
            {
                let me = this;
                window.setTimeout(function() {
                    me.tree.getView().getEl().setScrollTop(0);
                }, 0);
            }
            
            store.suspendEvents();
            store.getRootNode().insertBefore(record, store.getRootNode().getChildAt(0));
            this._filter();
            store.resumeEvents();
            this.tree.getView().refresh();
            
            return id;
        }
        catch (e)
        {
            this.getLogger().error({
                message: "Cannot create the request entry",
                details: e
            })
        }
    },
    
    
    /**
     * Create the second level nodes
     * @param {String} id The parent id
     * @param {Object[]} messages The messages that will create the sub nodes
     * @private
     */
    _createBottomLevelModelEntry: function (id, messages)
    {
        let rs = [];
        
        for (let i = 0; i < messages.length; i++)
        {
            var message = messages[i];
            
            let r = Ext.create("Ametys.plugins.coreui.system.requesttracker.RequestTrackerTool.RequestEntry", {
                "name": id + "." + i,
                "response": null,
                
                "type": null,
                "date": null,
                "globalDuration": null,
                "serverSideDuration": null,
                
                "message": message,
                "status": null,
                
                "iconCls": 'a-tree-glyph ametysicon-desktop-envelope-open',
                "leaf": true
            });
            
            rs.push(r);
        }
        
        return rs;
    },        
    
    _updateEntry: function(observerId, responseType, response)
    {
        try
        {
            let record = this.store.byIdMap[observerId];
            if (record == null)
            {
                // Let's ignore this request arrival, because we were not listening when it started 
                return;
            }
            
            this.store.suspendEvents();
            
            // update child firt to reflect on parent values
            this._updateBottomLevelModelEntry(record, response);
            
            record.set({
                "response": response,
                "globalDuration": (new Date().getTime() - record.get("date").getTime()) / 1000.0,
                "serverSideDuration": response && response.responseXML ? Ext.dom.Query.selectNode("/responses/times", response.responseXML).getAttribute("duration") / 1000.0 : Infinity,
                "status": responseType
            }, { commit: true });

            this.store.resumeEvents();
            this.tree.getView().refresh();
            
            // If the current selection was just updated, let's refresh the right panel
            let selection = this.tree.getSelectionModel().getSelection();
            if (selection.length > 0 && this._isMsg(selection[0]) && selection[0].parentNode.id == record.id)
            {
                this._onSelectionChange(this.tree.getView(), selection);
            }
        }
        catch (e)
        {
            this.getLogger().error({
                message: "Cannot update the request entry",
                details: e
            })
        }
    },
	
    _updateBottomLevelModelEntry: function (record, response)
    {
        let children = record.get('children');
        for (let i = 0; i < children.length; i++)
        {
            let subRecord = children[i];

            var responseElement =  response && response.responseXML ? Ext.dom.Query.selectNode("/responses/response[@id='" + i + "']", response.responseXML) : null;

            subRecord.set({
                "response": response,
                "serverSideDuration": response && response.responseXML ? Ext.dom.Query.selectNode("/responses/times/time[@id='" + i + "']", response.responseXML).getAttribute("duration") / 1000.0 : Infinity,
                "status": responseElement ? responseElement.getAttribute("code") : "?"
            }, { commit: true });
        }
    },
	
	/**
	 * Observer of the Ametys.data.ServerComm for sent request
	 * @param {Object} sendOptions The options used in Ametys.data.ServerComm#_sendMessages 
	 */
	onRequestDeparture: function (sendOptions)
	{
        sendOptions.observerId = this._createEntry("async", sendOptions.messages);
	},
	
	/**
	 * Observer of the Ametys.data.ServerComm for request back
	 * @param {Object} sendOptions The options used in Ametys.data.ServerComm#_sendMessages 
	 * @param {Number} responseType 0 for success, 1 for canceled and 2 for failure
	 * @param {Object} [response] The XMLHTTPResponse when available
	 */
	onRequestArrival: function (sendOptions, responseType, response)
	{
        this._updateEntry(sendOptions.observerId, responseType, response);
	},

	/**
	 * Observer of the Ametys.data.ServerComm for sent synchronous request
	 * @param {Object} message The message sent. Argument of Ametys.data.ServerComm#_sendSynchronousMessage
	 */
	onSyncRequestDeparture: function (message)
	{
        let me = this;
        window.setTimeout(function() {
            message.observerId = me._createEntry("sync", [message]);
        });
	},
	
	/**
	 * Observer of the Ametys.data.ServerComm back from a synchronous request
	 * @param {Object} message The message sent. Argument of Ametys.data.ServerComm#_sendSynchronousMessage
	 * @param {Number} responseType 0 for success and 2 for failure
	 * @param {Object} [response] The XMLHTTPResponse when available
	 */
	onSyncRequestArrival: function (message, responseType, response)
	{
        // We asynchronously update the grid, otherwise this can lead to side effect (especially if the request is done while rendering a cell in a grid RUNTIME-3980)
        let me = this;
        window.setTimeout(function() {
            me._updateEntry(message.observerId, responseType, response);
        });
	},
	
	onClose: function ()
	{
		this.callParent(arguments);
		Ametys.data.ServerComm._observer = {};
	},
	
    /**
     * Load and display the stacktrace of the currently selected request
     * @param {Object} span The dom element that triggered the open trace
     */
    openTrace: function ()
    {
        let selection = this.tree.getSelectionModel().getSelection();
        if (selection.length > 0)
        {
            let record = selection[0];
            let callstack = record.get("callstack");
            
            if (callstack)
            {
                Ametys.plugins.coreui.system.devmode.StacktraceHelper.unminifyStacktrace(callstack, Ext.bind(this._openTraceCb, this, [record.getId()], true));
            }
        }
    },

    /**
     * Callback after unminifying the stacktrace
     * @param {String} response The unminified callstack, or null
     * @param {String} recordId The id for which the unminify was asked
     */
    _openTraceCb: function (response, recordId)
    {
        if (recordId == this.tree.getSelectionModel().getSelection()[0].getId())
        {
            this.rightCallstackPanel.update(this._messageCallstackTpl.applyTemplate({unminified: true, callstack: Ext.String.stacktraceToHTML(response, 1)}));
        }
    },
    
    _clearFilter: function()
    {
        let toolbar = this.tree.getDockedComponent("toolbar");

        toolbar.getComponent("textfilter").reset();
    },
    
    _filter: function()
    {
        let me = this;
        let toolbar = this.tree.getDockedComponent("toolbar");

        let textFilter = new String(toolbar.getComponent("textfilter").getValue()).trim().toLowerCase();
        
        let syncFilter = toolbar.getComponent("priorityfiltersync").pressed
        let longFilter = toolbar.getComponent("priorityfilterlong").pressed
        let majorFilter = toolbar.getComponent("priorityfiltermajor").pressed
        let normalFilter = toolbar.getComponent("priorityfilternormal").pressed
        let minorFilter = toolbar.getComponent("priorityfilterminor").pressed
        let idleFilter = toolbar.getComponent("priorityfilteridle").pressed
        let beaconFilter = toolbar.getComponent("priorityfilterbeacon").pressed

        this.tree.getStore().clearFilter();
            
        if (textFilter || !syncFilter || !longFilter || !majorFilter || !minorFilter || !idleFilter || !beaconFilter)
        {
            this.tree.getStore().filterBy(filter);
        }
        
        function filter(record)
        {
            let isVisible = false;
            if (me._isMsg(record))
            {
                if (!syncFilter && record.get('message').priority == Ametys.data.ServerComm.PRIORITY_SYNCHRONOUS
                    || !longFilter && record.get('message').priority== Ametys.data.ServerComm.PRIORITY_LONG_REQUEST
                    || !majorFilter && record.get('message').priority== Ametys.data.ServerComm.PRIORITY_MAJOR
                    || !normalFilter && record.get('message').priority== Ametys.data.ServerComm.PRIORITY_NORMAL
                    || !minorFilter && record.get('message').priority== Ametys.data.ServerComm.PRIORITY_MINOR
                    || !idleFilter && record.get('message').priority== Ametys.data.ServerComm.PRIORITY_IDLE
                    || !beaconFilter && record.get('message').priority== Ametys.data.ServerComm.PRIORITY_BEACON)
                {
                    isVisible = false;
                }
                else
                {
                    isVisible = me._clientCallToString(record).toLowerCase().includes(textFilter)
                            || Ext.JSON.encode(record.get('message').parameters || '').toLowerCase().includes(textFilter);
                }
            }
            else
            {
                for (let i = 0; !isVisible && i < record.childNodes.length; i++) 
                {
                    isVisible = filter(record.childNodes[i]);
                }
                        
                if (isVisible && textFilter)
                {
                    // me.tree.expandNode(record);
                }
            }
            
            return isVisible;
        }
    }
});