pluf/workflow

View on GitHub
src/Imp/StateMachineImpl.php

Summary

Maintainability
F
3 days
Test Coverage
<?php
namespace Pluf\Workflow\Imp;

use Pluf\Workflow\ActionExecutionService;
use Pluf\Workflow\ErrorCodes;
use Pluf\Workflow\ImmutableLinkedState;
use Pluf\Workflow\ImmutableState;
use Pluf\Workflow\StateContext;
use Pluf\Workflow\StateMachine;
use Pluf\Workflow\StateMachineConfiguration;
use Pluf\Workflow\StateMachineContext;
use Pluf\Workflow\StateMachineData;
use Pluf\Workflow\StateMachineDataReader;
use Pluf\Workflow\StateMachineDataWriter;
use Pluf\Workflow\Visitor;
use Pluf\Workflow\Exceptions\IllegalStateException;
use Pluf\Workflow\Exceptions\TransitionException;
use Pluf\Workflow\IO\SCXMLVisitor;
use Pluf\Workflow\Imp\Events\StartEventImpl;
use Pluf\Workflow\Imp\Events\TerminateEventImpl;
use Pluf\Workflow\Imp\Events\TransitionBeginEventImpl;
use Pluf\Workflow\Imp\Events\TransitionCompleteEventImpl;
use Pluf\Workflow\Imp\Events\TransitionDeclinedEventImpl;
use Pluf\Workflow\Imp\Events\TransitionEndEventImpl;
use Pluf\Workflow\Imp\Events\TransitionExceptionEventImpl;
use Throwable;

/**
 * The Abstract state machine provide several extension ability to cover different extension granularity.
 *
 * <ol>
 * <li>Method <b>beforeStateExit</b>/<b>afterStateEntry</b> is used to add custom logic on all kinds of state exit/entry.</li>
 * <li>Method <b>exit[stateName]</b>/<b>entry[stateName]</b> is extension method which is used to add custom logic on specific state.</li>
 * <li>Method <b>beforeTransitionBegin</b>/<b>afterTransitionComplete</b> is used to add custom logic on all kinds of transition
 * accepted all conditions.</li>
 * <li>Method <b>transitFrom[fromStateName]To[toStateName]On[eventName]</b> is used to add custom logic on specific transition
 * accepted all conditions.</li>
 * <li>Method <b>transitFromAnyTo[toStateName]On[eventName]</b> is used to add custom logic on any state transfer to specific target
 * state on specific event happens, so as the <b>transitFrom[fromStateName]ToAnyOn[eventName]</b>, <b>transitFrom[fromState]To[ToStateName]</b>,
 * and <b>on[EventName]</b>.</li>
 * </ol>
 */
class StateMachineImpl implements StateMachine
{
    use AssertTrait;
    use EventHandlerTrait;

    private ?ActionExecutionService $executor = null;

    private ?StateMachineData $data = null;

    private $implementation;

    private string $status = 'INITIALIZED';

    private ?QueuedEvents $queuedEvents;

    // LinkedBlockingDeque
    private ?QueuedEvents $queuedTestEvents;

    // LinkedBlockingDeque
    private bool $processingTestEvent = false;

    private $startEvent, $finishEvent, $terminateEvent;

    // MvelScriptManager
    private $scriptManager;

    // state machine options
    private bool $autoStartEnabled = true;

    private bool $autoTerminateEnabled = true;

    private bool $delegatorModeEnabled = false;

    private int $transitionTimeout = - 1;

    private bool $dataIsolateEnabled = false;

    private bool $debugModeEnabled = false;

    private bool $remoteMonitorEnabled = false;

    private array $extraParamTypes = [];

    private $lastException = null;

    private bool $entryPoint = false;

    // TransitionException
    public function __construct($initialStateId, $states, StateMachineConfiguration $configuration)
    {
        $this->data = FSM::newStateMachineData($states);
        $this->data->write()->setIdentifier($configuration->getIdProvider()
            ->get());
        $this->data->write()->setInitialState($initialStateId);
        $this->data->write()->setCurrentState(null);

        // retrieve options value from state machine configuration
        $this->autoStartEnabled = $configuration->isAutoStartEnabled();
        $this->autoTerminateEnabled = $configuration->isAutoTerminateEnabled();
        $this->dataIsolateEnabled = $configuration->isDataIsolateEnabled();
        $this->debugModeEnabled = $configuration->isDebugModeEnabled();
        $this->delegatorModeEnabled = $configuration->isDelegatorModeEnabled();

        $this->queuedEvents = new QueuedEvents();
        $this->queuedTestEvents = new QueuedEvents();
    }

    private function processEvent($event, $context, StateMachineData $originalData, ExecutionServiceImpl $executionService, bool $DataIsolateEnabled): bool
    {
        $localData = $originalData;
        $fromState = $localData->read()->getCurrentRawState();
        $fromStateId = $fromState->getStateId();
        $toStateId = null;
        try {
            // TODO: maso, useing named method insted of events (remove this one)
            $this->beforeTransitionBegin($fromStateId, $event, $context);

            if ($this->dataIsolateEnabled) {
                // use local data to isolation transition data write
                $localData = FSM::newStateMachineData($originalData->read()->originalStates());
                $localData->dump($originalData->read());
                // XXX: must use in container
            }

            $result = FSM::newResult(false, $fromState, null);
            $stateContext = FSM::newStateContext($this, $localData, $fromState, $event, $context, $result, $executionService);
            $fromState->internalFire($stateContext);
            $toStateId = $result->getTargetState()->getStateId();

            if ($result->isAccepted()) {
                $executionService->execute();
                $localData->write()->setLastState($fromStateId);
                $localData->write()->setCurrentState($toStateId);
                if ($this->dataIsolateEnabled) {
                    // import local data after transition accepted
                    $originalData->dump($localData->read());
                }
                $this->fire('transitionComplete', new TransitionCompleteEventImpl($fromStateId, $toStateId, $event, $context, $this));
                $this->afterTransitionCompleted($fromStateId, $this->getCurrentState(), $event, $context);
                return true;
            } else {
                $this->fire('TransitionDeclined', new TransitionDeclinedEventImpl($fromStateId, $event, $context, $this));
                $this->afterTransitionDeclined($fromStateId, $event, $context);
            }
        } catch (Throwable $e) {
            // set state machine in error status first which means state machine cannot process event anymore
            // unless this exception has been resolved and state machine status set back to normal again.
            $this->setStatus('ERROR');
            // wrap any exception into transition exception
            $this->lastException = ($e instanceof TransitionException) ? $e : new TransitionException('Fail to execute the action', ErrorCodes::FSM_TRANSITION_ERROR, $e, $fromStateId, $toStateId, $event, $context, 'UNKNOWN');
            $this->fire('transitionException', new TransitionExceptionEventImpl($this->lastException, $fromStateId, $localData->read()
                ->getCurrentState(), $event, $context, $this));
            $this->afterTransitionCausedException($fromStateId, $toStateId, $event, $context);
        } finally {
            $executionService->reset();
            $this->fire('transitionEnd', new TransitionEndEventImpl($fromStateId, $toStateId, $event, $context, $this));
            $this->afterTransitionEnd($fromStateId, $this->getCurrentState(), $event, $context);
        }
        return false;
    }

    private function processEvents(): void
    {
        if ($this->isIdle()) {
            $this->setStatus('BUSY');
            try {
                $eventInfo = null;
                $event = null;
                $context = null;
                while (($eventInfo = $this->queuedEvents->poll()) != null) {
                    // TODO: response to cancel operation
                    $event = $eventInfo->first;
                    $context = $eventInfo->second;
                    $this->processEvent($event, $context, $this->data, $this->executor, $this->dataIsolateEnabled);
                }
                $rawState = $this->data->read()->getCurrentRawState();
                if ($this->autoTerminateEnabled && $rawState->isRootState() && $rawState->isFinalState()) {
                    $this->terminate($context);
                }
            } finally {
                if ($this->getStatus() == 'BUSY') {
                    $this->setStatus('IDLE');
                }
            }
        }
    }

    private function internalFire($event, $context, bool $insertAtFirst = false): void
    {
        if ($this->getStatus() == 'INITIALIZED') {
            if ($this->autoStartEnabled) {
                $this->start($context);
            } else {
                throw new IllegalStateException("The state machine is not running.");
            }
        }
        if ($this->getStatus() == 'TERMINATED') {
            throw new IllegalStateException("The state machine is already terminated.");
        }
        if ($this->getStatus() == 'ERROR') {
            throw new IllegalStateException("The state machine is corruptted.");
        }
        if ($insertAtFirst) {
            $this->queuedEvents->addFirst(new EventPair($event, $context));
        } else {
            $this->queuedEvents->addLast(new EventPair($event, $context));
        }
        $this->processEvents();
    }

    public function isEntryPoint(): bool
    {
        return $this->entryPoint;
    }

    /**
     * This is an entry point
     *
     * @param bool $entryPoint
     * @return self
     */
    public function setEntryPoint(bool $entryPoint = true): self
    {
        $this->entryPoint = $entryPoint;
        return $this;
    }

    /**
     * Clean all queued events
     */
    protected function cleanQueuedEvents()
    {
        $this->queuedEvents->clear();
    }

    public function fireEvent($event, $context = null, bool $insertAtFirst = false): self
    {
        $isEntryPoint = $this->isEntryPoint();
        if ($isEntryPoint) {
            StateMachineContext::set($this);
        } else if ($this->delegatorModeEnabled && StateMachineContext::currentInstance() != $this) {
            $currentInstance = StateMachineContext::currentInstance();
            $currentInstance->fire($event, $context);
            return $this;
        }
        try {
            if (StateMachineContext::isTestEvent()) {
                $this->internalTest($event, $context);
            } else {
                $this->internalFire($event, $context, $insertAtFirst);
            }
        } finally {
            if ($isEntryPoint) {
                StateMachineContext::set(null);
            }
        }
        return $this;
    }

    /**
     *
     * @deprecated No need in php
     * @param mixed $event
     * @param mixed $context
     */
    public function untypedFire($event, $context)
    {
        $this->fireEvent($event, $context);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::fireImmediate()
     */
    public function fireImmediate($event, $context): self
    {
        return $this->fireEvent($event, $context, true);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::isRemoteMonitorEnabled()
     */
    public function isRemoteMonitorEnabled(): bool
    {
        return $this->remoteMonitorEnabled;
    }

    private function internalTest($event, $context)
    {
        $this->checkState($this->status != 'ERROR' && $this->status != 'TERMINATED', "Cannot test state machine under " . $this->status . " status.");

        $testResult = null;
        $this->queuedTestEvents->add(new EventPair($event, $context));
        if (! isProcessingTestEvent) {
            $this->processingTestEvent = true;
            $cloneData = $this->dumpSavedData();
            $dummyExecutor = $this->getDummyExecutor();

            if ($this->getStatus() == 'INITIALIZED') {
                if ($this->autoStartEnabled) {
                    $this->internalStart($context, $cloneData, $dummyExecutor);
                } else {
                    throw new IllegalStateException("The state machine is not running.");
                }
            }
            try {
                $eventInfo = null;
                while (($eventInfo = $this->queuedTestEvents->poll()) != null) {
                    $testEvent = $eventInfo->first();
                    $testContext = $eventInfo->second();
                    $this->processEvent($testEvent, $testContext, $cloneData, $dummyExecutor, false);
                }
                $testResult = $this->resolveState($cloneData->read()
                    ->currentState(), $cloneData);
            } finally {
                $this->processingTestEvent = false;
            }
        }
        return $testResult;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::test()
     */
    public function test($event, $context)
    {
        $isEntryPoint = $this->isEntryPoint();
        if ($isEntryPoint) {
            StateMachineContext::set($this, true);
        }
        try {
            return $this->internalTest(event, context);
        } finally {
            if ($isEntryPoint) {
                StateMachineContext::set(null);
            }
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::canAccept()
     */
    public function canAccept($event): bool
    {
        $testRawState = $this->getCurrentRawState();
        if ($testRawState == null) {
            if ($this->autoStartEnabled) {
                $testRawState = $this->getInitialRawState();
            } else {
                return false;
            }
        }
        return array_key_exists($event, $testRawState->getAcceptableEvents());
    }

    /**
     * Checks if the statemachin is in Idle state
     *
     * @return bool
     */
    protected function isIdle(): bool
    {
        return $this->getStatus() != 'BUSY';
    }

    protected function afterTransitionCausedException($from, $to, $event, $context)
    {
        $le = $this->getLastException();
        // if ($le->getTargetException() != null) {
        // $this->logger->error("Transition caused exception", $le->getTargetException());
        // }
        throw $le;
    }

    protected function beforeTransitionBegin($from, $event, $context): void
    {
        // TODO:

        // +
        $this->fire('transitionBegin', new TransitionBeginEventImpl($from, $event, $context, $this));
    }

    protected function afterTransitionCompleted($from, $to, $event, $ontext): void
    {
        // TODO: call registerd callables
    }

    protected function afterTransitionEnd($from, $to, $event, $context): void
    {
        // TODO: call registerd callables
    }

    protected function afterTransitionDeclined($from, $event, $context): void
    {
        // TODO: call registerd callables
    }

    protected function beforeActionInvoked($from, $to, $event, $context): void
    {
        // TODO: call registerd callables
    }

    protected function afterActionInvoked($from, $to, $event, $context): void
    {
        // TODO: call registerd callables
    }

    private function resolveRawState(ImmutableState $rawState): ImmutableState
    {
        if ($rawState instanceof ImmutableLinkedState) {
            return $rawState->getLinkedStateMachine($this)->getCurrentRawState();
        }
        return $rawState;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getCurrentRawState()
     */
    public function getCurrentRawState(): ImmutableState
    {
        $rawState = $this->data->read()->getCurrentRawState();
        return $this->resolveRawState($rawState);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getLastRawState()
     */
    public function getLastRawState(): ImmutableState
    {
        $lastRawState = $this->data->read()->getLastRawState();
        return $this->resolveRawState($lastRawState);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getInitialRawState()
     */
    public function getInitialRawState(): ImmutableState
    {
        return $this->getRawStateFrom($this->getInitialState());
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getRawStateFrom()
     */
    public function getRawStateFrom($stateId): ImmutableState
    {
        return $this->data->read()->getRawStateFrom($stateId);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getAllRawStates()
     */
    public function getAllRawStates(): array
    {
        return $this->data->read()->getRawStates();
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getAllStates()
     */
    public function getAllStates(): array
    {
        return $this->data->read()->getStates();
    }

    /**
     * Gets state from the local data
     *
     * @param mixed $state
     * @param mixed $localData
     * @return string
     */
    private function resolveState($state, $localData)
    {
        $resolvedState = $state;
        $rawState = $localData->read()->getRawStateFrom($resolvedState);
        if ($rawState instanceof ImmutableLinkedState) {
            $resolvedState = $rawState->getLinkedStateMachine($this)->getCurrentState();
        }
        return $resolvedState;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getCurrentState()
     */
    public function getCurrentState()
    {
        return $this->resolveState($this->data->read()
            ->getCurrentState(), $this->data);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getLastState()
     */
    public function getLastState()
    {
        return $this->resolveState($this->data->read()
            ->getLastState(), $this->data);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getInitialState()
     */
    public function getInitialState()
    {
        return $this->data->read()->getInitialState();
    }

    /**
     * Finds the state and all other entries
     *
     * A state is entry if it is the parent of the current entry state
     *
     * @param ImmutableState $origin
     * @param StateContext $stateContext
     */
    private function entryAll(ImmutableState $origin, ?StateContext $stateContext)
    {
        $stack = [];

        $state = $origin;
        while ($state != null) {
            array_push($stack, $state);
            $state = $state->getParentState();
        }
        while (! empty($stack)) {
            $state = array_pop($stack);
            $state->entry($stateContext);
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::start()
     */
    public function start($context = null): self
    {
        if (! $this->isStarted()) {
            $this->setStatus('BUSY');
            $this->internalStart($context, $this->data, $this->executor);
            $this->setStatus('IDLE');
            $this->processEvents();
        }
        return $this;
    }

    private function internalStart($context, StateMachineData $localData, ActionExecutionService $executionService)
    {
        $initialRawState = $localData->read()->getInitialRawState();
        $stateContext = FSM::newStateContext($this, $localData, $initialRawState, $this->getStartEvent(), $context, null, $executionService);

        $this->entryAll($initialRawState, $stateContext);
        $historyState = $initialRawState->enterByHistory($stateContext);
        $executionService->execute();
        $localData->write()->setCurrentState($historyState->getStateId());
        $localData->write()->setStartContext($context);
        $this->fire('start', new StartEventImpl($this));
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::isStarted()
     */
    public function isStarted(): bool
    {
        return $this->getStatus() == 'IDLE' || $this->getStatus() == 'BUSY';
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::isTerminated()
     */
    public function isTerminated(): bool
    {
        return $this->getStatus() == 'TERMINATED';
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::isError()
     */
    public function isError(): bool
    {
        return $this->getStatus() == 'ERROR';
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getStatus()
     */
    public function getStatus(): string
    {
        return $this->status;
    }

    /**
     * Sets the status of the machine
     *
     * @param string $status
     */
    protected function setStatus(string $status)
    {
        $this->status = $status;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getLastActiveChildStateOf()
     */
    public function getLastActiveChildStateOf($parentStateId)
    {
        return $this->data->read()->lastActiveChildStateOf($parentStateId);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getSubStatesOn()
     */
    public function getSubStatesOn($parentStateId): array
    {
        return $this->data->read()->subStatesOn($parentStateId);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::terminate()
     */
    public function terminate($context = null): void
    {
        if ($this->isTerminated()) {
            return;
        }

        $stateContext = FSM::newStateContext($this, $this->data, $this->data->read()->getCurrentRawState(), $this->getTerminateEvent(), $context, null, $this->executor);
        $this->exitAll($this->data->read()
            ->getCurrentRawState(), $stateContext);
        $this->executor->execute();

        $this->setStatus('TERMINATED');
        $this->fire('terminate', new TerminateEventImpl($this));
    }

    private function exitAll(ImmutableState $current, $stateContext)
    {
        $state = $current;
        while ($state != null) {
            $state->exit($stateContext);
            $state = $state->getParentState();
        }
    }

    public function accept(Visitor $visitor): void
    {
        $visitor->visitOnEntry($this);
        forEach ($this->getAllRawStates() as $state) {
            if ($state->getParentState() == null) {
                $state->accept(visitor);
            }
        }
        $visitor->visitOnExit($this);
    }

    /**
     * Set type of state machine
     *
     * State machine type is a class that implements all functional parts of FSM.
     *
     * @param string $stateMachineType
     *            to use
     */
    public function setTypeOfStateMachine($stateMachineType): self
    {
        $this->data->write()->setTypeOfStateMachine($stateMachineType);
        return $this;
    }

    /**
     * Sets type of states.
     *
     * @param string $stateType
     * @return self
     */
    public function setTypeOfState(string $stateType): self
    {
        $this->data->write()->setTypeOfState($stateType);
        return $this;
    }

    /**
     * Sets type of events
     *
     * @param string $eventType
     * @return self
     */
    public function setTypeOfEvent(string $eventType): self
    {
        $this->data->write()->setTypeOfEvent($eventType);
        return $this;
    }

    /**
     * Sets type of context
     *
     * @deprecated context must pass throw the DI
     * @param string $contextType
     * @return self
     */
    public function setTypeOfContext(?string $contextType): self
    {
        $this->data->write()->setTypeOfContext($contextType);
        return $this;
    }

    /**
     * Sets script manager
     *
     * @param mixed $scriptManager
     * @return self
     */
    public function setScriptManager($scriptManager): self
    {
        $this->assertEmpty($this->scriptManager);
        $this->scriptManager = $scriptManager;
        return $this;
    }

    /**
     * Sets start event
     *
     * @param mixed $startEvent
     * @return self
     */
    public function setStartEvent($startEvent): self
    {
        $this->assertEmpty($this->startEvent);
        $this->startEvent = $startEvent;
        return $this;
    }

    function getStartEvent()
    {
        return $this->startEvent;
    }

    /**
     * Sets termination event
     *
     * @param mixed $terminateEvent
     * @return self
     */
    public function setTerminateEvent($terminateEvent): self
    {
        $this->assertEmpty($this->terminateEvent);
        $this->terminateEvent = $terminateEvent;
        return $this;
    }

    function getTerminateEvent()
    {
        return $this->terminateEvent;
    }

    /**
     * Sets finis events
     *
     * @param mixed $finishEvent
     * @return self
     */
    public function setFinishEvent($finishEvent): self
    {
        $this->assertEmpty($this->finishEvent);
        $this->finishEvent = $finishEvent;
        return $this;
    }

    function getFinishEvent()
    {
        return $this->finishEvent;
    }

    /**
     *
     * @deprecated not case in PHP
     * @param mixed $extraParamTypes
     */
    function setExtraParamTypes($extraParamTypes): self
    {
        $this->assertEmpty($this->extraParamTypes);
        $this->extraParamTypes = $extraParamTypes;
        return $this;
    }

    public function isContextSensitive(): bool
    {
        return true;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::typeOfContext()
     */
    public function typeOfContext(): string
    {
        return $this->data->read()->typeOfContext();
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::typeOfEvent()
     */
    public function typeOfEvent(): string
    {
        return $this->data->read()->typeOfEvent();
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::typeOfState()
     */
    public function typeOfState(): string
    {
        return $this->data->read()->typeOfState();
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getLastException()
     */
    public function getLastException(): TransitionException
    {
        return $this->lastException;
    }

    protected function setLastException(TransitionException $lastException)
    {
        $this->lastException = $lastException;
    }

    /**
     * Internal use only
     *
     * @return int size of exector
     */
    public function getExecutorListenerSize(): int
    {
        return $this->executor->getListenerSize();
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getIdentifier()
     */
    public function getIdentifier(): string
    {
        return $this->data->read()->identifier();
    }

    /**
     * Sets the action executer
     *
     * @param ActionExecutionService $actionExecutionService
     * @return self
     */
    public function setActionExecutionService(ActionExecutionService $actionExecutionService): self
    {
        $this->assertEmpty($this->executor, 'Trying to set executor twic');
        $this->executor = $actionExecutionService;
        return $this;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getImplementation()
     */
    public function getImplementation()
    {
        if (! isset($this->implementation)) {
            // New instance
            $type = $this->data->read()->getTypeOfStateMachine();
            // TODO: use invoker to instance the state machine
            $this->implementation = new $type();
        }
        return $this->implementation;
    }

    /**
     * Sets state machine implementation
     *
     * @param mixed $stateMachineImplementation
     */
    public function setImplementation($stateMachineImplementation)
    {
        $this->assertEmpty($this->implementation, 'Trying to set implimentation twic');
        $this->implementation = $stateMachineImplementation;
    }

    // private interface DeclarativeListener {
    // Object getListenTarget();
    // }

    // private Object newListenerMethodProxy(final Object listenTarget,
    // final Method listenerMethod, final Class listenerInterface, final String condition) {
    // final String listenerMethodName = ReflectUtils.getStatic(
    // ReflectUtils.getField(listenerInterface, "METHOD_NAME")).toString();
    // AsyncExecute asyncAnnotation = ReflectUtils.getAnnotation(listenTarget.getClass(), AsyncExecute.class);
    // if(asyncAnnotation==null) {
    // asyncAnnotation = listenerMethod.getAnnotation(AsyncExecute.class);
    // }
    // final boolean isAsync = asyncAnnotation!=null;
    // final long timeout = asyncAnnotation!=null ? asyncAnnotation.timeout() : -1;
    // InvocationHandler invocationHandler = new InvocationHandler() {
    // @Override
    // public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // if(method.getName().equals("getListenTarget")) {
    // return listenTarget;
    // } else if(method.getName().equals(listenerMethodName)) {
    // if(args[0] instanceof TransitionEvent) {
    // @SuppressWarnings("unchecked")
    // TransitionEvent<T, S, E, C> event = (TransitionEvent<T, S, E, C>)args[0];
    // return invokeTransitionListenerMethod(listenTarget, listenerMethod, condition, event);
    // } else if(args[0] instanceof ActionEvent) {
    // @SuppressWarnings("unchecked")
    // ActionEvent<T, S, E, C> event = (ActionEvent<T, S, E, C>)args[0];
    // return invokeActionListenerMethod(listenTarget, listenerMethod, condition, event);
    // } else if(args[0] instanceof StartEvent || args[0] instanceof TerminateEvent) {
    // @SuppressWarnings("unchecked")
    // StateMachineEvent<T, S, E, C> event = (StateMachineEvent<T, S, E, C>)args[0];
    // return invokeStateMachineListenerMethod(listenTarget, listenerMethod, condition, event);
    // } else {
    // throw new IllegalArgumentException("Unable to recognize argument type "+args[0].getClass().getName()+".");
    // }
    // } else if(method.getName().equals("equals")) {
    // return super.equals(args[0]);
    // } else if(method.getName().equals("hashCode")) {
    // return super.hashCode();
    // } else if(method.getName().equals("toString")) {
    // return super.toString();
    // } else if(isAsync && method.getName().equals("timeout")) {
    // return timeout;
    // }
    // throw new UnsupportedOperationException("Cannot invoke method "+method.getName()+".");
    // }
    // };
    // Class[] implementedInterfaces = isAsync ?
    // new Class[]{listenerInterface, DeclarativeListener.class, AsyncEventListener.class} :
    // new Class[]{listenerInterface, DeclarativeListener.class};
    // Object proxyListener = Proxy.newProxyInstance(StateMachine.class.getClassLoader(),
    // implementedInterfaces, invocationHandler);
    // return proxyListener;
    // }

    // ------------------------------------------------------------------------------
    // IO
    // ------------------------------------------------------------------------------
    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::getDescription()
     */
    public function getDescription(): string
    {
        $read = $this->data->read();
        $description = '';
        $description .= "id=\"" . $read->identifier() . "\" ";
        $description .= "fsm-type=\"" . $read->getTypeOfStateMachine() . "\" ";
        $description .= "state-type=\"" . $read->getTypeOfState() . "\" ";
        $description .= "event-type=\"" . $read->getTypeOfEvent() . "\" ";
        $description .= "context-type=\"" . $read->getTypeOfContext() . "\" ";

        // Converter<E> eventConverter = ConverterProvider.INSTANCE.getConverter(typeOfEvent());
        // if(getStartEvent()!=null) {
        // builder.append("start-event=\"");
        // builder.append(eventConverter.convertToString(getStartEvent()));
        // builder.append("\" ");
        // }
        // if(getTerminateEvent()!=null) {
        // builder.append("terminate-event=\"");
        // builder.append(eventConverter.convertToString(getTerminateEvent()));
        // builder.append("\" ");
        // }
        // if(getFinishEvent()!=null) {
        // builder.append("finish-event=\"");
        // builder.append(eventConverter.convertToString(getFinishEvent()));
        // builder.append("\" ");
        // }
        // builder.append("context-insensitive=\"").append(isContextSensitive()).append("\" ");

        // if(extraParamTypes!=null && extraParamTypes.length>0) {
        // builder.append("extra-parameters=\"[");
        // for(int i=0; i<extraParamTypes.length; ++i) {
        // if(i>0) builder.append(",");
        // builder.append(extraParamTypes[i].getName());
        // }
        // builder.append("]\" ");
        // }
        return $description;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::exportXMLDefinition()
     */
    public function exportXMLDefinition(bool $beautifyXml): string
    {
        // SquirrelProvider.getInstance().newInstance(SCXMLVisitor.class);
        // TODO: get from DI
        $visitor = new SCXMLVisitor();
        $this->accept($visitor);
        return $visitor->getScxml($beautifyXml);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::dumpSavedData()
     */
    public function dumpSavedData(): StateMachineDataReader
    {
        $savedData = FSM::newStateMachineData($this->data->read()->originalStates());
        $savedData->dump($this->data->read());

        // process linked state if any
        $this->saveLinkedStateData($this->data->read(), $savedData->write());
        return $savedData->read();
    }

    private function saveLinkedStateData(StateMachineDataReader $src, StateMachineDataWriter $target)
    {
        $this->dumpLinkedStateFor($src->currentRawState(), $target);
        // dumpLinkedStateFor(src.lastRawState(), target);
        // TODO-hhe: dump linked state info for last active child state
        // TODO-hhe: dump linked state info for parallel state
    }

    private function dumpLinkedStateFor(ImmutableState $rawState, StateMachineDataWriter $target)
    {
        if ($rawState != null && $rawState instanceof ImmutableLinkedState) {
            $linkStateData = $rawState->getLinkedStateMachine($this)->dumpSavedData();
            $target->linkedStateDataOn($rawState->getStateId(), $linkStateData);
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\StateMachine::loadSavedData()
     */
    public function loadSavedData(StateMachineDataReader $savedData): bool
    {
        // Preconditions.checkNotNull(savedData, "Saved data cannot be null");
        $this->data->dump($savedData);
        // process linked state if any
        forEach ($savedData->linkedStates() as $linkedState) {
            $linkedStateData = $savedData->linkedStateDataOf($linkedState);
            $rawState = $this->data->read()->rawStateFrom($linkedState);
            if ($linkedStateData != null && $rawState instanceof ImmutableLinkedState) {
                $rawState->getLinkedStateMachine($this)->loadSavedData($linkedStateData);
            }
        }
        $this->setStatus('IDLE');
        return true;
    }
}