/***************************************************************************
 * 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.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.html.HtmlOutputText;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.AjaxBehaviorEvent;

import kieker.analysis.model.analysisMetaModel.MIAnalysisMetaModelFactory;
import kieker.analysis.model.analysisMetaModel.MIDisplay;
import kieker.analysis.model.analysisMetaModel.MIDisplayConnector;
import kieker.analysis.model.analysisMetaModel.MIPlugin;
import kieker.analysis.model.analysisMetaModel.MIProject;
import kieker.analysis.model.analysisMetaModel.MIView;
import kieker.analysis.model.analysisMetaModel.impl.MAnalysisMetaModelFactory;
import kieker.common.logging.Log;
import kieker.common.logging.LogFactory;
import kieker.common.util.registry.Registry;
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.AbstractPluginDecorator;
import kieker.webgui.domain.pluginDecorators.FilterDecorator;
import kieker.webgui.domain.pluginDecorators.ReaderDecorator;
import kieker.webgui.domain.pluginDecorators.RepositoryDecorator;
import kieker.webgui.domain.pluginDecorators.VisualizationDecorator;
import kieker.webgui.service.IProjectService;
import kieker.webgui.web.beans.application.GlobalPropertiesBean;
import kieker.webgui.web.beans.request.view.CopyViewBean;
import kieker.webgui.web.beans.request.view.EditViewBean;
import kieker.webgui.web.beans.request.view.NewViewBean;
import kieker.webgui.web.beans.session.UserBean;
import kieker.webgui.web.utility.CockpitLayout;

import org.primefaces.component.behavior.ajax.AjaxBehavior;
import org.primefaces.component.behavior.ajax.AjaxBehaviorListenerImpl;
import org.primefaces.component.dashboard.Dashboard;
import org.primefaces.component.panel.Panel;
import org.primefaces.context.RequestContext;
import org.primefaces.event.DashboardReorderEvent;
import org.primefaces.model.DashboardColumn;
import org.primefaces.model.DashboardModel;
import org.primefaces.model.DefaultDashboardColumn;
import org.primefaces.model.DefaultDashboardModel;

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

/**
 * The {@link CockpitEditorBean} contains the necessary data behind an instance of the cockpit editor.<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 CockpitEditorBean {

	private static final Log LOG = LogFactory.getLog(CockpitEditorBean.class);

	private static final String DASHBOARD_ID = "dashboard";
	private static final String PANEL_ID_PREFIX = "displayConnector_";
	private static final int NUMBER_OF_COCKPIT_COLUMNS = 2;

	private final Registry<MIDisplayConnector> displayConnectors = new Registry<MIDisplayConnector>();
	private final MIAnalysisMetaModelFactory factory = MAnalysisMetaModelFactory.eINSTANCE;

	private ComponentListContainer availableComponents;
	private CockpitLayout cockpitLayout;
	private boolean unsavedModifications;
	private String projectName;
	private MIProject project;
	private long timeStampSinceLastSaving;

	private MIDisplayConnector selectedNode;
	private MIView activeView;
	private MIView selectedView;

	private DashboardModel dashboardModel;
	private Dashboard dashboard;

	@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 CockpitEditorBean() {
		this.availableComponents = new ComponentListContainer(Collections.<ReaderDecorator>emptyList(), Collections.<FilterDecorator>emptyList(),
				Collections.<RepositoryDecorator>emptyList(), Collections.<VisualizationDecorator>emptyList());
	}

	/**
	 * 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 initalize() {
		try {
			// Make sure that the initialization will only be done for the init request.
			if (!FacesContext.getCurrentInstance().isPostback()) {
				this.loadProject();

				if (this.project != null) {
					this.resetTimeStampFromLastSaving();
					this.reloadAvailableComponents();

					this.createDashboardComponent();
					this.loadCockpitLayout();
				}

				this.unsavedModifications = false;
			}
		} catch (final ProjectLoadException ex) {
			CockpitEditorBean.LOG.error("An error occured while loading the project.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgProjectLoadingException());
		} catch (final NullPointerException ex) {
			CockpitEditorBean.LOG.error("An error occured while loading the project.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, "An error occured while loading the project.");
		}
	}

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

	private void loadCockpitLayout() {
		this.cockpitLayout = new CockpitLayout(this.project, this.projectService.getCockpitLayout(this.projectName), NUMBER_OF_COCKPIT_COLUMNS);
	}

	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);
		}
	}

	/**
	 * This is a dummy method returning just a collection of null objects. This is necessary due to Primefaces.
	 * 
	 * @return A collection with three null objects.
	 */
	public Collection<Object> getProperties() {
		return Collections.nCopies(3, null);
	}

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

	public MIView getSelectedView() {
		return this.selectedView;
	}

	public void setSelectedView(final MIView view) {
		this.selectedView = view;
	}

	public MIView getActiveView() {
		return this.activeView;
	}

	/**
	 * Sets the active view and updates the dashboard.
	 * 
	 * @param view
	 *            The new view.
	 */
	public void setActiveView(final MIView view) {
		this.activeView = view;

		this.fillDashboard();
	}

	/**
	 * Deletes the given view from the model.
	 * 
	 * @param view
	 *            The view to be removed.
	 * 
	 */
	public void deleteView(final MIView view) {
		this.project.getViews().remove(view);
		this.cockpitLayout.removeView(view);

		this.setModificationsFlag();
	}

	/**
	 * Copies a view.
	 * 
	 * @param copyViewBean
	 *            The bean containing the necessary data to copy the view.
	 */
	public void copyView(final CopyViewBean copyViewBean) {
		// Not implemented yet
	}

	/**
	 * Edits an existing view.
	 * 
	 * @param editViewBean
	 *            The bean containing the necessary data to edit the view.
	 */
	public void editView(final EditViewBean editViewBean) {
		if ((this.project != null) && this.checkViewName(editViewBean.getViewName(), editViewBean.getMessageTarget().getClientId())) {
			this.selectedView.setName(editViewBean.getViewName());
			this.selectedView.setDescription(editViewBean.getViewDescription());

			this.setModificationsFlag();
		}
	}

	/**
	 * This method adds a new view to the project.
	 * 
	 * @param newViewBean
	 *            The bean containing the necessary data to create the view.
	 */
	public void addView(final NewViewBean newViewBean) {
		if ((this.project != null) && this.checkViewName(newViewBean.getViewName(), newViewBean.getMessageTarget().getClientId())) {
			// Create the view and add it to our project
			final MIView view = this.factory.createView();
			view.setName(newViewBean.getViewName());
			if (newViewBean.getViewDescription().isEmpty()) {
				view.setDescription("No description available.");
			} else {
				view.setDescription(newViewBean.getViewDescription());
			}

			this.project.getViews().add(view);
			this.cockpitLayout.addView(view);

			this.setModificationsFlag();
		}
	}

	public Dashboard getDashboard() {
		return this.dashboard;
	}

	/**
	 * Setter for the property {@link CockpitEditorBean#dashboard}.
	 * 
	 * @param dashboard
	 *            The new value for the property.
	 */
	public void setDashboard(final Dashboard dashboard) {
		this.dashboard = dashboard;
		this.dashboard.setModel(this.dashboardModel);
	}

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

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

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

	public MIDisplayConnector getSelectedNode() {
		return this.selectedNode;
	}

	private void createDashboardComponent() {
		final FacesContext facesContext = FacesContext.getCurrentInstance();
		final Application application = facesContext.getApplication();

		// Create the Primefaces dashboard component
		this.dashboard = (Dashboard) application.createComponent(facesContext, "org.primefaces.component.Dashboard", "org.primefaces.component.DashboardRenderer");
		this.dashboard.setId(DASHBOARD_ID);

		// Create the corresponding model with the correct number of columns
		this.dashboard.setModel(new DefaultDashboardModel());

		for (int i = 0; i < NUMBER_OF_COCKPIT_COLUMNS; i++) {
			this.dashboard.getModel().addColumn(new DefaultDashboardColumn());
		}

		// Remember the dashboard model
		this.dashboardModel = this.dashboard.getModel();
	}

	/**
	 * Fills the dashboard using the currently active view.
	 */
	private void fillDashboard() {
		// Dump the old entries
		this.clearDashboard();

		// Now add the entries from the current view
		if (this.activeView != null) {

			final List<List<MIDisplayConnector>> layout = this.cockpitLayout.getCurrentLayout(this.activeView);

			for (int col = 0; col < NUMBER_OF_COCKPIT_COLUMNS; col++) {
				final DashboardColumn column = this.dashboard.getModel().getColumn(col);

				for (final MIDisplayConnector displayConnector : layout.get(col)) {
					final Panel panel = this.createPanelFromDisplayConnector(displayConnector);

					this.dashboard.getChildren().add(panel);
					column.addWidget(panel.getId());
				}
			}
		}
	}

	private boolean checkViewName(final Object value, final String clientId) {
		final String viewName = (String) value;

		for (final MIView view : this.project.getViews()) {
			if (view.getName().equals(viewName)) {
				RequestContext.getCurrentInstance().addCallbackParam("fail", "true");
				FacesContext.getCurrentInstance().addMessage(clientId, new FacesMessage(FacesMessage.SEVERITY_ERROR, "", "Name already used"));
				return false;
			}
		}

		return true;
	}

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

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

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

	/**
	 * Performs an update on the dashboard once a name has been changed.
	 */
	public void updateName() {
		this.fillDashboard();
	}

	/**
	 * This method can be used to get the description of a {@link MIDisplay}. Currently it is a little bit expensive to search for the description.
	 * 
	 * @param display
	 *            The display whose description should be extracted.
	 * @return The description for the display or a substitute if none is available. This is in either case human readable.
	 */
	public String getDescription(final MIDisplay display) {
		final String parentClassname = display.getParent().getClassname();
		AbstractPluginDecorator<? extends MIPlugin> parentContainer = null;

		// Find the correct parent container
		for (final FilterDecorator plugin : this.availableComponents.getFilters()) {
			if (plugin.getClassname().equals(parentClassname)) {
				parentContainer = plugin;
				break;
			}
		}
		if (parentContainer == null) {
			for (final ReaderDecorator plugin : this.availableComponents.getReaders()) {
				if (plugin.getClassname().equals(parentClassname)) {
					parentContainer = plugin;
					break;
				}
			}
		}

		// If we have now the correct parent, we can search for the correct display instance.
		if (parentContainer != null) {
			return parentContainer.getDisplayDescription(display.getName());
		}

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

	/**
	 * This method tries to save the current project and informs the user about success or fail.
	 * 
	 * @param overwriteNewerProject
	 *            This flag determines whether a newer project should be overwritten.
	 */
	public void saveProject(final boolean overwriteNewerProject) {
		try {
			this.projectService.saveProject(this.projectName, this.project, this.timeStampSinceLastSaving, overwriteNewerProject, this.userBean.getUsername(), null,
					this.cockpitLayout.serializeToString());

			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_INFO, this.globalPropertiesBean.getMsgProjectSaved());
			// Update the time stamp!
			this.resetTimeStampFromLastSaving();

			this.clearModificationsFlag();
		} catch (final IOException ex) {
			CockpitEditorBean.LOG.error("An error occured while saving the projet.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, "An error occured while saving the project.");
		} catch (final NewerProjectException ex) {
			CockpitEditorBean.LOG.info("The project has been modified externally in the meanwhile.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_WARN, "The project has been modified externally in the meanwhile.");
			// Give the user the possibility to force-save the project
			RequestContext.getCurrentInstance().execute("forceSaveDlg.show()");
		} catch (final ProjectNotExistingException ex) {
			CockpitEditorBean.LOG.error("A project with the given name does not exist.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, "A project with the given name does not exist.");
		}
	}

	private Panel createPanelFromDisplayConnector(final MIDisplayConnector connector) {
		final FacesContext fc = FacesContext.getCurrentInstance();
		final Application application = fc.getApplication();
		final Panel panel = (Panel) application.createComponent(fc, "org.primefaces.component.Panel", "org.primefaces.component.PanelRenderer");
		final String id = this.displayConnectorToID(connector);

		// Set the usual properties of the panel
		panel.setId(id);
		panel.setHeader(connector.getName());
		panel.setClosable(true);
		panel.setToggleable(false);

		final HtmlOutputText text = new HtmlOutputText();
		text.setValue(connector.getDisplay().getName());
		panel.getChildren().add(text);

		// The following code makes sure that the application detects the close event
		final AjaxBehavior behaviour = new AjaxBehavior();
		behaviour.setProcess("@this");
		behaviour.addAjaxBehaviorListener(new AjaxBehaviorListenerImpl() {

			private static final long serialVersionUID = 1L;

			@Override
			public void processAjaxBehavior(final AjaxBehaviorEvent event) throws AbortProcessingException {
				CockpitEditorBean.this.panelCloseEvent(event);
			}

		});
		panel.addClientBehavior("close", behaviour);

		return panel;
	}

	private void panelCloseEvent(final AjaxBehaviorEvent event) {
		if (this.activeView != null) {
			final String id = ((Panel) event.getSource()).getId();
			final MIDisplayConnector connector = this.idToDisplayConnector(id);

			this.activeView.getDisplayConnectors().remove(connector);
			this.cockpitLayout.removeDisplayConnector(this.activeView, connector);

			this.setModificationsFlag();
		}
	}

	/**
	 * This method adds the given display to the currently active view. If no view exists, this method does nothing.
	 * 
	 * @param display
	 *            The display which should be added to the current view.
	 */
	public void addDisplayToView(final MIDisplay display) {
		if (this.activeView != null) {
			final MIDisplayConnector connector = this.factory.createDisplayConnector();
			connector.setDisplay(display);
			connector.setName("Display " + this.displayConnectors.getSize() + 1);
			this.activeView.getDisplayConnectors().add(connector);

			// Now add it to the dashboard as well
			final Panel panel = this.createPanelFromDisplayConnector(connector);
			this.getDashboard().getChildren().add(panel);
			final DashboardColumn column = this.dashboardModel.getColumn(0);
			column.addWidget(panel.getId());

			this.cockpitLayout.addDisplayConnector(this.activeView, connector, 0);

			this.setModificationsFlag();

		}
	}

	/**
	 * This handler should be executed when the user moves an element within the dashboard.
	 * 
	 * @param event
	 *            The move event.
	 */
	public void handleReorder(final DashboardReorderEvent event) {
		final MIDisplayConnector connector = this.idToDisplayConnector(event.getWidgetId());

		// Primefaces uses null as sender column index, if the sender is the same as the receiver. We correct this.
		final int senderIndex;
		if (event.getSenderColumnIndex() != null) {
			senderIndex = event.getSenderColumnIndex();
		} else {
			senderIndex = event.getColumnIndex();
		}

		this.cockpitLayout.moveDisplayConnector(this.activeView, connector, senderIndex, event.getColumnIndex(), event.getItemIndex());

		this.setModificationsFlag();
	}

	/**
	 * This method is used as a validator for new display connector names.
	 * 
	 * @param context
	 *            The context of the validation.
	 * @param toValidate
	 *            The components which has be validated.
	 * @param value
	 *            The new value.
	 */
	public void validateDisplayConnectorName(final FacesContext context, final UIComponent toValidate, final Object value) {
		if ((value instanceof String) && (toValidate instanceof UIInput)) {
			final boolean nameExists = this.existsDisplayConnectorName((String) value);
			((UIInput) toValidate).setValid(!nameExists);
		}
	}

	/**
	 * This is the event if a node has been clicked and should be selected.
	 */
	public void nodeSelected() {
		final Map<String, String> paramMap = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap();

		final String fullID = paramMap.get("id");
		final String shortID = fullID.substring(fullID.indexOf(':') + "displayConnector_".length() + 1);

		final MIDisplayConnector connector = this.displayConnectors.get(Integer.parseInt(shortID));
		if (connector != null) {
			this.selectedNode = connector;
		}
	}

	/**
	 * This method checks whether a display connector with the given name exists already.
	 * 
	 * @param name
	 *            The name to be checked.
	 * @return true iff the name exists already.
	 */
	private boolean existsDisplayConnectorName(final String name) {
		// Make sure a view is selected
		if (this.activeView == null) {
			return false;
		}

		boolean result = false;
		// Run through all display connectors and check the name against the given one
		for (final MIDisplayConnector connector : this.activeView.getDisplayConnectors()) {
			if (connector.getName().equals(name)) {
				result = true;
				break;
			}
		}

		// The name has not been found
		return result;
	}

	private void clearDashboard() {
		// Run through all columns of the dashboard model and remove the items
		final List<DashboardColumn> columns = this.dashboard.getModel().getColumns();
		for (final DashboardColumn column : columns) {
			column.getWidgets().clear();
		}

		// Now remove the items from the dashboard
		this.dashboard.getChildren().clear();
	}

	private String displayConnectorToID(final MIDisplayConnector displayConnector) {
		return PANEL_ID_PREFIX + this.displayConnectors.get(displayConnector);
	}

	private MIDisplayConnector idToDisplayConnector(final String id) {
		final String shortID = id.substring(PANEL_ID_PREFIX.length());
		final int intID = Integer.valueOf(shortID);

		return this.displayConnectors.get(intID);
	}

}
