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

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import kieker.analysis.model.analysisMetaModel.MIAnalysisComponent;
import kieker.analysis.model.analysisMetaModel.MIAnalysisMetaModelFactory;
import kieker.analysis.model.analysisMetaModel.MIDisplay;
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.MIProperty;
import kieker.analysis.model.analysisMetaModel.MIReader;
import kieker.analysis.model.analysisMetaModel.MIRepository;
import kieker.analysis.model.analysisMetaModel.MIRepositoryConnector;
import kieker.common.logging.Log;
import kieker.common.logging.LogFactory;
import kieker.webgui.common.ClassContainer;
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.domain.pluginDecorators.VisualizationDecorator;

import net.vidageek.mirror.dsl.Mirror;

import org.springframework.stereotype.Service;

/**
 * This service provides methods to convert a given {@link Class} instance (the one from a reflection loaded repository or plugin) to a meta model instance. This is
 * done by using the annotations and methods which will be accessed via the reflection API.
 * 
 * @author Nils Christian Ehmke
 */
@Service
public final class Class2ModelInstanceConverter {

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

	private static final MIAnalysisMetaModelFactory FACTORY = MIAnalysisMetaModelFactory.eINSTANCE;

	/**
	 * Default constructor. <b>Do not use this constructor. This bean is Spring managed.</b>
	 */
	public Class2ModelInstanceConverter() {
		// No code necessary
	}

	/**
	 * Converts the given {@link kieker.analysis.plugin.reader.AbstractReaderPlugin} class instance into a model instance using the given parameters.
	 * 
	 * @param clazz
	 *            The class to convert.
	 * @param classContainer
	 *            The container which will be used during the reflection calls.
	 * 
	 * @return A model instance representing the given class as a meta model component.
	 */
	public ReaderDecorator convertReaderClass2ModelInstance(final Class<?> clazz, final ClassContainer classContainer) {
		return (ReaderDecorator) this.convertComponentClass2ModelInstance(clazz, classContainer, Type.Reader);
	}

	/**
	 * Converts the given {@link kieker.analysis.plugin.filter.AbstractFilterPlugin} instance into a model instance using the given parameters.
	 * 
	 * @param clazz
	 *            The class to convert.
	 * @param classContainer
	 *            The container which will be used during the reflection calls.
	 * 
	 * @return A model instance representing the given class as a meta model component.
	 */
	public FilterDecorator convertFilterClass2ModelInstance(final Class<?> clazz, final ClassContainer classContainer) {
		return (FilterDecorator) this.convertComponentClass2ModelInstance(clazz, classContainer, Type.Filter);
	}

	public VisualizationDecorator convertVisualizationClass2ModelInstance(final Class<?> clazz, final ClassContainer classContainer) {
		return (VisualizationDecorator) this.convertComponentClass2ModelInstance(clazz, classContainer, Type.Visualization);
	}

	/**
	 * Converts the given {@link kieker.analysis.repository.AbstractRepository} instance into a model instance using the given parameters.
	 * 
	 * @param clazz
	 *            The class to convert.
	 * @param classContainer
	 *            The container which will be used during the reflection calls.
	 * 
	 * @return A model instance representing the given class as a meta model component.
	 */
	public RepositoryDecorator convertRepositoryClass2ModelInstance(final Class<?> clazz, final ClassContainer classContainer) {
		return (RepositoryDecorator) this.convertComponentClass2ModelInstance(clazz, classContainer, Type.Repository);
	}

	private AbstractAnalysisComponentDecorator<?> convertComponentClass2ModelInstance(final Class<?> clazz, final ClassContainer classContainer, final Type type) {
		final MIAnalysisComponent plugin = this.createSuitableModelInstance(clazz, classContainer);

		final Collection<MIProperty> properties = new ArrayList<MIProperty>();
		final Collection<MIInputPort> inputPorts = new ArrayList<MIInputPort>();
		final Collection<MIOutputPort> outputPorts = new ArrayList<MIOutputPort>();
		final Collection<MIDisplay> displays = new ArrayList<MIDisplay>();
		final Collection<MIRepositoryConnector> repositories = new ArrayList<MIRepositoryConnector>();
		final Map<String, String> propertyDescriptions = new HashMap<String, String>();
		final Map<String, String> displayDescriptions = new HashMap<String, String>();

		String description = "";
		String dependency = "";
		boolean fullyInitialized = true;

		try {
			description = this.fillDescription(clazz, classContainer);
			dependency = this.fillDependency(clazz, classContainer);
			this.fillProperties(clazz, classContainer, properties, propertyDescriptions);
			plugin.getProperties().addAll(properties);
			if ((type == Type.Filter) || (type == Type.Reader) || (type == Type.Visualization)) {
				this.fillOutputPorts(clazz, classContainer, outputPorts, (MIPlugin) plugin);
				this.fillDisplays(clazz, classContainer, displays, displayDescriptions);
				this.fillRepositories(clazz, classContainer, repositories);
				((MIPlugin) plugin).getOutputPorts().addAll(outputPorts);
				((MIPlugin) plugin).getDisplays().addAll(displays);
				((MIPlugin) plugin).getRepositories().addAll(repositories);
				if ((type == Type.Filter) || (type == Type.Visualization)) {
					this.fillInputPorts(clazz, classContainer, inputPorts, (MIFilter) plugin);
					((MIFilter) plugin).getInputPorts().addAll(inputPorts);
				}
			}
		} catch (final Throwable ex) { // NOCS NOPMD (Throwable)
			Class2ModelInstanceConverter.LOG.warn("A component with the classname '" + clazz.getCanonicalName() + "' could not be initialized.", ex);

			plugin.getProperties().clear();
			if ((type == Type.Filter) || (type == Type.Reader)) {
				((MIPlugin) plugin).getOutputPorts().clear();
				((MIPlugin) plugin).getDisplays().clear();
				if (type == Type.Filter) {
					((MIFilter) plugin).getDisplays().clear();
				}
			}

			fullyInitialized = false;
		}
		plugin.setClassname(clazz.getName());
		plugin.setName(clazz.getSimpleName());

		// Decide what to return
		AbstractAnalysisComponentDecorator<? extends MIAnalysisComponent> result;
		switch (type) {
		case Filter:
			result = new FilterDecorator((MIFilter) plugin, Collections.unmodifiableMap(propertyDescriptions), description, dependency, fullyInitialized,
					Collections.unmodifiableMap(displayDescriptions));
			break;
		case Reader:
			result = new ReaderDecorator((MIReader) plugin, Collections.unmodifiableMap(propertyDescriptions), description, dependency, fullyInitialized,
					Collections.unmodifiableMap(displayDescriptions));
			break;
		case Repository:
			result = new RepositoryDecorator((MIRepository) plugin, Collections.unmodifiableMap(propertyDescriptions), description, dependency, fullyInitialized);
			break;
		case Visualization:
			result = new VisualizationDecorator((MIFilter) plugin, Collections.unmodifiableMap(propertyDescriptions), description, dependency, fullyInitialized,
					Collections.unmodifiableMap(displayDescriptions));
			break;
		default:
			result = null;
			break;
		}

		return result;
	}

	private void fillInputPorts(final Class<?> clazz, final ClassContainer classContainer, final Collection<MIInputPort> inputPorts, final MIFilter parent) {
		final Collection<Method> methods = this.getInputPortMethods(clazz, classContainer);
		for (final Method method : methods) {
			final Annotation inputPortAnnotation = method.getAnnotation(classContainer.getInputPortAnnotationClass());
			final MIInputPort newPort = Class2ModelInstanceConverter.FACTORY.createInputPort();
			newPort.setName((String) new Mirror().on(inputPortAnnotation).invoke().method("name").withoutArgs());
			newPort.setParent(parent);
			inputPorts.add(newPort);
		}
	}

	private void fillDisplays(final Class<?> clazz, final ClassContainer classContainer, final Collection<MIDisplay> displays,
			final Map<String, String> displayDescriptions) {
		final Collection<Method> methods = this.getDisplayMethods(clazz, classContainer);
		for (final Method method : methods) {
			final Annotation displayAnnotation = method.getAnnotation(classContainer.getDisplayAnnotationClass());
			final MIDisplay newDisplay = Class2ModelInstanceConverter.FACTORY.createDisplay();
			newDisplay.setName((String) new Mirror().on(displayAnnotation).invoke().method("name").withoutArgs());
			displays.add(newDisplay);
			displayDescriptions.put(newDisplay.getName(), (String) new Mirror().on(displayAnnotation).invoke().method("description").withoutArgs());
		}
	}

	private Collection<Method> getAnnotatedMethods(final Class<?> clazz, final Class<? extends Annotation> annotation) {
		final Collection<Method> result = new ArrayList<Method>();

		final Method[] methods = clazz.getMethods();
		for (final Method method : methods) {
			if (method.isAnnotationPresent(annotation)) {
				result.add(method);
			}
		}

		return result;
	}

	private Collection<Method> getInputPortMethods(final Class<?> clazz, final ClassContainer classContainer) {
		return this.getAnnotatedMethods(clazz, classContainer.getInputPortAnnotationClass());
	}

	private Collection<Method> getDisplayMethods(final Class<?> clazz, final ClassContainer classContainer) {
		return this.getAnnotatedMethods(clazz, classContainer.getDisplayAnnotationClass());
	}

	private void fillOutputPorts(final Class<?> clazz, final ClassContainer classContainer, final Collection<MIOutputPort> outputPorts, final MIPlugin parent) {
		final Annotation annotation = this.getSuitableAnnotation(clazz, classContainer);
		final Annotation[] outputPortAnnotations = (Annotation[]) new Mirror().on(annotation).invoke().method("outputPorts").withoutArgs();

		for (final Annotation outputPortAnnotation : outputPortAnnotations) {
			final MIOutputPort newPort = Class2ModelInstanceConverter.FACTORY.createOutputPort();
			newPort.setName((String) new Mirror().on(outputPortAnnotation).invoke().method("name").withoutArgs());
			newPort.setParent(parent);
			outputPorts.add(newPort);
		}
	}

	private void fillProperties(final Class<?> clazz, final ClassContainer classContainer, final Collection<MIProperty> properties,
			final Map<String, String> propertyDescriptions) {
		final Annotation annotation = this.getSuitableAnnotation(clazz, classContainer);
		final Annotation[] propertyAnnotations = (Annotation[]) new Mirror().on(annotation).invoke().method("configuration").withoutArgs();

		for (final Annotation propertyAnnotation : propertyAnnotations) {
			final MIProperty newProperty = Class2ModelInstanceConverter.FACTORY.createProperty();
			newProperty.setName((String) new Mirror().on(propertyAnnotation).invoke().method("name").withoutArgs());
			newProperty.setValue((String) new Mirror().on(propertyAnnotation).invoke().method("defaultValue").withoutArgs());
			properties.add(newProperty);
			propertyDescriptions.put(newProperty.getName(), (String) new Mirror().on(propertyAnnotation).invoke().method("description").withoutArgs());
		}
	}

	private void fillRepositories(final Class<?> clazz, final ClassContainer classContainer, final Collection<MIRepositoryConnector> repositories) {
		final Annotation annotation = this.getSuitableAnnotation(clazz, classContainer);
		final Annotation[] repositoryPortAnnotations = (Annotation[]) new Mirror().on(annotation).invoke().method("repositoryPorts").withoutArgs();

		for (final Annotation repositoryPortAnnotation : repositoryPortAnnotations) {
			final MIRepositoryConnector newConnector = Class2ModelInstanceConverter.FACTORY.createRepositoryConnector();
			newConnector.setName((String) new Mirror().on(repositoryPortAnnotation).invoke().method("name").withoutArgs());

			repositories.add(newConnector);
		}
	}

	private String fillDependency(final Class<?> clazz, final ClassContainer classContainer) {
		final Annotation annotation = this.getSuitableAnnotation(clazz, classContainer);
		return (String) new Mirror().on(annotation).invoke().method("dependencies").withoutArgs();
	}

	private String fillDescription(final Class<?> clazz, final ClassContainer classContainer) {
		final Annotation annotation = this.getSuitableAnnotation(clazz, classContainer);
		return (String) new Mirror().on(annotation).invoke().method("description").withoutArgs();
	}

	private Annotation getSuitableAnnotation(final Class<?> clazz, final ClassContainer classContainer) {
		// Get the two potential annotations
		final Annotation annotationPlugin = clazz.getAnnotation(classContainer.getPluginAnnotationClass());
		final Annotation annotationRepository = clazz.getAnnotation(classContainer.getRepositoryAnnotationClass());

		// Find out which of them exists
		if (annotationPlugin != null) {
			return annotationPlugin;
		} else {
			return annotationRepository;
		}
	}

	private MIAnalysisComponent createSuitableModelInstance(final Class<?> clazz, final ClassContainer classContainer) {
		if (classContainer.getAbstractReaderPluginClass().isAssignableFrom(clazz)) {
			return Class2ModelInstanceConverter.FACTORY.createReader();
		} else if (classContainer.getAbstractFilterPluginClass().isAssignableFrom(clazz)) {
			return Class2ModelInstanceConverter.FACTORY.createFilter();
		} else {
			return Class2ModelInstanceConverter.FACTORY.createRepository();
		}
	}

	/**
	 * Tells whether the given class is "programmaticOnly".
	 * 
	 * @param clazz
	 *            The class of the plugin or repository.
	 * @param classContainer
	 *            The container which will be used for the reflection access.
	 * 
	 * @return The state of the programmaticOnly flag of the plugin or repository.
	 */
	public boolean isProgrammaticOnly(final Class<?> clazz, final ClassContainer classContainer) {
		final Annotation annotation = this.getSuitableAnnotation(clazz, classContainer);
		return (Boolean) new Mirror().on(annotation).invoke().method("programmaticOnly").withoutArgs();
	}

	private static enum Type {
		Reader, Filter, Repository, Visualization
	}

}
