/*
 *  Copyright 2014 Anyware Services
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * This tool does allow to edit and execute a script
 * @private
 */
Ext.define('Ametys.plugins.cms.search.ScriptTool', {
	extend: "Ametys.plugins.cms.search.ContentSearchTool",
	
	/**
	 * @cfg {Boolean} [readOnly=false] set to 'true' to open tool in read-only mode
	 */
	readOnly: false,

    searchButtonText: "{{i18n plugin.cms:UITOOL_SEARCH_BUTTON_EXECUTE}}",
    
    searchButtonTooltipText: "{{i18n plugin.cms:UITOOL_SEARCH_BUTTON_EXECUTE_DESC}}",
    
    searchButtonIconCls: 'ametysicon-play124',
    
    /**
     * @private
     * @property {String} _asyncExecuteButtonText The text for the "async execute" button
     */
    _asyncExecuteButtonText: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPT_TAB_EXECUTE_ASYNC_LABEL}}",
    
    /**
     * @private
     * @property {String} _asyncExecuteButtonTooltipText The text for the "async execute" tooltip
     */
    _asyncExecuteButtonTooltipText: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPT_TAB_EXECUTE_ASYNC_DESC}}",
    
    /**
     * @private
     * @property {String} _asyncExecuteButtonIconCls The CSS classes for "async execute" button
     */
    _asyncExecuteButtonIconCls: 'ametysicon-play124',
    
    /**
     * @private
     * @property {String} _asyncExecuteButtonIconCls The CSS classes for "async execute" tooltip
     */
    _asyncExecuteButtonTooltipIconCls: 'ametysicon-play124 decorator-ametysicon-arrow-circle-right-double',

    /**
     * Does not support export (to prevent reexecuting the script every time).
     * Force export buttons to be disabled
     * @private
     */
    ignoreExportButton: true,

    /**
     * Does not support print (to prevent reexecuting the script every time).
     * Force print button to be disabled
     * @private
     */
    ignorePrintButton: true,
    
    /**
     * The number of lines in the editor footer, currently forced at 3. 
     * @private
     */
    _footerLineCount: 3,

    /**
     * Toggle the edition of the header and footer of the editor
     * @private
     */
    _allowHeaderFooterEdit: false,


	getMBSelectionInteraction: function()
	{
	    return Ametys.tool.Tool.MB_TYPE_LISTENING;
	},

    constructor: function(config)
    {
        config.enablePagination = false;
        this.callParent(arguments);
    },
	
    // Does not support formating, because the columns are calculated from the script
    // This method is set to undefined to disable buttons depending on it
    getCurrentFormatting: undefined,

	setParams: function(params)
	{
		var initialTitle = this.getInitialConfig('title') || '';
		if (params.title && !Ext.String.startsWith(params.title, initialTitle + ' - '))
		{
			params.title = initialTitle + ' - ' + params.title;
            this.setTitle(params.title);
		}

        if (params.values && params.values.script)
        {
            this._allowHeaderFooterEdit = true;
            this._editor.setValue(params.values.script);
            this._allowHeaderFooterEdit = false;
        }
		
		this.readOnly = params.readOnly === true;
		
		this.callParent(arguments);
	},
	
	createPanel: function()
	{
        this.searchPanel = this._createSearchFormPanel();
        
        this.console = this._createConsolePanel();

		this.mainPanel = new Ext.Panel({
			region: 'center',
			layout: 'border',
            cls: 'uitool-script',
			items: Ext.Array.merge(
                [this.searchPanel],
		        this.getBottomPanelItems(),
		        [this.console]
            )
		});
		return this.mainPanel;
	},
    
    _createConsolePanel: function()
    {
        return new Ext.Panel({
            region: 'east',
            title: "{{i18n plugin.cms:UITOOL_SCRIPT_CONSOLE}}",
            html: '',
            scrollable: true,
            collapsible: true,
            collapsed: true,
            cls: 'coreui-script-result-tool',
            width: 300,
            split: true
        });
    },
	
	/**
	 * Creates the script editor
	 * @private
	 */
	_createSearchFormPanel: function ()
	{	
		this.form = this._createScriptFormPanel();
		
		var me = this;
		var cfg = this._getSearchFormPanelConfig();
        var panelCfg = Ext.apply (cfg, {
        	region: 'north',
        	split: true,
			border: false,
			
            ui: 'light',
        	header: {
        		titlePosition: 1
        	},
        	
        	stateful: true,
            stateId: this.getId() + '$script-search-form',
            
			scrollable: true,
			bodyPadding: 10,
			
			title: "{{i18n plugin.cms:UITOOL_SCRIPT_SEARCH_CRITERIA}}",
			collapsible: true,
            titleCollapse: true,
            animCollapse: false,
            
            minHeight: 250,
            // maxHeight: Ametys.plugins.cms.search.ContentSearchTool.FORM_DEFAULT_MAX_HEIGHT,
            
			layout: {
				type: 'vbox',
				align: 'stretch'
			},
			
			items: [this.form],
			
			listeners: {
                'resize': function (cmp, width, height)
                {
                    if (this._oldHeight && height != this._oldHeight && !this.collapsed)
                    {
                        // The panel was manually resized: save new ratio
                        this._oldHeight = height;
                        this._heightRatio = height / me.mainPanel.getHeight();
                    }
                }
            },
            
            applyState: function (state)
            {
                this._heightRatio = state.ratio;
            },
            
            getState: function ()
            {
                // Save the height ratio
                return {
                    ratio: this._heightRatio
                }
            }
        });
		
        var panel = Ext.create ('Ext.Panel', panelCfg);
        
        return panel;
	},
	
	/**
	 * @private
	 * Create the form for script editor
	 * @return {Ext.form.FormPanel} the form
	 */
	_createScriptFormPanel: function()
	{
        var footerLine1 = "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPT_FOOTER_HINT_LINE1}}".replace(/\r\n|\r|\n/, "");
        var footerLine2 = "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPT_FOOTER_HINT_LINE2}}".replace(/\r\n|\r|\n/, "");


        this._editor = Ext.create ("Ametys.form.field.CodeAdvanced", {
            cls: 'uitool-script-editor',
            name: "script",
            mode: 'typescript',
            
            readOnly: this.readOnly,
            flex: 1,
            stateful: true,
            stateId: this.getId() + "$code",            

            value: "function main() {\n    \n    \n    \n    \n}\n" + footerLine1 + "\n" + footerLine2,
            
            listeners: {
                beforechange: {
                    fn: function (editor, newValue, oldValue, event) {
                        if (!this._allowHeaderFooterEdit 
                            && event.changes.filter(change => change.range.startLineNumber == 1 || change.range.endLineNumber > oldValue.split("\n").length - this._footerLineCount).length > 0)
                        {
                            let me = this;
                            window.setTimeout(function() { me._updateDecorations(editor); }, 1); // Depending on the modification we refuse, the decoration may be destroyed... (select a few line including the last ones, and type one character for example)
                            return false;
                        }
                    },
                    scope: this
                },
                change: {
                    fn: function (editor, newValue, oldValue) {
                        this._updateDecorations(editor);
                    },
                    scope: this
                },
                initialize: {
                    fn: function (editor) {
                        let me = this;
                        window.setTimeout(function() { me._updateDecorations(editor); }, 0);

                        let ed = editor._monaco;
                        
                        // Replace the default Ctrl+A
                                                ed.addAction({
                            id: 'custom-select-all',
                            label: 'Select All Editable',
                            keybindings: [
                                monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyA
                            ],
                            precondition: null,
                            keybindingContext: null,
                            run: function(ed) { me._selectAll(); }
                        });
                        
                        // load definition for Ametys API
                        Ametys.data.ServerComm.send({
                            plugin: 'core-ui',
                            url: 'script/typescript-definitions.d.ts',
                            callback: { 
                                handler: function(response) 
                                {
                                    monaco.languages.typescript.javascriptDefaults.addExtraLib(response.textContent, 'ts:ametys.d.ts');
                                    monaco.languages.typescript.typescriptDefaults.addExtraLib(response.textContent, 'ts:ametys.d.ts');
                                },
                                scope: this
                            },
                            responseType: 'text',
                            waitMessage: { target: me.getContentPanel() },
                            errorMessage: true
                        });                        
                    },
                    scope: this
                }
            }
		});
		
		var formCfg =  {
            itemId: 'script-search-form-panel',
            scrollable:  true,
            
            flex: 1,
            items: this._editor,

            layout: 'fit',
            
            getJsonValues: function() 
            {
                var values  = {},
                    fields  = this.form.getFields().items,
                    fLen    = fields.length,
                    field;

                for (var f = 0; f < fLen; f++) 
                {
                    field = fields[f];
                    values[field.getName()] = field.getJsonValue();
                }
                
                return values;
            }
        };
        
        return Ext.create ('Ext.form.FormPanel', formCfg);
	},
    
    _getSearchFormPanelBBar: function()
    {
        var items = this.callParent(arguments);
        
        // insert just after the 'search' button
        var searchBtnIndex = Ext.Array.indexOf(
            items.map(function(b) {
	            return b.itemId;
	        }),
            'search'
        );
        var searchBtn = items[searchBtnIndex];
        var searchSubItem = Ext.applyIf({
            itemId: 'search-menu-item'
        }, searchBtn); // copy search btn
        var menu = {
            items: [
                searchSubItem,
                {
	                itemId: 'search-async-menu-item',
	                text: this._asyncExecuteButtonText,
	                iconCls: this._asyncExecuteButtonIconCls,
	                handler: this._executeAsync,
	                scope: this,
	                tooltip: {
	                    title: this._asyncExecuteButtonText,
	                    text: this._asyncExecuteButtonTooltipText,
	                    glyphIcon: this._asyncExecuteButtonTooltipIconCls,
	                    inribbon: false
	                }
                }
            ]
        };
        var splitBtn = Ext.applyIf({
            xtype: 'splitbutton',  
            menu: menu
        }, searchBtn); // we have to keep its item id otherwise superclass will not be able to retrieve it !
        var removeCount = 1;
        Ext.Array.replace(items, searchBtnIndex, removeCount, [splitBtn]);
        
        return items;
    },
    
    /**
     * @private
     * Launches the search/the script asynchronously.
     * @param {Ext.button.Button} btn The clicked button
     */
    _executeAsync: function(btn)
    {
        var script = this._editor.getValue();
        // remove the surrounding function: function main { ... }
        script = script.substring(script.indexOf('{') + 1, script.lastIndexOf('}'));

        if (Ametys.plugins.coreui.script.ScriptParameters.hasScriptParameters(script))
        {
            Ametys.log.ErrorDialog.display({
                title: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPT_PARAMETERS_ASYNC}}", 
                text: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPT_PARAMETERS_ASYNC_ERROR}}",
                category: "Ametys.plugins.coreui.script.ScriptParameters"
            });
            return;
        }


        Ametys.plugins.coreui.script.AsyncScriptDialog.open({
            serverCallFn: callMethodExecuteAsyncScript,
            scope: this
        });
        
        function callMethodExecuteAsyncScript(params)
        {
            this.serverCall(
                "add",
                [
                    "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPT_EXECUTE_ASYNC_TASK_TITLE}}",
                    "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPT_EXECUTE_ASYNC_TASK_DESCRIPTION}}",
                    "NOW",
                    "",
                    "org.ametys.core.schedule.Script",
                    {
                        "org.ametys.core.schedule.Script$recipient": params.recipient,
                        "org.ametys.core.schedule.Script$script": script,
                        "org.ametys.core.schedule.Script$workspace": Ametys.WORKSPACE_NAME,
                        "org.ametys.core.schedule.Script$selection": this._convertedSelection || '',
                        "org.ametys.core.schedule.Script$model": "search-ui.default"
                    }
                ],
                Ext.bind(_executeAsyncScriptCb, this),
                {
                    waitMessage: {
                        msg: "{{i18n plugin.cms:UITOOL_SEARCH_WAITING_MESSAGE}}",
                        target: this.mainPanel
                    },
                    errorMessage: true
                }
            );
        }
        
        function _executeAsyncScriptCb()
        {
	        Ametys.notify({
	            title: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPT_EXECUTE_ASYNC_NOTIFY_TITLE}}",
	            description: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPT_EXECUTE_ASYNC_NOTIFY_DESC}}"
	        });
        }
    },

	_onFormInitialized: function()
	{
		// Nothing
	},

	/**
	 * @inheritdoc
	 */
	_getAdditionalExtensions: function (location) 
	{
		return Ametys.plugins.cms.search.ScriptToolExtensions.getAdditionalButtons(location);
	},
	
	/**
	 * @inheritdoc
	 */
	_getStoreCfg: function()
	{
        return {
            remoteSort: false,
            proxy: {
                type: 'memory',
                reader: this._getReaderCfg()
            },
            
            listeners: {
                'beforeload': this._onBeforeLoad,
                'load': this._onLoad,
                scope: this
            },
             
            sortOnLoad: true
        };
	},
    
    _callExecuteScript: function(script, parameters, callback) 
    {
        Ametys.data.ServerComm.callMethod({
            role: 'org.ametys.cms.scripts.CmsScriptHandler',
            methodName: 'executeScript',
            parameters: [{
                script: script,
                parameters: parameters,
                selection: this._convertedSelection || '',
                model: "search-ui.default"
            }],
            callback: {
                handler: callback,
                scope: this
            },
            waitMessage: {
                msg: "{{i18n plugin.cms:UITOOL_SEARCH_WAITING_MESSAGE}}",
                target: this.mainPanel
            },
            errorMessage: true
        });
    },
    
    /**
     * @private
     * Loads data in the gird store
     */
    _loadDataInStore: function()
    {
        let me = this;
        
        let script = this._editor.getValue() || null;
        
        Ametys.plugins.coreui.script.ScriptParameters.askScriptParameters(script,
            function(parameters) {
                if (parameters)
                {
                    let p = me.getParams();
                    let values = {};
                    for (let paramName of Object.keys(parameters))
                    {
                        values[paramName] = parameters[paramName].value;
                    }
    
                    p.parameters = values;
                    // Calling set params to save params correctly, but not this tool instance to avoid re-execution of the script
                    Ametys.tool.Tool.prototype.setParams.call(me, p)
                }

                me._callExecuteScript(script, parameters, executeScriptCb);
            },
            undefined, // title
            undefined, // description
            { values: me.getParams().parameters } // existing values
        );
        
        function executeScriptCb(data)
        {
            this._updateColumns(data);
            
            var contents = data.contents || [];
            this.store.getProxy().setData(contents);
            this.store.load();
            
            this._updateConsole(data);
        }
    },
    
    /**
     * @private
     * Updates the columns
     * @param {Object} data The server data
     */
    _updateColumns: function(data)
    {
        if (Ext.Object.isEmpty(data) || Ext.Object.isEmpty(data.columns))
        {
            return;
        }
        
        var rawColumns = data.columns;
        var fields = Ametys.plugins.cms.search.SearchGridHelper.getFieldsFromJson(rawColumns);
        this._addDefaultContentFields(fields);
        var columns = Ametys.plugins.cms.search.SearchGridHelper.getColumnsFromJson(rawColumns, true, this.grid, true);
        var sorters = Ametys.plugins.cms.search.SearchGridHelper.getSortersFromJson(columns, this.grid);

        // Update model
        Ext.data.schema.Schema.get('default').getEntity(this._modelName).replaceFields(fields, true);
        
        this.grid.reconfigure(this.store, columns);

        this.store.sorters = null; // There is no other way to clean old sorters...
        this.store.setSorters(sorters);
    },

    _configureProxy: function(data)
    {
        // Do nothing. Prevents reconfiguring the store proxy every time
    },
	
	/**
	 * Execute the script
	 * @private
	 */
	_launchSearch: function()
	{
		this.showUpToDate();
		if (this._mask)
		{
			this._mask.hide();
			delete this._mask;
		}
		
		this._executeScript();
		this._message = Ametys.data.ServerComm._messages[Ametys.data.ServerComm._messages.length - 1];
	},
	
	/**
	 * Execute the script
	 * @private
	 */
	_executeScript: function ()
	{
        this._convertedSelection = this._convertCurrentSelection();

        this._loadDataInStore();
	},
	
	/**
	 * @private
	 * Convert the current message bus selection to a script.
	 * @return {String} A comma separated list of script instruction to resolve current bus selection ids.
	 */
	_convertCurrentSelection: function ()
	{
		var message = Ametys.message.MessageBus.getCurrentSelectionMessage();
		var contentTargets = message.getTargets(Ametys.message.MessageTarget.CONTENT);
		
		var contents = [];
		for (var i = 0; contentTargets != null && i < contentTargets.length; i++)
		{
			contents.push(contentTargets[i].getParameters().id);
		}
		return contents;
	},
	
	/**
	 * @inheritdoc
	 */
	_getResultGridCfg: function()
	{
		var cfg = this.callParent(arguments);
		
		// disable pagination
		cfg.enablePagination = false;
		
		// add info toolbar
		cfg.dockedItems = cfg.dockedItems || [];
		cfg.dockedItems.push(this._getInfoToolBarCfg());
		
		return cfg;
	},
	
	/**
	 * @protected
	 * Get the config to be used to create the info tool bar.
	 * @param {Object} config The result grid config object.
	 * @return {Object} The config object
	 */
	_getInfoToolBarCfg: function(config)
	{
		return {
			xtype: 'toolbar',
			itemId: 'infobar',
			cls: 'infobar',
			dock: 'bottom',
			items: [
				{
					xtype: 'component',
					itemId: 'result',
					cls: 'result',
					html: "{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_NO_RESULT}}"
				}
			]
		};
	},
	
	/**
	 * @inheritdoc
	 */
	_onLoad: function (store, records, successful, operation)
	{
		this.callParent(arguments);
		
		this._infoBar = this._infoBar || this.grid.getDockedComponent('infobar');
		if (this._infoBar)
		{
			var limit = 100; // Hardcoded limit, must correspond to the server side limit.
			
			this._infoBarResultCmp = this._infoBarResultCmp || this._infoBar.down('#result');
			if (this._infoBarResultCmp)
			{
				if (!records || records.length == 0)
				{
					this._infoBarResultCmp.update("{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_NO_RESULT}}");
				}
				else if (records.length == 1)
				{
					this._infoBarResultCmp.update("{{i18n UITOOL_SCRIPT_SEARCH_RESULT_TEXT_RESULT}}");
				}
				else
				{
					var totalCount = store.getCount();
					var infoResult = Ext.String.format(
						"{{i18n UITOOL_SCRIPT_SEARCH_RESULT_TEXT_RESULTS}}", 
						totalCount
					);
					
					if (totalCount > limit)
					{
						infoResult += ' ' + Ext.String.format(
							"{{i18n UITOOL_SCRIPT_SEARCH_RESULT_TEXT_RESULTS_LIMIT}}", 
							limit
						);
					}
					
					this._infoBarResultCmp.update(infoResult);
				}
			}
		}
    },
    
    /**
     * @private
     * Updates the console
     * @param {Object} data The server data
     */
    _updateConsole: function(data)
    {
        var result = data.result !== undefined && data.result !== null ? data.result : '[{{i18n UITOOL_SCRIPT_SEARCH_RESULT_TEXT_NO_RESULTS}}]';
        var stdout = data.output || '';
        var stderr = data.error || data.message || '';
        
        if (Ext.isArray(result) || Ext.isObject(result))
        {
            result = Ext.JSON.prettyEncode(result);
        }
        
        this.console.body.update(Ametys.plugins.coreui.script.ScriptToolHelper.formatConsoleOutput(data));
        this.console.expand();
	},

    /**
     * Called by the key binding Ctrl-A, overrides the default selection to only the editable lines.
     * @private
     */
    _selectAll: function()
    {
        if (this._editor)
        {
            let ed = this._editor._monaco;
            
            var model = ed.getModel();
            var totalLines = model.getLineCount();
            var startLine = 2; // Ligne après le header
            var endLine = totalLines - this._footerLineCount; // Ligne avant le footer
    
            if (endLine >= startLine) {
                var endColumn = model.getLineMaxColumn(endLine);
                ed.setSelection(new monaco.Range(startLine, 1, endLine, endColumn));
                ed.revealLineInCenter(startLine);
            }
        }
    },
    
    _getDefaultButtons: function() 
    {
        var items = this.callParent(arguments); 
        
        items.push({
            // Help
            itemId: 'help',
            iconCls: 'ametysicon-sign-question',
            handler: this._openHelp,
            scope: this,
            tooltip: {
                title: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPTHELP_TITLE}}",
                text: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_TOOLS_SCRIPTHELP_DESCRIPTION}}",
                glyphIcon: 'ametysicon-sign-question',
                inribbon: false
            }
        });
        
        return items;
    },
    
    _openHelp: function()
    {
        Ametys.tool.ToolsManager.openTool('uitool-admin-scripthelp');
    },
    
    _updateDecorations: function(editor)
    {
        let model = editor._monaco.getModel();
        
        let totalLines = model.getLineCount();
        let decorations = [];
      
        // Décoration pour la première ligne
        decorations.push({
            range: new monaco.Range(1, 1, 1, model.getLineMaxColumn(1)),
            options: {
                isWholeLine: true,
                className: 'code-readonly-line',
                glyphMarginClassName: 'code-readonly-glyph'
            }
        });
      
        // Décoration pour les dernières lignes (footer)
        for (var i = 0; i < this._footerLineCount; i++)
        {
            var lineNumber = totalLines - this._footerLineCount + i + 1;
            decorations.push({
                range: new monaco.Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)),
                options: {
                    isWholeLine: true,
                    className: 'code-readonly-line',
                    glyphMarginClassName: 'code-readonly-glyph'
                }
            });
        }
      
        // Appliquer les décorations
        this._decorationIds = model.deltaDecorations(this._decorationIds || [], decorations);
    }
});