/*
 * Copyright 2006-2019 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.core.utils.incubator;

import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.toolkit.modules.concurrency.api.AsyncCallbackExceptionPolicy;
import de.rcenvironment.toolkit.modules.concurrency.api.AsyncOrderedExecutionQueue;

/**
 * An abstract base class to simplify the creation of state machine implementations.
 * 
 * @param <S> the Enum defining the states of this state machine
 * @param <E> the type representing events posted to this state maching
 * @author Robert Mischke
 */
public abstract class AbstractStateMachine<S extends Enum<?>, E> {

    private S currentState;

    private AsyncOrderedExecutionQueue eventQueue;

    public AbstractStateMachine(S initialState) {
        this.currentState = initialState;
        this.eventQueue = ConcurrencyUtils.getFactory().createAsyncOrderedExecutionQueue(AsyncCallbackExceptionPolicy.LOG_AND_PROCEED);
    }

    /**
     * Asynchronously post an event to the state machine. Events are processed in order, and each event may or may not change the state
     * machine's state.
     * 
     * @param event the event to post, which the state machine's logic will process asynchronously
     */
    public void postEvent(final E event) {
        eventQueue.enqueue(new Runnable() {

            @Override
            public void run() {
                processEventInternal(event);
            }
        });
    }

    /**
     * Triggers a state transition. Setting the new state is always a blocking operation. Whether the actions caused by the state change are
     * blocking or asynchronous is left to the subclasses' implementations.
     * 
     * @param newState the state this state machine should transition to
     * @throws StateChangeException if the state change failed; the internal state will be unchanged
     */
    private synchronized void processEventInternal(E event) {
        try {
            S newState = processEvent(currentState, event);
            if (newState == null || newState == currentState) {
                // no state change requested; done
                return;
            }
            try {
                checkProposedStateChange(currentState, newState);
            } catch (StateChangeException e) {
                // re-wrap to add old and new state information
                throw new StateChangeException(e, currentState, newState);
            }
            S oldState = currentState;
            currentState = newState;
            // internal state change handler first...
            onStateChanged(oldState, newState);
        } catch (StateChangeException e) {
            onStateChangeException(event, e);
        }
    }

    public synchronized S getState() {
        return currentState;
    }

    protected abstract S processEvent(S oldState, E event) throws StateChangeException;

    protected void checkProposedStateChange(S oldState, S newState) throws StateChangeException {}

    protected void onStateChanged(S oldState, S newState) {}

    protected abstract void onStateChangeException(E event, StateChangeException e);

}
