/***************************************************************************
 * 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.service.impl.utility;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import kieker.analysis.AnalysisController;
import kieker.analysis.AnalysisController.STATE;
import kieker.common.logging.Log;
import kieker.common.logging.LogFactory;
import kieker.webgui.common.ClassContainer;
import kieker.webgui.common.exception.AnalysisDisplayReloadException;
import kieker.webgui.common.exception.AnalysisInitializationException;
import kieker.webgui.common.exception.ProjectNotExistingException;
import kieker.webgui.common.exception.ReflectionException;
import kieker.webgui.domain.DisplayType;
import kieker.webgui.persistence.IProjectDAO;

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

/**
 * This class represents an initialized analysis within the web application. It encapsulates an {@link AnalysisController} instance, which have been loaded via the
 * reflection API. It manages the access to the controller and handles the necessary display objects.
 * 
 * @author Nils Christian Ehmke
 */
public final class Analysis {

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

	private static final long MAX_THREAD_WAIT_TIME_MS = 1000;

	private final Map<String, Map<String, Object>> displayPlugins = new HashMap<String, Map<String, Object>>();
	private final Map<String, Map<String, Method>> displayMethods = new HashMap<String, Map<String, Method>>();
	private final Map<String, Object> pluginIDMap;
	private final ClassContainer classContainer;
	private final Object analysisController;
	private final Object analysisControllerThread;

	/**
	 * Creates a new analysis using the given parameters.
	 * 
	 * @param projectName
	 *            The name of the project.
	 * @param projectDAO
	 *            The data access object for the projects.
	 * 
	 * @throws AnalysisInitializationException
	 *             If an error occurred during the instantiation of the analysis.
	 * @throws IOException
	 *             If something went wrong while loading the file.
	 * @throws ProjectNotExistingException
	 *             If a project with the given name does not exist.
	 */
	public Analysis(final String projectName, final IProjectDAO projectDAO) throws AnalysisInitializationException, ProjectNotExistingException, IOException {
		try {
			final File projectFile = projectDAO.getProjectFile(projectName);
			final ClassLoader classLoader = projectDAO.getClassLoader(projectName, this);
			this.classContainer = new ClassContainer(classLoader);
			final Class<?> analysisControllerClass = this.classContainer.getAnalysisControllerClass();
			final Class<?> analsisControllerThreadClass = this.classContainer.getAnalysisControllerThreadClass();

			// Load the project
			final Object modelProject = new Mirror().on(analysisControllerClass).invoke().method("loadFromFile").withArgs(projectFile.getAbsoluteFile());

			// Create the analysis controller and the necessary thread
			final Object controllerAndMapping = new Mirror().on(analysisControllerClass).invoke().method("createAnalysisController")
					.withArgs(modelProject, classLoader);

			this.analysisController = new Mirror().on(controllerAndMapping).invoke().method("getController").withoutArgs();
			this.analysisControllerThread = new Mirror().on(analsisControllerThreadClass).invoke().constructor().withArgs(this.analysisController);
			@SuppressWarnings("unchecked")
			final Map<Object, Object> pluginMap = (Map<Object, Object>) new Mirror().on(controllerAndMapping).invoke().method("getPluginMap").withoutArgs();

			this.pluginIDMap = new HashMap<String, Object>();
			for (final Map.Entry<Object, Object> entry : pluginMap.entrySet()) {
				final String key = (String) new Mirror().on(entry.getKey()).invoke().method("getId").withoutArgs();
				this.pluginIDMap.put(key, entry.getValue());
			}

			this.loadDisplayObjectsAndMethods(modelProject);
		} catch (final ReflectionException ex) {
			throw new AnalysisInitializationException("An error occured while instantiating the analysis.", ex);
		} catch (final NullPointerException ex) {
			throw new AnalysisInitializationException("An error occured while instantiating the analysis.", ex);
		} catch (final IllegalAccessException ex) {
			throw new AnalysisInitializationException("An error occured while instantiating the analysis.", ex);
		} catch (final InstantiationException ex) {
			throw new AnalysisInitializationException("An error occured while instantiating the analysis.", ex);
		} catch (final MirrorException ex) {
			throw new AnalysisInitializationException("An error occured while instantiating the analysis.", ex);
		}

	}

	/**
	 * Initializes an emergency shutdown.
	 */
	public void emergencyShutdown() {
		// Not implemented yet
	}

	@SuppressWarnings("unchecked")
	private void loadDisplayObjectsAndMethods(final Object modelProject) throws InstantiationException, IllegalAccessException {
		final List<Object> views = (List<Object>) new Mirror().on(modelProject).invoke().method("getViews").withoutArgs();

		for (final Object view : views) {
			final List<Object> displayConnectors = (List<Object>) new Mirror().on(view).invoke().method("getDisplayConnectors").withoutArgs();
			final String viewName = (String) new Mirror().on(view).invoke().method("getName").withoutArgs();

			if (!this.displayMethods.containsKey(viewName)) {
				this.displayMethods.put(viewName, new HashMap<String, Method>());
			}
			if (!this.displayPlugins.containsKey(viewName)) {
				this.displayPlugins.put(viewName, new HashMap<String, Object>());
			}
			final Map<String, Method> methodMap = this.displayMethods.get(viewName);
			final Map<String, Object> displayPluginMap = this.displayPlugins.get(viewName);

			for (final Object displayConnector : displayConnectors) {
				final Object display = new Mirror().on(displayConnector).invoke().method("getDisplay").withoutArgs();
				final String displayName = (String) new Mirror().on(display).invoke().method("getName").withoutArgs();
				final String displayConnectorName = (String) new Mirror().on(displayConnector).invoke().method("getName").withoutArgs();
				final Object displayParent = new Mirror().on(display).invoke().method("getParent").withoutArgs();

				final String key = (String) new Mirror().on(displayParent).invoke().method("getId").withoutArgs();
				final Object plugin = this.pluginIDMap.get(key);

				if (plugin == null) {
					continue;
				}

				final Method[] methods = plugin.getClass().getMethods();
				for (final Method method : methods) {
					final Annotation displayAnnoation = method.getAnnotation(this.classContainer.getDisplayAnnotationClass());
					if (displayAnnoation != null) {
						final String potentialDisplayName = (String) new Mirror().on(displayAnnoation).invoke().method("name").withoutArgs();
						if (displayName.equals(potentialDisplayName)) {
							methodMap.put(displayConnectorName, method);
							displayPluginMap.put(displayConnectorName, plugin);
							break;
						}
					}
				}
			}
		}
	}

	/**
	 * Reloads the displays of the given project.
	 * 
	 * @param projectName
	 *            The name of the project.
	 * @param projectDAO
	 *            The data access object for the projects.
	 * 
	 * @throws AnalysisDisplayReloadException
	 *             If something went wrong while reloading the displays.
	 */
	public void reloadDisplays(final String projectName, final IProjectDAO projectDAO) throws AnalysisDisplayReloadException {
		try {
			final File projectFile = projectDAO.getProjectFile(projectName);
			final Object modelProject = new Mirror().on(this.classContainer.getAnalysisControllerClass()).invoke().method("loadFromFile")
					.withArgs(projectFile.getAbsoluteFile());

			this.displayMethods.clear();
			this.displayPlugins.clear();

			this.loadDisplayObjectsAndMethods(modelProject);
		} catch (final IllegalAccessException ex) {
			throw new AnalysisDisplayReloadException("An error occured while reloading the displays.", ex);
		} catch (final InstantiationException ex) {
			throw new AnalysisDisplayReloadException("An error occured while reloading the displays.", ex);
		}
	}

	/**
	 * Starts the analysis.
	 */
	public void start() {
		try {
			new Mirror().on(this.analysisControllerThread).invoke().method("start").withoutArgs();
		} catch (final MirrorException ex) {
			LOG.error("An error occured during a reflection method call.", ex);
		}
	}

	/**
	 * Stops the analysis.
	 */
	public void stop() {
		try {
			new Mirror().on(this.analysisControllerThread).invoke().method("terminate").withoutArgs();
			new Mirror().on(this.analysisControllerThread).invoke().method("join").withArgs(Analysis.MAX_THREAD_WAIT_TIME_MS);
		} catch (final MirrorException ex) {
			LOG.error("An error occured during a reflection method call.", ex);
		}
	}

	/**
	 * Delivers the current state of the analysis.
	 * 
	 * @return The state.
	 */
	public STATE getCurrentState() {
		try {
			final Enum<?> state = (Enum<?>) new Mirror().on(this.analysisController).invoke().method("getState").withoutArgs();
			STATE retState = null;

			if (state != null) {
				if (STATE.FAILED.toString().equals(state.toString())) {
					retState = STATE.FAILED;
				}
				if (STATE.READY.toString().equals(state.toString())) {
					retState = STATE.READY;
				}
				if (STATE.RUNNING.toString().equals(state.toString())) {
					retState = STATE.RUNNING;
				}
				if (STATE.TERMINATED.toString().equals(state.toString())) {
					retState = STATE.TERMINATED;
				}
				if (STATE.TERMINATING.toString().equals(state.toString())) {
					retState = STATE.TERMINATING;
				}
			}
			return retState;
		} catch (final MirrorException ex) {
			LOG.error("An error occured during a reflection method call.", ex);
			return null;
		}
	}

	/**
	 * This method delivers the display object of the (currently running) analysis. Technically it is an instance of {@code AbstractDisplay}, but in fact the project
	 * specific class loader has been used.
	 * 
	 * @param viewName
	 *            The name of the view.
	 * @param displayName
	 *            The name of the display.
	 * 
	 * @return A display object for the given parameters.
	 */
	public Object getDisplay(final String viewName, final String displayName) {
		final Object plugin = this.displayPlugins.get(viewName).get(displayName);
		final Method method = this.displayMethods.get(viewName).get(displayName);

		return new Mirror().on(plugin).invoke().method(method).withoutArgs();
	}

	/**
	 * Delivers the log entries for the current analysis.
	 * 
	 * @return An array containing the log entries (if available).
	 */
	public Object[] getLogEntries() {
		try {
			return (Object[]) new Mirror().on(this.classContainer.getLogImplWebguiLoggingClass()).invoke().method("getEntries")
					.withArgs(AnalysisController.class.getName());
		} catch (final MirrorException ex) {
			return new Object[0];
		}
	}

	/**
	 * Delivers the type of the given display connector.
	 * 
	 * @param viewName
	 *            The name of the view.
	 * @param displayConnectorName
	 *            The name of the display connector.
	 * 
	 * @return The type of the display connector.
	 */
	public DisplayType getDisplayType(final String viewName, final String displayConnectorName) {
		try {
			final Class<?> parameter = this.displayMethods.get(viewName).get(displayConnectorName).getReturnType();
			if (parameter == null) {
				return null;
			} else if (parameter == this.classContainer.getImageClass()) {
				return DisplayType.IMAGE;
			} else if (parameter == this.classContainer.getPlainTextClass()) {
				return DisplayType.PLAIN_TEXT;
			} else if (parameter == this.classContainer.getHtmlTextClass()) {
				return DisplayType.HTML_TEXT;
			} else if (parameter == this.classContainer.getXYPlotClass()) {
				return DisplayType.XY_PLOT;
			} else if (parameter == this.classContainer.getMeterGaugeClass()) {
				return DisplayType.METER_GAUGE;
			} else if (parameter == this.classContainer.getTagCloudClass()) {
				return DisplayType.TAG_CLOUD;
			} else if (parameter == this.classContainer.getPieChartClass()) {
				return DisplayType.PIE_CHART;
			} else {
				return DisplayType.UNKNOWN;
			}
		} catch (final NullPointerException ex) {
			return DisplayType.UNKNOWN;
		}
	}

}
