/*
 *  Copyright 2017 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.
 */

/**
 * A log tool displayed logs from the server. It can be configured to show only a specific category of logs.
 * @private  
 */
Ext.define('Ametys.plugins.coreui.log.ServerLogTool', {
    extend: "Ametys.plugins.coreui.log.AbstractLogTool",
    
    /**
     * @cfg {Number} [refreshTimer=2000] The logs refresh wait time between , in milliseconds. 
     * The tool also waits for the previous request to resolve before launching a new timer, which can result in a few additional seconds between refreshes if there are a lot of data to process or if the server lags.
     */
    
    /**
     * @property {Number} _lastLogTimestamp The most recent timestamp received.
     * @private
     */
    
    /**
     * @property {Number} _timerId The id of the timer for the next refresh.
     * @private
     */    
    /**
     * @property {Boolean} _isRunning True if the timer is running.
     */
    /**
     * @private
     * @property {String[]} logsCategory The list of categories displayed. See Ametys.plugins.coreui.log.ServerLogTool#setParams.
     */
	
	/**
     * @private
     * @property {String[]} serverRole The role of server component to get log events. See Ametys.plugins.coreui.log.ServerLogTool#setParams.
     */
	/**
     * @private
     * @property {String[]} serverId The id of extension to get log events. See Ametys.plugins.coreui.log.ServerLogTool#setParams.
     */

    constructor: function(config)
    {
        this.callParent(arguments);
        
        this._detailsTpl = new Ext.XTemplate(
            "<tpl if='timestamp'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_TIMESTAMP_TEXT}}</b> {timestamp}<br /></tpl>",
            "<tpl if='user'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_USER_TEXT}}</b> {user}<br /></tpl>",
            "<tpl if='thread'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_THREAD_TEXT}}</b> {thread}<br /></tpl>",
            "<tpl if='level'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_LEVEL_TEXT}}</b> {level}<br /></tpl>",
            "<tpl if='category'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CATEGORY_TEXT}}</b> {category}<br /></tpl>",
            "<tpl if='requestURI'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_REQUESTURI_TEXT}}</b> {requestURI}<br /></tpl>",
            "<tpl if='message'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_MESSAGE_TEXT}}</b> <div class='log-message'>{message}</div><br /></tpl>",
            "<tpl if='location'><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_LOCATION_TEXT}}</b> {location}<br /></tpl>",
            "<tpl if='callstack'><br /><b>{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CALLSTACK_TEXT}}</b><br />{callstack}</tpl>"
        );
        
        Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onModified, this);
    },
    
    getMBSelectionInteraction: function()
    {
        return Ametys.tool.Tool.MB_TYPE_ACTIVE;
    },
    
    /**
     * Listener when a category has been modified
     * Will update the state of the buttons effectively upon the current selection.
     * @param {Ametys.message.Message} message The modified message.
     * @protected
     */
    _onModified: function (message)
    {
        var me = this;
        var modifiedCategories = [];
        
        var targets = message.getTargets(Ametys.message.MessageTarget.LOG_CATEGORY);
        if (targets.length > 0)
        {
            Ametys.message.MessageTargetFactory.createTargets({
                id: Ametys.message.MessageTarget.LOG_CATEGORY,
                parameters: {
                    ids: this.logsCategory
                }
            }, function(targets) {
                var levels = {};
                
                Ext.Array.each(targets, function (target) {
                   var params = target.getParameters();
                   levels[params.id] = params.effectiveLevel;
                });
                
                me._updateHint(levels);            
            });
        }
    },
    
    sendCurrentSelection: function()
    {
        if (this.logsCategory && this.logsCategory.length > 0)
        {
            Ext.create('Ametys.message.Message', {
                type: Ametys.message.Message.SELECTION_CHANGED,
                targets: {
                    id: Ametys.message.MessageTarget.LOG_CATEGORY,
                    parameters: {
                        ids: this.logsCategory
                    }
                },
                callback: Ext.bind(this._newSelectionSent, this)
            });
        }
        else
        {
            Ext.create('Ametys.message.Message', {
                type: Ametys.message.Message.SELECTION_CHANGED,
                targets: {
                    id: Ametys.message.MessageTarget.LOG_CATEGORY,
                    parameters: {
                        ids: ['root']
                    }
                }
            });
        }
    },
    
    /**
     * @private
     * When sendCurrentSelection sent a new selection => let's update the hint with level info
     * @param {Ametys.message.Message} message The selection message
     */
    _newSelectionSent: function(message)
    {
        var levels = {};
        
        Ext.Array.each(message.getTargets(), function (target) {
           var params = target.getParameters();
           levels[params.id] = params.effectiveLevel;
        });
        
        this._updateHint(levels);
    },
    
    /**
     * @private
     * Update the hint part of the tool
     * @param {Object} logLevels The association category - loglevel 
     */
    _updateHint: function(logLevels)
    {
        if (!this.isNotDestroyed())
        {
            return;
        }
        
        var hint = this.grid.getDockedItems("#categories-hint")[0];
        if (hint)
        {
            if (this.logsCategory.length > 0)
            {
                var logsLink = [];
                for (var i = 0; i < this.logsCategory.length; i++)
                {
                    let canOpen = (Ametys.tool.ToolsManager.getFactory("uitool-admin-logslevel") != null); 
                    let hrefString = (canOpen ? " href=\"javascript:void(0)\"" : "");
                    let onclickString = (canOpen ? " onclick=\"Ametys.tool.ToolsManager.openTool('uitool-admin-logslevel', {'expandCategories' : ['" + this.logsCategory[i] + "']})\"" : "");
                    let titleString = (canOpen ? " title=\"" + Ext.String.format("{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CATEGORIE_OPEN_HINT}}", this.logsCategory[i]) + "\"" : "");
                    
                    logsLink.push("<a class=\"link\"" 
                                + hrefString 
                                + titleString
                                + onclickString 
                                + ">" 
                                + this.logsCategory[i] 
                                + (logLevels && logLevels[this.logsCategory[i]] ? " (" + "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CATEGORIES_HINT_LEVEL}} " + logLevels[this.logsCategory[i]] + ")" : "")
                                + "</a>")
                }
                
                hint.items.get(0).update(Ext.String.format("{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CATEGORIES_HINT}}", logsLink.join(", ")));
                hint.show();
            }
            else
            {
                hint.hide();
            }
        }
    },
    
	/**
	 * @inheritdoc
	 * @param {String/String[]} params.category Limit the logs to one or more specified categories. If empty or omitted, all categories are displayed. 
	 * @param {String} [serverRole=org.ametys.core.ui.RibbonControlsManager] The server role used to get logs.
	 * @param {String} serverId The server id to get logs. If no serverRole is configured, expected id is the id of a button controller. Can not be null.
	 */
    setParams: function(params)
    {
        if (this._timer)
        {
            clearTimeout(this._timer);
        }

        this.callParent(arguments);

        if (params.title)
        {
            this.setTitle(Ext.String.format("{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_LABEL_WITH_CATEGORIES}}", params.title));
        }
        else
        {
            this.setTitle(this.getInitialConfig().title);
        }

		this.serverRole = params.serverRole || 'org.ametys.core.ui.RibbonControlsManager';
		this.serverId = params.serverId;
        this.logsCategory = Ext.Array.from(params.category || null);
        this._refreshTimer = params.refreshTimer || 2000;

        this._updateHint();
        
        this._lastLogTimestamp = null;
        this._isRunning = true;
        this._unsetPauseDecorator();
        this._logsQueue = [];

        this._store.removeAll();
        this.showOutOfDate();
    },
    
    refresh: function(manual)
    {
        this.updateLogs(true);
        this.showUpToDate();
        if (manual)
        {
            this.showRefreshing();
        }
    },
    
    getStore: function()
    {
        this._store = Ext.create("Ext.data.ArrayStore",{
            autoDestroy: true,
            trackRemoved: false,
            proxy: { type: 'memory' },
            sorters: [{property: 'timestamp', direction: 'DESC'}],
            model: "Ametys.plugins.coreui.log.ServerLogTool.ServerLogEntry"
        });
        
        return this._store;
    },

    getColumns: function()
    {
        return [{
                stateId: 'grid-timestamp',
                header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_TIMESTAMP_HEADER}}",
                width: 130,
                sortable: true,
                dataIndex: 'timestamp',
                renderer: Ext.bind(this.datetimeRenderer, this)
            },
            {
                stateId: 'grid-user',
                header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_USER_HEADER}}",
                width: 100,
                sortable: true,
                dataIndex: 'user',
                renderer: Ext.bind(this.defaultRenderer, this),
                filter: {
                    type: 'string'
                }
            },
            {
                stateId: 'grid-thread',
                header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_THREAD_HEADER}}",
                width: 150,
                sortable: true,
                dataIndex: 'thread',
                hidden: true,
                renderer: Ext.bind(this.defaultRenderer, this),
                filter: {
                    type: 'string'
                }
            },
            {
                stateId: 'grid-level',
                header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_LEVEL_HEADER}}",
                width: 75,
                sortable: true,
                dataIndex: 'level',
                renderer: Ext.bind(this.defaultRenderer, this),
                filter: {
                    type: 'list',
                    store: Ext.create('Ext.data.Store', {
                        fields: ['id','text'],
                        data: [
                           {id: "DEBUG", text: 'Debug'},
                           {id: "INFO", text: 'Info'},
                           {id: "WARN", text: 'Warn'},
                           {id: "ERROR", text: 'Error'},
                           {id: "FATAL", text: 'Fatal'}
                        ]
                    })
                }
            },
            {
                stateId: 'grid-category',
                header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CATEGORY_HEADER}}",
                width: 250,
                flex: 0.25,
                sortable: true,
                dataIndex: 'category',
                renderer: Ext.bind(this.defaultRenderer, this),
                filter: {
                    type: 'string'
                }
            },
            {
                stateId: 'grid-request-uri',
                header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_REQUESTURI_HEADER}}",
                width: 300,
                sortable: true,
                dataIndex: 'requestURI',
                flex: 0.25,
                renderer: Ext.bind(this.defaultRenderer, this),
                filter: {
                    type: 'string'
                }
            },
            {
                stateId: 'grid-message',
                header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_MESSAGE_HEADER}}",
                width: 400,
                sortable: true,
                dataIndex: 'message',
                flex: 0.5,
                renderer: Ext.bind(this.defaultRenderer, this),
                filter: {
                    type: 'string'
                }
            },
            {
                stateId: 'grid-location',
                header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_LOCATION_HEADER}}",
                width: 100,
                sortable: true,
                dataIndex: 'location',
                hidden: true,
                renderer: Ext.bind(this.defaultRenderer, this),
                filter: {
                    type: 'string'
                }
            },
            {
                stateId: 'grid-callstack',
                header: "{{i18n PLUGINS_CORE_UI_TOOLS_SERVERLOGS_CALLSTACK_HEADER}}",
                width: 100,
                sortable: true,
                dataIndex: 'callstack',
                hidden: true,
                renderer: Ext.bind(this.defaultRenderer, this),
                filter: {
                    type: 'string'
                }
            }];
    },
    
    getDetailsText: function(records)
    {
        var record = records[0];
        
        var data = {
            timestamp: Ext.Date.format(new Date(record.get("timestamp")), "d/m H:i:s.u"),
            user: record.get('user'),
            level: record.get('level'),
            category: record.get('category'),
            requestURI: Ext.String.htmlEncode(record.get('requestURI')),
            message: this.formatLogMessage(Ext.String.htmlEncode(record.get('message'))),
            thread: record.get('thread'),
            location: Ext.String.htmlEncode(record.get('location')),
            callstack: record.get('callstack') ? Ext.String.stacktraceJavaToHTML(record.get('callstack')) : null
        };
        
        return this._detailsTpl.applyTemplate(data);
    },

    getDockedItems: function()
    {
        return [{
            dock: 'top',
            xtype: 'container',
            hidden: true,
            itemId: 'categories-hint',
            layout: 'fit',
            cls: 'x-component-tool-hintmessage',
            items: [{
                xtype: 'component',
                cls: 'hintmessage-limited-content'
            }]
        }];
    },

    /**
     * Update the grid by calling the server to retrieve the lastest logs.
     * @param [force=false] force the refreshing, (ignoring the running state)
     */
    updateLogs: function(force)
    {
        if (!this._isRunning && !force)
        {
            return;
        }
		
		Ametys.data.ServerComm.callMethod({
			role: this.serverRole,
		 	id: this.serverId,
		 	methodName: "getEvents",
		 	parameters: [this._lastLogTimestamp, this.logsCategory],
		 	callback: {
		 		handler: this._updateLogsCb,
				scope: this
			}
		});   
    },
    
    /**
     * True if the tool is fetching live update, false if the log rotation has been paused.
     */
    isRunning: function()
    {
        return this._isRunning;
    },
    
    /**
     * Callback after receiving the logs from the server.
     * @param {Object[]} response The logs.
     * @private
     */
    _updateLogsCb: function(response)
    {
        if (!this.isNotDestroyed())
        {
            return;
        }
        
        var logs = [];
        Ext.Array.each(response, function (data) {
            logs.push(Ext.create("Ametys.plugins.coreui.log.ServerLogTool.ServerLogEntry", {
                timestamp: data.timestamp,
                user: data.user,
                level: data.level,
                category: data.category,
                requestURI: data.requestURI,
                message: data.message,
                location: data.location,
                thread: data.thread,
                callstack: data.callstack
            }));
            this._lastLogTimestamp = Math.max(data.timestamp, this._lastLogTimestamp);
        }, this);
        
        if (logs.length > 0)
        {
            this._logsQueue = this._logsQueue.concat(logs);
            
            var count = this._logsQueue.length;
            if (count > 1000)
            {
                this._logsQueue.splice(0, count - 1000);
            }

            this._store.loadData(this._logsQueue);
        }
        
        if (this._isRunning)
        {
            if (this._timer)
            {
                clearTimeout(this._timer);
            }
            this._timer = setTimeout(Ext.bind(this.showOutOfDate, this), this._refreshTimer);
        }
        
        this.showRefreshed();
    },
    
    onClose: function()
    {
        if (this._isRunning)
        {
            this._isRunning = false;
            clearTimeout(this._timer);
            this._timer = null;
        }
        
        this.callParent(arguments);
    },
    
    getRowClass: function(record)
    {
        return "msg-level-" + record.get('levelCode');
    },
    
    /**
     * Default renderer for the columns. Apply the coloration.
     * @param {Number} value The level
     * @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
     * @private
     */
    defaultRenderer: function(value, metadata, record)
    {
        return Ext.String.htmlEncode(value);
    },
    
    /**
     * Datetime renderer for the columns. Format the timestamp into a human readable time.
     * @param {Number} value The level
     * @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
     * @private
     */
    datetimeRenderer: function(value, metadata, record)
    {
        return Ext.Date.format(new Date(value), "d/m H:i:s.u");
    },
    
    /**
     * Action to pause or unpause sending update requests to the server.
     * @param {Boolean} pause True to pause, false to unpause.
     * @private
     */
    pauseUpdates: function(pause)
    {
        if (!pause && !this._isRunning)
        {
            // Reset the original decorator
            this._unsetPauseDecorator();
            this._isRunning = true;
            // we force the manual to display the refreshing spinner
            this.refresh(true);
        }
        
        if (pause && this._isRunning)
        {
            // Set a decorator to show the log are paused
            this._setPauseDecorator();
            this._isRunning = false;
            clearTimeout(this._timer);
            this._timer = null;
        }
    },
    
    _setPauseDecorator: function()
    {
        this.setIconDecorator('decorator-ametysicon-multimedia-command-pause');
        this.setIconDecoratorType('action-ui');
    },
    
    _unsetPauseDecorator: function()
    {
        this.setIconDecorator(this.getInitialConfig('icon-decorator'));
        this.setIconDecoratorType(this.getInitialConfig('icon-decorator-type'));
    },
    
    /**
     * Clear the logs from the grid.
     * @private 
     */
    clearLogs: function()
    {
        this._store.removeAll();
        this._logsQueue = [];
    },
    
    /**
     * Format a log message to keep new lines and indentation spaces
     * @param {String} message The log message
     * @return {String} The html formatted log message
     */
    formatLogMessage: function(message)
    {
        return message.split(/\r?\n/g)
            .map(function (line) {
                return line.replace(/^\s+/, function (startOfLine) {
                    // replace the empty chars at the start of the line by non breakable spaces
                    return startOfLine.replace(/ /g,'\xA0').replace(/\t/g,'\xA0\xA0\xA0\xA0');
                });
            })
            .join('<br />');
    }
});