<?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) 2013-2016 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
 *
 */

use oat\tao\model\state\StateStorage;
use qtism\common\storage\IStream;
use qtism\runtime\tests\AbstractSessionManager;
use qtism\common\storage\MemoryStream;
use qtism\runtime\storage\binary\BinaryAssessmentTestSeeker;
use qtism\runtime\storage\binary\AbstractQtiBinaryStorage;
use qtism\runtime\storage\common\StorageException;
use qtism\data\AssessmentTest;
use qtism\runtime\tests\AssessmentTestSession;
use qtism\runtime\storage\binary\QtiBinaryStreamAccess;
use oat\taoQtiTest\models\files\QtiFlysystemFileManager;
use oat\oatbox\service\ServiceManager;
use oat\oatbox\service\ServiceManagerAwareInterface;
use oat\oatbox\service\ServiceManagerAwareTrait;

/**
 * A QtiSm AssessmentTestSession Storage Service implementation for TAO.
 *
 * It is able to retrieve test sessions related to a given user and a given
 * test definition.
 *
 * @author Jérôme Bogaerts <jerome@taotesting.com>
 *
 */
class taoQtiTest_helpers_TestSessionStorage extends AbstractQtiBinaryStorage implements ServiceManagerAwareInterface
{
    use ServiceManagerAwareTrait;
    use oat\oatbox\mutex\LockTrait;

    /**
     * The last recorded error.
     *
     * @var integer
     */
    private $lastError = -1;
   
    /**
     * The URI (Uniform Resource Identifier) of the user the Test Session belongs to.
     *
     * @var string
     */
    private $userUri;

    /**
     * @var AssessmentTestSession
     */
    private static $session;

    /**
     * Create a new TestSessionStorage object.
     *
     * @param AbstractSessionManager $manager The session manager to be used to create new AssessmentTestSession and AssessmentItemSession objects.
     * @param BinaryAssessmentTestSeeker $seeker The seeker making able the storage engine to index AssessmentTest's components.
     * @param string $userUri The URI (Uniform Resource Identifier) of the user the Test Session belongs to.
     */
    public function __construct(AbstractSessionManager $manager, BinaryAssessmentTestSeeker $seeker, $userUri)
    {
        parent::__construct($manager, $seeker);
        $this->setUserUri($userUri);
    }
   
    /**
     * Get the last retrieved error. -1 means
     * no error.
     *
     * @return integer
     */
    public function getLastError()
    {
        return $this->lastError;
    }
   
    /**
     * Set the last retrieved error. -1 means
     * no error.
     *
     * @param integer $lastError
     */
    public function setLastError($lastError)
    {
        $this->lastError = $lastError;
    }
   
    /**
     * Get the URI (Uniform Resource Identifier) of the user the Test Session belongs to.
     *
     * @return string
     */
    public function getUserUri()
    {
        return $this->userUri;
    }
   
    /**
     * Set the URI (Uniform Resource Identifier) of the user the Test Session belongs to.
     *
     * @param string $userUri
     */
    public function setUserUri($userUri)
    {
        $this->userUri = $userUri;
    }

    /**
     * @param AssessmentTest $test
     * @param string $sessionId
     * @param bool $forReadingOnly
     * @return AssessmentTestSession
     * @throws StorageException
     */
    public function retrieve(AssessmentTest $test, $sessionId, $forReadingOnly = false)
    {
        if ($forReadingOnly === false) {
            return $this->retrieveSessionInWriteMode($test, $sessionId);
        } else {
            return $this->retrieveSessionInReadMode($test, $sessionId);
        }
    }

    /**
     * @param AssessmentTest $test
     * @param string $sessionId
     * @return taoQtiTest_helpers_TestSession
     * @throws StorageException
     */
    private function retrieveSessionInReadMode(AssessmentTest $test, string $sessionId): taoQtiTest_helpers_TestSession
    {
        if (!$this->sessionExists($sessionId)) {
            $this->setLastError(-1);
            self::$session = parent::retrieve($test, $sessionId);
            self::$session->setReadOnly(true);
        }

        return self::$session;
    }

    /**
     * @param AssessmentTest $test
     * @param string $sessionId
     * @return taoQtiTest_helpers_TestSession
     * @throws StorageException
     */
    private function retrieveSessionInWriteMode(AssessmentTest $test, string $sessionId): taoQtiTest_helpers_TestSession
    {
        if ($this->sessionExists($sessionId) && self::$session->isLocked()) {
            return self::$session;
        }

        $this->setLastError(-1);
        self::$session = parent::retrieve($test, $sessionId);
        $this->lockSession(self::$session);

        return self::$session;
    }

    /**
     * @param AssessmentTest $test
     * @param string $sessionId
     * @return AssessmentTestSession
     * @throws StorageException
     */
    public function instantiate(AssessmentTest $test, $sessionId = '')
    {
        $session = parent::instantiate($test, $sessionId);
        $this->lockSession($session);
        return $session;
    }

    /**
     * @param AssessmentTestSession $assessmentTestSession
     * @throws StorageException
     */
    public function persist(AssessmentTestSession $assessmentTestSession)
    {
        if ($assessmentTestSession->isReadOnly()) {
            throw new StorageException(
                'Readonly test session cannot be stored. Test session id: ' . $assessmentTestSession->getSessionId(),
                StorageException::PERSITANCE
            );
        }
        parent::persist($assessmentTestSession);
    }

    /**
     * @param AssessmentTestSession $session
     */
    private function lockSession(AssessmentTestSession $session)
    {
        if ($session->isLocked()) {
            return;
        }

        $lock = $this->createLock('AssessmentTestSession_' . $session->getSessionId(), 30);
        $lock->acquire(true);
        $session->setReadOnly(false);
        $session->setLock($lock);
    }

    protected function getRetrievalStream($sessionId)
    {
    
        $storageService = $this->getServiceLocator()->get(tao_models_classes_service_StateStorage::SERVICE_ID);
        $userUri = $this->getUserUri();
       
        if (is_null($userUri) === true) {
            $msg = "Could not retrieve current user URI.";
            throw new StorageException($msg, StorageException::RETRIEVAL);
        }

        $data = $storageService->get($userUri, $sessionId);
       
        $stateEmpty = (empty($data) === true);
        $stream = new MemoryStream(($stateEmpty === true) ? '' : $data);
        $stream->open();
       
        if ($stateEmpty === false) {
            // Consume additional error (short signed integer).
            $this->setLastError($stream->read(2));
        }
       
        $stream->close();
        return $stream;
    }
   
    protected function persistStream(AssessmentTestSession $assessmentTestSession, MemoryStream $stream)
    {
        /** @var tao_models_classes_service_StateStorage $storageService */
        $storageService = $this->getServiceLocator()->get(tao_models_classes_service_StateStorage::SERVICE_ID);;
        $userUri = $this->getUserUri();
       
        if (is_null($userUri) === true) {
            $msg = "Could not retrieve current user URI.";
            throw new StorageException($msg, StorageException::RETRIEVAL);
        }

        $data = $this->getLastError() . $stream->getBinary();
        if (!$storageService->set($userUri, $assessmentTestSession->getSessionId(), $data)) {
            throw new StorageException('Can\'t write into storage at ' . static::class);
        }
    }
   
    public function exists($sessionId)
    {
        $storageService = $this->getServiceLocator()->get(tao_models_classes_service_StateStorage::SERVICE_ID);
        $userUri = $this->getUserUri();
       
        if (is_null($userUri) === true) {
            $msg = "Could not retrieve current user URI.";
            throw new StorageException($msg, StorageException::RETRIEVAL);
        }
       
        return $storageService->has($userUri, $sessionId);
    }

    /**
     * @param string $sessionId
     * @return bool
     */
    public function delete($sessionId)
    {
        /** @var StateStorage $storageService */
        $storageService = ServiceManager::getServiceManager()->get(StateStorage::SERVICE_ID);

        return $storageService->del($this->getUserUri(), $sessionId);
    }

    protected function createBinaryStreamAccess(IStream $stream)
    {
        return new QtiBinaryStreamAccess(
            $stream,
            $this->getServiceLocator()->get(QtiFlysystemFileManager::SERVICE_ID)
        );
    }

    public function getServiceLocator()
    {
        if ($this->serviceLocator === null) {
            return ServiceManager::getServiceManager();
        }
        return $this->serviceLocator;
    }

    /**
     * @param string $sessionId
     * @return bool
     */
    private function sessionExists(string $sessionId): bool
    {
        return self::$session && self::$session->getSessionId() === $sessionId;
    }

    /**
     * @param $forReadingOnly
     * @return bool
     */
    private function assessModeChangedToWrite($forReadingOnly): bool
    {
        return !$forReadingOnly && self::$session->isReadOnly();
    }
}