/*
  This file is a part of Angry IP Scanner source code,
  see http://www.angryip.org/ for more information.
  Licensed under GPLv2.
 */
package net.azib.ipscan.core.state;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Generic StateMachine implementation.
 * It holds the current state and performs transitions with corresponding methods.
 * <p/>
 * Note: the class is abstract because notification of listeners often should happen in the correct thread,
 * so subclasses should provide this functionality.
 *
 * @author Anton Keks
 */
public abstract class StateMachine {
	
	public enum Transition {INIT, START, STOP, NEXT, COMPLETE, RESET, RESCAN, CONTINUE}
	
	private volatile ScanningState state = ScanningState.IDLE;
	
	private ReentrantReadWriteLock listenersLock = new ReentrantReadWriteLock();
	private List<StateTransitionListener> transitionListeners = new ArrayList<>();
	
	/**
	 * @return true if current state is as specified
	 */
	public boolean inState(ScanningState state) {
		return this.state == state;
	}
	
	/**
	 * @return current state
	 */
	public ScanningState getState() {
		return state;
	}

	/**
	 * Registers state transition listener.
	 */
	public void addTransitionListener(StateTransitionListener listener) {
		try {
			listenersLock.writeLock().lock();
			transitionListeners.add(listener);
		}
		finally {
			listenersLock.writeLock().unlock();
		}
	}
	
	/**
	 * Unregisters the listener
	 */
	public void removeTransitionListener(StateTransitionListener listener) {
		try {
			listenersLock.writeLock().lock();
			transitionListeners.remove(listener);
		}
		finally {
			listenersLock.writeLock().unlock();
		}
	}

	/**
	 * Transitions to the specified state, notifying all listeners.
	 * Note: this method is intentionally not public, use specific methods to make desired transitions.
	 */
	void transitionTo(ScanningState newState, Transition transition) {
		if (state != newState) {
			state = newState;
			notifyAboutTransition(transition);
		}
	}

	protected void notifyAboutTransition(Transition transition) {		
		try {
			listenersLock.readLock().lock();
			for (var listener : transitionListeners) {
				listener.transitionTo(state, transition);
			}			
		}
		finally {
			listenersLock.readLock().unlock();
		}
	}

	/**
	 * Transitions to the next state in the sequence.
	 * Called when user presses the scan button.
	 */
	public void transitionToNext() {
		// killing state cannot be transitioned from by pressing a button
		if (state != ScanningState.KILLING) {
			transitionTo(state.next(), Transition.NEXT);
		}
	}

	/**
	 * Transitions to the stopping state
	 */
	public void stop() {
		if (state == ScanningState.SCANNING) {
			transitionTo(ScanningState.STOPPING, Transition.STOP);
		}
		else if (state == ScanningState.STOPPING) {
			// notify anyway to ensure that manual stopping and automatic stopping work well together
			notifyAboutTransition(Transition.STOP);
		}
		else {
			throw new IllegalStateException("Attempt to stop from " + state);
		}
	}

	/**
	 * Transitions back to the idle state
	 */
	public void complete() {
		if (state == ScanningState.STOPPING || state == ScanningState.KILLING) {
			transitionTo(ScanningState.IDLE, Transition.COMPLETE);
		}		
		else {
			throw new IllegalStateException("Attempt to complete from " + state);
		}
	}

	/**
	 * Transitions to the RESTARTING state in order to rescan previously scanned results.
	 */
	public void rescan() {
		if (state == ScanningState.IDLE) {
			transitionTo(ScanningState.RESTARTING, Transition.RESCAN);
		}
		else {
			throw new IllegalStateException("Attempt to rescan from " + state);
		}
	}

	/**
	 * Starts the scanning process
	 */
	public void startScanning() {
		if (state == ScanningState.STARTING || state == ScanningState.RESTARTING) {
			transitionTo(ScanningState.SCANNING, Transition.START);
		}
		else {
			throw new IllegalStateException("Attempt to go scanning from " + state);
		}
	}

	/**
	 * Continues previously aborted scanning process
	 */
	public void continueScanning() {
		if (state == ScanningState.IDLE) {
			transitionTo(ScanningState.STARTING, Transition.CONTINUE);
		}
		else {
			throw new IllegalStateException("Attempt to continue scanning from " + state);
		}
	}

	/**
	 * Inits everyone on startup
	 */
	public void init() {
		state = ScanningState.IDLE;
		notifyAboutTransition(Transition.INIT);
	}

	/**
	 * Resets the machine to the initial state
	 */
	public void reset() {
		// no transition notifications
		state = ScanningState.IDLE;
	}

}
