<?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) 2018-2020 (original work) Open Assessment Technologies SA ;
 */

declare(strict_types=1);

namespace oat\taoQtiTestPreviewer\controller;

use Exception;
use common_exception_Error;
use oat\tao\helpers\Base64;
use tao_helpers_Http as HttpHelper;
use oat\taoItems\model\pack\Packer;
use common_Exception as CommonException;
use taoItems_models_classes_ItemsService;
use oat\generis\model\OntologyAwareTrait;
use tao_actions_ServiceModule as ServiceModule;
use oat\taoItems\model\media\ItemMediaResolver;
use oat\taoQtiTestPreviewer\models\ItemPreviewer;
use oat\tao\model\media\sourceStrategy\HttpSource;
use oat\tao\model\routing\AnnotationReader\security;
use common_exception_BadRequest as BadRequestException;
use taoQtiTest_helpers_TestRunnerUtils as TestRunnerUtils;
use oat\taoQtiTestPreviewer\models\PreviewLanguageService;
use common_exception_Unauthorized as UnauthorizedException;
use common_exception_NotImplemented as NotImplementedException;
use common_exception_MissingParameter as MissingParameterException;
use common_exception_NoImplementation as NoImplementationException;
use common_exception_UserReadableException as UserReadableException;
use tao_models_classes_FileNotFoundException as FileNotFoundException;

/**
 * Class Previewer
 *
 * @package oat\taoQtiTestPreviewer\controller
 */
class Previewer extends ServiceModule
{
    use OntologyAwareTrait;

    /**
     * Previewer constructor.
     *
     * @security("hide")
     */
    public function __construct()
    {
        parent::__construct();

        // Prevent anything to be cached by the client.
        TestRunnerUtils::noHttpClientCache();
    }

    /**
     * Initializes the delivery session
     */
    public function init(): void
    {
        $code = 200;

        try {
            $requestParams = $this->getPsrRequest()->getQueryParams();
            $serviceCallId = $requestParams['serviceCallId'];

            $response = [
                'success' => $serviceCallId === 'previewer',
                'itemIdentifier' => null,
                'itemData' => null,
            ];
        } catch (Exception $e) {
            $response = $this->getErrorResponse($e);
            $code = $this->getErrorCode($e);
        }

        $this->returnJson($response, $code);
    }

    /**
     * Provides the definition data and the state for a particular item
     *
     * @param taoItems_models_classes_ItemsService $itemsService
     */
    public function getItem(taoItems_models_classes_ItemsService $itemsService): void
    {
        $code = 200;

        try {
            $requestParams = $this->getPsrRequest()->getQueryParams();

            $itemUri = $requestParams['itemUri'] ?? '';
            $resultId = $requestParams['resultId'] ?? '';

            $response = [
                'baseUrl' => '',
                'content' => [],
            ];

            // Previewing a result.
            if ($resultId !== '') {
                if (!isset($requestParams['itemDefinition'])) {
                    throw new MissingParameterException('itemDefinition', $this->getRequestURI());
                }

                if (!isset($requestParams['deliveryUri'])) {
                    throw new MissingParameterException('deliveryUri', $this->getRequestURI());
                }

                $itemDefinition = $requestParams['itemDefinition'];
                $delivery = $this->getResource($requestParams['deliveryUri']);

                /** @var ItemPreviewer $itemPreviewer */
                $itemPreviewer = $this->getServiceLocator()->get(ItemPreviewer::class);
                $itemPreviewer->setServiceLocator($this->getServiceLocator());

                /** @var PreviewLanguageService $previewLanguageService */
                $previewLanguageService = $this->getServiceLocator()->get(PreviewLanguageService::class);
                $previewLanguage = $previewLanguageService->getPreviewLanguage($delivery->getUri(), $resultId);

                $response['content'] = $itemPreviewer
                    ->setItemDefinition($itemDefinition)
                    ->setUserLanguage($previewLanguage)
                    ->setDelivery($delivery)
                    ->loadCompiledItemData();

                $response['baseUrl'] = $itemPreviewer->getBaseUrl();
            } elseif ($itemUri) {
                $item = $this->getResource($itemUri);
                $lang = $this->getSession()->getDataLanguage();

                if (!$itemsService->hasItemContent($item, $lang)) {
                    $this->returnJson($response, $code);
                    return;
                }

                $packer = new Packer($item, $lang);
                $packer->setServiceLocator($this->getServiceLocator());

                $itemPack = $packer->pack();
                $response['content'] = $itemPack->JsonSerialize();
                $response['baseUrl'] = _url('asset', null, null, [
                    'uri' => $itemUri,
                    'path' => '',
                ]);
            } else {
                throw new BadRequestException('Either itemUri or resultId needs to be provided.');
            }

            $response['success'] = true;
        } catch (Exception $e) {
            $response = $this->getErrorResponse($e);
            $code = $this->getErrorCode($e);
        }

        $this->returnJson($response, $code);
    }

    /**
     * Gets access to an asset
     *
     * @throws CommonException
     * @throws FileNotFoundException
     * @throws common_exception_Error
     */
    public function asset(): void
    {
        $requestParams = $this->getPsrRequest()->getQueryParams();

        $item = $this->getResource($requestParams['uri']);
        $lang = $this->getSession()->getDataLanguage();
        $resolver = new ItemMediaResolver($item, $lang);

        $asset = $resolver->resolve($requestParams['path']);
        $mediaSource = $asset->getMediaSource();
        $mediaIdentifier = $asset->getMediaIdentifier();

        if ($mediaSource instanceof HttpSource || Base64::isEncodedImage($mediaIdentifier)) {
            throw new CommonException('Only tao files available for rendering through item preview');
        }

        $info = $mediaSource->getFileInfo($mediaIdentifier);
        $stream = $mediaSource->getFileStream($mediaIdentifier);

        HttpHelper::returnStream($stream, $info['mime']);
    }

    /**
     * Stores the state object and the response set of a particular item
     */
    public function submitItem(): void
    {
        $code = 200;

        try {
            $requestParams = $this->getPsrRequest()->getQueryParams();
            $itemUri = $requestParams['itemUri'];
            $jsonPayload = $this->getPayload();
            $response = $this->getItemPreviewer()->processResponses($itemUri, $jsonPayload);
        } catch (Exception $e) {
            $response = $this->getErrorResponse($e);
            $code = $this->getErrorCode($e);
        }

        $this->returnJson($response, $code);
    }

    /**
     * Gets an error response array
     *
     * @param Exception $e
     *
     * @return array
     */
    protected function getErrorResponse(Exception $e): array
    {
        $response = [
            'success' => false,
            'type' => 'error',
        ];

        if ($e instanceof FileNotFoundException) {
            $response['type'] = 'FileNotFound';
            $response['message'] = __('File not found');
        } elseif ($e instanceof UnauthorizedException) {
            $response['code'] = 403;
            $response['message'] = $e->getUserMessage();
        } elseif ($e instanceof UserReadableException) {
            $response['message'] = $e->getUserMessage();
        } elseif ($e instanceof Exception) {
            $response['type'] = 'exception';
            $response['code'] = $e->getCode();
            $response['message'] = $e->getMessage();
        } else {
            $response['message'] = __('An error occurred!');
        }

        return $response;
    }

    /**
     * Gets an HTTP response code
     *
     * @param Exception $e
     *
     * @return int
     */
    protected function getErrorCode(Exception $e): int
    {
        switch (true) {
            case $e instanceof NotImplementedException:
            case $e instanceof NoImplementationException:
            case $e instanceof UnauthorizedException:
                $code = 403;
                break;

            case $e instanceof FileNotFoundException:
                $code = 404;
                break;

            default:
                $code = 500;
                break;
        }

        return $code;
    }

    /**
     * @return ItemPreviewer
     */
    private function getItemPreviewer(): ItemPreviewer
    {
        /** @var ItemPreviewer $itemPreviewer */
        $itemPreviewer = $this->getServiceLocator()->get(ItemPreviewer::class);

        return $itemPreviewer;
    }

    /**
     * Gets payload from the request
     *
     * @return array|mixed|object|null
     */
    private function getPayload()
    {
        $jsonPayload = $this->getPsrRequest()->getParsedBody();

        return json_decode($jsonPayload['itemResponse'], true);
    }
}