/**
 * 
 */
package org.zkoss.fsm;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * A Finite State Machine implementation. This state machine is callback based,
 * which differs from the standard FSM from textbook. Easier to use and faster 
 * to develop and debug.
 * @since 6.0.0
 * @author simonpai
 */
public abstract class StateMachine<E, C, IN> {
	
	protected final Map<E, StateCtx<E, C, IN>> _states = 
		new HashMap<E, StateCtx<E, C, IN>>();
	protected E _current;
	protected boolean _run;
	protected int _step;
	protected boolean _debug = false;
	
	/**
	 * Construct a state machine
	 */
	public StateMachine() {
		init();
		reset();
	}
	
	
	
	// system //
	/**
	 * Set debug mode, where {@link #onDebug(String)} is called at certain timing to
	 * assist user develop the state machine
	 */
	public StateMachine<E, C, IN> setDebugMode(boolean mode) {
		_debug = mode;
		return this;
	}
	
	
	
	// definition //
	/**
	 * Set the state by token
	 * @return the state
	 */
	public StateCtx<E, C, IN> setState(E token, StateCtx<E, C, IN> state) {
		if(state == null) throw new IllegalArgumentException(
				"State cannot be null. Use removeState() to remove a state.");
		_states.put(token, state.setMaster(this));
		return state;
	}
	
	/**
	 * Remove the state by token.
	 * @return the removed state
	 */
	public StateCtx<E, C, IN> removeState(E token) {
		return _states.remove(token).setMaster(null);
	}
	
	/**
	 * Get the state by token. Automatically creates one if it does not exist.
	 */
	public StateCtx<E, C, IN> getState(E token) {
		return getState(token, true);
	}
	
	/**
	 * Get the state by token. 
	 * @param autoCreate if true, automatically creates one if it does not exist.
	 */
	public StateCtx<E, C, IN> getState(E token, boolean autoCreate) {
		StateCtx<E, C, IN> result = _states.get(token);
		if(result == null && autoCreate) 
			_states.put(token, result = new StateCtx<E, C, IN>().setMaster(this));
		return result;
	}
	
	/**
	 * Called at the constructor of state machine
	 */
	protected void init() {}
	
	/**
	 * Determines the initial state upon meeting the input character and class
	 */
	protected abstract E getLandingState(IN input, C inputClass);
	
	/**
	 * Determines the class for an input character.
	 */
	protected abstract C getClass(IN input);
	
	
	
	// event handler //
	/**
	 * This method is called at constructor and when reseting the machine.
	 */
	protected void onReset() {}
	
	/**
	 * This method is called when the machine takes the first character.
	 */
	protected void onStart(IN input, C inputClass, E landing) {}
	
	/**
	 * This method is called before executing a step
	 */
	protected void beforeStep(IN input, C inputClass, E origin) {}
	
	/**
	 * This method is called after executing a step
	 */
	protected void afterStep(IN input, C inputClass, E origin, E destination) {}
	
	/**
	 * This method is called when the machine stops
	 * @param endOfInput true if the machine stops due to end of input
	 */
	protected void onStop(boolean endOfInput) {}
	
	/**
	 * This method is called when the machine rejects an input character
	 */
	protected void onReject(IN input) {
		throw new StateMachineException(_step, _current, input);
	}
	
	/**
	 * This method is call at certain situations when debug mode is on.
	 * @see #setDebugMode(boolean)
	 */
	protected void onDebug(String message){}
	
	
	
	// operation //
	/**
	 * Feed the machine a stream of characters
	 */
	public final void run(Iterator<IN> inputs) { // TODO: iterable
		_run = true;
		while(_run && inputs.hasNext())
			run(inputs.next());
		
		boolean endOfInput = !inputs.hasNext();
		onStop(endOfInput);
		if(_current != null)
			getState(_current).onStop(endOfInput);
		doDebug("");
		doDebug("Stop");
		doDebug("");
	}
	
	/**
	 * Feed the machine a single character
	 */
	public final void run(IN input) {
		
		C inputClass = getClass(input);
		
		doDebug("");
		doDebug("Step " + _step);
		doDebug("* Input: " + input + " (" + inputClass + ")");
		
		final E origin = _current;
		E destination = null;
		
		beforeStep(input, inputClass, origin);
		
		if(inputClass == null) {
			doReject(input);
			return;
		}
		if(origin == null){
			destination = getLandingState(input, inputClass); // dest
			if(destination == null) {
				doReject(input);
				return;
			}
			onStart(input, inputClass, destination);
			getState(destination).onLand(input, inputClass, origin);
			
		} else {
			StateCtx<E, C, IN> state = getState(origin);
			
			if(state.isLeaving(input, inputClass)) {
				destination = state.getDestination(input, inputClass); // dest
				if(destination == null) {
					doReject(input);
					return;
				}
				state.onLeave(input, inputClass, destination);
				state.doTransit(input, inputClass);
				getState(destination).onLand(input, inputClass, origin);
				
			} else if(state.isReturning(input, inputClass)) {
				destination = origin; // dest
				state.onReturn(input, inputClass);
				
			} else { // rejected by state
				state.onReject(input, inputClass);
				doReject(input);
				return;
			}
		}
		
		_current = destination;
		
		doDebug("* State: " + origin + " -> " + destination);
		
		afterStep(input, inputClass, origin, destination);
		_step++;
	}
	
	/**
	 * Starts the machine with a stream of input characters.
	 */
	public final void start(Iterator<IN> inputs) { // TODO: take iterable
		reset();
		run(inputs);
	}
	
	/**
	 * Starts the machine with a single input character.
	 */
	public final void start(IN input) {
		reset();
		run(input);
	}
	
	/**
	 * Terminates the machine.
	 */
	public final void terminate() {
		reset();
	}
	
	
	
	// status query //
	// TODO: enhance
	/**
	 * Return the current state
	 */
	public E getCurrentState() {
		return _current;
	}
	
	/**
	 * Return true if the machine is stopped
	 */
	public boolean isTerminated() {
		return !_run && _current == null;
	}
	
	/**
	 * Return true if the machine is suspended
	 */
	public boolean isSuspended() {
		return !_run && _current != null;
	}
	
	
	
	// default internal operation //
	/**
	 * Suspend the machine
	 */
	protected final void suspend() {
		_run = false;
	}
	
	/**
	 * Reject a character
	 */
	protected final void doReject(IN input) {
		_run = false;
		onReject(input);
	}
	
	/**
	 * Manually send a debug message
	 * @see #onDebug(String)
	 * @see #setDebugMode(boolean)
	 */
	protected final void doDebug(String message) {
		if(_debug) onDebug(message);
	}
	
	private final void reset() {
		_current = null;
		_run = false;
		_step = 0;
		doDebug("");
		doDebug("Reset");
		onReset();
	}
	
	/*package*/ final void terminateAt(IN input) {
		getState(_current).onLeave(input, null, null);
		reset();
	}
	
	
	
	// exception //
	public static class StateMachineException extends RuntimeException {
		private static final long serialVersionUID = -6580348498729948101L;
		
		private int _step;
		private Object _state;
		private Object _input;
		
		public StateMachineException(int step, Object state, Object input) {
			this(step, state, input, "Rejected at step " + step + 
					" with current state: " + state + ", input: " + input);
		}
		
		public StateMachineException(int step, Object state, Object input, 
				String message) {
			super(message);
			_step = step;
			_state = state;
			_input = input;
		}
		
		public StateMachineException(String message) {
			super(message);
		}
		
		public int getStep() {
			return _step;
		}
		
		public Object getState() {
			return _state;
		}
		
		public Object getInput() {
			return _input;
		}
	}
	

}
