/***************************************************************************
 * Copyright 2014 Kieker Project (http://kieker-monitoring.net)
 *
 * 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.
 ***************************************************************************/

package kieker.webgui.web.beans.view;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;

import kieker.analysis.AnalysisController;
import kieker.analysis.model.analysisMetaModel.MIAnalysisComponent;
import kieker.analysis.model.analysisMetaModel.MIAnalysisMetaModelFactory;
import kieker.analysis.model.analysisMetaModel.MIDependency;
import kieker.analysis.model.analysisMetaModel.MIDisplayConnector;
import kieker.analysis.model.analysisMetaModel.MIFilter;
import kieker.analysis.model.analysisMetaModel.MIInputPort;
import kieker.analysis.model.analysisMetaModel.MIOutputPort;
import kieker.analysis.model.analysisMetaModel.MIPlugin;
import kieker.analysis.model.analysisMetaModel.MIProject;
import kieker.analysis.model.analysisMetaModel.MIProperty;
import kieker.analysis.model.analysisMetaModel.MIReader;
import kieker.analysis.model.analysisMetaModel.MIRepository;
import kieker.analysis.model.analysisMetaModel.MIRepositoryConnector;
import kieker.analysis.model.analysisMetaModel.MIView;
import kieker.analysis.model.analysisMetaModel.impl.MAnalysisMetaModelFactory;
import kieker.analysis.plugin.annotation.Property;
import kieker.common.logging.Log;
import kieker.common.logging.LogFactory;
import kieker.webgui.common.exception.NewerProjectException;
import kieker.webgui.common.exception.ProjectLoadException;
import kieker.webgui.common.exception.ProjectNotExistingException;
import kieker.webgui.domain.ComponentListContainer;
import kieker.webgui.domain.pluginDecorators.AbstractAnalysisComponentDecorator;
import kieker.webgui.domain.pluginDecorators.FilterDecorator;
import kieker.webgui.domain.pluginDecorators.ReaderDecorator;
import kieker.webgui.domain.pluginDecorators.RepositoryDecorator;
import kieker.webgui.service.IProjectService;
import kieker.webgui.web.beans.application.GlobalPropertiesBean;
import kieker.webgui.web.beans.session.UserBean;
import kieker.webgui.web.utility.IGraphListener;

import org.primefaces.context.RequestContext;
import org.primefaces.event.FileUploadEvent;
import org.primefaces.model.UploadedFile;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

/**
 * This bean contains the necessary data behind an instance of the analysis editor. It provides various methods to manipulate the current project with respect to the
 * analysis components. The connection to the graph within the editor is done with {@link AnalysisEditorGraphBean}. <br>
 * </br>
 * 
 * The class is a Spring managed bean with view scope to make sure that one user (even in one session) can open multiple projects at a time without causing any
 * problems.
 * 
 * @author Nils Christian Ehmke
 */
@Component
@Scope("view")
public final class AnalysisEditorBean implements IGraphListener {

	private static final MIAnalysisMetaModelFactory MODEL_FACTORY = MAnalysisMetaModelFactory.eINSTANCE;
	private static final Log LOG = LogFactory.getLog(AnalysisEditorBean.class);

	private static final String[] PARAMETER_NAMES_SAVE_PROJECT = { "overwriteNewerProject", "layoutString" };
	private static final Class<?>[] PARAMETER_TYPES_SAVE_PROJECT = { Boolean.class, String.class };

	private static final String JS_CMD_SHOW_FORCE_SAVE_DIALOG = "forceSaveDlg.show()";

	private MIAnalysisComponent globalConfigurationInstance;
	private MIAnalysisComponent selectedComponent;
	private MIProject project;

	private ComponentListContainer availableComponents;

	private boolean unsavedModifications;
	private String projectName;
	private long timeStampFromLastSaving;

	@Autowired
	private AnalysisEditorGraphBean analysisEditorGraphBean;
	@Autowired
	private GlobalPropertiesBean globalPropertiesBean;
	@Autowired
	private IProjectService projectService;
	@Autowired
	private UserBean userBean;

	/**
	 * Creates a new instance of this class. <b>Do not call this constructor manually. It will only be accessed by Spring.</b>
	 */
	public AnalysisEditorBean() {
		this.availableComponents = ComponentListContainer.EMPTY_CONTAINER;
	}

	/**
	 * This method initializes the bean by using the current project name to load the project. <b>Do not call this method manually. It will only be accessed by
	 * Spring.</b>
	 */
	public void initialize() {
		try {
			// Make sure that the initialization will only be done for the initial request. During all other requests, the method call has to be ignored.
			if (!FacesContext.getCurrentInstance().isPostback()) {
				this.loadProject();
				this.enrichProjectWithLibraries();
				this.reloadAvailableComponents();

				this.clearModificationsFlag();
				this.resetTimeStampFromLastSaving();
			}
		} catch (final ProjectLoadException ex) {
			AnalysisEditorBean.LOG.error("An error occured while loading the project.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgProjectLoadingException());
		} catch (final NullPointerException ex) {
			// This exception can occur when a property has not been initialized
			AnalysisEditorBean.LOG.error("An error occured while loading the project.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgProjectLoadingException());
		}
	}

	/**
	 * This method initializes the graph by delivering the necessary JavaScript commands to the client. It prints all current existing plugins, repositories and
	 * their connections.
	 */
	public void initializeGraph() {
		try {
			// Initialize the graph
			this.analysisEditorGraphBean.declareGraph();

			// Initialize the component for the project configuration
			this.initializeGlobalConfigurationInstance();
			this.analysisEditorGraphBean.addGlobalConfigurationInstance(this.globalConfigurationInstance);

			this.analysisEditorGraphBean.addProject(this.project);

			// Now we have to set the default grid size and color of the user
			this.analysisEditorGraphBean.setGridColor(this.userBean.getGridColor());
			this.analysisEditorGraphBean.setGridSize(this.userBean.getGridSize());

			// Perform either an initial auto layout or use the saved layout - if it exists
			final String layout = this.projectService.getAnalysisLayout(this.projectName);

			if (layout != null) {
				this.analysisEditorGraphBean.loadLayout(layout);
			} else {
				this.analysisEditorGraphBean.startAutoLayout();
			}

			// Make sure that the currentAnalysisEditorGraphBean knows "this" as well.
			this.analysisEditorGraphBean.addGraphListener(this);

			this.analysisEditorGraphBean.initializeListeners();
		} catch (final NullPointerException ex) {
			// This exception can occur when a property has not been initialized
			LOG.error("An error occured while initializing the graph.", ex);
		}
	}

	/**
	 * This method is the handler for the file upload. It tries to upload the given file as a library and informs the user via the growl-component.
	 * 
	 * @param event
	 *            The upload event.
	 */
	public void handleLibraryFileUpload(final FileUploadEvent event) {
		try {
			final UploadedFile uploadedFile = event.getFile();

			// Delegate the task to upload the library
			this.projectService.uploadLibrary(uploadedFile, this.projectName);

			// Seems like it worked. We can add the library to our model and inform the user.
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_INFO, this.globalPropertiesBean.getMsgLibraryUploaded());
			this.enrichProjectWithLibrary(uploadedFile.getFileName());

			// We have to reinitialize the available components! This is necessary as some of the already existing classes could need the newly loaded classes.
			this.reloadAvailableComponents();

			this.setModificationsFlag();
		} catch (final IOException ex) {
			AnalysisEditorBean.LOG.error("An error occured while uploading the library.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgLibraryUploadingException());
		} catch (final ProjectNotExistingException ex) {
			AnalysisEditorBean.LOG.error("Project does not exist.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgProjectNotExistingException());
		}
	}

	/**
	 * Removes the library with the given name and reloads the available components.
	 * 
	 * @param name
	 *            The name of the library to be removed.
	 */
	public void deleteLibrary(final String name) {
		try {
			// Delegate the task
			if (this.projectService.deleteLibrary(this.projectName, name)) {
				this.reloadAvailableComponents();
				this.setModificationsFlag();
			}
		} catch (final IOException ex) {
			AnalysisEditorBean.LOG.error("An error occured while removing the library.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, "An error occured while removing the library.");
		} catch (final ProjectNotExistingException ex) {
			AnalysisEditorBean.LOG.error("An error occured while removing the library.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, "An error occured while removing the library.");
		}
	}

	/**
	 * This method delivers the available libraries of this project. The first element is always the kieker-library.
	 * 
	 * @return The available libraries.
	 */
	public List<String> getLibraries() {
		try {
			final List<String> result = this.projectService.listAllLibraries(this.projectName);
			result.add(0, "Kieker");

			return result;
		} catch (final ProjectNotExistingException ex) {
			AnalysisEditorBean.LOG.error("The project does not exist.", ex);
		} catch (final NullPointerException ex) {
			// This exception occurs when a property has not been initialized correctly.
			AnalysisEditorBean.LOG.error("A null pointer exception occured.", ex);
		}
		return new ArrayList<String>();
	}

	/**
	 * This method tries to save the current project and informs the user about success or fail. There should be two parameters within the request parameter map
	 * (layoutString and overwriteNewerProject) as this method is called technically from JavaScript.
	 */
	public void saveProject() {
		// Get the parameters
		final Object[] parameters = GlobalPropertiesBean.convertObjectsFromParameterMap(PARAMETER_NAMES_SAVE_PROJECT, PARAMETER_TYPES_SAVE_PROJECT);
		final boolean overwriteNewerProject = (Boolean) parameters[0];
		final String currentLayout = (String) parameters[1];

		// Add the project configuration to the project, as those are stored within the global configuration component.
		this.project.getProperties().clear();
		for (final MIProperty mProperty : this.globalConfigurationInstance.getProperties()) {
			final MIProperty mCopy = MODEL_FACTORY.createProperty();
			mCopy.setName(mProperty.getName());
			mCopy.setValue(mProperty.getValue());

			this.project.getProperties().add(mCopy);
		}

		try {
			this.projectService.saveProject(this.projectName, this.project, this.timeStampFromLastSaving, overwriteNewerProject, this.userBean.getUsername(),
					currentLayout, null);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_INFO, this.globalPropertiesBean.getMsgProjectSaved());

			this.resetTimeStampFromLastSaving();
			this.clearModificationsFlag();
		} catch (final IOException ex) {
			AnalysisEditorBean.LOG.error("An error occured while saving the project.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgProjectSavingException());
		} catch (final NewerProjectException ex) {
			AnalysisEditorBean.LOG.info("The project has been modified externally in the meanwhile.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_WARN, this.globalPropertiesBean.getMsgProjectModified());
			// Give the user the possibility to force-save the project
			RequestContext.getCurrentInstance().execute(JS_CMD_SHOW_FORCE_SAVE_DIALOG);
		} catch (final ProjectNotExistingException ex) {
			AnalysisEditorBean.LOG.error("The project does not exist.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgProjectNotExistingException());
		}
	}

	/**
	 * This method adds a new repository to the current model, using the given container to create it.
	 * 
	 * @param container
	 *            The container which delivers the copy of the repository.
	 */
	public void addRepository(final RepositoryDecorator container) {
		// Create a new instance for the model
		final MIRepository repository = container.newCopy();

		// Add it to the project - and to the graph
		this.project.getRepositories().add(repository);
		this.analysisEditorGraphBean.addComponent(repository);

		this.setModificationsFlag();
	}

	/**
	 * This method adds a new reader to the current model, using the given container to create it.
	 * 
	 * @param container
	 *            The container which delivers the copy of the reader.
	 */
	public void addReader(final ReaderDecorator container) {
		// Create a new instance for the model
		final MIPlugin plugin = container.newCopy();

		// Add it to the project - and to the graph
		this.project.getPlugins().add(plugin);
		this.analysisEditorGraphBean.addComponent((MIReader) plugin);

		this.setModificationsFlag();
	}

	/**
	 * This method adds a new filter to the current model, using the given container to create it.
	 * 
	 * @param container
	 *            The container which delivers the copy of the filter.
	 */
	public void addFilter(final FilterDecorator container) {
		// Create a new instance for the model
		final MIPlugin plugin = container.newCopy();

		// Add it to the project - and to the graph
		this.project.getPlugins().add(plugin);
		this.analysisEditorGraphBean.addComponent((MIFilter) plugin);

		this.setModificationsFlag();
	}

	/**
	 * This method delivers the properties of the currently selected plugin, but it adds also the name- and the class-properties as a string to the list. The first
	 * element is always the class name, the second one is always the plugin name. If the currently selected component is the global configuration instance though,
	 * those properties will not be added.
	 * 
	 * @return A list with all properties of the plugin plus the name- and class-properties.
	 */
	public List<Object> getAdvancedPluginProperties() {
		final List<Object> result = new ArrayList<Object>();

		// Add the properties as strings
		if (this.selectedComponent != this.globalConfigurationInstance) {
			result.add("Name");
			result.add("ClassName");
		}

		// Get the original properties of the plugin
		result.addAll(this.selectedComponent.getProperties());

		return result;
	}

	/**
	 * Delivers a human readable description of the given property.<br>
	 * 
	 * </br> The current implementation is not very fast, as it searches through all available components. If
	 * necessary this method can be modified in order to run faster (for example by using hash maps).
	 * 
	 * @param component
	 *            The parent of the property.
	 * @param property
	 *            The property name.
	 * 
	 * @return A human readable description and a substitution if there is no description.
	 */
	public String getDescription(final MIAnalysisComponent component, final String property) {
		final String className = component.getClassname();
		AbstractAnalysisComponentDecorator<? extends MIAnalysisComponent> container = null;

		// Find the container which contains the component
		if (component instanceof MIReader) {
			for (final ReaderDecorator reader : this.availableComponents.getReaders()) {
				if (reader.getClassname().equals(className)) {
					container = reader;
					break;
				}
			}
		} else if (component instanceof MIFilter) {
			for (final FilterDecorator filter : this.availableComponents.getFilters()) {
				if (filter.getClassname().equals(className)) {
					container = filter;
					break;
				}
			}
		} else {
			for (final RepositoryDecorator repository : this.availableComponents.getRepositories()) {
				if (repository.getClassname().equals(className)) {
					container = repository;
					break;
				}
			}
		}

		// Extract the description
		if (container != null) {
			return container.getPropertyDescription(property);
		}

		// Nothing found
		return "No description available.";
	}

	@Override
	public void componentSelectedEvent(final MIAnalysisComponent newSelectedComponent) {
		this.selectedComponent = newSelectedComponent;
	}

	@Override
	public void componentDeletedEvent(final MIAnalysisComponent deletedComponent) {
		// Remove the component from the project
		if (deletedComponent instanceof MIPlugin) {
			this.project.getPlugins().remove(deletedComponent);

			// Remove the corresponding connections
			final List<MIInputPort> toBeRemoved = new ArrayList<MIInputPort>();
			for (final MIPlugin plugin : this.project.getPlugins()) {
				for (final MIOutputPort oPort : plugin.getOutputPorts()) {
					toBeRemoved.clear();
					for (final MIInputPort iPort : oPort.getSubscribers()) {
						if (iPort.getParent() == deletedComponent) {
							toBeRemoved.add(iPort);
						}
					}
					oPort.getSubscribers().removeAll(toBeRemoved);
				}
			}
		} else {
			this.project.getRepositories().remove(deletedComponent);

			// Remove the corresponding connections
			for (final MIPlugin plugin : this.project.getPlugins()) {
				for (final MIRepositoryConnector repoConn : plugin.getRepositories()) {
					if (repoConn.getRepository() == deletedComponent) {
						repoConn.setRepository(null);
					}
				}
			}
		}

		// Remove the corresponding displays from the views
		for (final MIView view : this.project.getViews()) {
			final Collection<MIDisplayConnector> toBeRemoved = new ArrayList<MIDisplayConnector>();
			for (final MIDisplayConnector connector : view.getDisplayConnectors()) {
				if (connector.getDisplay().getParent() == deletedComponent) {
					toBeRemoved.add(connector);
				}
			}
			view.getDisplayConnectors().removeAll(toBeRemoved);
		}

		// Deselect the currently selected node if it is the one which has just been removed
		if (this.selectedComponent == deletedComponent) {
			this.selectedComponent = null; // NOPMD
		}

		this.setModificationsFlag();
	}

	@Override
	public void connectionAddedEvent(final MIRepositoryConnector sourcePort, final MIRepository target) {
		sourcePort.setRepository(target);

		this.setModificationsFlag();
	}

	@Override
	public void connectionAddedEvent(final MIOutputPort outputPort, final MIInputPort targetPort) {
		outputPort.getSubscribers().add(targetPort);

		this.setModificationsFlag();
	}

	@Override
	public void connectionDeletedEvent(final MIRepositoryConnector sourcePort, final MIRepository targetRepo) {
		sourcePort.setRepository(null);

		this.setModificationsFlag();
	}

	@Override
	public void connectionDeletedEvent(final MIOutputPort sourcePort, final MIInputPort targetPort) {
		sourcePort.getSubscribers().remove(targetPort);

		this.setModificationsFlag();
	}

	/**
	 * This method should be called if the grid color has been modified.
	 * 
	 * @param event
	 *            The change event.
	 */
	public void gridColorListener(final ValueChangeEvent event) {
		this.analysisEditorGraphBean.setGridColor((String) event.getNewValue());
	}

	/**
	 * This method should be called if the grid size has been modified.
	 * 
	 * @param event
	 *            The change event.
	 */
	public void gridSizeListener(final ValueChangeEvent event) {
		this.analysisEditorGraphBean.setGridSize((Integer) event.getNewValue());
	}

	public boolean isUnsavedModification() {
		return this.unsavedModifications;
	}

	public boolean isGlobalConfigComponentSelected() {
		return this.selectedComponent == this.globalConfigurationInstance;
	}

	public MIAnalysisComponent getSelectedPlugin() {
		return this.selectedComponent;
	}

	public MIProject getProject() {
		return this.project;
	}

	public void setProjectName(final String projectName) {
		this.projectName = projectName;
	}

	public String getProjectName() {
		return this.projectName;
	}

	public ComponentListContainer getAvailableComponents() {
		return this.availableComponents;
	}

	private void reloadAvailableComponents() {
		this.availableComponents = this.projectService.getAvailableComponents(this.projectName);
	}

	private void loadProject() throws ProjectLoadException {
		try {
			this.project = this.projectService.loadProject(this.projectName);
		} catch (final IOException ex) {
			LOG.error("An error occured while loading the project.", ex);
			throw new ProjectLoadException("An error occured while loading the project.", ex);
		} catch (final ProjectNotExistingException ex) {
			LOG.info("A project with the given name does not exist.", ex);
			throw new ProjectLoadException("A project with the given name does not exist.", ex);
		}
	}

	private void enrichProjectWithLibraries() throws ProjectLoadException {
		try {
			// Run through all libraries within the lib folder of the project and add them to the project. Removing all existing dependencies beforehand makes sure
			// that we avoid double entries. And it avoids the problem with invalid dependencies
			this.project.getDependencies().clear();
			final List<String> libraries = this.projectService.listAllLibraries(this.projectName);

			for (final String library : libraries) {
				this.enrichProjectWithLibrary(library);
			}

		} catch (final ProjectNotExistingException ex) {
			throw new ProjectLoadException("The project does not exist.", ex);
		}
	}

	private void enrichProjectWithLibrary(final String fileName) {
		final MIDependency mDependency = MODEL_FACTORY.createDependency();
		mDependency.setFilePath(fileName);

		this.project.getDependencies().add(mDependency);
	}

	private void resetTimeStampFromLastSaving() {
		try {
			this.timeStampFromLastSaving = this.projectService.getCurrTimeStamp(this.projectName);
		} catch (final ProjectNotExistingException ex) {
			LOG.warn("Could not reset the time stamp", ex);
		}
	}

	/**
	 * This method sets the property {@link AnalysisEditorBean#unsavedModifications} to false and refreshes the necessary components within the analysis
	 * editor to make this visible.
	 */
	private void clearModificationsFlag() {
		this.unsavedModifications = false;
		RequestContext.getCurrentInstance().update("menuForm");
	}

	/**
	 * This method sets the property {@link AnalysisEditorBean#unsavedModifications} to true and refreshes the necessary components within the analysis editor
	 * to make this visible.
	 */
	private void setModificationsFlag() {
		this.unsavedModifications = true;
		RequestContext.getCurrentInstance().update("menuForm");
	}

	private void initializeGlobalConfigurationInstance() {
		this.globalConfigurationInstance = MODEL_FACTORY.createFilter();

		final kieker.analysis.annotation.AnalysisController annotation = AnalysisController.class.getAnnotation(kieker.analysis.annotation.AnalysisController.class);
		final Property[] properties = annotation.configuration();
		final Map<String, String> propertyMap = new HashMap<String, String>();

		for (final Property property : properties) {
			propertyMap.put(property.name(), property.defaultValue());
		}

		for (final MIProperty mProperty : this.project.getProperties()) {
			propertyMap.put(mProperty.getName(), mProperty.getValue());
		}

		for (final Map.Entry<String, String> property : propertyMap.entrySet()) {
			final MIProperty mProperty = MODEL_FACTORY.createProperty();
			mProperty.setName(property.getKey());
			mProperty.setValue(property.getValue());

			this.globalConfigurationInstance.getProperties().add(mProperty);
		}
	}

}
