<?php
/*
 * Copyright (C) 2025 Xibo Signage Ltd
 *
 * Xibo - Digital Signage - https://xibosignage.com
 *
 * This file is part of Xibo.
 *
 * Xibo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * Xibo 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Xibo.  If not, see <http://www.gnu.org/licenses/>.
 */
namespace Xibo\Controller;

use Carbon\Carbon;
use GuzzleHttp\Client;
use Illuminate\Support\Str;
use Intervention\Image\ImageManagerStatic as Img;
use Psr\Http\Message\ResponseInterface;
use Respect\Validation\Validator as v;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Connector\ProviderDetails;
use Xibo\Connector\ProviderImport;
use Xibo\Entity\Media;
use Xibo\Entity\SearchResult;
use Xibo\Entity\SearchResults;
use Xibo\Event\LibraryProviderEvent;
use Xibo\Event\LibraryProviderImportEvent;
use Xibo\Event\LibraryProviderListEvent;
use Xibo\Event\MediaDeleteEvent;
use Xibo\Event\MediaFullLoadEvent;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\FolderFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\PermissionFactory;
use Xibo\Factory\PlaylistFactory;
use Xibo\Factory\ScheduleFactory;
use Xibo\Factory\TagFactory;
use Xibo\Factory\UserFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Factory\WidgetFactory;
use Xibo\Helper\ByteFormatter;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Environment;
use Xibo\Helper\HttpsDetect;
use Xibo\Helper\LinkSigner;
use Xibo\Helper\XiboUploadHandler;
use Xibo\Service\MediaService;
use Xibo\Service\MediaServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\LibraryFullException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Widget\Render\WidgetDownloader;

/**
 * Class Library
 * @package Xibo\Controller
 */
class Library extends Base
{
    /** @var EventDispatcherInterface */
    private $dispatcher;

    /**
     * @var UserFactory
     */
    private $userFactory;

    /**
     * @var ModuleFactory
     */
    private $moduleFactory;

    /**
     * @var TagFactory
     */
    private $tagFactory;

    /**
     * @var MediaFactory
     */
    private $mediaFactory;

    /**
     * @var WidgetFactory
     */
    private $widgetFactory;

    /**
     * @var PlaylistFactory
     */
    private $playlistFactory;

    /**
     * @var LayoutFactory
     */
    private $layoutFactory;

    /**
     * @var PermissionFactory
     */
    private $permissionFactory;

    /**
     * @var UserGroupFactory
     */
    private $userGroupFactory;

    /** @var  DisplayFactory */
    private $displayFactory;

    /** @var ScheduleFactory  */
    private $scheduleFactory;

    /** @var FolderFactory */
    private $folderFactory;
    /**
     * @var MediaServiceInterface
     */
    private $mediaService;

    /**
     * Set common dependencies.
     * @param UserFactory $userFactory
     * @param ModuleFactory $moduleFactory
     * @param TagFactory $tagFactory
     * @param MediaFactory $mediaFactory
     * @param WidgetFactory $widgetFactory
     * @param PermissionFactory $permissionFactory
     * @param LayoutFactory $layoutFactory
     * @param PlaylistFactory $playlistFactory
     * @param UserGroupFactory $userGroupFactory
     * @param DisplayFactory $displayFactory
     * @param ScheduleFactory $scheduleFactory
     * @param FolderFactory $folderFactory
     */
    public function __construct(
        $userFactory,
        $moduleFactory,
        $tagFactory,
        $mediaFactory,
        $widgetFactory,
        $permissionFactory,
        $layoutFactory,
        $playlistFactory,
        $userGroupFactory,
        $displayFactory,
        $scheduleFactory,
        $folderFactory
    ) {
        $this->moduleFactory = $moduleFactory;
        $this->mediaFactory = $mediaFactory;
        $this->widgetFactory = $widgetFactory;
        $this->userFactory = $userFactory;
        $this->tagFactory = $tagFactory;
        $this->permissionFactory = $permissionFactory;
        $this->layoutFactory = $layoutFactory;
        $this->playlistFactory = $playlistFactory;
        $this->userGroupFactory = $userGroupFactory;
        $this->displayFactory = $displayFactory;
        $this->scheduleFactory = $scheduleFactory;
        $this->folderFactory = $folderFactory;
    }

    /**
     * Get Module Factory
     * @return ModuleFactory
     */
    public function getModuleFactory()
    {
        return $this->moduleFactory;
    }

    /**
     * Get Media Factory
     * @return MediaFactory
     */
    public function getMediaFactory()
    {
        return $this->mediaFactory;
    }

    /**
     * Get Permission Factory
     * @return PermissionFactory
     */
    public function getPermissionFactory()
    {
        return $this->permissionFactory;
    }

    /**
     * Get Widget Factory
     * @return WidgetFactory
     */
    public function getWidgetFactory()
    {
        return $this->widgetFactory;
    }

    /**
     * Get Layout Factory
     * @return LayoutFactory
     */
    public function getLayoutFactory()
    {
        return $this->layoutFactory;
    }

    /**
     * Get Playlist Factory
     * @return PlaylistFactory
     */
    public function getPlaylistFactory()
    {
        return $this->playlistFactory;
    }

    /**
     * @return TagFactory
     */
    public function getTagFactory()
    {
        return $this->tagFactory;
    }

    /**
     * @return FolderFactory
     */
    public function getFolderFactory()
    {
        return $this->folderFactory;
    }

    public function useMediaService(MediaServiceInterface $mediaService)
    {
        $this->mediaService = $mediaService;
    }

    public function getMediaService()
    {
        return $this->mediaService->setUser($this->getUser());
    }

    /**
     * Displays the page logic
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws GeneralException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function displayPage(Request $request, Response $response)
    {
        $sanitizedParams = $this->getSanitizer($request->getQueryParams());
        $mediaId = $sanitizedParams->getInt('mediaId');

        if ($mediaId !== null) {
            $media = $this->mediaFactory->getById($mediaId);
            if (!$this->getUser()->checkViewable($media)) {
                throw new AccessDeniedException();
            }

            // Thumbnail
            $module = $this->moduleFactory->getByType($media->mediaType);
            $media->setUnmatchedProperty('thumbnail', '');
            if ($module->hasThumbnail) {
                $media->setUnmatchedProperty(
                    'thumbnail',
                    $this->urlFor($request, 'library.download', [
                        'id' => $media->mediaId
                    ], [
                        'preview' => 1
                    ])
                );
            }
            $media->setUnmatchedProperty('fileSizeFormatted', ByteFormatter::format($media->fileSize));

            $this->getState()->template = 'library-direct-media-details';
            $this->getState()->setData([
                'media' => $media
            ]);
        } else {
            // Users we have permission to see
            $this->getState()->template = 'library-page';
            $this->getState()->setData([
                'modules' => $this->moduleFactory->getLibraryModules(),
                'validExt' => implode('|', $this->moduleFactory->getValidExtensions([]))
            ]);
        }

        return $this->render($request, $response);
    }

    /**
     * Set Enable Stats Collection of a media
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @throws \Xibo\Support\Exception\DuplicateEntityException
     * @SWG\Put(
     *  path="/library/setenablestat/{mediaId}",
     *  operationId="mediaSetEnableStat",
     *  tags={"library"},
     *  summary="Enable Stats Collection",
     *  description="Set Enable Stats Collection? to use for the collection of Proof of Play statistics for a media.",
     *  @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="enableStat",
     *      in="formData",
     *      description="The option to enable the collection of Media Proof of Play statistics",
     *      type="string",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=204,
     *      description="successful operation"
     *  )
     * )
     */
    public function setEnableStat(Request $request, Response $response, $id)
    {
        // Get the Media
        $media = $this->mediaFactory->getById($id);

        // Check Permissions
        if (!$this->getUser()->checkViewable($media)) {
            throw new AccessDeniedException();
        }

        $enableStat = $this->getSanitizer($request->getParams())->getString('enableStat');

        $media->enableStat = $enableStat;
        $media->save(['saveTags' => false]);

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 204,
            'message' => sprintf(__('For Media %s Enable Stats Collection is set to %s'), $media->name, __($media->enableStat))
        ]);

        return $this->render($request, $response);
    }

    /**
     * Set Enable Stat Form
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function setEnableStatForm(Request $request, Response $response, $id)
    {
        // Get the Media
        $media = $this->mediaFactory->getById($id);

        // Check Permissions
        if (!$this->getUser()->checkViewable($media)) {
            throw new AccessDeniedException();
        }

        $data = [
            'media' => $media,
        ];

        $this->getState()->template = 'library-form-setenablestat';
        $this->getState()->setData($data);

        return $this->render($request, $response);
    }

    /**
     * Prints out a Table of all media items
     *
     * @SWG\Get(
     *  path="/library",
     *  operationId="librarySearch",
     *  tags={"library"},
     *  summary="Library Search",
     *  description="Search the Library for this user",
     *  @SWG\Parameter(
     *      name="mediaId",
     *      in="query",
     *      description="Filter by Media Id",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="media",
     *      in="query",
     *      description="Filter by Media Name",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="type",
     *      in="query",
     *      description="Filter by Media Type",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="ownerId",
     *      in="query",
     *      description="Filter by Owner Id",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="retired",
     *      in="query",
     *      description="Filter by Retired",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="tags",
     *      in="query",
     *      description="Filter by Tags - comma seperated",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="exactTags",
     *      in="query",
     *      description="A flag indicating whether to treat the tags filter as an exact match",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="logicalOperator",
     *      in="query",
     *      description="When filtering by multiple Tags, which logical operator should be used? AND|OR",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="duration",
     *      in="query",
     *      description="Filter by Duration - a number or less-than,greater-than,less-than-equal or great-than-equal followed by a | followed by a number",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="fileSize",
     *      in="query",
     *      description="Filter by File Size - a number or less-than,greater-than,less-than-equal or great-than-equal followed by a | followed by a number",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="ownerUserGroupId",
     *      in="query",
     *      description="Filter by users in this UserGroupId",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="folderId",
     *      in="query",
     *      description="Filter by Folder ID",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="isReturnPublicUrls",
     *      in="query",
     *      description="Should the thumbail URLs be authenticated S3 style public URL, default = false",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(
     *          type="array",
     *          @SWG\Items(ref="#/definitions/Media")
     *      )
     *  )
     * )
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function grid(Request $request, Response $response)
    {
        $user = $this->getUser();

        $parsedQueryParams = $this->getSanitizer($request->getQueryParams());

        // Variables used for link signing
        $isReturnPublicUrls = $parsedQueryParams->getCheckbox('isReturnPublicUrls') == 1;
        $thumbnailRouteName = $isReturnPublicUrls ? 'library.public.thumbnail' : 'library.thumbnail';
        $encryptionKey = $this->getConfig()->getApiKeyDetails()['encryptionKey'];
        $rootUrl = (new HttpsDetect())->getUrl();

        // Construct the SQL
        $mediaList = $this->mediaFactory->query($this->gridRenderSort($parsedQueryParams), $this->gridRenderFilter([
            'mediaId' => $parsedQueryParams->getInt('mediaId'),
            'name' => $parsedQueryParams->getString('media'),
            'useRegexForName' => $parsedQueryParams->getCheckbox('useRegexForName'),
            'nameExact' => $parsedQueryParams->getString('nameExact'),
            'type' => $parsedQueryParams->getString('type'),
            'types' => $parsedQueryParams->getArray('types'),
            'tags' => $parsedQueryParams->getString('tags'),
            'exactTags' => $parsedQueryParams->getCheckbox('exactTags'),
            'ownerId' => $parsedQueryParams->getInt('ownerId'),
            'retired' => $parsedQueryParams->getInt('retired'),
            'duration' => $parsedQueryParams->getInt('duration'),
            'fileSize' => $parsedQueryParams->getString('fileSize'),
            'ownerUserGroupId' => $parsedQueryParams->getInt('ownerUserGroupId'),
            'assignable' => $parsedQueryParams->getInt('assignable'),
            'folderId' => $parsedQueryParams->getInt('folderId'),
            'onlyMenuBoardAllowed' => $parsedQueryParams->getInt('onlyMenuBoardAllowed'),
            'layoutId' => $parsedQueryParams->getInt('layoutId'),
            'includeLayoutBackgroundImage' => ($parsedQueryParams->getInt('layoutId') != null) ? 1 : 0,
            'orientation' => $parsedQueryParams->getString('orientation', ['defaultOnEmptyString' => true]),
            'logicalOperator' => $parsedQueryParams->getString('logicalOperator'),
            'logicalOperatorName' => $parsedQueryParams->getString('logicalOperatorName'),
            'unreleasedOnly' => $parsedQueryParams->getCheckbox('unreleasedOnly'),
            'unusedOnly' => $parsedQueryParams->getCheckbox('unusedOnly'),
        ], $parsedQueryParams));

        // Add some additional row content
        foreach ($mediaList as $media) {
            $media->setUnmatchedProperty('revised', ($media->parentId != 0) ? 1 : 0);

            // Thumbnail
            $media->setUnmatchedProperty('thumbnail', '');
            try {
                $module = $this->moduleFactory->getByType($media->mediaType);
                if ($module->hasThumbnail) {
                    $renderThumbnail = true;
                    // for video, check if the cover image exists here.
                    if ($media->mediaType === 'video') {
                        $libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION');
                        $renderThumbnail = file_exists($libraryLocation . $media->mediaId . '_videocover.png');
                    }

                    if ($renderThumbnail) {
                        $thumbnailUrl = $this->urlFor($request, $thumbnailRouteName, [
                            'id' => $media->mediaId,
                        ]);

                        if ($isReturnPublicUrls) {
                            // If we are coming from the API we should remove the /api part of the URL
                            if ($this->isApi($request)) {
                                $thumbnailUrl = str_replace('/api/', '/', $thumbnailUrl);
                            }

                            // Sign the link.
                            $thumbnailUrl = $rootUrl . $thumbnailUrl . '?' . LinkSigner::getSignature(
                                $rootUrl,
                                $thumbnailUrl,
                                time() + 3600,
                                $encryptionKey,
                            );
                        }

                        $media->setUnmatchedProperty('thumbnail', $thumbnailUrl);
                    }
                }
            } catch (NotFoundException) {
                $this->getLog()->error('Module ' . $media->mediaType . ' not found');
            }

            $media->setUnmatchedProperty('fileSizeFormatted', ByteFormatter::format($media->fileSize));

            // Media expiry
            $media->setUnmatchedProperty('mediaExpiresIn', __('Expires %s'));
            $media->setUnmatchedProperty('mediaExpiryFailed', __('Expired '));
            $media->setUnmatchedProperty('mediaNoExpiryDate', __('Never'));

            if ($this->isApi($request)) {
                $media->excludeProperty('mediaExpiresIn');
                $media->excludeProperty('mediaExpiryFailed');
                $media->excludeProperty('mediaNoExpiryDate');
                $media->expires = ($media->expires == 0)
                    ? 0
                    : Carbon::createFromTimestamp($media->expires)->format(DateFormatHelper::getSystemFormat());
                continue;
            }

            $media->includeProperty('buttons');

            switch ($media->released) {
                case 1:
                    $media->setUnmatchedProperty('releasedDescription', '');
                    break;

                case 2:
                    $media->setUnmatchedProperty(
                        'releasedDescription',
                        __('The uploaded image is too large and cannot be processed, please use another image.')
                    );
                    break;

                default:
                    $media->setUnmatchedProperty(
                        'releasedDescription',
                        __('This image will be resized according to set thresholds and limits.')
                    );
            }

            switch ($media->enableStat) {
                case 'On':
                    $media->setUnmatchedProperty(
                        'enableStatDescription',
                        __('This Media has enable stat collection set to ON')
                    );
                    break;

                case 'Off':
                    $media->setUnmatchedProperty(
                        'enableStatDescription',
                        __('This Media has enable stat collection set to OFF')
                    );
                    break;

                default:
                    $media->setUnmatchedProperty(
                        'enableStatDescription',
                        __('This Media has enable stat collection set to INHERIT')
                    );
            }

            if ($parsedQueryParams->getCheckbox('fullScreenScheduleCheck')) {
                $fullScreenCampaignId = $this->hasFullScreenLayout($media);
                $media->setUnmatchedProperty('hasFullScreenLayout', (!empty($fullScreenCampaignId)));
                $media->setUnmatchedProperty('fullScreenCampaignId', $fullScreenCampaignId);
            }

            $media->buttons = [];

            // Buttons
            if ($this->getUser()->featureEnabled('library.modify')
                && $user->checkEditable($media)
            ) {
                // Edit
                $media->buttons[] = array(
                    'id' => 'content_button_edit',
                    'url' => $this->urlFor($request, 'library.edit.form', ['id' => $media->mediaId]),
                    'text' => __('Edit')
                );

                // Copy Button
                $media->buttons[] = array(
                    'id' => 'media_button_copy',
                    'url' => $this->urlFor($request, 'library.copy.form', ['id' => $media->mediaId]),
                    'text' => __('Copy')
                );

                // Select Folder
                if ($this->getUser()->featureEnabled('folder.view')) {
                    $media->buttons[] = [
                        'id' => 'library_button_selectfolder',
                        'url' => $this->urlFor($request, 'library.selectfolder.form', ['id' => $media->mediaId]),
                        'text' => __('Select Folder'),
                        'multi-select' => true,
                        'dataAttributes' => [
                            [
                                'name' => 'commit-url', 'value' => $this->urlFor($request, 'library.selectfolder', [
                                    'id' => $media->mediaId
                                ])
                            ],
                            ['name' => 'commit-method', 'value' => 'put'],
                            ['name' => 'id', 'value' => 'library_button_selectfolder'],
                            ['name' => 'text', 'value' => __('Move to Folder')],
                            ['name' => 'rowtitle', 'value' => $media->name],
                            ['name' => 'form-callback', 'value' => 'moveFolderMultiSelectFormOpen']
                        ]
                    ];
                }
            }

            if ($this->getUser()->featureEnabled('library.modify')
                && $user->checkDeleteable($media)
            ) {
                // Delete Button
                $media->buttons[] = [
                    'id' => 'content_button_delete',
                    'url' => $this->urlFor($request,'library.delete.form', ['id' => $media->mediaId]),
                    'text' => __('Delete'),
                    'multi-select' => true,
                    'dataAttributes' => [
                        ['name' => 'commit-url', 'value' => $this->urlFor($request,'library.delete', ['id' => $media->mediaId])],
                        ['name' => 'commit-method', 'value' => 'delete'],
                        ['name' => 'id', 'value' => 'content_button_delete'],
                        ['name' => 'text', 'value' => __('Delete')],
                        ['name' => 'sort-group', 'value' => 1],
                        ['name' => 'rowtitle', 'value' => $media->name],
                        ['name' => 'form-callback', 'value' => 'setDefaultMultiSelectFormOpen']
                    ]
                ];
            }

            if ($this->getUser()->featureEnabled('library.modify')
                && $user->checkPermissionsModifyable($media)
            ) {
                // Permissions
                $media->buttons[] = [
                    'id' => 'content_button_permissions',
                    'url' => $this->urlFor($request,'user.permissions.form', ['entity' => 'Media', 'id' => $media->mediaId]),
                    'text' => __('Share'),
                    'multi-select' => true,
                    'dataAttributes' => [
                        ['name' => 'commit-url', 'value' => $this->urlFor($request,'user.permissions.multi', ['entity' => 'Media', 'id' => $media->mediaId])],
                        ['name' => 'commit-method', 'value' => 'post'],
                        ['name' => 'id', 'value' => 'content_button_permissions'],
                        ['name' => 'text', 'value' => __('Share')],
                        ['name' => 'rowtitle', 'value' => $media->name],
                        ['name' => 'sort-group', 'value' => 2],
                        ['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
                        ['name' => 'custom-handler-url', 'value' => $this->urlFor($request,'user.permissions.multi.form', ['entity' => 'Media'])],
                        ['name' => 'content-id-name', 'value' => 'mediaId']
                    ]
                ];
            }

            // Download
            // No feature permissions here, anyone can get a file based on sharing.
            $media->buttons[] = ['divider' => true];
            $media->buttons[] = array(
                'id' => 'content_button_download',
                'linkType' => '_self', 'external' => true,
                'url' => $this->urlFor($request, 'library.download', ['id' => $media->mediaId]) . '?attachment=' . urlencode($media->fileName),
                'text' => __('Download')
            );

            // Set Enable Stat
            if ($this->getUser()->featureEnabled('library.modify')
                && $this->getUser()->checkEditable($media)
            ) {
                $media->buttons[] = ['divider' => true];

                $media->buttons[] = array(
                    'id' => 'library_button_setenablestat',
                    'url' => $this->urlFor($request,'library.setenablestat.form', ['id' => $media->mediaId]),
                    'text' => __('Enable stats collection?'),
                    'multi-select' => true,
                    'dataAttributes' => array(
                        array('name' => 'commit-url', 'value' => $this->urlFor($request,'library.setenablestat', ['id' => $media->mediaId])),
                        array('name' => 'commit-method', 'value' => 'put'),
                        array('name' => 'id', 'value' => 'library_button_setenablestat'),
                        array('name' => 'text', 'value' => __('Enable stats collection?')),
                        array('name' => 'rowtitle', 'value' => $media->name),
                        ['name' => 'form-callback', 'value' => 'setEnableStatMultiSelectFormOpen']
                    )
                );
            }

            if ($this->getUser()->featureEnabled(['schedule.view', 'layout.view'])) {
                $media->buttons[] = ['divider' => true];

                $media->buttons[] = array(
                    'id' => 'usage_report_button',
                    'url' => $this->urlFor($request, 'library.usage.form', ['id' => $media->mediaId]),
                    'text' => __('Usage Report')
                );
            }

            // Schedule
            if ($this->getUser()->featureEnabled('schedule.add')
                && in_array($media->mediaType, ['image', 'video'])
                && ($this->getUser()->checkEditable($media)
                    || $this->getConfig()->getSetting('SCHEDULE_WITH_VIEW_PERMISSION') == 1)
            ) {
                $media->buttons[] = [
                    'id' => 'library_button_schedule',
                    'url' => $this->urlFor(
                        $request,
                        'schedule.add.form',
                        ['id' => $media->mediaId, 'from' => 'Library']
                    ),
                    'text' => __('Schedule')
                ];
            }
        }

        $this->getState()->template = 'grid';
        $this->getState()->recordsTotal = $this->mediaFactory->countLast();
        $this->getState()->setData($mediaList);

        return $this->render($request, $response);
    }

    /**
     * @SWG\Get(
     *  path="/library/search",
     *  operationId="librarySearchAll",
     *  tags={"library"},
     *  summary="Library Search All",
     *  description="Search all library files from local and connectors",
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(
     *          type="array",
     *          @SWG\Items(ref="#/definitions/SearchResult")
     *      )
     *  )
     * )
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws GeneralException
     * @throws NotFoundException
     */
    public function search(Request $request, Response $response): Response
    {
        $parsedQueryParams = $this->getSanitizer($request->getQueryParams());
        $provider = $parsedQueryParams->getString('provider', ['default' => 'local']);

        $searchResults = new SearchResults();
        if ($provider === 'local') {
            // Sorting options.
            // only allow from a preset list
            $sortCol = match ($parsedQueryParams->getString('sortCol')) {
                'mediaId' => '`media`.`mediaId`',
                'orientation' => '`media`.`orientation`',
                'width' => '`media`.`width`',
                'height' => '`media`.`height`',
                'duration' => '`media`.`duration`',
                'fileSize' => '`media`.`fileSize`',
                'createdDt' => '`media`.`createdDt`',
                'modifiedDt' => '`media`.`modifiedDt`',
                default => '`media`.`name`',
            };
            $sortDir = match ($parsedQueryParams->getString('sortDir')) {
                'DESC' => ' DESC',
                default => ' ASC'
            };

            $mediaList = $this->mediaFactory->query([$sortCol . $sortDir], $this->gridRenderFilter([
                'name' => $parsedQueryParams->getString('media'),
                'useRegexForName' => $parsedQueryParams->getCheckbox('useRegexForName'),
                'nameExact' => $parsedQueryParams->getString('nameExact'),
                'type' => $parsedQueryParams->getString('type'),
                'types' => $parsedQueryParams->getArray('types'),
                'tags' => $parsedQueryParams->getString('tags'),
                'exactTags' => $parsedQueryParams->getCheckbox('exactTags'),
                'ownerId' => $parsedQueryParams->getInt('ownerId'),
                'folderId' => $parsedQueryParams->getInt('folderId'),
                'assignable' => 1,
                'retired' => 0,
                'orientation' => $parsedQueryParams->getString('orientation', ['defaultOnEmptyString' => true])
            ], $parsedQueryParams));

            // Add some additional row content
            foreach ($mediaList as $media) {
                $searchResult = new SearchResult();
                $searchResult->id = $media->mediaId;
                $searchResult->source = 'local';
                $searchResult->type = $media->mediaType;
                $searchResult->title = $media->name;
                $searchResult->width = $media->width;
                $searchResult->height = $media->height;
                $searchResult->description = '';
                $searchResult->duration = $media->duration;

                // Thumbnail
                $module = $this->moduleFactory->getByType($media->mediaType);
                if ($module->hasThumbnail) {
                    $searchResult->thumbnail = $this->urlFor($request, 'library.download', [
                            'id' => $media->mediaId
                        ], [
                            'preview' => 1,
                            'isThumb' => 1
                        ]);
                }

                // Add the result
                $searchResults->data[] = $searchResult;
            }
        } else {
            $this->getLog()->debug('Dispatching event, for provider ' . $provider);

            // Do we have a type filter
            $types = $parsedQueryParams->getArray('types');
            $type = $parsedQueryParams->getString('type');
            if ($type !== null) {
                $types[] = $type;
            }

            // Hand off to any other providers that may want to provide results.
            $event = new LibraryProviderEvent(
                $searchResults,
                $parsedQueryParams->getInt('start', ['default' => 0]),
                $parsedQueryParams->getInt('length', ['default' => 10]),
                $parsedQueryParams->getString('media'),
                $types,
                $parsedQueryParams->getString('orientation'),
                $provider
            );

            try {
                $this->getDispatcher()->dispatch($event, $event->getName());
            } catch (\Exception $exception) {
                $this->getLog()->error('Library search: Exception in dispatched event: ' . $exception->getMessage());
                $this->getLog()->debug($exception->getTraceAsString());
            }
        }

        return $response->withJson($searchResults);
    }

    /**
     * Get list of Library providers with their details.
     *
     * @param Request $request
     * @param Response $response
     * @return Response|ResponseInterface
     */
    public function providersList(Request $request, Response $response): Response|\Psr\Http\Message\ResponseInterface
    {
        $event = new LibraryProviderListEvent();
        $this->getDispatcher()->dispatch($event, $event->getName());

        $providers = $event->getProviders();

        return $response->withJson($providers);
    }

    /**
     * Media Delete Form
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function deleteForm(Request $request, Response $response, $id)
    {
        $media = $this->mediaFactory->getById($id);

        if (!$this->getUser()->checkDeleteable($media)) {
            throw new AccessDeniedException();
        }

        $this->getDispatcher()->dispatch(MediaFullLoadEvent::$NAME, new MediaFullLoadEvent($media));
        $media->load(['deleting' => true]);

        $this->getState()->template = 'library-form-delete';
        $this->getState()->setData([
            'media' => $media,
        ]);

        return $this->render($request, $response);
    }

    /**
     * Delete Media
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @throws \Xibo\Support\Exception\DuplicateEntityException
     * @SWG\Delete(
     *  path="/library/{mediaId}",
     *  operationId="libraryDelete",
     *  tags={"library"},
     *  summary="Delete Media",
     *  description="Delete Media from the Library",
     *  @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media ID to Delete",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="forceDelete",
     *      in="formData",
     *      description="If the media item has been used should it be force removed from items that uses it?",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="purge",
     *      in="formData",
     *      description="Should this Media be added to the Purge List for all Displays?",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Response(
     *      response=204,
     *      description="successful operation"
     *  )
     * )
     */
    public function delete(Request $request, Response $response, $id)
    {
        $media = $this->mediaFactory->getById($id);
        $params = $this->getSanitizer($request->getParams());

        if (!$this->getUser()->checkDeleteable($media)) {
            throw new AccessDeniedException();
        }

        // Check
        $this->getDispatcher()->dispatch(new MediaFullLoadEvent($media), MediaFullLoadEvent::$NAME);
        $media->load(['deleting' => true]);

        if ($media->isUsed() && $params->getCheckbox('forceDelete') == 0) {
            throw new InvalidArgumentException(__('This library item is in use.'));
        }

        $this->getDispatcher()->dispatch(
            new MediaDeleteEvent($media, null, $params->getCheckbox('purge')),
            MediaDeleteEvent::$NAME
        );

        // Delete
        $media->delete();

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 204,
            'message' => sprintf(__('Deleted %s'), $media->name)
        ]);

        return $this->render($request, $response);
    }

    /**
     * Add a file to the library
     *  expects to be fed by the blueimp file upload handler
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @SWG\Post(
     *  path="/library",
     *  operationId="libraryAdd",
     *  tags={"library"},
     *  summary="Add Media",
     *  description="Add Media to the Library, optionally replacing an existing media item, optionally adding to a playlist.",
     *  @SWG\Parameter(
     *      name="files",
     *      in="formData",
     *      description="The Uploaded File",
     *      type="file",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="name",
     *      in="formData",
     *      description="Optional Media Name",
     *      type="string",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="oldMediaId",
     *      in="formData",
     *      description="Id of an existing media file which should be replaced with the new upload",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="updateInLayouts",
     *      in="formData",
     *      description="Flag (0, 1), set to 1 to update this media in all layouts (use with oldMediaId) ",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="deleteOldRevisions",
     *      in="formData",
     *      description="Flag (0 , 1), to either remove or leave the old file revisions (use with oldMediaId)",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="tags",
     *      in="formData",
     *      description="Comma separated string of Tags that should be assigned to uploaded Media",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="expires",
     *      in="formData",
     *      description="Date in Y-m-d H:i:s format, will set expiration date on the uploaded Media",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="playlistId",
     *      in="formData",
     *      description="A playlistId to add this uploaded media to",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="widgetFromDt",
     *      in="formData",
     *      description="Date in Y-m-d H:i:s format, will set widget start date. Requires a playlistId.",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="widgetToDt",
     *      in="formData",
     *      description="Date in Y-m-d H:i:s format, will set widget end date. Requires a playlistId.",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="deleteOnExpiry",
     *      in="formData",
     *      description="Flag (0, 1), set to 1 to remove the Widget from the Playlist when the widgetToDt has been reached",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="applyToMedia",
     *      in="formData",
     *      description="Flag (0, 1), set to 1 to apply the widgetFromDt as the expiry date on the Media",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="folderId",
     *      in="formData",
     *      description="Folder ID to which this object should be assigned to",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation"
     *  )
     * )
     */
    public function add(Request $request, Response $response)
    {
        $parsedBody = $this->getSanitizer($request->getParams());
        $options = $parsedBody->getArray('options', ['default' => []]);

        // Folders
        $folderId = $parsedBody->getInt('folderId');

        if ($folderId === 1) {
            $this->checkRootFolderAllowSave();
        }

        if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
            $folderId = $this->getUser()->homeFolderId;
        }

        if ($parsedBody->getInt('playlistId') !== null) {
            $playlist = $this->playlistFactory->getById($parsedBody->getInt('playlistId'));

            if ($playlist->isDynamic === 1) {
                throw new InvalidArgumentException(__('This Playlist is dynamically managed so cannot accept manual assignments.'), 'isDynamic');
            }
        }

        $options = array_merge([
            'oldMediaId' => null,
            'updateInLayouts' => 0,
            'deleteOldRevisions' => 0,
            'allowMediaTypeChange' => 0
        ], $options);

        $libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');

        // Handle any expiry date provided.
        // this can come from the API via `expires` or via a widgetToDt
        $expires = $parsedBody->getDate('expires');
        $widgetFromDt = $parsedBody->getDate('widgetFromDt');
        $widgetToDt = $parsedBody->getDate('widgetToDt');

        // If applyToMedia has been selected, and we have a widgetToDt, then use that as our expiry
        if ($widgetToDt !== null && $parsedBody->getCheckbox('applyToMedia', ['checkboxReturnInteger' => false])) {
            $expires = $widgetToDt;
        }

        // Validate that this date is in the future.
        if ($expires !== null && $expires->isBefore(Carbon::now())) {
            throw new InvalidArgumentException(__('Cannot set Expiry date in the past'), 'expires');
        }

        // Make sure the library exists
        MediaService::ensureLibraryExists($libraryFolder);

        // Get Valid Extensions
        if ($parsedBody->getInt('oldMediaId', ['default' => $options['oldMediaId']]) !== null) {
            $media = $this->mediaFactory->getById($parsedBody->getInt('oldMediaId', ['default' => $options['oldMediaId']]));
            $folderId = $media->folderId;
            $validExt = $this->moduleFactory->getValidExtensions(['type' => $media->mediaType, 'allowMediaTypeChange' => $options['allowMediaTypeChange']]);
        } else {
            $validExt = $this->moduleFactory->getValidExtensions();
        }

        // Make sure there is room in the library
        $libraryLimit = $this->getConfig()->getSetting('LIBRARY_SIZE_LIMIT_KB') * 1024;

        $options = [
            'userId' => $this->getUser()->userId,
            'controller' => $this,
            'oldMediaId' => $parsedBody->getInt('oldMediaId', ['default' => $options['oldMediaId']]),
            'widgetId' => $parsedBody->getInt('widgetId'),
            'updateInLayouts' => $parsedBody->getCheckbox('updateInLayouts', ['default' => $options['updateInLayouts']]),
            'deleteOldRevisions' => $parsedBody->getCheckbox('deleteOldRevisions', ['default' => $options['deleteOldRevisions']]),
            'allowMediaTypeChange' => $options['allowMediaTypeChange'],
            'displayOrder' => $parsedBody->getInt('displayOrder'),
            'playlistId' => $parsedBody->getInt('playlistId'),
            'accept_file_types' => '/\.' . implode('|', $validExt) . '$/i',
            'libraryLimit' => $libraryLimit,
            'libraryQuotaFull' => ($libraryLimit > 0 && $this->getMediaService()->libraryUsage() > $libraryLimit),
            'expires' => $expires === null ? null : $expires->format('U'),
            'widgetFromDt' => $widgetFromDt === null ? null : $widgetFromDt->format('U'),
            'widgetToDt' => $widgetToDt === null ? null : $widgetToDt->format('U'),
            'deleteOnExpiry' => $parsedBody->getCheckbox('deleteOnExpiry', ['checkboxReturnInteger' => true]),
            'oldFolderId' => $folderId,
        ];

        // Output handled by UploadHandler
        $this->setNoOutput(true);

        $this->getLog()->debug('Hand off to Upload Handler with options: ' . json_encode($options));

        // Hand off to the Upload Handler provided by jquery-file-upload
        new XiboUploadHandler($libraryFolder . 'temp/', $this->getLog()->getLoggerInterface(), $options);

        // Explicitly set the Content-Type header to application/json
        $response = $response->withHeader('Content-Type', 'application/json');

        return $this->render($request, $response);
    }

    /**
     * Edit Form
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function editForm(Request $request, Response $response, $id)
    {
        $media = $this->mediaFactory->getById($id);

        if (!$this->getUser()->checkEditable($media)) {
            throw new AccessDeniedException();
        }

        $media->enableStat = ($media->enableStat == null) ? $this->getConfig()->getSetting('MEDIA_STATS_ENABLED_DEFAULT') : $media->enableStat;

        $this->getState()->template = 'library-form-edit';
        $this->getState()->setData([
            'media' => $media,
            'validExtensions' => implode('|', $this->moduleFactory->getValidExtensions(['type' => $media->mediaType])),
            'expiryDate' => ($media->expires == 0 ) ? null : Carbon::createFromTimestamp($media->expires)->format(DateFormatHelper::getSystemFormat(), $media->expires)
        ]);

        return $this->render($request, $response);
    }

    /**
     * Edit Media
     *
     * @SWG\Put(
     *  path="/library/{mediaId}",
     *  operationId="libraryEdit",
     *  tags={"library"},
     *  summary="Edit Media",
     *  description="Edit a Media Item in the Library",
     *  @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media ID to Edit",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="name",
     *      in="formData",
     *      description="Media Item Name",
     *      type="string",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="duration",
     *      in="formData",
     *      description="The duration in seconds for this Media Item",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="retired",
     *      in="formData",
     *      description="Flag indicating if this media is retired",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="tags",
     *      in="formData",
     *      description="Comma separated list of Tags",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="updateInLayouts",
     *      in="formData",
     *      description="Flag indicating whether to update the duration in all Layouts the Media is assigned to",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="expires",
     *      in="formData",
     *      description="Date in Y-m-d H:i:s format, will set expiration date on the Media item",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="folderId",
     *      in="formData",
     *      description="Folder ID to which this media should be assigned to",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Media")
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @throws \Xibo\Support\Exception\DuplicateEntityException
     */
    public function edit(Request $request, Response $response, $id)
    {
        $media = $this->mediaFactory->getById($id);
        $sanitizedParams = $this->getSanitizer($request->getParams());

        if (!$this->getUser()->checkEditable($media)) {
            throw new AccessDeniedException();
        }

        if ($media->mediaType == 'font') {
            throw new InvalidArgumentException(__('Sorry, Fonts do not have any editable properties.'));
        }

        $media->name = $sanitizedParams->getString('name');
        $media->duration = $sanitizedParams->getInt('duration');
        $media->retired = $sanitizedParams->getCheckbox('retired');

        if ($this->getUser()->featureEnabled('tag.tagging')) {
            if (is_array($sanitizedParams->getParam('tags'))) {
                $tags = $this->tagFactory->tagsFromJson($sanitizedParams->getArray('tags'));
            } else {
                $tags = $this->tagFactory->tagsFromString($sanitizedParams->getString('tags'));
            }

            $media->updateTagLinks($tags);
        }

        $media->enableStat = $sanitizedParams->getString('enableStat');
        $media->folderId = $sanitizedParams->getInt('folderId', ['default' => $media->folderId]);
        $media->orientation = $sanitizedParams->getString('orientation', ['default' => $media->orientation]);

        if ($media->hasPropertyChanged('folderId')) {
            if ($media->folderId === 1) {
                $this->checkRootFolderAllowSave();
            }
            $folder = $this->folderFactory->getById($media->folderId);
            $media->permissionsFolderId = ($folder->getPermissionFolderId() == null)
                ? $folder->id
                : $folder->getPermissionFolderId();
        }

        if ($sanitizedParams->getDate('expires') != null) {
            if ($sanitizedParams->getDate('expires')->format('U') > Carbon::now()->format('U')) {
                $media->expires = $sanitizedParams->getDate('expires')->format('U');
            } else {
                throw new InvalidArgumentException(__('Cannot set Expiry date in the past'), 'expires');
            }
        } else {
            $media->expires = 0;
        }

        // Should we update the media in all layouts?
        if ($sanitizedParams->getCheckbox('updateInLayouts') == 1
            || $media->hasPropertyChanged('enableStat')
        ) {
            foreach ($this->widgetFactory->getByMediaId($media->mediaId, 0) as $widget) {
                if ($widget->useDuration == 1) {
                    $widget->calculateDuration($this->moduleFactory->getByType($widget->type));
                } else {
                    $widget->calculatedDuration = $media->duration;
                }
                $widget->save();
            }
        }

        $media->save();

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Edited %s'), $media->name),
            'id' => $media->mediaId,
            'data' => $media
        ]);

        return $this->render($request, $response);
    }

    /**
     * Tidy Library
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function tidyForm(Request $request, Response $response)
    {
        if ($this->getConfig()->getSetting('SETTING_LIBRARY_TIDY_ENABLED') != 1) {
            throw new ConfigurationException(__('Sorry this function is disabled.'));
        }

        // Work out how many files there are
        $media = $this->mediaFactory->query(null, ['unusedOnly' => 1, 'ownerId' => $this->getUser()->userId]);

        $sumExcludingGeneric = 0;
        $countExcludingGeneric = 0;
        $sumGeneric = 0;
        $countGeneric = 0;

        foreach ($media as $item) {
            if ($item->mediaType == 'genericfile') {
                $countGeneric++;
                $sumGeneric = $sumGeneric + $item->fileSize;
            }
            else {
                $countExcludingGeneric++;
                $sumExcludingGeneric = $sumExcludingGeneric + $item->fileSize;
            }
        }

        $this->getState()->template = 'library-form-tidy';
        $this->getState()->setData([
            'sumExcludingGeneric' => ByteFormatter::format($sumExcludingGeneric),
            'sumGeneric' => ByteFormatter::format($sumGeneric),
            'countExcludingGeneric' => $countExcludingGeneric,
            'countGeneric' => $countGeneric,
        ]);

        return $this->render($request, $response);
    }

    /**
     * Tidies up the library
     *
     * @SWG\Delete(
     *  path="/library/tidy",
     *  operationId="libraryTidy",
     *  tags={"library"},
     *  summary="Tidy Library",
     *  description="Routine tidy of the library, removing unused files.",
     *  @SWG\Parameter(
     *      name="tidyGenericFiles",
     *      in="formData",
     *      description="Also delete generic files?",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation"
     *  )
     * )
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function tidy(Request $request, Response $response)
    {
        if ($this->getConfig()->getSetting('SETTING_LIBRARY_TIDY_ENABLED') != 1) {
            throw new ConfigurationException(__('Sorry this function is disabled.'));
        }

        $tidyGenericFiles = $this->getSanitizer($request->getParams())->getCheckbox('tidyGenericFiles');

        $this->getLog()->audit('Media', 0, 'Tidy library started', [
            'tidyGenericFiles' => $tidyGenericFiles,
            'initiator' => $this->getUser()->userId
        ]);

        // Get a list of media that is not in use (for this user)
        $media = $this->mediaFactory->query(null, ['unusedOnly' => 1, 'ownerId' => $this->getUser()->userId]);

        $i = 0;
        foreach ($media as $item) {
            if ($tidyGenericFiles != 1 && $item->mediaType == 'genericfile') {
                continue;
            }

            // Eligible for delete
            $i++;
            $this->getDispatcher()->dispatch(new MediaDeleteEvent($item), MediaDeleteEvent::$NAME);
            $item->delete();
        }

        $this->getLog()->audit('Media', 0, 'Tidy library complete', [
            'countDeleted' => $i,
            'initiator' => $this->getUser()->userId
        ]);

        // Return
        $this->getState()->hydrate([
            'message' => __('Library Tidy Complete'),
            'countDeleted' => $i
        ]);

        return $this->render($request, $response);
    }

    /**
     * @return string
     */
    public function getLibraryCacheUri()
    {
        return $this->getConfig()->getSetting('LIBRARY_LOCATION') . '/cache';
    }

    /**
     * @SWG\Get(
     *  path="/library/download/{mediaId}/{type}",
     *  operationId="libraryDownload",
     *  tags={"library"},
     *  summary="Download Media",
     *  description="Download a Media file from the Library",
     *  produces={"application/octet-stream"},
     *  @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media ID to Download",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="type",
     *      in="path",
     *      description="The Module Type of the Download",
     *      type="string",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(type="file"),
     *      @SWG\Header(
     *          header="X-Sendfile",
     *          description="Apache Send file header - if enabled.",
     *          type="string"
     *      ),
     *      @SWG\Header(
     *          header="X-Accel-Redirect",
     *          description="nginx send file header - if enabled.",
     *          type="string"
     *      )
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws \Xibo\Support\Exception\GeneralException
     */
    public function download(Request $request, Response $response, $id)
    {
        $this->setNoOutput();

        // We can download by mediaId or by mediaName.
        if (is_numeric($id)) {
            $media = $this->mediaFactory->getById($id);
        } else {
            $media = $this->mediaFactory->getByName($id);
        }

        $this->getLog()->debug('download: Download request for mediaId ' . $id
            . '. Media is a ' . $media->mediaType . ', is system file:' . $media->moduleSystemFile);

        // Create the appropriate module
        if ($media->mediaType === 'module') {
            $module = $this->moduleFactory->getByType('image');
        } else {
            $module = $this->moduleFactory->getByType($media->mediaType);
        }

        // We are not able to download region specific modules
        if ($module->regionSpecific == 1) {
            throw new NotFoundException(__('Cannot download region specific module'));
        }

        // Hand over to the widget downloader
        $downloader = new WidgetDownloader(
            $this->getConfig()->getSetting('LIBRARY_LOCATION'),
            $this->getConfig()->getSetting('SENDFILE_MODE'),
            $this->getConfig()->getSetting('DEFAULT_RESIZE_LIMIT', 6000)
        );
        $downloader->useLogger($this->getLog()->getLoggerInterface());

        $params = $this->getSanitizer($request->getParams());

        // Check if preview is allowed for the module
        if ($params->getCheckbox('preview') == 1 && $module->allowPreview === 1) {
            $this->getLog()->debug('download: preview mode, seeing if we can output an image/video');

            // Output a 1px image if we're not allowed to see the media.
            if (!$this->getUser()->checkViewable($media)) {
                echo Img::make($this->getConfig()->uri('img/1x1.png', true))->encode();
                return $this->render($request, $response->withHeader('Content-Type', 'image/png'));
            }

            // Various different behaviours for the different types of file.
            if ($module->type === 'image') {
                $response = $downloader->imagePreview(
                    $params,
                    $media->storedAs,
                    $response,
                    $this->getConfig()->uri('img/1x1.png', true),
                );
            } else if ($module->type === 'video') {
                $response = $downloader->imagePreview(
                    $params,
                    $media->mediaId . '_videocover.png',
                    $response,
                    $this->getConfig()->uri('img/1x1.png', true),
                );
            } else {
                $response = $downloader->download($media, $request, $response, $media->getMimeType());
            }
        } else {
            $this->getLog()->debug('download: not preview mode, expect a full download');

            // We are not a preview, and therefore we ought to check sharing before we download
            if (!$this->getUser()->checkViewable($media)) {
                throw new AccessDeniedException();
            }

            $response = $downloader->download($media, $request, $response, null, $params->getString('attachment'));
        }

        return $this->render($request, $response);
    }

    /**
     * Thumbnail for the libary page
     *  this is called by library-page datatable
     *
     * @SWG\Get(
     *  path="/library/thumbnail/{mediaId}",
     *  operationId="libraryThumbnail",
     *  tags={"library"},
     *  summary="Download Thumbnail",
     *  description="Download thumbnail for a Media file from the Library",
     *  produces={"application/octet-stream"},
     *  @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media ID to Download",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(type="file"),
     *      @SWG\Header(
     *          header="X-Sendfile",
     *          description="Apache Send file header - if enabled.",
     *          type="string"
     *      ),
     *      @SWG\Header(
     *          header="X-Accel-Redirect",
     *          description="nginx send file header - if enabled.",
     *          type="string"
     *      )
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws \Xibo\Support\Exception\GeneralException
     */
    public function thumbnail(Request $request, Response $response, $id, bool $isForceGrantAccess = false)
    {
        $this->setNoOutput();

        // We can download by mediaId or by mediaName.
        if (is_numeric($id)) {
            $media = $this->mediaFactory->getById($id);
        } else {
            $media = $this->mediaFactory->getByName($id);
        }

        $this->getLog()->debug('thumbnail: Thumbnail request for mediaId ' . $id
            . '. Media is a ' . $media->mediaType);

        // Permissions.
        if (!$this->getUser()->checkViewable($media) && !$isForceGrantAccess) {
            // Output a 1px image if we're not allowed to see the media.
            echo Img::make($this->getConfig()->uri('img/1x1.png', true))->encode();
            return $this->render($request, $response);
        }

        // Hand over to the widget downloader
        $downloader = new WidgetDownloader(
            $this->getConfig()->getSetting('LIBRARY_LOCATION'),
            $this->getConfig()->getSetting('SENDFILE_MODE'),
            $this->getConfig()->getSetting('DEFAULT_RESIZE_LIMIT', 6000)
        );
        $downloader->useLogger($this->getLog()->getLoggerInterface());

        $response = $downloader->thumbnail(
            $media,
            $response,
            $this->getConfig()->uri('img/error.png', true)
        );

        return $this->render($request, $response);
    }

    /**
     * Public Thumbnail
     *  this is an unauthenticated route (publicRoutes)
     *  we need to authenticate using the S3 link signing
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Slim\Http\Response
     * @throws \Xibo\Support\Exception\AccessDeniedException
     * @throws \Xibo\Support\Exception\GeneralException
     */
    public function thumbnailPublic(Request $request, Response $response, $id): Response
    {
        // Authenticate.
        $params = $this->getSanitizer($request->getParams());

        // Has the URL expired
        if (time() > $params->getInt('X-Amz-Expires')) {
            throw new AccessDeniedException(__('Expired'));
        }

        // Validate the URL.
        $encryptionKey = $this->getConfig()->getApiKeyDetails()['encryptionKey'];
        $signature = $params->getString('X-Amz-Signature');

        $calculatedSignature = \Xibo\Helper\LinkSigner::getSignature(
            (new HttpsDetect())->getUrl(),
            $request->getUri()->getPath(),
            $params->getInt('X-Amz-Expires'),
            $encryptionKey,
            $params->getString('X-Amz-Date'),
            true,
        );

        if ($signature !== $calculatedSignature) {
            throw new AccessDeniedException(__('Invalid URL'));
        }

        $this->getLog()->debug('thumbnailPublic: authorised for ' . $id);

        $res = $this->thumbnail($request, $response, $id, true);

        // Pass to the thumbnail route
        return $res->withHeader('Access-Control-Allow-Origin', '*');
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function mcaas(Request $request, Response $response, $id)
    {
        // This is only available through the API
        if (!$this->isApi($request)) {
            throw new AccessDeniedException(__('Route is available through the API'));
        }

        $options = [
            'oldMediaId' => $id,
            'updateInLayouts' => 1,
            'deleteOldRevisions' => 1,
            'allowMediaTypeChange' => 1
        ];

        // Call Add with the oldMediaId
        return $this->add($request->withParsedBody(['options' => $options]), $response);
    }

    /**
     * @SWG\Post(
     *  path="/library/{mediaId}/tag",
     *  operationId="mediaTag",
     *  tags={"library"},
     *  summary="Tag Media",
     *  description="Tag a Media with one or more tags",
     * @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media Id to Tag",
     *      type="integer",
     *      required=true
     *   ),
     * @SWG\Parameter(
     *      name="tag",
     *      in="formData",
     *      description="An array of tags",
     *      type="array",
     *      required=true,
     *      @SWG\Items(type="string")
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Media")
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @throws \Xibo\Support\Exception\DuplicateEntityException
     */
    public function tag(Request $request, Response $response, $id)
    {
        // Edit permission
        // Get the media
        $media = $this->mediaFactory->getById($id);

        // Check Permissions
        if (!$this->getUser()->checkEditable($media)) {
            throw new AccessDeniedException();
        }

        $tags = $this->getSanitizer($request->getParams())->getArray('tag');

        if (count($tags) <= 0) {
            throw new InvalidArgumentException(__('No tags to assign'));
        }

        foreach ($tags as $tag) {
            $media->assignTag($this->tagFactory->tagFromString($tag));
        }

        $media->save(['validate' => false]);

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Tagged %s'), $media->name),
            'id' => $media->mediaId,
            'data' => $media
        ]);

        return $this->render($request, $response);
    }

    /**
     * @SWG\Post(
     *  path="/library/{mediaId}/untag",
     *  operationId="mediaUntag",
     *  tags={"library"},
     *  summary="Untag Media",
     *  description="Untag a Media with one or more tags",
     * @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media Id to Untag",
     *      type="integer",
     *      required=true
     *   ),
     * @SWG\Parameter(
     *      name="tag",
     *      in="formData",
     *      description="An array of tags",
     *      type="array",
     *      required=true,
     *      @SWG\Items(type="string")
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Media")
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @throws \Xibo\Support\Exception\DuplicateEntityException
     */
    public function untag(Request $request, Response $response, $id)
    {
        // Edit permission
        // Get the media
        $media = $this->mediaFactory->getById($id);

        // Check Permissions
        if (!$this->getUser()->checkEditable($media)) {
            throw new AccessDeniedException();
        }

        $tags = $this->getSanitizer($request->getParams())->getArray('tag');

        if (count($tags) <= 0) {
            throw new InvalidArgumentException(__('No tags to unassign'), 'tag');
        }

        foreach ($tags as $tag) {
            $media->unassignTag($this->tagFactory->tagFromString($tag));
        }

        $media->save(['validate' => false]);

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Untagged %s'), $media->name),
            'id' => $media->mediaId,
            'data' => $media
        ]);

        return $this->render($request, $response);
    }

    /**
     * Library Usage Report Form
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function usageForm(Request $request, Response $response, $id)
    {
        $media = $this->mediaFactory->getById($id);
        $sanitizedParams = $this->getSanitizer($request->getParams());

        if (!$this->getUser()->checkViewable($media)) {
            throw new AccessDeniedException();
        }

        // Get a list of displays that this mediaId is used on
        $displays = $this->displayFactory->query($this->gridRenderSort($sanitizedParams), $this->gridRenderFilter(['disableUserCheck' => 1, 'mediaId' => $id], $sanitizedParams));

        $this->getState()->template = 'library-form-usage';
        $this->getState()->setData([
            'media' => $media,
            'countDisplays' => count($displays)
        ]);

        return $this->render($request, $response);
    }

    /**
     * @SWG\Get(
     *  path="/library/usage/{mediaId}",
     *  operationId="libraryUsageReport",
     *  tags={"library"},
     *  summary="Get Library Item Usage Report",
     *  description="Get the records for the library item usage report",
     * @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media Id",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *     response=200,
     *     description="successful operation"
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function usage(Request $request, Response $response, $id)
    {
        $media = $this->mediaFactory->getById($id);
        $sanitizedParams = $this->getSanitizer($request->getParams());

        if (!$this->getUser()->checkViewable($media)) {
            throw new AccessDeniedException();
        }

        // Get a list of displays that this mediaId is used on by direct assignment
        $displays = $this->displayFactory->query($this->gridRenderSort($sanitizedParams), $this->gridRenderFilter(['mediaId' => $id], $sanitizedParams));

        // have we been provided with a date/time to restrict the scheduled events to?
        $mediaFromDate = $sanitizedParams->getDate('mediaEventFromDate');
        $mediaToDate = $sanitizedParams->getDate('mediaEventToDate');

        // Media query array
        $mediaQuery = [
            'mediaId' => $id
        ];

        if ($mediaFromDate !== null) {
            $mediaQuery['futureSchedulesFrom'] = $mediaFromDate->format('U');
        }

        if ($mediaToDate !== null) {
            $mediaQuery['futureSchedulesTo'] = $mediaToDate->format('U');
        }

        // Query for events
        $events = $this->scheduleFactory->query(null, $mediaQuery);

        // Total records returned from the schedules query
        $totalRecords = $this->scheduleFactory->countLast();

        foreach ($events as $row) {
            /* @var \Xibo\Entity\Schedule $row */

            // Generate this event
            // Assess the date?
            if ($mediaFromDate !== null && $mediaToDate !== null) {
                try {
                    $scheduleEvents = $row->getEvents($mediaFromDate, $mediaToDate);
                } catch (GeneralException $e) {
                    $this->getLog()->error('Unable to getEvents for ' . $row->eventId);
                    continue;
                }

                // Skip events that do not fall within the specified days
                if (count($scheduleEvents) <= 0)
                    continue;

                $this->getLog()->debug('EventId ' . $row->eventId . ' as events: ' . json_encode($scheduleEvents));
            }

            // Load the display groups
            $row->load();

            foreach ($row->displayGroups as $displayGroup) {
                foreach ($this->displayFactory->getByDisplayGroupId($displayGroup->displayGroupId) as $display) {
                    $found = false;

                    // Check to see if our ID is already in our list
                    foreach ($displays as $existing) {
                        if ($existing->displayId === $display->displayId) {
                            $found = true;
                            break;
                        }
                    }

                    if (!$found)
                        $displays[] = $display;
                }
            }
        }

        if ($this->isApi($request) && $displays == []) {
            $displays = [
                'data' =>__('Specified Media item is not in use.')];
        }

        $this->getState()->template = 'grid';
        $this->getState()->recordsTotal = $totalRecords;
        $this->getState()->setData($displays);

        return $this->render($request, $response);
    }

    /**
     * @SWG\Get(
     *  path="/library/usage/layouts/{mediaId}",
     *  operationId="libraryUsageLayoutsReport",
     *  tags={"library"},
     *  summary="Get Library Item Usage Report for Layouts",
     *  description="Get the records for the library item usage report for Layouts",
     * @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media Id",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *     response=200,
     *     description="successful operation"
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function usageLayouts(Request $request, Response $response, $id)
    {
        $media = $this->mediaFactory->getById($id);

        if (!$this->getUser()->checkViewable($media)) {
            throw new AccessDeniedException();
        }

        $sanitizedParams = $this->getSanitizer($request->getParams());
        $layouts = $this->layoutFactory->query(
            $this->gridRenderSort($sanitizedParams),
            $this->gridRenderFilter([
                'mediaId' => $id,
                'showDrafts' => 1
            ], $sanitizedParams)
        );

        if (!$this->isApi($request)) {
            foreach ($layouts as $layout) {
                $layout->includeProperty('buttons');

                // Add some buttons for this row
                if ($this->getUser()->checkEditable($layout)) {
                    // Design Button
                    $layout->buttons[] = array(
                        'id' => 'layout_button_design',
                        'linkType' => '_self', 'external' => true,
                        'url' => $this->urlFor($request,'layout.designer', ['id' => $layout->layoutId]),
                        'text' => __('Design')
                    );
                }

                // Preview
                $layout->buttons[] = array(
                    'id' => 'layout_button_preview',
                    'external' => true,
                    'url' => '#',
                    'onclick' => 'createMiniLayoutPreview',
                    'onclickParam' => $this->urlFor($request, 'layout.preview', ['id' => $layout->layoutId]),
                    'text' => __('Preview Layout')
                );
            }
        }

        if ($this->isApi($request) && $layouts == []) {
            $layouts = [
                'data' =>__('Specified Media item is not in use.')
            ];
        }

        $this->getState()->template = 'grid';
        $this->getState()->recordsTotal = $this->layoutFactory->countLast();
        $this->getState()->setData($layouts);

        return $this->render($request, $response);
    }

    /**
     * Copy Media form
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function copyForm(Request $request, Response $response, $id)
    {
        // Get the Media
        $media = $this->mediaFactory->getById($id);

        // Check Permissions
        if (!$this->getUser()->checkViewable($media)) {
            throw new AccessDeniedException();
        }

        $this->getState()->template = 'library-form-copy';
        $this->getState()->setData([
            'media' => $media,
        ]);

        return $this->render($request, $response);
    }

    /**
     * Copies a Media
     *
     * @SWG\Post(
     *  path="/library/copy/{mediaId}",
     *  operationId="mediaCopy",
     *  tags={"library"},
     *  summary="Copy Media",
     *  description="Copy a Media, providing a new name and tags if applicable",
     *  @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The media ID to Copy",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="name",
     *      in="formData",
     *      description="The name for the new Media",
     *      type="string",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="tags",
     *      in="formData",
     *      description="The Optional tags for new Media",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Response(
     *      response=201,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Media"),
     *      @SWG\Header(
     *          header="Location",
     *          description="Location of the new record",
     *          type="string"
     *      )
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @throws \Xibo\Support\Exception\DuplicateEntityException
     */
    public function copy(Request $request, Response $response, $id)
    {
        // Get the Media
        $media = $this->mediaFactory->getById($id);
        $sanitizedParams = $this->getSanitizer($request->getParams());

        // Check Permissions
        if (!$this->getUser()->checkViewable($media)) {
            throw new AccessDeniedException();
        }

        // Load the media for Copy
        $media = clone $media;

        // Set new Name and tags
        $media->name = $sanitizedParams->getString('name');

        if ($this->getUser()->featureEnabled('tag.tagging')) {
            if (is_array($sanitizedParams->getParam('tags'))) {
                $tags = $this->tagFactory->tagsFromJson($sanitizedParams->getArray('tags'));
            } else {
                $tags = $this->tagFactory->tagsFromString($sanitizedParams->getString('tags'));
            }
            $media->updateTagLinks($tags);
        }

        // Set the Owner to user making the Copy
        $media->setOwner($this->getUser()->userId);

        // Set from global setting
        if ($media->enableStat == null) {
            $media->enableStat = $this->getConfig()->getSetting('MEDIA_STATS_ENABLED_DEFAULT');
        }

        // Save the new Media
        $media->save();

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 201,
            'message' => sprintf(__('Copied as %s'), $media->name),
            'id' => $media->mediaId,
            'data' => $media
        ]);

        return $this->render($request,  $response);
    }


    /**
     * @SWG\Get(
     *  path="/library/{mediaId}/isused/",
     *  operationId="mediaIsUsed",
     *  tags={"library"},
     *  summary="Media usage check",
     *  description="Checks if a Media is being used",
     * @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media Id",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *     response=200,
     *     description="successful operation"
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function isUsed(Request $request, Response $response, $id)
    {
        // Get the Media
        $media = $this->mediaFactory->getById($id);
        $this->getDispatcher()->dispatch(new MediaFullLoadEvent($media), MediaFullLoadEvent::$NAME);

        // Check Permissions
        if (!$this->getUser()->checkViewable($media)) {
            throw new AccessDeniedException();
        }

        // Get count, being the number of times the media needs to appear to be true ( or use the default 0)
        $count = $this->getSanitizer($request->getParams())->getInt('count', ['default' => 0]);

        // Check and return result
        $this->getState()->setData([
            'isUsed' => $media->isUsed($count)
        ]);

        return $this->render($request, $response);

    }

    /**
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws GeneralException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function uploadFromUrlForm(Request $request, Response $response)
    {
        $this->getState()->template = 'library-form-uploadFromUrl';

        $this->getState()->setData([
            'uploadSizeMessage' => sprintf(__('This form accepts files up to a maximum size of %s'), Environment::getMaxUploadSize())
        ]);

        return $this->render($request, $response);
    }

    /**
     * Upload Media via URL
     *
     * @SWG\Post(
     *  path="/library/uploadUrl",
     *  operationId="uploadFromUrl",
     *  tags={"library"},
     *  summary="Upload Media from URL",
     *  description="Upload Media to CMS library from an external URL",
     *  @SWG\Parameter(
     *      name="url",
     *      in="formData",
     *      description="The URL to the media",
     *      type="string",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="type",
     *      in="formData",
     *      description="The type of the media, image, video etc",
     *      type="string",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="extension",
     *      in="formData",
     *      description="Optional extension of the media, jpg, png etc. If not set in the request it will be retrieved from the headers",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="enableStat",
     *      in="formData",
     *      description="The option to enable the collection of Media Proof of Play statistics, On, Off or Inherit.",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="optionalName",
     *      in="formData",
     *      description="An optional name for this media file, if left empty it will default to the file name",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="expires",
     *      in="formData",
     *      description="Date in Y-m-d H:i:s format, will set expiration date on the Media item",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="folderId",
     *      in="formData",
     *      description="Folder ID to which this media should be assigned to",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Response(
     *      response=201,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Media"),
     *      @SWG\Header(
     *          header="Location",
     *          description="Location of the new record",
     *          type="string"
     *      )
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws LibraryFullException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @throws \Xibo\Support\Exception\DuplicateEntityException
     */
    public function uploadFromUrl(Request $request, Response $response)
    {
        $sanitizedParams = $this->getSanitizer($request->getParams());

        // Params
        $url = $sanitizedParams->getString('url');
        $type = $sanitizedParams->getString('type');
        $optionalName = $sanitizedParams->getString('optionalName');
        $extension = $sanitizedParams->getString('extension');
        $enableStat = $sanitizedParams->getString('enableStat', [
            'default' => $this->getConfig()->getSetting('MEDIA_STATS_ENABLED_DEFAULT')
        ]);

        // Folders
        $folderId = $sanitizedParams->getInt('folderId');
        if ($folderId === 1) {
            $this->checkRootFolderAllowSave();
        }

        if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
            $folderId = $this->getUser()->homeFolderId;
        }

        $folder = $this->folderFactory->getById($folderId, 0);

        if ($sanitizedParams->hasParam('expires')) {
            if ($sanitizedParams->getDate('expires')->format('U') > Carbon::now()->format('U')) {
                $expires = $sanitizedParams->getDate('expires')->format('U');
            } else {
                throw new InvalidArgumentException(__('Cannot set Expiry date in the past'), 'expires');
            }
        } else {
            $expires = 0;
        }

        // Validate the URL
        if (!v::url()->notEmpty()->validate($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
            throw new InvalidArgumentException(__('Provided URL is invalid'), 'url');
        }

        // remote file size
        $downloadInfo = $this->getMediaService()->getDownloadInfo($url);

        // check if we have extension provided in the request (available via API)
        // if not get it from the headers
        if (!empty($extension)) {
            $ext = $extension;
        } else {
            $ext = $downloadInfo['extension'];
        }

        // Unsupported links (ie Youtube links, etc) will return a null extension, thus, throw an error
        if (is_null($ext)) {
            throw new NotFoundException(sprintf(__('Extension %s is not supported.'), $ext));
        }

        // Initialise the library and do some checks
        $this->getMediaService()
            ->initLibrary()
            ->checkLibraryOrQuotaFull(true)
            ->checkMaxUploadSize($downloadInfo['size']);

        // check if we have type provided in the request (available via API), if not get the module type from
        // the extension
        if (!empty($type)) {
            $module = $this->getModuleFactory()->getByType($type);
        } else {
            $module = $this->getModuleFactory()->getByExtension($ext);
            $module = $this->getModuleFactory()->getByType($module->type);
        }

        // if we were provided with optional Media name set it here, otherwise get it from download info
        $name = empty($optionalName) ? htmlspecialchars($downloadInfo['filename']) : $optionalName;

        // double check that provided Module Type and Extension are valid
        if (!Str::contains($module->getSetting('validExtensions'), $ext)) {
            throw new NotFoundException(
                sprintf(
                    __('Invalid Module type or extension. Module type %s does not allow for %s extension'),
                    $module->type,
                    $ext
                )
            );
        }

        // add our media to queueDownload and process the downloads
        $media = $this->mediaFactory->queueDownload(
            $name,
            str_replace(' ', '%20', htmlspecialchars_decode($url)),
            $expires,
            [
                'fileType' => strtolower($module->type),
                'duration' => $module->defaultDuration,
                'extension' => $ext,
                'enableStat' => $enableStat,
                'folderId' => $folder->getId(),
                'permissionsFolderId' => $folder->getPermissionFolderIdOrThis()
            ]
        );

        $this->mediaFactory->processDownloads(
            function (Media $media) use ($module) {
                // Success
                $this->getLog()->debug('Successfully uploaded Media from URL, Media Id is ' . $media->mediaId);
                $libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');
                $realDuration = $module->fetchDurationOrDefaultFromFile($libraryFolder . $media->storedAs);
                if ($realDuration !== $media->duration) {
                    $media->updateDuration($realDuration);
                }
            },
            function (Media $media) {
                throw new InvalidArgumentException(__('Download rejected for an unknown reason.'));
            },
            function ($message) {
                // Download rejected.
                throw new InvalidArgumentException(sprintf(__('Download rejected due to %s'), $message));
            }
        );

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 201,
            'message' => __('Media upload from URL was successful'),
            'id' => $media->mediaId,
            'data' => $media
        ]);

        return $this->render($request, $response);
    }

    /**
     * This is called when video finishes uploading.
     * Saves provided base64 image as an actual image to the library
     *
     * @param Request $request
     * @param Response $response
     *
     * @return Response
     * @throws AccessDeniedException
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\DuplicateEntityException
     */
    public function addThumbnail($request, $response)
    {
        $sanitizedParams = $this->getSanitizer($request->getParams());
        $libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION');
        MediaService::ensureLibraryExists($libraryLocation);

        $imageData = $request->getParam('image');
        $mediaId = $sanitizedParams->getInt('mediaId');
        $media = $this->mediaFactory->getById($mediaId);

        if (!$this->getUser()->checkEditable($media)) {
            throw new AccessDeniedException();
        }

        try {
            Img::configure(array('driver' => 'gd'));

            // Load the image
            $image = Img::make($imageData);
            $image->save($libraryLocation . $mediaId . '_' . $media->mediaType . 'cover.png');
        } catch (\Exception $exception) {
            $this->getLog()->error('Exception adding Video cover image. e = ' . $exception->getMessage());
            throw new InvalidArgumentException(__('Invalid image data'));
        }

        $media->width = $image->getWidth();
        $media->height = $image->getHeight();
        $media->orientation = ($media->width >= $media->height) ? 'landscape' : 'portrait';
        $media->save(['saveTags' => false, 'validate' => false]);

        return $response->withStatus(204);
    }

    /**
     * Select Folder Form
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function selectFolderForm(Request $request, Response $response, $id)
    {
        // Get the Media
        $media = $this->mediaFactory->getById($id);

        // Check Permissions
        if (!$this->getUser()->checkEditable($media)) {
            throw new AccessDeniedException();
        }

        $data = [
            'media' => $media
        ];

        $this->getState()->template = 'library-form-selectfolder';
        $this->getState()->setData($data);

        return $this->render($request, $response);
    }

    /**
     * @SWG\Put(
     *  path="/library/{id}/selectfolder",
     *  operationId="librarySelectFolder",
     *  tags={"library"},
     *  summary="Media Select folder",
     *  description="Select Folder for Media",
     *  @SWG\Parameter(
     *      name="mediaId",
     *      in="path",
     *      description="The Media ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="folderId",
     *      in="formData",
     *      description="Folder ID to which this object should be assigned to",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Campaign")
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws ConfigurationException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @throws \Xibo\Support\Exception\DuplicateEntityException
     */
    public function selectFolder(Request $request, Response $response, $id)
    {
        // Get the Media
        $media = $this->mediaFactory->getById($id);

        // Check Permissions
        if (!$this->getUser()->checkEditable($media)) {
            throw new AccessDeniedException();
        }

        $folderId = $this->getSanitizer($request->getParams())->getInt('folderId');
        if ($folderId === 1) {
            $this->checkRootFolderAllowSave();
        }

        $media->folderId = $folderId;
        $folder = $this->folderFactory->getById($media->folderId);
        $media->permissionsFolderId = ($folder->getPermissionFolderId() == null) ? $folder->id : $folder->getPermissionFolderId();

        $media->save(['saveTags' => false]);

        if ($media->parentId != 0) {
            $this->updateMediaRevision($media, $folderId);
        }

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 204,
            'message' => sprintf(__('Media %s moved to Folder %s'), $media->name, $folder->text)
        ]);

        return $this->render($request, $response);
    }

    /**
     * Connector import.
     *
     *  Note: this doesn't have a Swagger document because it is only available via the web UI.
     *
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @throws \Xibo\Support\Exception\GeneralException
     */
    public function connectorImport(Request $request, Response $response)
    {
        $params = $this->getSanitizer($request->getParams());
        $items = $params->getArray('items');

        // Folders
        $folderId = $params->getInt('folderId');
        if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
            $folderId = $this->getUser()->homeFolderId;
        }
        $folder = $this->folderFactory->getById($folderId, 0);

        // Stats
        $enableStat = $params->getString('enableStat', [
            'default' => $this->getConfig()->getSetting('MEDIA_STATS_ENABLED_DEFAULT')
        ]);

        // Initialise the library.
        $this->getMediaService()
            ->initLibrary()
            ->checkLibraryOrQuotaFull(true);

        $libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION');

        // Hand these off to the connector to format into a downloadable response.
        $importQueue = [];
        foreach ($items as $item) {
            $import = new ProviderImport();
            $import->searchResult = new SearchResult();
            $import->searchResult->provider = new ProviderDetails();
            $import->searchResult->provider->id = $item['provider']['id'];
            $import->searchResult->title = $item['title'];
            $import->searchResult->id = $item['id'];
            $import->searchResult->type = $item['type'];
            $import->searchResult->download = $item['download'];
            $import->searchResult->duration = (int)$item['duration'];
            $import->searchResult->videoThumbnailUrl = $item['videoThumbnailUrl'];
            $importQueue[] = $import;
        }
        $event = new LibraryProviderImportEvent($importQueue);
        $this->getDispatcher()->dispatch($event, $event->getName());

        // Pull out our events and upload
        foreach ($importQueue as $import) {
            try {
                // Has this been configured for upload?
                if ($import->isConfigured) {
                    // Make sure we have a URL
                    if (empty($import->url)) {
                        throw new InvalidArgumentException('Missing or invalid URL', 'url');
                    }

                    // This ensures that apiRef will be unique for each provider and resource id
                    $apiRef = $import->searchResult->provider->id . '_' . $import->searchResult->id;

                    // Queue this for upload.
                    // Use a module to make sure our type, etc is supported.
                    // make sure the name is not longer than 100 characters.
                    $name = $import->searchResult->title;
                    if (strlen($name) >= 100) {
                        $name = trim(preg_replace('/\s+?(\S+)?$/', '', substr($name, 0, 95)), ', ');
                    }
                    $module = $this->getModuleFactory()->getByType($import->searchResult->type);
                    $import->media = $this->mediaFactory->queueDownload(
                        $name,
                        str_replace(' ', '%20', htmlspecialchars_decode($import->url)),
                        0,
                        [
                            'fileType' => strtolower($module->type),
                            'duration' => !(empty($import->searchResult->duration))
                                ? $import->searchResult->duration
                                : $module->defaultDuration,
                            'enableStat' => $enableStat,
                            'folderId' => $folder->getId(),
                            'permissionsFolderId' => $folder->permissionsFolderId,
                            'apiRef' => $apiRef
                        ]
                    );
                } else {
                    throw new GeneralException(__('Not configured by any active connector.'));
                }
            } catch (\Exception $e) {
                $import->setError($e->getMessage());
            }
        }

        // Process all of those downloads
        $this->mediaFactory->processDownloads(
            function (Media $media) use ($importQueue, $libraryLocation) {
                // Success
                // if we have video thumbnail url from provider, download it now
                foreach ($importQueue as $import) {
                    /** @var ProviderImport $import */
                    if ($import->media->getId() === $media->getId()
                        && $media->mediaType === 'video'
                        && !empty($import->searchResult->videoThumbnailUrl)
                    ) {
                        try {
                            $filePath = $libraryLocation . $media->getId() . '_' . $media->mediaType . 'cover.png';

                            // Expect a quick download.
                            $client = new Client($this->getConfig()->getGuzzleProxy(['timeout' => 20]));
                            $client->request(
                                'GET',
                                $import->searchResult->videoThumbnailUrl,
                                ['sink' => $filePath]
                            );

                            list($imgWidth, $imgHeight) = @getimagesize($filePath);
                            $media->updateOrientation($imgWidth, $imgHeight);
                        } catch (\Exception $exception) {
                            // if we failed, corrupted file might still be created, remove it here
                            unlink($libraryLocation . $media->getId() . '_' . $media->mediaType . 'cover.png');
                            $this->getLog()->error(sprintf(
                                'Downloading thumbnail for video %s, from url %s, failed with message %s',
                                $media->name,
                                $import->searchResult->videoThumbnailUrl,
                                $exception->getMessage()
                            ));
                        }
                    }
                }
            },
            function ($media) use ($importQueue) {
                // Failure
                // Pull out the import which failed.
                foreach ($importQueue as $import) {
                    /** @var ProviderImport $import */
                    if ($import->media->getId() === $media->getId()) {
                        $import->setError(__('Download failed'));
                    }
                }
            }
        );

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 200,
            'message' => __('Imported'),
            'data' => $event->getItems()
        ]);

        return $this->render($request, $response);
    }

    /**
     * Check if we already have a full screen Layout for this Media
     * @param Media $media
     * @return int|null
     * @throws NotFoundException
     */
    private function hasFullScreenLayout(Media $media): ?int
    {
        return $this->layoutFactory->getLinkedFullScreenLayout('media', $media->mediaId)?->campaignId;
    }

    /**
     * Update media files with revisions
     * @param Media $media
     * @param $folderId
     */
    private function updateMediaRevision(Media $media, $folderId)
    {
        $oldMedia = $this->mediaFactory->getParentById($media->mediaId);
        $oldMedia->folderId = $folderId;
        $folder = $this->folderFactory->getById($oldMedia->folderId);
        $folder->permissionsFolderId = ($folder->getPermissionFolderId() == null) ? $folder->id : $folder->getPermissionFolderId();

        $oldMedia->save(['saveTags' => false, 'validate' => false]);
    }
}
