/*
 * Copyright 2015-2016 the original author or authors.
 *
 * 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.springframework.statemachine.config;

import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.StateMachineException;
import org.springframework.statemachine.config.builders.StateMachineConfigBuilder;
import org.springframework.statemachine.config.builders.StateMachineConfigurationBuilder;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineConfigurer;
import org.springframework.statemachine.config.builders.StateMachineModelBuilder;
import org.springframework.statemachine.config.builders.StateMachineModelConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateBuilder;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionBuilder;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.common.annotation.AnnotationBuilder;
import org.springframework.statemachine.config.common.annotation.ObjectPostProcessor;
import org.springframework.statemachine.config.model.DefaultStateMachineModel;
import org.springframework.statemachine.config.model.ConfigurationData;
import org.springframework.statemachine.config.model.StatesData;
import org.springframework.statemachine.config.model.TransitionsData;

/**
 * {@code StateMachineBuilder} provides a builder pattern for
 * {@link StateMachine} using a similar concepts found from a
 * normal annotation based configuration.
 *
 * @author Janne Valkealahti
 *
 */
public class StateMachineBuilder {

	/**
	 * Gets a builder for a {@link StateMachine}.
	 *
	 * @param <S> the type of state
	 * @param <E> the type of event
	 * @return the builder
	 */
	public static <S, E> Builder<S, E> builder() {
		return new Builder<S, E>();
	}

	/**
	 * {@code Builder} implementation handling logic of building
	 * a {@link StateMachine} manually.
	 *
	 * @param <S> the type of state
	 * @param <E> the type of event
	 */
	public static class Builder<S, E> {

		private StateMachineConfigBuilder<S, E> builder;
		private BuilderStateMachineConfigurerAdapter<S, E> adapter;

		/**
		 * Instantiates a new builder.
		 */
		public Builder() {
			adapter = new BuilderStateMachineConfigurerAdapter<S, E>();
			builder = new StateMachineConfigBuilder<S, E>();
		}

		/**
		 * Configure model.
		 *
		 * @return the state machine model configurer
		 */
		public StateMachineModelConfigurer<S, E> configureModel() {
			return adapter.modelBuilder;
		}

		/**
		 * Configure configuration.
		 *
		 * @return the state machine configuration configurer
		 */
		public StateMachineConfigurationConfigurer<S, E> configureConfiguration() {
			return adapter.configurationBuilder;
		}

		/**
		 * Configure states.
		 *
		 * @return the state machine state configurer
		 */
		public StateMachineStateConfigurer<S, E> configureStates() {
			return adapter.stateBuilder;
		}

		/**
		 * Configure transitions.
		 *
		 * @return the state machine transition configurer
		 */
		public StateMachineTransitionConfigurer<S, E> configureTransitions() {
			return adapter.transitionBuilder;
		}

		/**
		 * Builds a {@link StateMachine}.
		 *
		 * @return the state machine
		 */
		public StateMachine<S, E> build() {
			try {
				builder.apply(adapter);
				StateMachineConfig<S, E> stateMachineConfig = builder.getOrBuild();

				TransitionsData<S, E> stateMachineTransitions = stateMachineConfig.getTransitions();
				StatesData<S, E> stateMachineStates = stateMachineConfig.getStates();
				ConfigurationData<S, E> stateMachineConfigurationConfig = stateMachineConfig.getStateMachineConfigurationConfig();

				ObjectStateMachineFactory<S, E> stateMachineFactory = null;
				if (stateMachineConfig.getModel() != null && stateMachineConfig.getModel().getFactory() != null) {
					stateMachineFactory = new ObjectStateMachineFactory<S, E>(
							new DefaultStateMachineModel<S, E>(stateMachineConfigurationConfig, null, null),
							stateMachineConfig.getModel().getFactory());
				} else {
					stateMachineFactory = new ObjectStateMachineFactory<S, E>(new DefaultStateMachineModel<S, E>(
							stateMachineConfigurationConfig, stateMachineStates, stateMachineTransitions), null);
				}

				stateMachineFactory.setHandleAutostartup(stateMachineConfigurationConfig.isAutoStart());

				if (stateMachineConfigurationConfig.getBeanFactory() != null) {
					stateMachineFactory.setBeanFactory(stateMachineConfigurationConfig.getBeanFactory());
				}
				if (stateMachineConfigurationConfig.getTaskExecutor() != null) {
					stateMachineFactory.setTaskExecutor(stateMachineConfigurationConfig.getTaskExecutor());
				} else {
					stateMachineFactory.setTaskExecutor(new SyncTaskExecutor());
				}
				if (stateMachineConfigurationConfig.getTaskScheduler() != null) {
					stateMachineFactory.setTaskScheduler(stateMachineConfigurationConfig.getTaskScheduler());
				} else {
					stateMachineFactory.setTaskScheduler(new ConcurrentTaskScheduler());
				}
				return stateMachineFactory.getStateMachine();
			} catch (Exception e) {
				throw new StateMachineException("Error building state machine", e);
			}
		}

	}

	private static class BuilderStateMachineConfigurerAdapter<S extends Object, E extends Object>
			implements StateMachineConfigurer<S, E> {

		private StateMachineModelBuilder<S, E> modelBuilder;
		private StateMachineTransitionBuilder<S, E> transitionBuilder;
		private StateMachineStateBuilder<S, E> stateBuilder;
		private StateMachineConfigurationBuilder<S, E> configurationBuilder;

		BuilderStateMachineConfigurerAdapter() {
			try {
				getStateMachineModelBuilder();
				getStateMachineTransitionBuilder();
				getStateMachineStateBuilder();
				getStateMachineConfigurationBuilder();
			} catch (Exception e) {
				throw new StateMachineException("Error instantiating builder adapter", e);
			}
		}

		@Override
		public void init(StateMachineConfigBuilder<S, E> config) throws Exception {
			config.setSharedObject(StateMachineModelBuilder.class, getStateMachineModelBuilder());
			config.setSharedObject(StateMachineTransitionBuilder.class, getStateMachineTransitionBuilder());
			config.setSharedObject(StateMachineStateBuilder.class, getStateMachineStateBuilder());
			config.setSharedObject(StateMachineConfigurationBuilder.class, getStateMachineConfigurationBuilder());
		}

		@Override
		public void configure(StateMachineConfigBuilder<S, E> builder) throws Exception {
		}

		@Override
		public boolean isAssignable(AnnotationBuilder<StateMachineConfig<S, E>> builder) {
			return false;
		}

		@Override
		public void configure(StateMachineModelConfigurer<S, E> model) throws Exception {
		}

		@Override
		public void configure(StateMachineConfigurationConfigurer<S, E> config) throws Exception {
		}

		@Override
		public void configure(StateMachineStateConfigurer<S, E> states) throws Exception {
		}

		@Override
		public void configure(StateMachineTransitionConfigurer<S, E> transitions) throws Exception {
		}

		protected final StateMachineModelBuilder<S, E> getStateMachineModelBuilder() throws Exception {
			if (modelBuilder != null) {
				return modelBuilder;
			}
			modelBuilder = new StateMachineModelBuilder<S, E>(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR, true);
			configure(modelBuilder);
			return modelBuilder;
		}

		protected final StateMachineTransitionBuilder<S, E> getStateMachineTransitionBuilder() throws Exception {
			if (transitionBuilder != null) {
				return transitionBuilder;
			}
			transitionBuilder = new StateMachineTransitionBuilder<S, E>(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR, true);
			return transitionBuilder;
		}

		protected final StateMachineStateBuilder<S, E> getStateMachineStateBuilder() throws Exception {
			if (stateBuilder != null) {
				return stateBuilder;
			}
			stateBuilder = new StateMachineStateBuilder<S, E>(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR, true);
			return stateBuilder;
		}

		protected final StateMachineConfigurationBuilder<S, E> getStateMachineConfigurationBuilder() throws Exception {
			if (configurationBuilder != null) {
				return configurationBuilder;
			}
			configurationBuilder = new StateMachineConfigurationBuilder<S, E>(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR, true);
			return configurationBuilder;
		}
	}
}
