/***************************************************************************
 * 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.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

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

import com.google.common.base.Functions;
import com.google.common.collect.Maps;

import kieker.analysis.AnalysisController;
import kieker.analysis.model.analysisMetaModel.MIProject;
import kieker.analysis.model.analysisMetaModel.MIView;
import kieker.common.logging.Log;
import kieker.common.logging.LogFactory;
import kieker.webgui.common.exception.InvalidAnalysisStateException;
import kieker.webgui.common.exception.ProjectLoadException;
import kieker.webgui.common.exception.ProjectNotExistingException;
import kieker.webgui.domain.DisplayType;
import kieker.webgui.service.IProjectService;
import kieker.webgui.web.beans.application.GlobalPropertiesBean;
import kieker.webgui.web.utility.CockpitLayout;
import kieker.webgui.web.utility.displaySettings.IDisplayConnectorSettings;
import kieker.webgui.web.utility.displaySettings.MeterGaugeDisplaySettings;
import kieker.webgui.web.utility.displaySettings.PieChartDisplaySettings;
import kieker.webgui.web.utility.displaySettings.XYPlotDisplaySettings;

import org.primefaces.model.chart.CartesianChartModel;
import org.primefaces.model.chart.LineChartSeries;
import org.primefaces.model.chart.MeterGaugeChartModel;
import org.primefaces.model.chart.PieChartModel;
import org.primefaces.model.tagcloud.DefaultTagCloudItem;
import org.primefaces.model.tagcloud.DefaultTagCloudModel;
import org.primefaces.model.tagcloud.TagCloudModel;

import net.vidageek.mirror.dsl.Mirror;
import net.vidageek.mirror.exception.MirrorException;

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

/**
 * This class is a {@code Spring} managed bean containing the necessary data behind an instance of the cockpit.<br>
 * <br/>
 * 
 * It has 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 CockpitBean {

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

	private static final int NUMBER_OF_COCKPIT_COLUMNS = 2;

	private final Map<MIView, Map<String, IDisplayConnectorSettings>> displaySettings = new ConcurrentHashMap<MIView, Map<String, IDisplayConnectorSettings>>();

	private CockpitLayout cockpitLayout;
	private String projectName;
	private MIProject project;
	private MIView activeView;
	private String selectedDisplay;

	@Autowired
	private GlobalPropertiesBean globalPropertiesBean;
	@Autowired
	private IProjectService projectService;

	/**
	 * Creates a new instance of this class. <b>Do not call this constructor manually. It will only be accessed by Spring.</b>
	 */
	public CockpitBean() {
		// No code necessary
	}

	/**
	 * 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.loadCockpitLayout();
					this.createSettingsMapsForViews();
				}
			}
		} catch (final ProjectLoadException ex) {
			CockpitBean.LOG.error("An error occured while loading the project.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgProjectLoadingException());
		} catch (final NullPointerException ex) {
			// This exception occurs, when the projectsBean has not been initialized
			CockpitBean.LOG.error("An error occured while loading the project.", ex);
			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgProjectLoadingException());
		}
	}

	public String getSelectedDisplay() {
		return this.selectedDisplay;
	}

	public void setSelectedDisplay(final String selectedDisplay) {
		this.selectedDisplay = selectedDisplay;
	}

	public void setActiveView(final MIView activeView) {
		this.activeView = activeView;
	}

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

	public CockpitLayout getCockpitLayout() {
		return this.cockpitLayout;
	}

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

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

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

	/**
	 * Delivers the current meter gauge settings container for the given connector name. If it does not exist, it will be created.
	 * 
	 * @param displayConnectorName
	 *            The name of the display connector.
	 * 
	 * @return The settings container for the given display connector.
	 */
	public MeterGaugeDisplaySettings getMeterGaugeDisplaySettings(final String displayConnectorName) {
		// Get the existing settings container
		final IDisplayConnectorSettings existingSettingsContainer = this.displaySettings.get(this.activeView).get(displayConnectorName);

		// Check if we have to create a new settings container
		if (existingSettingsContainer instanceof MeterGaugeDisplaySettings) {
			return (MeterGaugeDisplaySettings) existingSettingsContainer;
		} else {
			final MeterGaugeDisplaySettings newSettingsContainer = new MeterGaugeDisplaySettings();
			this.displaySettings.get(this.activeView).put(displayConnectorName, newSettingsContainer);
			return newSettingsContainer;
		}
	}

	/**
	 * Delivers the current xy plot settings container for the given connector name. If it does not exist, it will be created.
	 * 
	 * @param displayConnectorName
	 *            The name of the display connector.
	 * 
	 * @return The settings container for the given display connector.
	 */
	public XYPlotDisplaySettings getXYPlotDisplaySettings(final String displayConnectorName) {
		// Get the existing settings container
		final IDisplayConnectorSettings existingSettingsContainer = this.displaySettings.get(this.activeView).get(displayConnectorName);

		// Check if we have to create a new settings container
		if (existingSettingsContainer instanceof XYPlotDisplaySettings) {
			return (XYPlotDisplaySettings) existingSettingsContainer;
		} else {
			final XYPlotDisplaySettings newSettingsContainer = new XYPlotDisplaySettings();
			this.displaySettings.get(this.activeView).put(displayConnectorName, newSettingsContainer);
			return newSettingsContainer;
		}
	}

	/**
	 * Delivers the current pie chart settings container for the given connector name. If it does not exist, it will be created.
	 * 
	 * @param displayConnectorName
	 *            The name of the display connector.
	 * 
	 * @return The settings container for the given display connector.
	 */
	public PieChartDisplaySettings getPieChartDisplaySettings(final String displayConnectorName) {
		// Get the existing settings container
		final IDisplayConnectorSettings existingSettingsContainer = this.displaySettings.get(this.activeView).get(displayConnectorName);

		// Check if we have to create a new settings container
		if (existingSettingsContainer instanceof PieChartDisplaySettings) {
			return (PieChartDisplaySettings) existingSettingsContainer;
		} else {
			final PieChartDisplaySettings newSettingsContainer = new PieChartDisplaySettings();
			this.displaySettings.get(this.activeView).put(displayConnectorName, newSettingsContainer);
			return newSettingsContainer;
		}
	}

	/**
	 * Delivers a plain text update for the given display connector.
	 * 
	 * @param displayConnectorName
	 *            The name of the display connector.
	 * 
	 * @return The current plain text visualization of the given display connector.
	 */
	public String getPlainTextUpdate(final String displayConnectorName) {
		String result = "N/A";

		try {
			final Object displayObject = this.projectService.getDisplay(this.projectName, this.activeView.getName(), displayConnectorName);
			final String text = (String) new Mirror().on(displayObject).invoke().method("getText").withoutArgs();

			result = text;
		} catch (final MirrorException ex) {
			CockpitBean.LOG.warn("Reflection exception.", ex);
		} catch (final InvalidAnalysisStateException ex) {
			CockpitBean.LOG.info("Project is in invalid state.", ex);
		}

		return result;
	}

	/**
	 * Delivers a meter gauge update for the given display connector.
	 * 
	 * @param displayConnectorName
	 *            The name of the display connector.
	 * 
	 * @return The current meter gauge visualization of the given display connector.
	 */
	@SuppressWarnings("unchecked")
	public MeterGaugeChartModel getMeterGaugeUpdate(final String displayConnectorName) {
		final MeterGaugeChartModel model = new MeterGaugeChartModel();

		try {
			final Object displayObj = this.projectService.getDisplay(this.projectName, this.activeView.getName(), displayConnectorName);
			final Set<String> keys = (Set<String>) new Mirror().on(displayObj).invoke().method("getKeys").withoutArgs();
			final MeterGaugeDisplaySettings settings = this.getMeterGaugeDisplaySettings(displayConnectorName);

			// Fill the settings with the available plots
			settings.setAvailablePlots(Maps.asMap(keys, Functions.<String>identity()));
			if (settings.getVisiblePlot().isEmpty() && !keys.isEmpty()) {
				settings.setVisiblePlot(keys.iterator().next());
			}

			if (!settings.getVisiblePlot().isEmpty()) {
				final List<Number> intervals = (List<Number>) new Mirror().on(displayObj).invoke().method("getIntervals").withArgs(keys.iterator().next());
				final Number value = (Number) new Mirror().on(displayObj).invoke().method("getValue").withArgs(keys.iterator().next());

				model.setIntervals(intervals);
				final Number maxInterval = intervals.get(intervals.size() - 1);
				if (value.doubleValue() <= maxInterval.doubleValue()) {
					model.setValue(value);
				} else {
					model.setValue(maxInterval);
				}
			}
		} catch (final MirrorException ex) {
			CockpitBean.LOG.warn("Reflection exception.", ex);
		} catch (final InvalidAnalysisStateException ex) {
			CockpitBean.LOG.info("Project is in invalid state.", ex);
		}

		// Make sure that default values are set if necessary
		if (model.getIntervals().isEmpty()) {
			model.setIntervals(Arrays.asList((Number) 50));
			model.setValue(0);
		}

		return model;
	}

	/**
	 * Delivers a tag cloud update for the given display connector.
	 * 
	 * @param displayConnectorName
	 *            The name of the display connector.
	 * 
	 * @return The current tag cloud visualization of the given display connector.
	 */
	@SuppressWarnings("unchecked")
	public TagCloudModel getTagCloudUpdate(final String displayConnectorName) {
		final TagCloudModel model = new DefaultTagCloudModel();

		try {
			final Object displayObj = this.projectService.getDisplay(this.projectName, this.activeView.getName(), displayConnectorName);
			final Map<String, AtomicLong> counters = (Map<String, AtomicLong>) new Mirror().on(displayObj).invoke().method("getCounters").withoutArgs();

			for (final Map.Entry<String, AtomicLong> counter : counters.entrySet()) {
				model.addTag(new DefaultTagCloudItem(counter.getKey(), (int) counter.getValue().get()));
			}
		} catch (final MirrorException ex) {
			CockpitBean.LOG.warn("Reflection exception.", ex);
		} catch (final InvalidAnalysisStateException ex) {
			CockpitBean.LOG.info("Project is in invalid state.", ex);
		}

		return model;
	}

	/**
	 * Delivers a line chart update for the given display connector.
	 * 
	 * @param displayConnectorName
	 *            The name of the display connector.
	 * 
	 * @return The current line chart visualization of the given display connector.
	 */
	@SuppressWarnings("unchecked")
	public CartesianChartModel getXYPlotUpdate(final String displayConnectorName) {
		final CartesianChartModel model = new CartesianChartModel();

		try {
			final Object displayObj = this.projectService.getDisplay(this.projectName, this.activeView.getName(), displayConnectorName);
			final Set<String> keys = (Set<String>) new Mirror().on(displayObj).invoke().method("getKeys").withoutArgs();
			final XYPlotDisplaySettings settings = this.getXYPlotDisplaySettings(displayConnectorName);

			// Fill the settings with the available plots
			settings.setAvailablePlots(Maps.asMap(keys, Functions.<String>identity()));
			if (settings.getVisiblePlots().isEmpty() && !keys.isEmpty()) {
				settings.setVisiblePlots(keys);
			}

			for (final String key : keys) {
				if (settings.getVisiblePlots().contains(key)) {
					final Map<Object, Number> entries = (Map<Object, Number>) new Mirror().on(displayObj).invoke().method("getEntries").withArgs(key);
					final LineChartSeries lineChartSeries = new LineChartSeries();
					lineChartSeries.setLabel(key);

					if (entries.isEmpty()) {
						lineChartSeries.set(0, 0);
					} else {
						lineChartSeries.setData(entries);
					}
					model.addSeries(lineChartSeries);
				}
			}

		} catch (final MirrorException ex) {
			CockpitBean.LOG.warn("Reflection exception.", ex);
		} catch (final InvalidAnalysisStateException ex) {
			CockpitBean.LOG.info("Project is in invalid state.", ex);
		}

		// Make sure that default values are set if necessary
		if (model.getSeries().isEmpty()) {
			final LineChartSeries series = new LineChartSeries();
			series.set(0, 0);
			model.addSeries(series);
		}

		return model;
	}

	/**
	 * Delivers a pie chart update for the given display connector.
	 * 
	 * @param displayConnectorName
	 *            The name of the display connector.
	 * 
	 * @return The current pie chart visualization of the given display connector.
	 */
	@SuppressWarnings("unchecked")
	public PieChartModel getPieChartUpdate(final String displayConnectorName) {
		final PieChartModel model = new PieChartModel();

		try {
			final Object displayObj = this.projectService.getDisplay(this.projectName, this.activeView.getName(), displayConnectorName);
			final Set<String> keys = (Set<String>) new Mirror().on(displayObj).invoke().method("getKeys").withoutArgs();

			for (final String key : keys) {
				final Number value = (Number) new Mirror().on(displayObj).invoke().method("getValue").withArgs(key);
				model.set(key, value);
			}
		} catch (final MirrorException ex) {
			CockpitBean.LOG.warn("Reflection exception.", ex);
		} catch (final InvalidAnalysisStateException ex) {
			CockpitBean.LOG.info("Project is in invalid state.", ex);
		}

		// Make sure that default values are set if necessary
		if (model.getData().isEmpty()) {
			model.set(null, 1);
		}

		return model;
	}

	/**
	 * Delivers the display type of the given connector name. This method returns never null.
	 * 
	 * @param displayConnectorName
	 *            The name of the display connector.
	 * 
	 * @return The type of the display connector or {@code DisplayType.UNKNOWN} if the type could not be detected.
	 */
	public DisplayType getDisplayType(final String displayConnectorName) {
		DisplayType type = DisplayType.UNKNOWN;

		if ((displayConnectorName != null) && (this.activeView != null)) {
			type = this.projectService.getDisplayType(this.projectName, this.activeView.getName(), displayConnectorName);
		}

		return type;
	}

	/**
	 * Checks whether the analysis is currently running.
	 * 
	 * @return true if and only if the analysis is running.
	 */
	public boolean isAnalysisRunning() {
		try {
			return this.projectService.getCurrentState(this.projectName) == AnalysisController.STATE.RUNNING;
		} catch (final NullPointerException ex) {
			// This exception can occur, when the projectsBean has not been initialized
			LOG.warn("A null pointer exception occured.", ex);
		}
		return false;
	}

	/**
	 * Checks whether the analysis is currently in the ready state.
	 * 
	 * @return true if and only if the analysis is ready to be started.
	 */
	public boolean isAnalysisReady() {
		try {
			return this.projectService.getCurrentState(this.projectName) == AnalysisController.STATE.READY;
		} catch (final NullPointerException ex) {
			// This exception can occur, when the projectsBean has not been initialized
			LOG.warn("A null pointer exception occured.", ex);
		}
		return false;
	}

	/**
	 * Checks whether the analysis is not available.
	 * 
	 * @return true if and only if the analysis is <b>not</b> available.
	 */
	public boolean isAnalysisNotAvailable() {
		try {
			return this.projectService.getCurrentState(this.projectName) == null;
		} catch (final NullPointerException ex) {
			// This exception can occur, when the projectsBean has not been initialized
			LOG.warn("A null pointer exception occured.", ex);
		}
		return true;
	}

	/**
	 * Checks whether the analysis is currently terminated.
	 * 
	 * @return true if and only if the analysis has been terminated.
	 */
	public boolean isAnalysisTerminated() {
		try {
			return this.projectService.getCurrentState(this.projectName) == AnalysisController.STATE.TERMINATED;
		} catch (final NullPointerException ex) {
			// This exception can occur, when the projectsBean has not been initialized
			LOG.warn("A null pointer exception occured.", ex);
		}
		return false;
	}

	/**
	 * Checks whether the analysis is currently terminating.
	 * 
	 * @return true if and only if the analysis is currently terminating.
	 */
	public boolean isAnalysisTerminating() {
		try {
			return this.projectService.getCurrentState(this.projectName) == AnalysisController.STATE.TERMINATING;
		} catch (final NullPointerException ex) {
			// This exception can occur, when the projectsBean has not been initialized
			LOG.warn("A null pointer exception occured.", ex);
		}
		return false;
	}

	/**
	 * Checks whether the analysis is currently in the failed state.
	 * 
	 * @return true if and only if the analysis has failed.
	 */
	public boolean isAnalysisFailed() {
		try {
			return this.projectService.getCurrentState(this.projectName) == AnalysisController.STATE.FAILED;
		} catch (final NullPointerException ex) {
			// This exception can occur, when the projectsBean has not been initialized
			LOG.warn("A null pointer exception occured.", ex);
		}
		return false;
	}

	private void createSettingsMapsForViews() {
		for (final MIView view : this.project.getViews()) {
			this.displaySettings.put(view, new ConcurrentHashMap<String, IDisplayConnectorSettings>());
		}
	}

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

}
