/*
 * Copyright 2015-2017 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.support;

import java.util.Collections;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.core.OrderComparator;
import org.springframework.messaging.Message;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.event.StateMachineEventPublisher;
import org.springframework.statemachine.listener.CompositeStateMachineListener;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.monitor.CompositeStateMachineMonitor;
import org.springframework.statemachine.processor.StateMachineHandlerCallHelper;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
import org.springframework.util.Assert;

/**
 * Support and helper class for base state machine implementation.
 *
 * @author Janne Valkealahti
 *
 * @param <S> the type of state
 * @param <E> the type of event
 */
public abstract class StateMachineObjectSupport<S, E> extends LifecycleObjectSupport implements BeanNameAware {

	private static final Log log = LogFactory.getLog(StateMachineObjectSupport.class);

	private final CompositeStateMachineListener<S, E> stateListener = new CompositeStateMachineListener<S, E>();
	private final CompositeStateMachineMonitor<S, E> stateMachineMonitor = new CompositeStateMachineMonitor<S, E>();

	/** Context application event publisher if exist */
	private volatile StateMachineEventPublisher stateMachineEventPublisher;

	/** Flag for application context events */
	private boolean contextEventsEnabled = true;

	private final StateMachineInterceptorList<S, E> interceptors = new StateMachineInterceptorList<S, E>();
	private String beanName;
	private volatile boolean handlersInitialized;
	private final StateMachineHandlerCallHelper<S, E> stateMachineHandlerCallHelper = new StateMachineHandlerCallHelper<S, E>();

	@Override
	protected void doStart() {
		super.doStart();
		if (!handlersInitialized) {
			try {
				stateMachineHandlerCallHelper.setBeanFactory(getBeanFactory());
				stateMachineHandlerCallHelper.afterPropertiesSet();
			} catch (Exception e) {
				log.error("Unable to initialize annotation handlers", e);
			} finally {
				handlersInitialized = true;
			}
		}
	}

	@Override
	public void setBeanName(String name) {
		beanName = name;
	}

	/**
	 * Returns a bean name known to context per contract
	 * with {@link BeanNameAware}.
	 *
	 * @return a bean name
	 */
	protected String getBeanName() {
		return beanName;
	}

	/**
	 * Gets the state machine event publisher.
	 *
	 * @return the state machine event publisher
	 */
	protected StateMachineEventPublisher getStateMachineEventPublisher() {
		if(stateMachineEventPublisher == null && getBeanFactory() != null) {
			if(log.isTraceEnabled()) {
				log.trace("getting stateMachineEventPublisher service from bean factory " + getBeanFactory());
			}
			stateMachineEventPublisher = StateMachineContextUtils.getEventPublisher(getBeanFactory());
		}
		return stateMachineEventPublisher;
	}

	/**
	 * Sets the state machine event publisher.
	 *
	 * @param stateMachineEventPublisher the new state machine event publisher
	 */
	public void setStateMachineEventPublisher(StateMachineEventPublisher stateMachineEventPublisher) {
		Assert.notNull(stateMachineEventPublisher, "StateMachineEventPublisher cannot be null");
		this.stateMachineEventPublisher = stateMachineEventPublisher;
	}

	/**
	 * Set if context application events are enabled. Events
	 * are enabled by default. Set this to false if you don't
	 * want state machine to send application context events.
	 *
	 * @param contextEventsEnabled the enabled flag
	 */
	public void setContextEventsEnabled(boolean contextEventsEnabled) {
		this.contextEventsEnabled = contextEventsEnabled;
	}

	protected CompositeStateMachineListener<S, E> getStateListener() {
		return stateListener;
	}

	protected CompositeStateMachineMonitor<S, E> getStateMachineMonitor() {
		return stateMachineMonitor;
	}

	protected void notifyStateChanged(StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnStateChanged(getBeanName(), stateContext);
			stateMachineHandlerCallHelper.callOnStateChanged(stateContext.getStateMachine().getId(), stateContext);
			stateListener.stateChanged(stateContext.getSource(), stateContext.getTarget());
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishStateChanged(this, stateContext.getSource(), stateContext.getTarget());
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyStateChanged", e);
		}
	}

	protected void notifyStateEntered(StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnStateEntry(getBeanName(), stateContext);
			stateMachineHandlerCallHelper.callOnStateEntry(stateContext.getStateMachine().getId(), stateContext);
			stateListener.stateEntered(stateContext.getTarget());
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishStateEntered(this, stateContext.getTarget());
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyStateEntered", e);
		}
	}

	protected void notifyStateExited(StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnStateExit(getBeanName(), stateContext);
			stateMachineHandlerCallHelper.callOnStateExit(stateContext.getStateMachine().getId(), stateContext);
			stateListener.stateExited(stateContext.getSource());
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishStateExited(this, stateContext.getSource());
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyStateExited", e);
		}
	}

	protected void notifyEventNotAccepted(StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnEventNotAccepted(getBeanName(), stateContext);
			stateMachineHandlerCallHelper.callOnEventNotAccepted(stateContext.getStateMachine().getId(), stateContext);
			stateListener.eventNotAccepted(stateContext.getMessage());
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishEventNotAccepted(this, stateContext.getMessage());
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyEventNotAccepted", e);
		}
	}

	protected void notifyTransitionStart(StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnTransitionStart(getBeanName(), stateContext);
			stateMachineHandlerCallHelper.callOnTransitionStart(stateContext.getStateMachine().getId(), stateContext);
			stateListener.transitionStarted(stateContext.getTransition());
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishTransitionStart(this, stateContext.getTransition());
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyTransitionStart", e);
		}
	}

	protected void notifyTransition(StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnTransition(getBeanName(), stateContext);
			stateMachineHandlerCallHelper.callOnTransition(stateContext.getStateMachine().getId(), stateContext);
			stateListener.transition(stateContext.getTransition());
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishTransition(this, stateContext.getTransition());
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyTransition", e);
		}
	}

	protected void notifyTransitionEnd(StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnTransitionEnd(getBeanName(), stateContext);
			stateMachineHandlerCallHelper.callOnTransitionEnd(stateContext.getStateMachine().getId(), stateContext);
			stateListener.transitionEnded(stateContext.getTransition());
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishTransitionEnd(this, stateContext.getTransition());
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyTransitionEnd", e);
		}
	}

	protected void notifyStateMachineStarted(StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnStateMachineStart(getBeanName(), stateContext);
			stateMachineHandlerCallHelper.callOnStateMachineStart(stateContext.getStateMachine().getId(), stateContext);
			stateListener.stateMachineStarted(stateContext.getStateMachine());
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishStateMachineStart(this, stateContext.getStateMachine());
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyStateMachineStarted", e);
		}
	}

	protected void notifyStateMachineStopped(StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnStateMachineStop(getBeanName(), stateContext);
			stateMachineHandlerCallHelper.callOnStateMachineStop(stateContext.getStateMachine().getId(), stateContext);
			stateListener.stateMachineStopped(stateContext.getStateMachine());
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishStateMachineStop(this, stateContext.getStateMachine());
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyStateMachineStopped", e);
		}
	}

	protected void notifyStateMachineError(StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnStateMachineError(getBeanName(), stateContext);
			stateMachineHandlerCallHelper.callOnStateMachineError(stateContext.getStateMachine().getId(), stateContext);
			stateListener.stateMachineError(stateContext.getStateMachine(), stateContext.getException());
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishStateMachineError(this, stateContext.getStateMachine(), stateContext.getException());
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyStateMachineError", e);
		}
	}

	protected void notifyExtendedStateChanged(Object key, Object value, StateContext<S, E> stateContext) {
		try {
			stateMachineHandlerCallHelper.callOnExtendedStateChanged(getBeanName(), key, value, stateContext);
			stateMachineHandlerCallHelper.callOnExtendedStateChanged(stateContext.getStateMachine().getId(), key, value, stateContext);
			stateListener.extendedStateChanged(key, value);
			stateListener.stateContext(stateContext);
			if (contextEventsEnabled) {
				StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
				if (eventPublisher != null) {
					eventPublisher.publishExtendedStateChanged(this, key, value);
				}
			}
		} catch (Throwable e) {
			log.warn("Error during notifyExtendedStateChanged", e);
		}
	}

	protected void notifyTransitionMonitor(StateMachine<S, E> stateMachine, Transition<S, E> transition, long duration) {
		try {
			stateMachineMonitor.transition(stateMachine, transition, duration);
		} catch (Exception e) {
			log.warn("Error during notifyTransitionMonitor", e);
		}
	}

	protected void notifyActionMonitor(StateMachine<S, E> stateMachine, Action<S, E> action, long duration) {
		try {
			stateMachineMonitor.action(stateMachine, action, duration);
		} catch (Exception e) {
			log.warn("Error during notifyTransitionMonitor", e);
		}
	}

	protected void stateChangedInRelay() {
		// TODO: this is a temporary tweak to know when state is
		//       changed in a submachine/regions order to give
		//       state machine a change to request executor login again
		//       which is needed when we use multiple thread. with multiple
		//       threads submachines may do their stuff after thread handling
		//       main machine has already finished its execution logic, thus
		//       re-scheduling is needed.
	}

	protected StateMachineInterceptorList<S, E> getStateMachineInterceptors() {
		return interceptors;
	}

	protected void setStateMachineInterceptors(List<StateMachineInterceptor<S,E>> interceptors) {
		Collections.sort(interceptors, new OrderComparator());
		this.interceptors.set(interceptors);
	}

	/**
	 * This class is used to relay listener events from a submachines which works
	 * as its own listener context. User only connects to main root machine and
	 * expects to get events for all machines from there.
	 */
	protected class StateMachineListenerRelay implements StateMachineListener<S,E> {

		@Override
		public void stateChanged(State<S, E> from, State<S, E> to) {
			stateListener.stateChanged(from, to);
			stateChangedInRelay();
		}

		@Override
		public void stateEntered(State<S, E> state) {
			stateListener.stateEntered(state);
		}

		@Override
		public void stateExited(State<S, E> state) {
			stateListener.stateExited(state);
		}

		@Override
		public void eventNotAccepted(Message<E> event) {
			stateListener.eventNotAccepted(event);
		}

		@Override
		public void transition(Transition<S, E> transition) {
			stateListener.transition(transition);
		}

		@Override
		public void transitionStarted(Transition<S, E> transition) {
			stateListener.transitionStarted(transition);
		}

		@Override
		public void transitionEnded(Transition<S, E> transition) {
			stateListener.transitionEnded(transition);
		}

		@Override
		public void stateMachineStarted(StateMachine<S, E> stateMachine) {
			stateListener.stateMachineStarted(stateMachine);
		}

		@Override
		public void stateMachineStopped(StateMachine<S, E> stateMachine) {
			stateListener.stateMachineStopped(stateMachine);
		}

		@Override
		public void stateMachineError(StateMachine<S, E> stateMachine, Exception exception) {
			stateListener.stateMachineError(stateMachine, exception);
		}

		@Override
		public void extendedStateChanged(Object key, Object value) {
			stateListener.extendedStateChanged(key, value);
		}

		@Override
		public void stateContext(StateContext<S, E> stateContext) {
			stateListener.stateContext(stateContext);
		}

	}

}
