/*
 *  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 class is the main point to open, close or get tools (through tools factories).
 * This class is also the entry point to get and set the tools layout implementation #getToolsLayout.
 * 
 * 		Ametys.tool.ToolsManager.addFactory(MyFactory);
 * 		Ametys.tool.ToolsManager.openTool(MyFactory.getId(), { ... });
 */
Ext.define("Ametys.tool.ToolsManager", 
	{
		singleton: true,
		mixins: {
			state: 'Ext.state.Stateful'
		},
        
        /**
         * @private
         * @property {Number} _WARN_IF_TOO_MANY_OPENED_TOOLS When there are strictly more opened tools, ask the user to reset (for performance purposes)
         */
        _WARN_IF_TOO_MANY_OPENED_TOOLS: 15, 
		
		/**
		 * @property {Object} _factories The registered factories stored by id
		 * @private
		 */
		_factories: {},

		/**
		 * @property {Object} _autoOpenedTools The tools automatically opened during startup. See the parameter config.autoOpenedTools of #init for the stucture.
		 * This property is used internally by #init, but is not an internal variable because it can be modified by #applyState when restoring a previous state.
		 * @private
		 */
		_autoOpenedTools: {},
        
        /**
         * @property {Object} _overridenDefaultLocation The keys are factories identifier, and the associated values are the default location that will override the {@link Ametys.tool.Tool#getDefaultLocation} call.
         * Theses values are set when the user manually moves tools.
         * @private
         */
        _overridenDefaultLocation: {},

		/**
		 * @property {Object} _tools The opened tools stored by unique identifier
		 * @private
		 */
		_tools: {},
		
		/**
		 * @property {Boolean} _initialized Is the tools manager initialized? (call to method #init done) so we can save state now... (avoid saving while reopening)
		 * @private
		 */
		_initialized: false,
		
		/**
		 * @property {Ametys.ui.tool.ToolsLayout} _layout The current tools layout implementation.
		 * @private
		 */
		
		/**
		 * @property {Ametys.ui.fluent.ribbon.Ribbon} _ribbon The current ribbon instance. See #setRibbon.
		 * @private
		 */
		_ribbon: null,
        
		/**
		 * Initialize the tools opened at startup.
		 * The values transmitted here can be ignored and replaced by the ones stored by the user.
		 * This method is called automatically during startup and should not be called after. 
		 * @param {Object} config The config object may have the following subobjects
		 * @param {Object[]} config.autoOpenedTools The tools to open
		 * @param {String} config.autoOpenedTools.toolId The factory id of the tool. See #openTool.
		 * @param {Object} config.autoOpenedTools.toolParams The parameters to open the tool. See #openTool.
		 * @param {String} [config.autoOpenedTools.forceToolLocation] The parameters where to open the tool. See #openTool.
         * @param {Object[]} [config.additionnalTools] The tools to open additionnaly
         * @param {String} config.additionnalTools.toolId The factory id of the tool. See #openTool.
         * @param {Object} config.additionnalTools.toolParams The parameters to open the tool. See #openTool.
         * @param {String} [config.additionnalTools.forceToolLocation] The parameters where to open the tool. See #openTool.
         * @param {Function} config.onError Callback if an error occurred during the init (even if an exception occurred during the onSucess
		 */
		init: function(config)
		{
            try
            {
    			this._autoOpenedTools = config ? config.autoOpenedTools || [] : [];
    			
                // Filter default tools to remove tools unaccessible (due to missing right for example)
                this._autoOpenedTools = this._autoOpenedTools.filter(function(toolConfig) {
                    if (this.getFactory(toolConfig.toolId) == undefined)
                    {
                        if (this.getLogger().isWarnEnabled())
                        {
                            this.getLogger().warn("Default tool " + toolConfig.toolId + " has no factory and will be ignored.");
                        }
                        return false;
                    }
                    return true;
                }, this);

    			// if the user do not want to remember tools, we do not reopen them by doing this: 
    			// we remember the default opened tools in the variable defaultTools, and after restoring the state, we crash the value 
    			var defaultTools = this._autoOpenedTools;
    			
                var additionnalTools = config ? config.additionnalTools || [] : [];
                
    			// restore state to overwrite values
                this.stateful = true;
                this.setStateful(true);
                this.stateId = this.self.getName();
                this.hasListeners = {}; // used by saveState
    			this.mixins.state.constructor.call(this);
    
    			// open tools in #_autoOpenedTools
    			if (Ametys.userprefs.UserPrefsDAO.getValue("remember-opened-tools") == "false")
    			{
    				this._autoOpenedTools = defaultTools;
                    this._autoOpenTools(additionnalTools);
    			}
                else if (Ametys.userprefs.UserPrefsDAO.getValue("remember-opened-tools") == "true" && this._autoOpenedTools.length > this._WARN_IF_TOO_MANY_OPENED_TOOLS)
                {
                    var me = this;
                    Ametys.MessageBox.show({
                        title: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_LOADING_PERFS_TITLE}}",
                        message: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_LOADING_PERFS_DESCRIPTION}}".replace("{0}", this._autoOpenedTools.length),
                        buttons: Ext.Msg.YESNO,
                        icon: Ext.Msg.WARNING,
                        fn: function(btn)
                        {
                            try
                            {
                                if (btn == 'yes')
                                {
                                    me._autoOpenedTools = defaultTools;
                                }
                                
                                me._autoOpenTools(additionnalTools);
                            }
                            catch (e)
                            {
                                me.getLogger().error({message: "Can not load many opened tools", details: e})
                                config.onError();
                                return;
                            }                            
                        }
                    });
                }
                else
                {
                    this._autoOpenTools(additionnalTools);
                }
            }
            catch (e)
            {
                this.getLogger().error({message: "Can not load opened tools", details: e})
                config.onError();
                return;
            }
		},
        
        /**
         * @private
         * Called at the end of the init to open the tools
         * @param {Object[]} tools  The tools to open
         */
        _autoOpenTools: function(tools)
        {
            var me = this;
            function act(tools)
            {
                for (var i = 0; i < tools.length; i++)
                {
                    var tool = me.openTool(tools[i].toolId, tools[i].toolParams, tools[i].forceToolLocation);
                    if (tool == null)
                    {
                        throw new Error("Cannot load tool '" + tools[i].toolId + "' at location '" + tools[i].forceToolLocation + "'")
                    }
                }
            }
            
            act(this._autoOpenedTools);
            
            this._initialized = true;
            // Additionnal tools needs to be focused and thus open after the init
            act(tools);
            
            Ametys.tool.ToolsManager.getToolsLayout().setAsInitialized();
        },
		
		/**
		 * Is the tools manager initialized ?
		 * @return {boolean} True if the tools manager is initialized
		 */
		isInitialized: function()
		{
			return this._initialized === true;
		},
		
        /**
         * @private
         * Used by #saveState to delegate save to plugins.
         * @return {Ext.plugin.Abstract[]} The plugins. Will be null here.
         */
        getPlugins: function() 
        {
            return null;
        },
        
		getState: function()
		{
			var state = {
				_autoOpenedTools: [],
                _overridenDefaultLocation: this._overridenDefaultLocation
			};
			
			// Do we want to remember opened tools?
			if (Ametys.userprefs.UserPrefsDAO.getValue("remember-opened-tools") != "false")
			{
				var locations = this.getToolsLayout().getSupportedLocations();
				for (var i = locations.length - 1; i >= 0; i--)
				{
					var location = locations[i];
					var toolsPanels = this.getToolsLayout().getToolsAtLocation(location);
					for (var j = 0; j < toolsPanels.length; j++)
					{
						var toolPanel = toolsPanels[j];
                        var tool = this.getTool(toolPanel.uiTool);
						
						state._autoOpenedTools.push({
							toolId: tool.getFactory().getId(),
							toolParams: tool.getParams(),
							forceToolLocation: location
						});
					}
				}
			}
			
			return state;
		},
		
		/**
		 * Handle a new Ametys.tool.ToolFactory
		 * @param {Ametys.tool.ToolFactory} factory The factory
		 */
		addFactory: function(factory)
		{
			if (this._factories[factory.getId()] != null && this.getLogger().isWarnEnabled())
			{
				this.getLogger().warn("Replacing factory '" + factory.getId() + "' with a new one");
			}
			else if (this.getLogger().isDebugEnabled())
			{
				this.getLogger().debug("Adding factory '" + factory.getId() + "'");
			}
			
			this._factories[factory.getId()] = factory;
		},
		
		/**
		 * Get a tool by its id
		 * @param {String} id The id of the tool to get
		 * @returns {Ametys.tool.Tool} The tool or null if the tool is not opened anymore
		 */
		getTool: function (id)
		{
			var tool = this._tools[id]
			if (tool == null && this.getLogger().isWarnEnabled())
			{
				this.getLogger().warn("Can not get tool of id '" + id + "' because it is not opened");
			}
			else if (this.getLogger().isDebugEnabled())
			{
				this.getLogger().debug("Getting tool of id '" + id + "'");
			}
				
			return tool;
		},
		
		/**
		 * Get the currently focused tool
		 * @returns {Ametys.tool.Tool} tool The tool focused. Can be null if no tools are opened
		 */
		getFocusedTool: function()
		{
            var toolPanel = this.getToolsLayout().getFocusedTool();
            if (!toolPanel)
            {
                return null;
            }
            else
            {
                return this.getTool(toolPanel.uiTool);
            }
		},
		
		/**
		 * Get the tools. Do not modify the return object.
		 * @returns {Object} The tools in an object where property name is the object unique identifier and the value the associated Ametys.tool.Tool object.
		 */
		getTools: function ()
		{
			return this._tools;
		},	
		
		/**
		 * Open a tool given an id and parameters. Ask the factory to open the tool : the tool may already be existing (even already opened) or created now.
		 * This is the main method to call when you want to open a tool.
		 * 
		 * This method creates or gets the tool through Ametys.tool.ToolFactory#openTool
		 * If the tool is new, it will add it to the graphic layout #getToolsLayout Ametys.ui.tool.ToolsLayout#addTool
		 * In all cases, it will then call Ametys.tool.Tool#setParams and Ametys.tool.Tool#activate
		 * 
		 * @param {String} toolId The id of the factory which will create the tool to open
		 * @param {Object} toolParams The parameters needed to open the tool. See the factory documentation for further details.
		 * @param {String} [forceToolLocation] A tool location (see Ametys.tool.ToolsLayout) to replace the Ametys.tool.Tool#getDefaultLocation.
		 * @returns {Ametys.tool.Tool} The new tool or undefined if an error occured while opening the tool
		 */
		openTool: function (toolId, toolParams, forceToolLocation)
		{
			try
			{
                Ext.suspendLayouts();
                
				var factory = this._factories[toolId];
				if (factory == null)
				{
					throw new Error("The factory '" + toolId + "' is not registered");
				}
				
				var tool = factory.openTool(toolParams);
                
				var effectiveLocation = null;
				
				// Is the tool already registered
				if (this._tools[tool.getId()] == null)
				{
					// Add a new tool
					if (this.getLogger().isInfoEnabled())
					{
						this.getLogger().info({message: "Opening a tool '" + tool.getId() + "' from factory '" + toolId + "'", details: "Parameters are " + toolParams});
					}
					
					this._tools[tool.getId()] =  tool;
					
                    // Determining location
                    if (forceToolLocation != null)
                    {
                        effectiveLocation = forceToolLocation;
                    }
                    else if (this._overridenDefaultLocation[tool.getFactory().getId()] != null)
                    {
                        effectiveLocation = this._overridenDefaultLocation[tool.getFactory().getId()];
                    }
                    else
                    {
                        effectiveLocation = tool.getDefaultLocation();
                    }
                    
                    effectiveLocation = this.getToolsLayout().getNearestSupportedLocation(effectiveLocation);
                    
                    // change the default location for tools of that kind
                    this._overridenDefaultLocation[tool.getFactory().getId()] = effectiveLocation;
                    if (this.isInitialized())
                    {
                        this.saveState();
                    }
                    
					this.getToolsLayout().addTool(tool.createWrapper(), effectiveLocation);
                    
                    tool.getWrapper().on("toolmoved", this._onToolPanelMoved, this, { toolId: tool.getId() });
				}
				else
				{
					if (forceToolLocation != null)
					{
                        // change the default location for tools of that kind
						this.moveTool(tool, forceToolLocation);
					}
					
					if (this.getLogger().isInfoEnabled())
					{
						this.getLogger().info({message: "Reopening a tool '" + tool.getId() + "' from factory '" + toolId + "'", details: "Parameters are " + toolParams});
					}
				}
				
				tool.setParams(toolParams);
				
				if (this.isInitialized() || effectiveLocation != null && this.getToolsLayout().getToolsAtLocation(effectiveLocation).length == 1)
				{
					tool.focus();
					
					if (this.isInitialized())
					{
						this.saveState();
					}
				}
				
				
				return tool;
			}
			catch (e)
			{
				Ametys.log.ErrorDialog.display({
					title: "{{i18n PLUGINS_CORE_UI_MSG_TOOLS_OPEN_ERROR_TITLE}}",
					text: "{{i18n PLUGINS_CORE_UI_MSG_TOOLS_OPEN_ERROR_TEXT}}",
					details: e,
					category: this.self.getName()
				});
			}
            finally 
            {
                Ext.resumeLayouts(true);
            }
		},
        
		changeToolIdentifier: function(oldToolId, newToolId)
        {
            if (this._tools[newToolId] != null)
            {
                throw new Error("The tool '" + newToolId + "' already exists");
            }

            var locations = this.getToolsLayout().getSupportedLocations();
            for (var i = locations.length - 1; i >= 0; i--)
            {
                var location = locations[i];
                var toolsPanels = this.getToolsLayout().getToolsAtLocation(location);
                for (var j = 0; j < toolsPanels.length; j++)
                {
                    var toolPanel = toolsPanels[j];
                    
                    if (toolPanel.uiTool == oldToolId)
                    {
                        toolPanel.uiTool = newToolId;
                    }
                }
            }

            let newTool = this._tools[oldToolId];
            delete this._tools[oldToolId];
            newTool._id = newToolId;
            this._tools[newToolId] = newTool;


            if (this.isInitialized())
            {
                this.saveState();
            }
            
            return this._tools[newToolId];
        },
        
        /**
         * @private
         * Listener when a toolPanel have been moved
         * @param {Ametys.ui.tool.ToolPanel} toolPanel The moved panel
         * @param {String} newLocation The new location
         * @param {Object} eOpts The options
         * @param {String} eOpts.toolId The tool identifier
         */
        _onToolPanelMoved: function(toolPanel, newLocation, eOpts)
        {
            if (this.isInitialized())
            {
                var tool = this.getTool(eOpts.toolId); 
                this._overridenDefaultLocation[tool.getFactory().getId()] = newLocation;
                this.saveState();
            }
        },
		
		/**
		 * Moves an opened tool to a new location of the Ametys.ui.tool.ToolsLayout
		 * @param {Ametys.tool.Tool} tool The already added tool to graphically move
		 * @param {String} newLocation The location where to move the tool to
		 **/
		moveTool: function(tool, newLocation)
		{
			this.getToolsLayout().moveTool(tool.getWrapper(), newLocation);
            
            if (this.isInitialized())
            {
                this.saveState();
            }
		},
        
        /**
         * Function invokes when the tool's parameters were updated
         * param {Ametys.tool.Tool} tool The updated tool
         */
        onToolParamsUpdated: function(tool)
        {
            if (this.isInitialized())
            {
                this.saveState();
            }
        },
		
		/**
		 * Get the factory by its id.
		 * Use this #openTool instead to create a tool.
		 * @param {String} toolId The id of the factory which will creates the tool to open
		 * @returns {Ametys.tool.ToolFactory} The factory registered for this id. Can be null.
		 */
		getFactory: function(toolId)
		{
			return this._factories[toolId];
		},
		
		/**
		 * Removes the tool from the list of opened tools
		 * @param {Ametys.tool.Tool} tool The id of the tool to get
		 */
		removeTool: function(tool)
		{
			if (this.getLogger().isDebugEnabled())
			{
				this.getLogger().debug("Removing tool '" + tool.getId() + "'");
			}
			
			this.getToolsLayout().removeTool(tool.getWrapper());
			
			delete this._tools[tool.getId()];
			
			if (this.isInitialized())
			{
				this.saveState();
			}
		},
		
		/**
		 * Refresh the out dated tools if they are in auto refresh state and if they are visible
		 * Internal call when necessary
		 */
		refreshTools: function ()
		{	
			var tools = this.getTools();
			for (var i in tools)
			{
				var tool = tools[i];
				
				this.refreshTool(tool);
			}
		},	
        
        /**
         * Refresh a out dated tool if it is in auto refresh state, visible and not refreshing
         * Internal call when necessary
         * @param {Ametys.tool.Tool} tool The tool to refresh if needed
         */
        refreshTool: function (tool)
        {   
            if (tool.isOutOfDate()
                && tool.getFactory().isAutoRefreshEnabled()
                && tool.getWrapper().isVisible()
                && !tool.isRefreshing())
            {
                // Test if tool is visible (if not, the tool will send an (activated) message that will makes the messagebus to recall the #refreshTools)
                tool.refresh();
            }
        },  
		
		/**
		 * Get the active impl of the Ametys.ui.tool.ToolsLayout
		 * @returns {Ametys.ui.tool.ToolsLayout} The active tools layout implementation. Cannot be null.
		 */
		getToolsLayout: function()
		{
			return this._layout;
		},
		
		/**
		 * Set the active implementation of the Ametys.ui.tool.ToolsLayout. This method has to be call once and once only.
		 * @param {String} layoutName The active tools layout implementation. Cannot be null.
         * @param {Object} layoutConfig The configuration to transmit to the new layout object.
		 * @throws If called a second time, an exception will be thrown (and logged)
		 */
		setToolsLayout: function(layoutName, layoutConfig)
		{
			if (this._layout == null)
			{
				this._layout = Ext.create(layoutName, layoutConfig);
			}
			else
			{
				var details = "Layout was '" + this._layout.self.getName() + "' and will not be changed to '" + layoutName + "'";
				this.getLogger().error({message: "setToolsLayout cannot be called twice", details: details});
				throw new Error(details); 
			}
		},

		/**
		 * Get the ribbon associated with the current layout.
		 * @return {Ametys.ui.fluent.ribbon.Ribbon} The current instance of the ribbon. Can be null.
		 */
		getRibbon: function()
		{
			return this._ribbon;
		},
		
		/**
		 * Set the ribbon instance associated. Call this during initialization. Used by all tools to set the current tool as title
		 * @param {Ametys.ui.fluent.ribbon.Ribbon} ribbon The ribbon instance
		 */
		setRibbon: function(ribbon)
		{
			this._ribbon = ribbon;
		}
	}
);