<?php

/**
 * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*
* Copyright (c) 2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
*/

use oat\taoQtiTest\models\runner\RunnerService;
use oat\taoQtiTest\models\runner\time\TimerLabelFormatterService;
use qtism\common\datatypes\QtiDuration;
use qtism\data\NavigationMode;
use qtism\data\SubmissionMode;
use qtism\runtime\common\Container;
use qtism\runtime\tests\AssessmentTestSession;
use qtism\runtime\tests\AssessmentTestSessionException;
use qtism\runtime\tests\AssessmentItemSession;
use qtism\runtime\tests\AssessmentItemSessionState;
use qtism\runtime\tests\AssessmentTestSessionState;
use qtism\runtime\tests\Jump;
use qtism\runtime\tests\RouteItem;
use oat\taoQtiTest\models\ExtendedStateService;
use oat\taoQtiTest\models\QtiTestCompilerIndex;
use oat\taoQtiTest\models\runner\rubric\QtiRunnerRubric;
use qtism\common\datatypes\QtiString;
use oat\oatbox\service\ServiceManager;
use oat\taoQtiTest\models\runner\RunnerServiceContext;

/**
* Utility methods for the QtiTest Test Runner.
*
* @author Jérôme Bogaerts <jerome@taotesting.com>
*
*/
class taoQtiTest_helpers_TestRunnerUtils
{

    /**
     * Temporary helper until proper ServiceManager integration
     * @return ServiceManager
     */
    protected static function getServiceManager()
    {
        return ServiceManager::getServiceManager();
    }

    /**
     * Temporary helper until proper ServiceManager integration
     * @return ExtendedStateService
     */
    public static function getExtendedStateService()
    {
        return self::getServiceManager()->get(ExtendedStateService::SERVICE_ID);
    }

    /**
     * Get the ServiceCall object representing how to call the current Assessment Item to be
     * presented to a candidate in a given Assessment Test $session.
     *
     * @param AssessmentTestSession $session An AssessmentTestSession Object.
     * @param string $testDefinition URI The URI of the knowledge base resource representing the folder where the QTI Test Definition is stored.
     * @param string $testCompilation URI The URI of the knowledge base resource representing the folder where the QTI Test Compilation is stored.
     * @return tao_models_classes_service_ServiceCall A ServiceCall object.
     */
    public static function buildItemServiceCall(AssessmentTestSession $session, $testDefinitionUri, $testCompilationUri)
    {

        $href = $session->getCurrentAssessmentItemRef()->getHref();

        // retrive itemUri & itemPath.
        $parts = explode('|', $href);

        $definition =  new core_kernel_classes_Resource(RunnerService::INSTANCE_TEST_ITEM_RUNNER_SERVICE);
        $serviceCall = new tao_models_classes_service_ServiceCall($definition);

        $uriResource = new core_kernel_classes_Resource(taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_URI);
        $uriParam = new tao_models_classes_service_ConstantParameter($uriResource, $parts[0]);
        $serviceCall->addInParameter($uriParam);

        $pathResource = new core_kernel_classes_Resource(taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_PATH);
        $pathParam = new tao_models_classes_service_ConstantParameter($pathResource, $parts[1]);
        $serviceCall->addInParameter($pathParam);

        $dataPathResource = new core_kernel_classes_Resource(taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_DATA_PATH);
        $dataPathParam = new tao_models_classes_service_ConstantParameter($dataPathResource, $parts[2]);
        $serviceCall->addInParameter($dataPathParam);

        $parentServiceCallIdResource = new core_kernel_classes_Resource(RunnerService::INSTANCE_FORMAL_PARAM_TEST_ITEM_RUNNER_PARENT_CALL_ID);
        $parentServiceCallIdParam = new tao_models_classes_service_ConstantParameter($parentServiceCallIdResource, $session->getSessionId());
        $serviceCall->addInParameter($parentServiceCallIdParam);

        $testDefinitionResource = new core_kernel_classes_Resource(taoQtiTest_models_classes_QtiTestService::INSTANCE_FORMAL_PARAM_TEST_DEFINITION);
        $testDefinitionParam = new tao_models_classes_service_ConstantParameter($testDefinitionResource, $testDefinitionUri);
        $serviceCall->addInParameter($testDefinitionParam);

        $testCompilationResource = new core_kernel_classes_Resource(taoQtiTest_models_classes_QtiTestService::INSTANCE_FORMAL_PARAM_TEST_COMPILATION);
        $testCompilationParam = new tao_models_classes_service_ConstantParameter($testCompilationResource, $testCompilationUri);
        $serviceCall->addInParameter($testCompilationParam);

        return $serviceCall;
    }

    /**
     * Build the Service Call ID of the current Assessment Item to be presented to a candidate
     * in a given Assessment Test $session.
     *
     * @return string A service call id composed of the session identifier,  the identifier of the item and its occurence number in the route.
     */
    public static function buildServiceCallId(AssessmentTestSession $session)
    {

        $sessionId = $session->getSessionId();
        $itemId = $session->getCurrentAssessmentItemRef()->getIdentifier();
        $occurence = $session->getCurrentAssessmentItemRefOccurence();
        return "${sessionId}.${itemId}.${occurence}";
    }

    /**
     * Set the initial outcomes defined in the rdf outcome map configuration file
     *
     * @param AssessmentTestSession $session
     * @param \oat\oatbox\user\User $testTaker
     * @throws common_exception_Error
     * @throws common_ext_ExtensionException
     */
    public static function setInitialOutcomes(AssessmentTestSession $session, \oat\oatbox\user\User $testTaker)
    {
        $rdfOutcomeMap = \common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('rdfOutcomeMap');
        if (is_array($rdfOutcomeMap)) {
            foreach ($rdfOutcomeMap as $outcomeId => $rdfPropUri) {
                //set outcome value
                $values = $testTaker->getPropertyValues($rdfPropUri);
                $outcome = $session->getVariable($outcomeId);
                if (!is_null($outcome) && count($values)) {
                    $outcome->setValue(new QtiString((string)$values[0]));
                }
            }
        }
    }

    /**
     * Preserve the outcomes variables set in the "rdfOutcomeMap" config
     * This is required to prevent those special outcomes from being reset before every outcome processing
     *
     * @param AssessmentTestSession $session
     * @throws common_ext_ExtensionException
     */
    public static function preserveOutcomes(AssessmentTestSession $session)
    {
        //preserve the special outcomes defined in the rdfOutcomeMap config
        $rdfOutcomeMap = \common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('rdfOutcomeMap');
        if (is_array($rdfOutcomeMap) === true) {
            $session->setPreservedOutcomeVariables(array_keys($rdfOutcomeMap));
        }
    }

    /**
     * Whether or not the current Assessment Item to be presented to the candidate is timed-out. By timed-out
     * we mean:
     *
     * * current Assessment Test level time limits are not respected OR,
     * * current Test Part level time limits are not respected OR,
     * * current Assessment Section level time limits are not respected OR,
     * * current Assessment Item level time limits are not respected.
     *
     * @param AssessmentTestSession $session The AssessmentTestSession object you want to know it is timed-out.
     * @return boolean
     */
    public static function isTimeout(AssessmentTestSession $session)
    {

        try {
            $session->checkTimeLimits(false, true, false);
        } catch (AssessmentTestSessionException $e) {
            return true;
        }

        return false;
    }

    /**
     * Get the URI referencing the current Assessment Item (in the knowledge base)
     * to be presented to the candidate.
     *
     * @param AssessmentTestSession $session An AssessmentTestSession object.
     * @return string A URI.
     */
    public static function getCurrentItemUri(AssessmentTestSession $session)
    {
        $href = $session->getCurrentAssessmentItemRef()->getHref();
        $parts = explode('|', $href);

        return $parts[0];
    }

    /**
     * Build the URL to be called to perform a given action on the Test Runner controller.
     *
     * @param AssessmentTestSession $session An AssessmentTestSession object.
     * @param string $action The action name e.g. 'moveForward', 'moveBackward', 'skip', ...
     * @param string $qtiTestDefinitionUri The URI of a reference to an Assessment Test definition in the knowledge base.
     * @param string $qtiTestCompilationUri The Uri of a reference to an Assessment Test compilation in the knowledge base.
     * @param string $standalone
     * @return string A URL to be called to perform an action.
     */
    public static function buildActionCallUrl(AssessmentTestSession $session, $action, $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone)
    {
        return _url($action, 'TestRunner', null, [
            'QtiTestDefinition' => $qtiTestDefinitionUri,
            'QtiTestCompilation' => $qtiTestCompilationUri,
            'standalone' => $standalone,
            'serviceCallId' => $session->getSessionId(),
        ]);
    }

    public static function buildServiceApi(AssessmentTestSession $session, $qtiTestDefinitionUri, $qtiTestCompilationUri)
    {
        $serviceCall = self::buildItemServiceCall($session, $qtiTestDefinitionUri, $qtiTestCompilationUri);
        $itemServiceCallId = self::buildServiceCallId($session);
        return tao_helpers_ServiceJavascripts::getServiceApi($serviceCall, $itemServiceCallId);
    }

    /**
     * Tell the client to not cache the current request. Supports HTTP 1.0 to 1.1.
     */
    public static function noHttpClientCache()
    {
        // From stackOverflow: http://stackoverflow.com/questions/49547/making-sure-a-web-page-is-not-cached-across-all-browsers
        // license is Creative Commons Attribution Share Alike (author Edward Wilde)
        header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1.
        header('Pragma: no-cache'); // HTTP 1.0.
        header('Expires: 0'); // Proxies.
    }

    /**
     * Make the candidate interact with the current Assessment Item to be presented. A new attempt
     * will begin automatically if the candidate still has available attempts. Otherwise,
     * nothing happends.
     *
     * @param AssessmentTestSession $session The AssessmentTestSession you want to make the candidate interact with.
     */
    public static function beginCandidateInteraction(AssessmentTestSession $session)
    {
        $itemSession = $session->getCurrentAssessmentItemSession();
        $itemSessionState = $itemSession->getState();

        $initial = $itemSessionState === AssessmentItemSessionState::INITIAL;
        $suspended = $itemSessionState === AssessmentItemSessionState::SUSPENDED;
        $remainingAttempts = $itemSession->getRemainingAttempts();
        $attemptable = $remainingAttempts === -1 || $remainingAttempts > 0;

        if ($initial === true || ($suspended === true && $attemptable === true)) {
            // Begin the very first attempt.
            $session->beginAttempt();
        }
        // Otherwise, the item is not attemptable bt the candidate.
    }

    /**
     * Whether or not the candidate taking the given $session is allowed
     * to skip the presented Assessment Item.
     *
     * @param AssessmentTestSession $session A given AssessmentTestSession object.
     * @return boolean
     */
    public static function doesAllowSkipping(AssessmentTestSession $session)
    {
        $doesAllowSkipping = true;
        $submissionMode = $session->getCurrentSubmissionMode();

        $routeItem = $session->getRoute()->current();
        $routeControl = $routeItem->getItemSessionControl();

        if (empty($routeControl) === false) {
            $doesAllowSkipping = $routeControl->getItemSessionControl()->doesAllowSkipping();
        }

        return $doesAllowSkipping && $submissionMode === SubmissionMode::INDIVIDUAL;
    }

    /**
     * Whether or not the candidate's response is validated
     *
     * @param AssessmentTestSession $session A given AssessmentTestSession object.
     * @return boolean
     */
    public static function doesValidateResponses(AssessmentTestSession $session)
    {
        $doesValidateResponses = true;
        $submissionMode = $session->getCurrentSubmissionMode();

        $routeItem = $session->getRoute()->current();
        $routeControl = $routeItem->getItemSessionControl();

        if (empty($routeControl) === false) {
            $doesValidateResponses = $routeControl->getItemSessionControl()->mustValidateResponses();
        }

        return $doesValidateResponses && $submissionMode === SubmissionMode::INDIVIDUAL;
    }

    /**
     * Whether or not the candidate taking the given $session is allowed to make
     * a comment on the presented Assessment Item.
     *
     * @param AssessmentTestSession $session A given AssessmentTestSession object.
     * @return boolean
     */
    public static function doesAllowComment(AssessmentTestSession $session)
    {
        $doesAllowComment = false;

        $routeItem = $session->getRoute()->current();
        $routeControl = $routeItem->getItemSessionControl();

        if (empty($routeControl) === false) {
            $doesAllowComment = $routeControl->getItemSessionControl()->doesAllowComment();
        }

        return $doesAllowComment;
    }

    /**
     * Build an array where each cell represent a time constraint (a.k.a. time limits)
     * in force. Each cell is actually an array with two keys:
     *
     * * 'source': The identifier of the QTI component emitting the constraint (e.g. AssessmentTest, TestPart, AssessmentSection, AssessmentItemRef).
     * * 'seconds': The number of remaining seconds until it times out.
     *
     * @param AssessmentTestSession $session An AssessmentTestSession object.
     * @return array
     */
    public static function buildTimeConstraints(AssessmentTestSession $session)
    {
        $constraints = [];
        /** @var TimerLabelFormatterService $timerLabelFormatter */
        $timerLabelFormatter = static::getServiceManager()->get(TimerLabelFormatterService::SERVICE_ID);

        foreach ($session->getTimeConstraints() as $tc) {
            // Only consider time constraints in force.
            if ($tc->getMaximumRemainingTime() !== false) {
                $label = method_exists($tc->getSource(), 'getTitle') ? $tc->getSource()->getTitle() : $tc->getSource()->getIdentifier();
                $constraints[] = [
                    'label' => $timerLabelFormatter->format($label),
                    'source' => $tc->getSource()->getIdentifier(),
                    'seconds' => self::getDurationWithMicroseconds($tc->getMaximumRemainingTime()),
                    'allowLateSubmission' => $tc->allowLateSubmission(),
                    'qtiClassName' => $tc->getSource()->getQtiClassName()
                ];
            }
        }

        return $constraints;
    }

    /**
     * Build an array where each cell represent a possible Assessment Item a candidate
     * can jump on during a given $session. Each cell is an array with two keys:
     *
     * * 'identifier': The identifier of the Assessment Item the candidate is allowed to jump on.
     * * 'position': The position in the route of the Assessment Item.
     *
     * @param AssessmentTestSession $session A given AssessmentTestSession object.
     * @return array
     */
    public static function buildPossibleJumps(AssessmentTestSession $session)
    {
        $jumps = [];

        foreach ($session->getPossibleJumps() as $jumpObject) {
            $jump = [];
            $jump['identifier'] = $jumpObject->getTarget()->getAssessmentItemRef()->getIdentifier();
            $jump['position'] = $jumpObject->getPosition();

            $jumps[] = $jump;
        }

        return $jumps;
    }

    /**
     * Build the context of the given candidate test $session as an associative array. This array
     * is especially usefull to transmit the test context to a view as JSON data.
     *
     * The returned array contains the following keys:
     *
     * * state: The state of test session.
     * * navigationMode: The current navigation mode.
     * * submissionMode: The current submission mode.
     * * remainingAttempts: The number of remaining attempts for the current item.
     * * isAdaptive: Whether or not the current item is adaptive.
     * * itemIdentifier: The identifier of the current item.
     * * itemSessionState: The state of the current assessment item session.
     * * timeConstraints: The time constraints in force.
     * * testTitle: The title of the test.
     * * testPartId: The identifier of the current test part.
     * * sectionTitle: The title of the current section.
     * * numberItems: The total number of items eligible to the candidate.
     * * numberCompleted: The total number items considered to be completed by the candidate.
     * * moveForwardUrl: The URL to be dereferenced to perform a moveNext on the session.
     * * moveBackwardUrl: The URL to be dereferenced to perform a moveBack on the session.
     * * skipUrl: The URL to be dereferenced to perform a skip on the session.
     * * commentUrl: The URL to be dereferenced to leave a comment about the current item.
     * * timeoutUrl: The URL to be dereferenced when the time constraints in force reach their maximum.
     * * canMoveBackward: Whether or not the candidate is allowed/able to move backward.
     * * jumps: The possible jumpers the candidate is allowed to undertake among eligible items.
     * * itemServiceApiCall: The JavaScript code to be executed to instantiate the current item.
     * * rubrics: The XHTML compiled content of the rubric blocks to be displayed for the current item if any.
     * * allowComment: Whether or not the candidate is allowed to leave a comment about the current item.
     * * allowSkipping: Whether or not the candidate is allowed to skip the current item.
     * * considerProgress: Whether or not the test driver view must consider to give a test progress feedback.
     *
     * @param AssessmentTestSession $session A given AssessmentTestSession object.
     * @param array $testMeta An associative array containing meta-data about the test definition taken by the candidate.
     * @param QtiTestCompilerIndex $itemIndex
     * @param string $qtiTestDefinitionUri The URI of a reference to an Assessment Test definition in the knowledge base.
     * @param string $qtiTestCompilationUri The Uri of a reference to an Assessment Test compilation in the knowledge base.
     * @param string $standalone
     * @param array $compilationDirs An array containing respectively the private and public compilation directories.
     * @return array The context of the candidate session.
     */
    public static function buildAssessmentTestContext(AssessmentTestSession $session, array $testMeta, $itemIndex, $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone, $compilationDirs)
    {
        $context = [];

        // The state of the test session.
        $context['state'] = $session->getState();

        // Default values for the test session context.
        $context['navigationMode'] = null;
        $context['submissionMode'] = null;
        $context['remainingAttempts'] = 0;
        $context['isAdaptive'] = false;

        $hasBeenPaused = false;
        if (common_ext_ExtensionsManager::singleton()->isEnabled('taoProctoring')) {
            $hasBeenPaused = \oat\taoProctoring\helpers\DeliveryHelper::getHasBeenPaused($session->getSessionId());
        }
        $context['hasBeenPaused'] = $hasBeenPaused;


        if ($session->getState() === AssessmentTestSessionState::INTERACTING) {
            $config = common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('testRunner');

            // The navigation mode.
            $context['navigationMode'] = $session->getCurrentNavigationMode();

            // The submission mode.
            $context['submissionMode'] = $session->getCurrentSubmissionMode();

            // The number of remaining attempts for the current item.
            $context['remainingAttempts'] = $session->getCurrentRemainingAttempts();

            // Whether or not the current step is time out.
            $context['isTimeout'] = self::isTimeout($session);

            // The identifier of the current item.
            $context['itemIdentifier'] = $session->getCurrentAssessmentItemRef()->getIdentifier();

            // The state of the current AssessmentTestSession.
            $context['itemSessionState'] = $session->getCurrentAssessmentItemSession()->getState();

            // Whether the current item is adaptive.
            $context['isAdaptive'] = $session->isCurrentAssessmentItemAdaptive();

            // Whether the current item is the very last one of the test.
            $context['isLast'] = $session->getRoute()->isLast();

            // The current position in the route.
            $context['itemPosition'] = $session->getRoute()->getPosition();

            // Time constraints.
            $context['timeConstraints'] = self::buildTimeConstraints($session);

            // Test title.
            $context['testTitle'] = $session->getAssessmentTest()->getTitle();

            // Test Part title.
            $context['testPartId'] = $session->getCurrentTestPart()->getIdentifier();

            // Current Section title.
            $context['sectionTitle'] = $session->getCurrentAssessmentSection()->getTitle();

            // Number of items composing the test session.
            $context['numberItems'] = $session->getRouteCount(AssessmentTestSession::ROUTECOUNT_FLOW);

            // Number of items completed during the test session.
            $context['numberCompleted'] = self::testCompletion($session);

            // Number of items presented during the test session.
            $context['numberPresented'] = $session->numberPresented();

            // Whether or not the progress of the test can be inferred.
            $context['considerProgress'] = self::considerProgress($session, $testMeta, $config);

            // Whether or not the deepest current section is visible.
            $context['isDeepestSectionVisible'] = $session->getCurrentAssessmentSection()->isVisible();

            // The URLs to be called to move forward/backward in the Assessment Test Session or skip or comment.
            $context['moveForwardUrl'] = self::buildActionCallUrl($session, 'moveForward', $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone);
            $context['moveBackwardUrl'] = self::buildActionCallUrl($session, 'moveBackward', $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone);
            $context['nextSectionUrl'] = self::buildActionCallUrl($session, 'nextSection', $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone);
            $context['skipUrl'] = self::buildActionCallUrl($session, 'skip', $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone);
            $context['commentUrl'] = self::buildActionCallUrl($session, 'comment', $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone);
            $context['timeoutUrl'] = self::buildActionCallUrl($session, 'timeout', $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone);
            $context['endTestSessionUrl'] = self::buildActionCallUrl($session, 'endTestSession', $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone);
            $context['keepItemTimedUrl'] = self::buildActionCallUrl($session, 'keepItemTimed', $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone);
            // If the candidate is allowed to move backward e.g. first item of the test.
            $context['canMoveBackward'] = $session->canMoveBackward();

            // The places in the test session where the candidate is allowed to jump to.
            $context['jumps'] = self::buildPossibleJumps($session);

            // The test review screen setup
            if (!empty($config['test-taker-review']) && $context['considerProgress']) {
                // The navigation map in order to build the test navigator
                $navigator = self::getNavigatorMap($session, $itemIndex);
                if ($navigator !== NavigationMode::LINEAR) {
                    $context['navigatorMap'] = $navigator['map'];
                    $context['itemFlagged'] = self::getItemFlag($session, $context['itemPosition']);
                } else {
                    $navigator = self::countItems($session);
                }

                // Extract the progression stats
                $context['numberFlagged'] = $navigator['numberItemsFlagged'];
                $context['numberItemsPart'] = $navigator['numberItemsPart'];
                $context['numberItemsSection'] = $navigator['numberItemsSection'];
                $context['numberCompletedPart'] = $navigator['numberCompletedPart'];
                $context['numberCompletedSection'] = $navigator['numberCompletedSection'];
                $context['numberPresentedPart'] = $navigator['numberPresentedPart'];
                $context['numberPresentedSection'] = $navigator['numberPresentedSection'];
                $context['numberFlaggedPart'] = $navigator['numberFlaggedPart'];
                $context['numberFlaggedSection'] = $navigator['numberFlaggedSection'];
                $context['itemPositionPart'] = $navigator['itemPositionPart'];
                $context['itemPositionSection'] = $navigator['itemPositionSection'];

                // The URLs to be called to move to a particular item in the Assessment Test Session or mark item for later review.
                $context['jumpUrl'] = self::buildActionCallUrl($session, 'jumpTo', $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone);
                $context['markForReviewUrl'] = self::buildActionCallUrl($session, 'markForReview', $qtiTestDefinitionUri, $qtiTestCompilationUri, $standalone);
            } else {
                // Setup data for progress bar when displaying position and timed section exit control
                $numberItems = self::countItems($session);
                $context['numberCompletedPart'] = $numberItems['numberCompletedPart'];
                $context['numberCompletedSection'] = $numberItems['numberCompletedSection'];
                $context['numberItemsSection'] = $numberItems['numberItemsSection'];
                $context['numberItemsPart'] = $numberItems['numberItemsPart'];
                $context['itemPositionPart'] = $numberItems['itemPositionPart'];
                $context['itemPositionSection'] = $numberItems['itemPositionSection'];
            }

            // The code to be executed to build the ServiceApi object to be injected in the QTI Item frame.
            $context['itemServiceApiCall'] = self::buildServiceApi($session, $qtiTestDefinitionUri, $qtiTestCompilationUri);

            // Rubric Blocks.
            /** @var QtiRunnerRubric $rubricBlockHelper */
            $rubricBlockHelper = self::getServiceManager()->get(QtiRunnerRubric::SERVICE_ID);
            $context['rubrics'] = $rubricBlockHelper->getRubricBlock($session->getRoute()->current(), $session, $compilationDirs);

            // Comment allowed? Skipping allowed? Logout or Exit allowed ?
            $context['allowComment'] = self::doesAllowComment($session);
            $context['allowSkipping'] = self::doesAllowSkipping($session);
            $context['exitButton'] = self::doesAllowExit($session);
            $context['logoutButton'] = self::doesAllowLogout($session);
            $context['categories'] = self::getCategories($session);

            // loads the specific config into the context object
            $configMap = [
                // name in config                   => name in context object
                'timerWarning'                      => 'timerWarning',
                'timerWarningForScreenreader'       => 'timerWarningForScreenreader',
                'progress-indicator'                => 'progressIndicator',
                'progress-indicator-scope'          => 'progressIndicatorScope',
                'test-taker-review'                 => 'reviewScreen',
                'test-taker-review-region'          => 'reviewRegion',
                'test-taker-review-scope'           => 'reviewScope',
                'test-taker-review-prevents-unseen' => 'reviewPreventsUnseen',
                'test-taker-review-can-collapse'    => 'reviewCanCollapse',
                'next-section'                      => 'nextSection',
                'keep-timer-up-to-timeout'          => 'keepTimerUpToTimeout',
            ];
            foreach ($configMap as $configKey => $contextKey) {
                if (isset($config[$configKey])) {
                    $context[$contextKey] = $config[$configKey];
                }
            }

            // optionally extend the context
            if (isset($config['extraContextBuilder']) && class_exists($config['extraContextBuilder'])) {
                $builder = new $config['extraContextBuilder']();
                if ($builder instanceof \oat\taoQtiTest\models\TestContextBuilder) {
                    $builder->extendAssessmentTestContext(
                        $context,
                        $session,
                        $testMeta,
                        $qtiTestDefinitionUri,
                        $qtiTestCompilationUri,
                        $standalone,
                        $compilationDirs
                    );
                } else {
                    common_Logger::d('Try to use an extra context builder class that is not an instance of \\oat\\taoQtiTest\\models\\TestContextBuilder!');
                }
            }
        }

        return $context;
    }

    /**
     * Gets the item reference for a particular item in the test
     *
     * @param AssessmentTestSession $session
     * @param string|Jump|RouteItem $itemPosition
     * @return null|string
     */
    public static function getItemRef(AssessmentTestSession $session, $itemPosition, RunnerServiceContext $context = null)
    {
        $sessionId = $session->getSessionId();

        $itemRef = null;
        $routeItem = null;

        if ($itemPosition && is_object($itemPosition)) {
            if ($itemPosition instanceof RouteItem) {
                $routeItem = $itemPosition;
            } elseif ($itemPosition instanceof Jump) {
                $routeItem = $itemPosition->getTarget();
            }
        } elseif ($context) {
            $itemId = '';
            $itemPosition = $context->getItemPositionInRoute($itemPosition, $itemId);

            if ($itemId !== '') {
                $itemRef = $itemId;
            } else {
                $routeItem = $session->getRoute()->getRouteItemAt($itemPosition);
            }
        } else {
            $jumps = $session->getPossibleJumps();
            foreach ($jumps as $jump) {
                if ($itemPosition == $jump->getPosition()) {
                    $routeItem = $jump->getTarget();
                    break;
                }
            }
        }

        if ($routeItem) {
            $itemRef = (string)$routeItem->getAssessmentItemRef();
        }

        return $itemRef;
    }

    /**
     * Sets an item to be reviewed
     * @param AssessmentTestSession $session
     * @param string|Jump|RouteItem $itemPosition
     * @param bool $flag
     * @return bool
     * @throws common_exception_Error
     */
    public static function setItemFlag(AssessmentTestSession $session, $itemPosition, $flag, RunnerServiceContext $context = null)
    {

        $itemRef = self::getItemRef($session, $itemPosition, $context);
        $result = self::getExtendedStateService()->setItemFlag($session->getSessionId(), $itemRef, $flag);

        return $result;
    }

    /**
     * Gets the marked for review state of an item
     * @param AssessmentTestSession $session
     * @param string|Jump|RouteItem $itemPosition
     * @return bool
     * @throws common_exception_Error
     */
    public static function getItemFlag(AssessmentTestSession $session, $itemPosition, RunnerServiceContext $context = null)
    {
        $result = false;

        $itemRef = self::getItemRef($session, $itemPosition, $context);
        if ($itemRef) {
            $result = self::getExtendedStateService()->getItemFlag($session->getSessionId(), $itemRef);
        }

        return $result;
    }

    /**
     * Gets the usage of an item
     * @param RouteItem $routeItem
     * @return string Return the usage, can be: default, informational, seeding
     */
    public static function getItemUsage(RouteItem $routeItem)
    {
        $itemRef = $routeItem->getAssessmentItemRef();
        $categories = $itemRef->getCategories()->getArrayCopy();
        $prefixCategory = 'x-tao-itemusage-';
        $prefixCategoryLen = strlen($prefixCategory);
        foreach ($categories as $category) {
            if (!strncmp($category, $prefixCategory, $prefixCategoryLen)) {
                // extract the option name from the category, transform to camelCase if needed
                return lcfirst(str_replace(' ', '', ucwords(strtr(substr($category, $prefixCategoryLen), ['-' => ' ', '_' => ' ']))));
            }
        }

        return 'default';
    }

    /**
     * Checks if an item is informational
     * @param RouteItem $routeItem
     * @param AssessmentItemSession $itemSession
     * @return bool
     */
    public static function isItemInformational(RouteItem $routeItem, AssessmentItemSession $itemSession)
    {
        return !count($itemSession->getAssessmentItem()->getResponseDeclarations()) || 'informational' == self::getItemUsage($routeItem);
    }

    /**
     * Checks if an item has been completed
     * @param RouteItem $routeItem
     * @param AssessmentItemSession $itemSession
     * @param bool $partially (optional) Whether or not consider partially responded sessions as responded.
     * @return bool
     */
    public static function isItemCompleted(RouteItem $routeItem, AssessmentItemSession $itemSession, $partially = true)
    {
        $completed = false;
        if ($routeItem->getTestPart()->getNavigationMode() === NavigationMode::LINEAR) {
            // In linear mode, we consider the item completed if it was presented.
            if ($itemSession->isPresented() === true) {
                $completed = true;
            }
        } else {
            // In nonlinear mode we consider:
            // - an adaptive item completed if it's completion status is 'completed'.
            // - a non-adaptive item to be completed if it is responded.
            $isAdaptive = $itemSession->getAssessmentItem()->isAdaptive();

            if ($isAdaptive === true && $itemSession['completionStatus']->getValue() === AssessmentItemSession::COMPLETION_STATUS_COMPLETED) {
                $completed = true;
            } elseif ($isAdaptive === false && $itemSession->isResponded($partially) === true) {
                $completed = true;
            }
        }

        return $completed;
    }

    /**
     * Gets infos about a particular item
     * @param AssessmentTestSession $session
     * @param Jump $jump
     * @return array
     */
    private static function getItemInfo(AssessmentTestSession $session, Jump $jump)
    {
        $itemSession = $jump->getItemSession();
        $routeItem = $jump->getTarget();
        return [
            'remainingAttempts' => $itemSession->getRemainingAttempts(),
            'answered' => self::isItemCompleted($routeItem, $itemSession),
            'viewed' => $itemSession->isPresented(),
            'flagged' => self::getItemFlag($session, $jump),
            'position' => $jump->getPosition()
        ];
    }

    /**
     * Builds a map of available jumps and count the flagged items
     * @param AssessmentTestSession $session
     * @param array $jumps
     * @return array
     */
    private static function getJumpsMap(AssessmentTestSession $session, $jumps)
    {
        $jumpsMap = [];
        $numberItemsFlagged = 0;
        foreach ($jumps as $jump) {
            $routeItem = $jump->getTarget();
            $partId = $routeItem->getTestPart()->getIdentifier();
            $sections = $routeItem->getAssessmentSections();
            $sections->rewind();
            $sectionId = key(current($sections));
            $itemId = $routeItem->getAssessmentItemRef()->getIdentifier();

            $jumpsMap[$partId][$sectionId][$itemId] = self::getItemInfo($session, $jump);
            if ($jumpsMap[$partId][$sectionId][$itemId]['flagged']) {
                $numberItemsFlagged++;
            }
        }

        return [
            'flagged' => $numberItemsFlagged,
            'map' => $jumpsMap,
        ];
    }

    /**
     * Gets the section map for navigation between test parts, sections and items.
     *
     * @param AssessmentTestSession $session
     * @param QtiTestCompilerIndex $itemIndex
     * @return array A navigator map (parts, sections, items so on)
     */
    private static function getNavigatorMap(AssessmentTestSession $session, $itemIndex)
    {

        // get jumps
        $jumps = $session->getPossibleJumps();

        // no jumps, notify linear-mode
        if (!$jumps->count()) {
            return NavigationMode::LINEAR;
        }

        $jumpsMapInfo = self::getJumpsMap($session, $jumps);
        $jumpsMap = $jumpsMapInfo['map'];
        $numberItemsFlagged = $jumpsMapInfo['flagged'];


        // the active test-part identifier
        $activePart = $session->getCurrentTestPart()->getIdentifier();

        // the active section identifier
        $activeSection = $session->getCurrentAssessmentSection()->getIdentifier();

        $route = $session->getRoute();

        $activeItem = $session->getCurrentAssessmentItemRef()->getIdentifier();
        if (isset($jumpsMap[$activePart][$activeSection][$activeItem])) {
            $jumpsMap[$activePart][$activeSection][$activeItem]['active'] = true;
        }

        // current position
        $oldPosition = $route->getPosition();

        $route->setPosition($oldPosition);

        // get config for the sequence number option
        $config = common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('testRunner');
        $forceTitles = !empty($config['test-taker-review-force-title']);
        $uniqueTitle = isset($config['test-taker-review-item-title']) ? $config['test-taker-review-item-title'] : '%d';
        $useTitle = !empty($config['test-taker-review-use-title']);
        $language = \common_session_SessionManager::getSession()->getInterfaceLanguage();

        $returnValue = [];
        $testParts   = [];
        $testPartIdx = 0;
        $numberItemsPart = 0;
        $numberItemsSection = 0;
        $numberCompletedPart = 0;
        $numberCompletedSection = 0;
        $numberPresentedPart = 0;
        $numberPresentedSection = 0;
        $numberFlaggedPart = 0;
        $numberFlaggedSection = 0;
        $itemPositionPart = 0;
        $itemPositionSection = 0;
        $itemPosition = $session->getRoute()->getPosition();

        foreach ($jumps as $jump) {
            $testPart = $jump->getTarget()->getTestPart();
            $id = $testPart->getIdentifier();

            if (isset($testParts[$id])) {
                continue;
            }

            $sections = [];

            if ($testPart->getNavigationMode() == NavigationMode::NONLINEAR) {
                $firstPositionPart = PHP_INT_MAX;
                foreach ($testPart->getAssessmentSections() as $sectionId => $section) {
                    $completed = 0;
                    $presented = 0;
                    $flagged = 0;
                    $items = [];
                    $firstPositionSection = PHP_INT_MAX;
                    $positionInSection = 0;

                    foreach ($section->getSectionParts() as $itemId => $item) {
                        if (isset($jumpsMap[$id][$sectionId][$itemId])) {
                            $jumpInfo = $jumpsMap[$id][$sectionId][$itemId];
                            $itemUri = strstr($item->getHref(), '|', true);
                            $resItem  =  new \core_kernel_classes_Resource($itemUri);
                            if ($jumpInfo['answered']) {
                                ++$completed;
                            }
                            if ($jumpInfo['viewed']) {
                                ++$presented;
                            }
                            if ($jumpInfo['flagged']) {
                                ++$flagged;
                            }
                            if ($forceTitles) {
                                $label = sprintf($uniqueTitle, ++$positionInSection);
                            } else {
                                if ($useTitle) {
                                    $label = $itemIndex->getItemValue($itemUri, $language, 'title');
                                } else {
                                    $label = '';
                                }

                                if (!$label) {
                                    $label = $itemIndex->getItemValue($itemUri, $language, 'label');
                                }

                                if (!$label) {
                                    $label = $resItem->getLabel();
                                }
                            }
                            $items[]  = array_merge(
                                [
                                    'id' => $itemId,
                                    'label' => $label,
                                ],
                                $jumpInfo
                            );

                            $firstPositionPart = min($firstPositionPart, $jumpInfo['position']);
                            $firstPositionSection = min($firstPositionSection, $jumpInfo['position']);
                        }
                    }

                    $sectionData = [
                        'id'       => $sectionId,
                        'active'   => $sectionId === $activeSection,
                        'label'    => $section->getTitle(),
                        'answered' => $completed,
                        'items'    => $items
                    ];
                    $sections[] = $sectionData;

                    if ($sectionData['active']) {
                        $numberItemsSection = count($items);
                        $itemPositionSection = $itemPosition - $firstPositionSection;
                        $numberCompletedSection = $completed;
                        $numberPresentedSection = $presented;
                        $numberFlaggedSection = $flagged;
                    }
                    if ($id === $activePart) {
                        $numberItemsPart += count($items);
                        $numberCompletedPart += $completed;
                        $numberPresentedPart += $presented;
                        $numberFlaggedPart += $flagged;
                    }
                }

                if ($id === $activePart) {
                    $itemPositionPart = $itemPosition - $firstPositionPart;
                }
            }

            $data = [
                'id'       => $id,
                'sections' => $sections,
                'active'   => $id === $activePart,
                'label'    => __('Part %d', ++$testPartIdx),
            ];
            if (empty($sections)) {
                $item = current(current($jumpsMap[$id]));
                $data['position'] = $item['position'];
                $data['itemId'] = key(current($jumpsMap[$id]));
            }
            $returnValue[] = $data;
            $testParts[$id] = false;
        }

        return [
            'map' => $returnValue,
            'numberItemsFlagged' => $numberItemsFlagged,
            'numberItemsPart' => $numberItemsPart,
            'numberItemsSection' => $numberItemsSection,
            'numberCompletedPart' => $numberCompletedPart,
            'numberCompletedSection' => $numberCompletedSection,
            'numberPresentedPart' => $numberPresentedPart,
            'numberPresentedSection' => $numberPresentedSection,
            'numberFlaggedPart' => $numberFlaggedPart,
            'numberFlaggedSection' => $numberFlaggedSection,
            'itemPositionPart' => $itemPositionPart,
            'itemPositionSection' => $itemPositionSection,
        ];
    }

    /**
     * Gets the number of items within the current section and the current part.
     *
     * @param AssessmentTestSession $session
     * @return array The list of counters (numberItemsSection and numberItemsPart)
     */
    private static function countItems(AssessmentTestSession $session)
    {
        // get jumps
        $jumps = self::getTestMap($session);

        // the active test-part identifier
        $activePart = $session->getCurrentTestPart()->getIdentifier();

        // the active section identifier
        $activeSection = $session->getCurrentAssessmentSection()->getIdentifier();

        $jumpsMapInfo = self::getJumpsMap($session, $jumps);
        $jumpsMap = $jumpsMapInfo['map'];
        $numberItemsFlagged = $jumpsMapInfo['flagged'];

        $testParts = [];
        $numberItemsPart = 0;
        $numberItemsSection = 0;
        $numberCompletedPart = 0;
        $numberCompletedSection = 0;
        $numberPresentedPart = 0;
        $numberPresentedSection = 0;
        $numberFlaggedPart = 0;
        $numberFlaggedSection = 0;
        $itemPositionPart = 0;
        $itemPositionSection = 0;
        $itemPosition = $session->getRoute()->getPosition();
        foreach ($jumps as $jump) {
            $testPart = $jump->getTarget()->getTestPart();
            $id = $testPart->getIdentifier();

            if (isset($testParts[$id])) {
                continue;
            }
            $testParts[$id] = true;

            $firstPositionPart = PHP_INT_MAX;
            foreach ($testPart->getAssessmentSections() as $sectionId => $section) {
                $completed = 0;
                $presented = 0;
                $flagged = 0;
                $numberItems = count($section->getSectionParts());
                $firstPositionSection = PHP_INT_MAX;
                foreach ($section->getSectionParts() as $itemId => $item) {
                    if (isset($jumpsMap[$id][$sectionId][$itemId])) {
                        $jumpInfo = $jumpsMap[$id][$sectionId][$itemId];

                        if ($jumpInfo['answered']) {
                            ++$completed;
                        }
                        if ($jumpInfo['viewed']) {
                            ++$presented;
                        }
                        if ($jumpInfo['flagged']) {
                            ++$flagged;
                        }

                        $firstPositionPart = min($firstPositionPart, $jumpInfo['position']);
                        $firstPositionSection = min($firstPositionSection, $jumpInfo['position']);
                    }
                }

                if ($sectionId === $activeSection) {
                    $numberItemsSection = $numberItems;
                    $itemPositionSection = $itemPosition - $firstPositionSection;
                    $numberCompletedSection = $completed;
                    $numberPresentedSection = $presented;
                    $numberFlaggedSection = $flagged;
                }
                if ($id === $activePart) {
                    $numberItemsPart += $numberItems;
                    $numberCompletedPart += $completed;
                    $numberPresentedPart += $presented;
                    $numberFlaggedPart += $flagged;
                }
            }
            if ($id === $activePart) {
                $itemPositionPart = $itemPosition - $firstPositionPart;
            }
        }

        return [
            'numberItemsFlagged' => $numberItemsFlagged,
            'numberItemsPart' => $numberItemsPart,
            'numberItemsSection' => $numberItemsSection,
            'numberCompletedPart' => $numberCompletedPart,
            'numberCompletedSection' => $numberCompletedSection,
            'numberPresentedPart' => $numberPresentedPart,
            'numberPresentedSection' => $numberPresentedSection,
            'numberFlaggedPart' => $numberFlaggedPart,
            'numberFlaggedSection' => $numberFlaggedSection,
            'itemPositionPart' => $itemPositionPart,
            'itemPositionSection' => $itemPositionSection,
        ];
    }

    /**
     * Gets the map of the reachable items.
     * @param AssessmentTestSession $session
     * @return array The map of the test
     */
    public static function getTestMap($session)
    {
        $map = [];

        if ($session->isRunning() !== false) {
            $route = $session->getRoute();
            $routeItems = $route->getAllRouteItems();
            $offset = $route->getRouteItemPosition($routeItems[0]);
            foreach ($routeItems as $routeItem) {
                $itemRef = $routeItem->getAssessmentItemRef();
                $occurrence = $routeItem->getOccurence();

                // get the session related to this route item.
                $store = $session->getAssessmentItemSessionStore();
                $itemSession = $store->getAssessmentItemSession($itemRef, $occurrence);
                $map[] = new Jump($offset, $routeItem, $itemSession);
                $offset++;
            }
        }

        return $map;
    }

    /**
     * Compute the the number of completed items during a given
     * candidate test $session.
     *
     * @param AssessmentTestSession $session
     * @return integer
     */
    public static function testCompletion(AssessmentTestSession $session)
    {
        $completed = $session->numberCompleted();

        if ($session->getCurrentNavigationMode() === NavigationMode::LINEAR && $completed > 0) {
            $completed--;
        }

        return $completed;
    }

    /**
     * Checks if the current test allows the progress bar to be displayed
     * @param AssessmentTestSession $session
     * @param array $testMeta
     * @param array $config
     * @return bool
     */
    public static function considerProgress(AssessmentTestSession $session, array $testMeta, array $config = [])
    {
        $considerProgress = true;

        if (!empty($config['progress-indicator-forced'])) {
            // Caution: this piece of code can introduce a heavy load on very large tests
            // The local optimisation made here concerns:
            // - only check the part branchRules if the progress indicator must be forced for all tests
            // - branchRules check is ignored when the navigation mode is non linear.
            //
            // TODO: Perform this check at compilation time and store a map of parts options.
            //       This can be also done for navigation map (see getNavigatorMap and getJumpsMap)

            $testPart = $session->getCurrentTestPart();
            if ($testPart->getNavigationMode() !== NavigationMode::NONLINEAR) {
                $branchings = $testPart->getComponentsByClassName('branchRule');

                if (count($branchings) > 0) {
                    $considerProgress = false;
                }
            }
        } else {
            if ($testMeta['preConditions'] === true) {
                $considerProgress = false;
            } elseif ($testMeta['branchRules'] === true) {
                $considerProgress = false;
            }
        }

        return $considerProgress;
    }

    /**
     * Checks if the current session can be exited. If a context is pass we use it over the session
     *
     * @param AssessmentTestSession $session
     * @param RunnerServiceContext $context
     * @return bool
     */
    public static function doesAllowExit(AssessmentTestSession $session, RunnerServiceContext $context = null)
    {
        $config = common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('testRunner');
        $exitButton = (isset($config['exitButton']) && $config['exitButton']);
        $categories = self::getCategories($session, $context);
        return ($exitButton && in_array('x-tao-option-exit', $categories));
    }

    /**
     * Checks if the test taker can logout
     *
     * @param AssessmentTestSession $session
     * @return type
     */
    public static function doesAllowLogout(AssessmentTestSession $session)
    {
        $config = common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('testRunner');
        return !(isset($config['exitButton']) && $config['exitButton']);
    }

    /**
     * Get the array of available categories for the current itemRef
     * If we have a non null context we use it over the session
     *
     * @param \qtism\runtime\tests\AssessmentTestSession $session
     * @param RunnerServiceContext $context
     * @return array
     */
    public static function getCategories(AssessmentTestSession $session, RunnerServiceContext $context = null)
    {
        if (!is_null($context)) {
            return $context->getCurrentAssessmentItemRef()->getCategories()->getArrayCopy();
        }
        return $session->getCurrentAssessmentItemRef()->getCategories()->getArrayCopy();
    }


    /**
     * Get the array of available categories for the test
     *
     * @param \qtism\runtime\tests\AssessmentTestSession $session
     * @return array
     */
    public static function getAllCategories(AssessmentTestSession $session)
    {
        $prevCategories = null;
        $assessmentItemRefs = $session->getAssessmentTest()->getComponentsByClassName('assessmentItemRef');

        /** @var \qtism\data\AssessmentItemRef $assessmentItemRef */
        foreach ($assessmentItemRefs as $assessmentItemRef) {
            $categories = $assessmentItemRef->getCategories();
            if (!is_null($prevCategories)) {
                $prevCategories->merge($categories);
            } else {
                $prevCategories = $categories;
            }
        }

        return (!is_null($prevCategories)) ? array_unique($prevCategories->getArrayCopy()) : [];
    }

    /**
     * Whether or not $value is considered as a null QTI value.
     *
     * @param $value
     * @return boolean
     */
    public static function isQtiValueNull($value)
    {
        return is_null($value) === true || ($value instanceof QtiString && $value->getValue() === '') || ($value instanceof Container && count($value) === 0);
    }

    /**
     * Gets the amount of seconds with the microseconds as fractional part from a Duration instance.
     * @param QtiDuration $duration
     * @return float|null
     */
    public static function getDurationWithMicroseconds($duration)
    {
        if ($duration) {
            if (method_exists($duration, 'getMicroseconds')) {
                return $duration->getMicroseconds(true) / 1e6;
            }
            return $duration->getSeconds(true);
        }
        return null;
    }
}