/***
 *
 * Copyright 2014 Andrew Hall
 *
 * 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 org.statefulj.framework.core;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javassist.CannotCompileException;
import javassist.NotFoundException;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodParameter;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.statefulj.common.utils.ReflectionUtils;
import org.statefulj.framework.core.actions.DomainEntityMethodInvocationAction;
import org.statefulj.framework.core.actions.MethodInvocationAction;
import org.statefulj.framework.core.annotations.StatefulController;
import org.statefulj.framework.core.annotations.Transition;
import org.statefulj.framework.core.annotations.Transitions;
import org.statefulj.framework.core.fsm.FSM;
import org.statefulj.framework.core.fsm.TransitionImpl;
import org.statefulj.framework.core.model.EndpointBinder;
import org.statefulj.framework.core.model.PersistenceSupportBeanFactory;
import org.statefulj.framework.core.model.ReferenceFactory;
import org.statefulj.framework.core.model.StatefulFSM;
import org.statefulj.framework.core.model.impl.MemoryPersistenceSupportBeanFactoryImpl;
import org.statefulj.framework.core.model.impl.ReferenceFactoryImpl;
import org.statefulj.framework.core.model.impl.StatefulFSMImpl;
import org.statefulj.fsm.model.impl.StateImpl;

/**
 * StatefulFactory is responsible for inspecting all StatefulControllers and building out
 * the StatefulJ framework.  The factory is invoked at post processing of the beans but before
 * the beans are instantiated
 *
 * @author Andrew Hall
 *
 */
public class StatefulFactory implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware {

	private ApplicationContext appContext;

	private static final String DEFAULT_PACKAGE = "org.statefulj";

	private static final Logger logger = LoggerFactory.getLogger(StatefulFactory.class);

	private final Pattern binder = Pattern.compile("(([^:]*):)?(.*)");

	private Map<Class<?>, Set<String>> entityToControllerMappings = new HashMap<Class<?>, Set<String>>();

	private MemoryPersistenceSupportBeanFactoryImpl memoryPersistenceFactory = new MemoryPersistenceSupportBeanFactoryImpl();

	private String[] packages;

	/**
	 * The default constructor will build the StatefulJ Framework using the binders and persisters from the
	 * "org.stateful" package
	 *
	 */
	public StatefulFactory() {
		this(DEFAULT_PACKAGE);
	}

	/**
	 * Will build the StatefulJ Framework using the binders and persisters from the packages specified
	 * in the parameter list.  This constructor will not scan the "org.stateful" packages unless
	 * explicitly provided in the parameter list
	 *
	 * @param packages This list of packages to scan for the binders and persisters
	 */
	public StatefulFactory(String... packages) {
		this.packages = packages;
	}

	// Resolver that injects the FSM for a given controller.  It is inferred by the ClassType or will use the bean Id specified by the value of the
	// FSM Annotation
	//
	class FSMAnnotationResolver extends QualifierAnnotationAutowireCandidateResolver {

		@Override
		public Object getSuggestedValue(DependencyDescriptor descriptor) {
			Object suggested = null;
			Field field = descriptor.getField();
			MethodParameter methodParameter = descriptor.getMethodParameter();

			boolean isStatefulFSM = false;
			String controllerId = null;
			Type genericFieldType = null;
			String fieldName = null;
			org.statefulj.framework.core.annotations.FSM fsmAnnotation = null;

			// If this is a StatefulFSM, parse out the Annotation and Type information
			//
			if (field != null) {
				if (isStatefulFSM(field)) {
					fsmAnnotation = field.getAnnotation(org.statefulj.framework.core.annotations.FSM.class);
					genericFieldType = field.getGenericType();
					fieldName = field.getName();
					isStatefulFSM = true;
				}
			} else if (methodParameter != null) {
				if (isStatefulFSM(methodParameter)) {
					fsmAnnotation = methodParameter.getParameterAnnotation(org.statefulj.framework.core.annotations.FSM.class);
					genericFieldType = methodParameter.getGenericParameterType();
					fieldName = methodParameter.getParameterName();
					isStatefulFSM = true;
				}
			}

			// If this is a StatefulFSM field, then resolve bean reference
			//
			if (isStatefulFSM) {

				// Determine the controllerId - either explicit or derived
				//
				controllerId = getControllerId(fsmAnnotation);
				if (StringUtils.isEmpty(controllerId)) {

					// Get the Managed Class
					//
					Class<?> managedClass = getManagedClass(fieldName, genericFieldType);

					// Fetch the Controller from the mapping
					//
					controllerId = deriveControllerId(fieldName, managedClass);
				}

				ReferenceFactory refFactory = new ReferenceFactoryImpl(controllerId);
				suggested = appContext.getBean(refFactory.getStatefulFSMId());
			}


			return (suggested != null) ? suggested : super.getSuggestedValue(descriptor);
		}

		private String getControllerId(org.statefulj.framework.core.annotations.FSM fsmAnnotation) {
			String controllerId = (fsmAnnotation != null ) ? fsmAnnotation.value() : null;
			return controllerId;
		}

		/**
		 * @param field
		 * @return
		 */
		private boolean isStatefulFSM(Field field) {
			return field != null && field.getType().isAssignableFrom(StatefulFSM.class);
		}

		/**
		 * @param methodParameter
		 * @return
		 */
		private boolean isStatefulFSM(MethodParameter methodParameter) {
			return methodParameter != null && methodParameter.getParameterType().isAssignableFrom(StatefulFSM.class);
		}

		/**
		 * @param fieldName
		 * @param managedClass
		 * @return
		 */
		private String deriveControllerId(String fieldName, Class<?> managedClass) {
			String controllerId;
			Set<String> controllers = entityToControllerMappings.get(managedClass);

			if (controllers == null) {
				throw new RuntimeException("Unable to resolve FSM for field " + fieldName);
			}
			if (controllers.size() > 1) {
				throw new RuntimeException("Ambiguous fsm for " + fieldName);
			}

			controllerId = controllers.iterator().next();
			return controllerId;
		}

		/**
		 * @param fieldName
		 * @param genericFieldType
		 * @return
		 */
		private Class<?> getManagedClass(String fieldName, Type genericFieldType) {
			Class<?> managedClass = null;

			if(genericFieldType instanceof ParameterizedType){
			    ParameterizedType aType = (ParameterizedType) genericFieldType;
			    Type[] fieldArgTypes = aType.getActualTypeArguments();
			    for(Type fieldArgType : fieldArgTypes){
			    	managedClass = (Class<?>) fieldArgType;
			    	break;
			    }
			}

			if (managedClass == null) {
				logger.error("Field {} isn't parametrized", fieldName);
				throw new RuntimeException("Field " + fieldName + " isn't paramertized");
			}
			return managedClass;
		}

	}


	/* Set the FSMAnnotationResolver to resolve all FSM annotations
	 *
	 * (non-Javadoc)
	 * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory)
	 */
	public void postProcessBeanFactory(final ConfigurableListableBeanFactory reg)
			throws BeansException {
		DefaultListableBeanFactory  bf = (DefaultListableBeanFactory) reg;
		bf.setAutowireCandidateResolver(new FSMAnnotationResolver());
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		this.appContext = applicationContext;
	}

	/*
	 * Override postProcessBeanDefinitionRegistry to dynamically generate all the StatefulJ beans for each StatefulController
	 *
	 * (non-Javadoc)
	 * @see org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(org.springframework.beans.factory.support.BeanDefinitionRegistry)
	 */
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry reg)
			throws BeansException {
		logger.debug("postProcessBeanDefinitionRegistry : enter");
		try {

			// Reflect over StatefulJ
			//
			Reflections reflections = new Reflections((Object[])this.packages);

			// Load up all Endpoint Binders
			//
			Map<String, EndpointBinder> binders = new HashMap<String, EndpointBinder>();
			loadEndpointBinders(reflections, binders);

			// Load up all PersistenceSupportBeanFactories
			//
			Map<Class<?>, PersistenceSupportBeanFactory> persistenceFactories = new HashMap<Class<?>, PersistenceSupportBeanFactory>();
			loadPersistenceSupportBeanFactories(reflections, persistenceFactories);

			// Map Controllers and Entities
			//
			Map<String, Class<?>> controllerToEntityMapping = new HashMap<String, Class<?>>();
			Map<Class<?>, String> entityToRepositoryMappings = new HashMap<Class<?>, String>();

			mapControllerAndEntityClasses(
					reg,
					controllerToEntityMapping,
					entityToRepositoryMappings,
					this.entityToControllerMappings);

			// Iterate thru all StatefulControllers and build the framework
			//
			for (Entry<String, Class<?>> entry : controllerToEntityMapping.entrySet()) {
				buildFramework(
						entry.getKey(),
						entry.getValue(),
						reg,
						entityToRepositoryMappings,
						binders,
						persistenceFactories);
			}

		} catch(Exception e) {
			throw new BeanCreationException("Unable to create bean", e);
		}
		logger.debug("postProcessBeanDefinitionRegistry : exit");
	}

	/**
	 * Iterate thru all beans and fetch the StatefulControllers
	 *
	 * @param reg
	 * @return
	 * @throws ClassNotFoundException
	 */
	private void mapControllerAndEntityClasses(
			BeanDefinitionRegistry reg,
			Map<String, Class<?>> controllerToEntityMapping,
			Map<Class<?>, String> entityToRepositoryMapping,
			Map<Class<?>, Set<String>> entityToControllerMappings) throws ClassNotFoundException {

		// Loop thru the bean registry
		//
		for(String bfName : reg.getBeanDefinitionNames()) {

			BeanDefinition bf = reg.getBeanDefinition(bfName);

			if (bf.isAbstract()) {
				logger.debug("Skipping abstract bean " + bfName);
				continue;
			}

			Class<?> clazz = getClassFromBeanDefinition(bf, reg);

			if (clazz == null) {
				logger.debug("Unable to resolve class for bean " + bfName);
				continue;
			}

			// If it's a StatefulController, map controller to the entity and the entity to the controller
			//
			if (ReflectionUtils.isAnnotationPresent(clazz, StatefulController.class)) {
				mapEntityWithController(controllerToEntityMapping, entityToControllerMappings, bfName, clazz);
			}

			// Else, if the Bean is a Repository, then map the
			// Entity associated with the Repo to the PersistenceSupport object
			//
			else if (RepositoryFactoryBeanSupport.class.isAssignableFrom(clazz)) {
				mapEntityToRepository(entityToRepositoryMapping, bfName, bf);
			}
		}
	}

	/**
	 * @param entityToRepositoryMapping
	 * @param bfName
	 * @param bf
	 * @throws ClassNotFoundException
	 */
	private void mapEntityToRepository(Map<Class<?>, String> entityToRepositoryMapping,
			String bfName, BeanDefinition bf) throws ClassNotFoundException {

		// Determine the Entity Class associated with the Repo
		//
		String value = (String)bf.getPropertyValues().getPropertyValue("repositoryInterface").getValue();
		Class<?> repoInterface = Class.forName(value);
		Class<?> entityType = null;
		for(Type type : repoInterface.getGenericInterfaces()) {
			if (type instanceof ParameterizedType) {
				ParameterizedType parmType = (ParameterizedType)type;
				if (Repository.class.isAssignableFrom((Class<?>)parmType.getRawType()) &&
				    parmType.getActualTypeArguments() != null &&
				    parmType.getActualTypeArguments().length > 0) {
					entityType = (Class<?>)parmType.getActualTypeArguments()[0];
					break;
				}
			}
		}

		if (entityType == null) {
			throw new RuntimeException("Unable to determine Entity type for class " + repoInterface.getName());
		}

		// Map Entity to the RepositoryFactoryBeanSupport bean
		//
		logger.debug("Mapped \"{}\" to repo \"{}\", beanId=\"{}\"", entityType.getName(), value, bfName);

		entityToRepositoryMapping.put(entityType, bfName);
	}

	/**
	 * @param controllerToEntityMapping
	 * @param entityToControllerMappings
	 * @param bfName
	 * @param clazz
	 */
	private void mapEntityWithController(Map<String, Class<?>> controllerToEntityMapping,
			Map<Class<?>, Set<String>> entityToControllerMappings, String bfName,
			Class<?> clazz) {
		logger.debug("Found StatefulController, class = \"{}\", beanName = \"{}\"", clazz.getName(), bfName);

		// Ctrl -> Entity
		//
		controllerToEntityMapping.put(bfName, clazz);

		// Entity -> Ctrls
		//
		Class<?> managedEntity = ReflectionUtils.getFirstClassAnnotation(clazz, StatefulController.class).clazz();
		Set<String> controllers = entityToControllerMappings.get(managedEntity);
		if (controllers == null) {
			controllers = new HashSet<String>();
			entityToControllerMappings.put(managedEntity, controllers);
		}
		controllers.add(bfName);
	}

	private void buildFramework(
			String statefulControllerBeanId,
			Class<?> statefulControllerClass,
			BeanDefinitionRegistry reg,
			Map<Class<?>, String> entityToRepositoryMappings,
			Map<String, EndpointBinder> binders,
			Map<Class<?>, PersistenceSupportBeanFactory> persistenceFactories
			) throws CannotCompileException, IllegalArgumentException, NotFoundException, IllegalAccessException, InvocationTargetException, ClassNotFoundException {

		// Determine the managed class
		//
		StatefulController scAnnotation = ReflectionUtils.getFirstClassAnnotation(statefulControllerClass, StatefulController.class);
		Class<?> managedClass = scAnnotation.clazz();

		// Is the the Controller and ManagedClass the same?  (DomainEntity)
		//
		boolean isDomainEntity = managedClass.equals(statefulControllerClass);

		// ReferenceFactory will generate all the necessary bean ids
		//
		ReferenceFactory referenceFactory = new ReferenceFactoryImpl(statefulControllerBeanId);

		// We need to map Transitions across all Methods
		//
		Map<String, Map<String, Method>> providersMappings = new HashMap<String, Map<String, Method>>();
		Map<Transition, Method> transitionMapping = new HashMap<Transition, Method>();
		Map<Transition, Method> anyMapping = new HashMap<Transition, Method>();
		Set<String> states = new HashSet<String>();
		Set<String> blockingStates = new HashSet<String>();

		// Fetch Repo info
		//
		String repoBeanId = getRepoId(entityToRepositoryMappings, managedClass);

		// Get the Persistence Factory
		//
		PersistenceSupportBeanFactory factory = null;
		BeanDefinition repoBeanDefinitionFactory = null;

		// If we don't have a repo mapped to this entity, fall back to memory persister
		//
		if (repoBeanId == null) {
			logger.warn("Unable to find Spring Data Repository for {}, using an in-memory persister", managedClass.getName());
			factory = this.memoryPersistenceFactory;
		} else {
			repoBeanDefinitionFactory = reg.getBeanDefinition(repoBeanId);
			Class<?> repoClassName = getClassFromBeanClassName(repoBeanDefinitionFactory);

			// Fetch the PersistenceFactory
			//
			factory = persistenceFactories.get(repoClassName);
		}

		// Map the Events and Transitions for the Controller
		//
		mapEventsTransitionsAndStates(
				statefulControllerClass,
				providersMappings,
				transitionMapping,
				anyMapping,
				states,
				blockingStates);

		// Do we have binders?
		//
		boolean hasBinders = (providersMappings.size() > 0);

		// Iterate thru the providers - building and registering each Binder
		//
		if (hasBinders) {
			for(Entry<String, Map<String, Method>> entry : providersMappings.entrySet()) {

				// Fetch the binder
				//
				EndpointBinder binder = binders.get(entry.getKey());

				// Check if we found the binder
				//
				if (binder == null) {
					logger.error("Unable to locate binder: {}", entry.getKey());
					throw new RuntimeException("Unable to locate binder: " + entry.getKey());
				}

				// Build out the Binder Class
				//
				Class<?> binderClass = binder.bindEndpoints(
						statefulControllerBeanId,
						statefulControllerClass,
						factory.getIdType(),
						isDomainEntity,
						entry.getValue(),
						referenceFactory);

				// Add the new Binder Class to the Bean Registry
				//
				registerBinderBean(entry.getKey(), referenceFactory, binderClass, reg);
			}
		}

		// -- Build the FSM infrastructure --

		// Build out a set of States
		//
		List<RuntimeBeanReference> stateBeans = new ManagedList<RuntimeBeanReference>();
		for(String state : states) {
			logger.debug("Registering state \"{}\"", state);
			String stateId = registerState(
					referenceFactory,
					statefulControllerClass,
					state,
					blockingStates.contains(state),
					reg);
			stateBeans.add(new RuntimeBeanReference(stateId));
		}

		// Build out the Action classes and the Transitions
		//
		RuntimeBeanReference controllerRef = new RuntimeBeanReference(statefulControllerBeanId);
		int cnt = 1;
		List<String> transitionIds = new LinkedList<String>();
		for(Entry<Transition, Method> entry : anyMapping.entrySet()) {
			for (String state : states) {
				Transition t = entry.getKey();
				String from = state;
				String to = (t.to().equals(Transition.ANY_STATE)) ? state : entry.getKey().to();
				String transitionId = referenceFactory.getTransitionId(cnt++);
				boolean reload = t.reload();
				registerActionAndTransition(
						referenceFactory,
						statefulControllerClass,
						from,
						to,
						reload,
						entry.getKey(),
						entry.getValue(),
						isDomainEntity,
						controllerRef,
						transitionId,
						reg);
				transitionIds.add(transitionId);
			}
		}
		for(Entry<Transition, Method> entry : transitionMapping.entrySet()) {
			Transition t = entry.getKey();
			boolean reload = t.reload();
			String transitionId = referenceFactory.getTransitionId(cnt++);
			registerActionAndTransition(
					referenceFactory,
					statefulControllerClass,
					entry.getKey().from(),
					entry.getKey().to(),
					reload,
					entry.getKey(),
					entry.getValue(),
					isDomainEntity,
					controllerRef,
					transitionId,
					reg);
			transitionIds.add(transitionId);
		}

		// Build out the Managed Entity Factory Bean
		//
		String factoryId = registerFactoryBean(
				referenceFactory,
				factory,
				scAnnotation,
				reg);

		// Build out the Managed Entity Finder Bean if we have endpoint binders; otherwise, it's not needed
		//
		String finderId = null;
		if (hasFinder(scAnnotation, repoBeanId)) {
			finderId = registerFinderBean(
					referenceFactory,
					factory,
					scAnnotation,
					repoBeanId,
					reg);
		}

		// Build out the Managed Entity State Persister Bean
		//
		String persisterId = registerPersisterBean(
				referenceFactory,
				factory,
				scAnnotation,
				managedClass,
				repoBeanId,
				repoBeanDefinitionFactory,
				stateBeans,
				reg);

		// Build out the FSM Bean
		//
		String fsmBeanId = registerFSM(
				referenceFactory,
				statefulControllerClass,
				scAnnotation,
				persisterId,
				managedClass,
				finderId,
				factory.getIdAnnotationType(),
				reg);

		// Build out the StatefulFSM Bean
		//
		String statefulFSMBeanId = registerStatefulFSMBean(
				referenceFactory,
				managedClass,
				fsmBeanId,
				factoryId,
				transitionIds,
				reg);

		// Build out the FSMHarness Bean if we have binders; otherwise, it's not needed
		//
		if (hasBinders) {
			registerFSMHarness(
					referenceFactory,
					factory,
					managedClass,
					statefulFSMBeanId,
					factoryId,
					finderId,
					repoBeanDefinitionFactory,
					reg);
		}
	}

	private void mapEventsTransitionsAndStates(
			Class<?> statefulControllerClass,
			Map<String, Map<String, Method>> providerMappings,
			Map<Transition, Method> transitionMapping,
			Map<Transition, Method> anyMapping,
			Set<String> states,
			Set<String> blockingStates) throws IllegalArgumentException, NotFoundException, IllegalAccessException, InvocationTargetException, CannotCompileException {


		// Walk up the Class hierarchy building out the FSM
		//
		if (statefulControllerClass == null) {
			return;
		} else {
			mapEventsTransitionsAndStates(
				statefulControllerClass.getSuperclass(),
				providerMappings,
				transitionMapping,
				anyMapping,
				states,
				blockingStates
			);
		}

		logger.debug("Mapping events and transitions for {}", statefulControllerClass);

		// Pull StateController Annotation
		//
		StatefulController ctrlAnnotation = statefulControllerClass.getAnnotation(StatefulController.class);

		if (ctrlAnnotation != null) {
			// Add Start State
			//
			states.add(ctrlAnnotation.startState());

			// Add the list of BlockingStates
			//
			blockingStates.addAll(Arrays.asList(ctrlAnnotation.blockingStates()));

			// Map the NOOP Transitions
			//
			for (Transition transition : ctrlAnnotation.noops()) {
				mapTransition(
						transition,
						null,
						providerMappings,
						transitionMapping,
						anyMapping,
						states);
			}
		}

		// TODO : As we map the events, we need to make sure that the method signature and return
		// types of all the handlers for the event are the same

		for(Method method : statefulControllerClass.getDeclaredMethods()) {

			// Map the set of transitions as defined by the Transitions annotation
			//
			Transitions transitions = method.getAnnotation(Transitions.class);
			if (transitions != null) {
				for(Transition transition : transitions.value()) {
					mapTransition(
							transition,
							method,
							providerMappings,
							transitionMapping,
							anyMapping,
							states);
				}
			}

			// Map the Transition annotation
			//
			Transition transition = method.getAnnotation(Transition.class);
			if (transition != null) {
				mapTransition(
						transition,
						method,
						providerMappings,
						transitionMapping,
						anyMapping,
						states);
			}
		}
	}

	private void mapTransition(
			Transition transition,
			Method method,
			Map<String, Map<String, Method>> providerMappings,
			Map<Transition, Method> transitionMapping,
			Map<Transition, Method> anyMapping,
			Set<String> states) {

		logger.debug(
				"Mapping {}:{}->{}",
				transition.from(),
				transition.event(),
				transition.to());

		Pair<String, String> providerEvent = parseEvent(transition.event());
		String provider = providerEvent.getLeft();
		if (provider != null) {
			Map<String, Method> eventMapping = providerMappings.get(provider);
			if (eventMapping == null) {
				eventMapping = new HashMap<String, Method>();
				providerMappings.put(provider, eventMapping);
			}

			// Add to the event mapping if this the first occurrence of an event, or the method
			// has more parameters than the existing mapping
			//
			Method existing = eventMapping.get(providerEvent.getRight());
			if (existing == null || method.getParameterTypes().length > existing.getParameterTypes().length) {
				eventMapping.put(providerEvent.getRight(), method);
			}
		}

		if (!transition.from().equals(Transition.ANY_STATE)) {
			states.add(transition.from());
			transitionMapping.put(transition, method);
		} else {
			anyMapping.put(transition, method);
		}
		if (!transition.to().equals(Transition.ANY_STATE)) {
			states.add(transition.to());
		}
	}

	private Pair<String, String> parseEvent(String event) {
		Matcher matcher = this.binder.matcher(event);
		if (!matcher.matches()) {
			throw new RuntimeException("Unable to parse event=" + event);
		}
		return new ImmutablePair<String, String>(matcher.group(2), matcher.group(3));
	}

	private void registerActionAndTransition(
			ReferenceFactory referenceFactory,
			Class<?> clazz,
			String from,
			String to,
			boolean reload,
			Transition transition,
			Method method,
			boolean isDomainEntity,
			RuntimeBeanReference controllerRef,
			String transitionId,
			BeanDefinitionRegistry reg) {

		// Remap to="Any" to to=from
		//
		to = (Transition.ANY_STATE.equals(to)) ? from : to;

		logger.debug(
				"Registered: {}({})->{}/{}",
				from,
				transition.event(),
				to,
				(method == null) ? "noop" : method.getName());

		// Build the Action Bean
		//
		RuntimeBeanReference actionRef = null;
		if (method != null) {
			String actionId = referenceFactory.getActionId(method);
			if (!reg.isBeanNameInUse(actionId)) {
				registerMethodInvocationAction(referenceFactory, method,
						isDomainEntity, controllerRef, reg, actionId);
			}
			actionRef = new RuntimeBeanReference(actionId);
		}

		registerTransition(referenceFactory, from, to, reload, transition,
				transitionId, reg, actionRef);
	}

	/**
	 * @param referenceFactory
	 * @param from
	 * @param to
	 * @param reload
	 * @param transition
	 * @param transitionId
	 * @param reg
	 * @param actionRef
	 */
	private void registerTransition(ReferenceFactory referenceFactory,
			String from, String to, boolean reload, Transition transition,
			String transitionId, BeanDefinitionRegistry reg,
			RuntimeBeanReference actionRef) {
		// Build the Transition Bean
		//
		BeanDefinition transitionBean = BeanDefinitionBuilder
				.genericBeanDefinition(TransitionImpl.class)
				.getBeanDefinition();

		String fromId = referenceFactory.getStateId(from);
		String toId = referenceFactory.getStateId(to);
		Pair<String, String> providerEvent = parseEvent(transition.event());

		ConstructorArgumentValues args = transitionBean.getConstructorArgumentValues();
		args.addIndexedArgumentValue(0, new RuntimeBeanReference(fromId));
		args.addIndexedArgumentValue(1, new RuntimeBeanReference(toId));
		args.addIndexedArgumentValue(2, providerEvent.getRight());
		args.addIndexedArgumentValue(3, actionRef);
		args.addIndexedArgumentValue(4,
				(transition.from().equals(Transition.ANY_STATE) &&
				 transition.to().equals(Transition.ANY_STATE)));
		args.addIndexedArgumentValue(5, reload);
		reg.registerBeanDefinition(transitionId, transitionBean);
	}

	/**
	 * @param referenceFactory
	 * @param method
	 * @param isDomainEntity
	 * @param controllerRef
	 * @param reg
	 * @param actionId
	 */
	private void registerMethodInvocationAction(
			ReferenceFactory referenceFactory, Method method,
			boolean isDomainEntity, RuntimeBeanReference controllerRef,
			BeanDefinitionRegistry reg, String actionId) {
		// Choose the type of invocationAction based off of
		// whether the controller is a DomainEntity
		//
		Class<?> methodInvocationAction = (isDomainEntity) ?
				DomainEntityMethodInvocationAction.class :
				MethodInvocationAction.class;

		BeanDefinition actionBean = BeanDefinitionBuilder
				.genericBeanDefinition(methodInvocationAction)
				.getBeanDefinition();

		ConstructorArgumentValues args = actionBean.getConstructorArgumentValues();
		args.addIndexedArgumentValue(0, method.getName());
		args.addIndexedArgumentValue(1, method.getParameterTypes());
		args.addIndexedArgumentValue(2, new RuntimeBeanReference(referenceFactory.getFSMId()));

		if (!isDomainEntity) {
			args.addIndexedArgumentValue(3, controllerRef);
		}

		reg.registerBeanDefinition(actionId, actionBean);
	}

	private String registerState(
			ReferenceFactory referenceFactory,
			Class<?> statefulControllerClass,
			String state,
			boolean isBlocking,
			BeanDefinitionRegistry reg) {

		String stateId = referenceFactory.getStateId(state);
		BeanDefinition stateBean = BeanDefinitionBuilder
				.genericBeanDefinition(StateImpl.class)
				.getBeanDefinition();

		ConstructorArgumentValues args = stateBean.getConstructorArgumentValues();
		args.addIndexedArgumentValue(0, state);
		args.addIndexedArgumentValue(1, false);
		args.addIndexedArgumentValue(2, isBlocking);

		reg.registerBeanDefinition(stateId, stateBean);

		return stateId;
	}

	private String registerFSM(
			ReferenceFactory referenceFactory,
			Class<?> statefulControllerClass,
			StatefulController scAnnotation,
			String persisterId,
			Class<?> managedClass,
			String finderId,
			Class<? extends Annotation> idAnnotationType,
			BeanDefinitionRegistry reg) {
		int retryAttempts = scAnnotation.retryAttempts();
		int retryInterval = scAnnotation.retryInterval();

		String fsmBeanId = referenceFactory.getFSMId();
		BeanDefinition fsmBean = BeanDefinitionBuilder
				.genericBeanDefinition(FSM.class)
				.getBeanDefinition();
		ConstructorArgumentValues args = fsmBean.getConstructorArgumentValues();
		args.addIndexedArgumentValue(0, fsmBeanId);
		args.addIndexedArgumentValue(1, new RuntimeBeanReference(persisterId));
		args.addIndexedArgumentValue(2, retryAttempts);
		args.addIndexedArgumentValue(3, retryInterval);
		args.addIndexedArgumentValue(4, managedClass);
		args.addIndexedArgumentValue(5, idAnnotationType);
		args.addIndexedArgumentValue(6, this.appContext);

		if (finderId != null) {
			args.addIndexedArgumentValue(7, new RuntimeBeanReference(finderId));
		}

		reg.registerBeanDefinition(fsmBeanId, fsmBean);
		return fsmBeanId;
	}

	private String registerStatefulFSMBean(
			ReferenceFactory referenceFactory,
			Class<?> statefulClass,
			String fsmBeanId,
			String factoryId,
			List<String> transitionIds,
			BeanDefinitionRegistry reg) {
		String statefulFSMBeanId = referenceFactory.getStatefulFSMId();
		BeanDefinition statefulFSMBean = BeanDefinitionBuilder
				.genericBeanDefinition(StatefulFSMImpl.class)
				.getBeanDefinition();
		ConstructorArgumentValues args = statefulFSMBean.getConstructorArgumentValues();
		args.addIndexedArgumentValue(0, new RuntimeBeanReference(fsmBeanId));
		args.addIndexedArgumentValue(1, statefulClass);
		args.addIndexedArgumentValue(2, new RuntimeBeanReference(factoryId));
		reg.registerBeanDefinition(statefulFSMBeanId, statefulFSMBean);
		statefulFSMBean.setDependsOn(transitionIds.toArray(new String[]{}));
		return statefulFSMBeanId;
	}

	private String registerBinderBean(
			String key,
			ReferenceFactory referenceFactory,
			Class<?> binderClass,
			BeanDefinitionRegistry reg) {
		BeanDefinition def = BeanDefinitionBuilder
				.genericBeanDefinition(binderClass)
				.getBeanDefinition();
		String binderId = referenceFactory.getBinderId(key);
		reg.registerBeanDefinition(binderId, def);
		return binderId;
	}

	private String registerFactoryBean(
			ReferenceFactory referenceFactory,
			PersistenceSupportBeanFactory persistenceFactory,
			StatefulController statefulContollerAnnotation,
			BeanDefinitionRegistry reg) {

		String factoryId = statefulContollerAnnotation.factoryId();

		if (StringUtils.isEmpty(factoryId)) {
			if (persistenceFactory == null) {
				throw new RuntimeException("PersistenceFactory is undefined and no factory bean was specified in the StatefulController Annotation for "  + statefulContollerAnnotation.clazz());
			}
			factoryId = referenceFactory.getFactoryId();
			reg.registerBeanDefinition(
					factoryId,
					persistenceFactory.buildFactoryBean(statefulContollerAnnotation.clazz()));
		}

		return factoryId;
	}

	private String registerFinderBean(
			ReferenceFactory referenceFactory,
			PersistenceSupportBeanFactory persistenceFactory,
			StatefulController statefulContollerAnnotation,
			String repoBeanId,
			BeanDefinitionRegistry reg) {

		String finderId = statefulContollerAnnotation.finderId();

		if (StringUtils.isEmpty(finderId)) {
			if (persistenceFactory == null) {
				throw new RuntimeException("PersistenceFactory is undefined and no finder bean was specified in the StatefulController Annotation for " + statefulContollerAnnotation.clazz());
			}
			if (StringUtils.isEmpty(repoBeanId)) {
				throw new RuntimeException("No Repository is defined for " + statefulContollerAnnotation.clazz());
			}
			finderId = referenceFactory.getFinderId();
			reg.registerBeanDefinition(
					finderId,
					persistenceFactory.buildFinderBean(repoBeanId));
		}

		return finderId;
	}

	private String registerPersisterBean(
			ReferenceFactory referenceFactory,
			PersistenceSupportBeanFactory persistenceFactory,
			StatefulController statefulContollerAnnotation,
			Class<?> statefulClass,
			String repoBeanId,
			BeanDefinition repoBeanDefinitionFactory,
			List<RuntimeBeanReference> stateBeans,
			BeanDefinitionRegistry reg) {

		String persisterId = statefulContollerAnnotation.persisterId();

		if (StringUtils.isEmpty(persisterId)) {
			if (persistenceFactory == null) {
				throw new RuntimeException("PersistenceFactory is undefined and no persister bean was specified in the StatefulController Annotation for " + statefulContollerAnnotation.clazz());
			}
			String startStateId = referenceFactory.getStateId(statefulContollerAnnotation.startState());
			persisterId = referenceFactory.getPersisterId();
			reg.registerBeanDefinition(
					persisterId,
					persistenceFactory.buildPersisterBean(
							statefulClass,
							repoBeanId,
							repoBeanDefinitionFactory,
							statefulContollerAnnotation.stateField(),
							startStateId,
							stateBeans));
		}

		return persisterId;
	}

	private String registerFSMHarness(
				ReferenceFactory referenceFactory,
				PersistenceSupportBeanFactory persistenceFactory,
				Class<?> statefulClass,
				String fsmBeanId,
				String factoryId,
				String finderId,
				BeanDefinition repoBeanFactory,
				BeanDefinitionRegistry reg) {
		String fsmHarnessId = referenceFactory.getFSMHarnessId();
		reg.registerBeanDefinition(
				fsmHarnessId,
				persistenceFactory.buildFSMHarnessBean(
						statefulClass,
						fsmBeanId,
						factoryId,
						finderId,
						repoBeanFactory));
		return fsmHarnessId;
	}

	private String getRepoId(Map<Class<?>, String> entityToRepositoryMappings, Class<?> clazz) {
		if (clazz != null) {
			String id = entityToRepositoryMappings.get(clazz);
			if (id != null) {
				return id;
			}
			id = getRepoId(entityToRepositoryMappings, clazz.getSuperclass());
			if (id != null) {
				return id;
			}
			for (Class<?> interfaze : clazz.getInterfaces()) {
				id = getRepoId(entityToRepositoryMappings, interfaze);
				if (id != null) {
					return id;
				}
			}
		}
		return null;
	}

	private Class<?> getClassFromBeanDefinition(BeanDefinition bf, BeanDefinitionRegistry reg) throws ClassNotFoundException {
		Class<?> clazz = null;

		if (bf.getBeanClassName() == null) {
			clazz = getClassFromFactoryMethod(bf, reg);
		} else {
			clazz = getClassFromBeanClassName(bf);
		}

		if (clazz == null) {
			clazz = getClassFromParentBean(bf, reg);
		}

		return clazz;
	}

	/**
	 * @param bf
	 * @return
	 * @throws ClassNotFoundException
	 */
	private Class<?> getClassFromBeanClassName(BeanDefinition bf)
			throws ClassNotFoundException {
		return Class.forName(bf.getBeanClassName());
	}

	/**
	 * @param bf
	 * @param reg
	 * @param clazz
	 * @return
	 * @throws ClassNotFoundException
	 */
	private Class<?> getClassFromParentBean(BeanDefinition bf, BeanDefinitionRegistry reg)
			throws ClassNotFoundException {
		Class<?> clazz = null;
		String parentBeanName = bf.getParentName();
		if (parentBeanName != null) {
			BeanDefinition parent = reg.getBeanDefinition(parentBeanName);
			if (parent != null) {
				clazz = this.getClassFromBeanDefinition(parent, reg);
			}
		}
		return clazz;
	}

	/**
	 * @param bf
	 * @param reg
	 * @param clazz
	 * @return
	 * @throws ClassNotFoundException
	 */
	private Class<?> getClassFromFactoryMethod(BeanDefinition bf, BeanDefinitionRegistry reg)
			throws ClassNotFoundException {
		Class<?> clazz = null;
		String factoryBeanName = bf.getFactoryBeanName();
		if (factoryBeanName != null) {
			BeanDefinition factory = reg.getBeanDefinition(factoryBeanName);
			if (factory != null) {
				String factoryClassName = factory.getBeanClassName();
				Class<?> factoryClass = Class.forName(factoryClassName);
				List<Method> methods = new LinkedList<Method>();
				methods.addAll(Arrays.asList(factoryClass.getMethods()));
				methods.addAll(Arrays.asList(factoryClass.getDeclaredMethods()));
				for (Method method : methods) {
					method.setAccessible(true);
					if (method.getName().equals(bf.getFactoryMethodName())) {
						clazz = method.getReturnType();
						break;
					}
				}
			}
		}
		return clazz;
	}

	/**
	 * @param reflections
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 */
	private void loadPersistenceSupportBeanFactories(Reflections reflections, Map<Class<?>, PersistenceSupportBeanFactory> persistenceFactories)
			throws InstantiationException, IllegalAccessException {
		Set<Class<? extends PersistenceSupportBeanFactory>> persistenceFactoryTypes = reflections.getSubTypesOf(PersistenceSupportBeanFactory.class);
		for(Class<?> persistenceFactoryType : persistenceFactoryTypes) {
			if (!Modifier.isAbstract(persistenceFactoryType.getModifiers())) {
				PersistenceSupportBeanFactory factory = (PersistenceSupportBeanFactory)persistenceFactoryType.newInstance();
				Class<?> key = factory.getKey();
				if (key != null) {
					persistenceFactories.put(key, factory);
				}
			}
		}
	}

	/**
	 * @return
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 */
	private void loadEndpointBinders(Reflections reflections, Map<String, EndpointBinder> binders) throws InstantiationException,
			IllegalAccessException {
		Set<Class<? extends EndpointBinder>> endpointBinders = reflections.getSubTypesOf(EndpointBinder.class);
		for(Class<?> binderClass : endpointBinders) {
			if (!Modifier.isAbstract(binderClass.getModifiers())) {
				EndpointBinder binder = (EndpointBinder)binderClass.newInstance();
				binders.put(binder.getKey(), binder);
			}
		}
	}

	/**
	 * @param scAnnotation
	 * @param repoBeanId
	 * @return
	 */
	private boolean hasFinder(StatefulController scAnnotation, String repoBeanId) {
		return !"".equals(scAnnotation.finderId()) || (repoBeanId != null && !repoBeanId.trim().equals(""));
	}

}