<?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 GeoJson\Feature\Feature;
use GeoJson\Feature\FeatureCollection;
use GeoJson\Geometry\Point;
use GuzzleHttp\Client;
use Intervention\Image\ImageManagerStatic as Img;
use Respect\Validation\Validator as v;
use RobThree\Auth\TwoFactorAuth;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Interfaces\PoolInterface;
use Xibo\Event\DisplayGroupLoadEvent;
use Xibo\Factory\DayPartFactory;
use Xibo\Factory\DisplayEventFactory;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\DisplayProfileFactory;
use Xibo\Factory\DisplayTypeFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\NotificationFactory;
use Xibo\Factory\PlayerVersionFactory;
use Xibo\Factory\RequiredFileFactory;
use Xibo\Factory\TagFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Helper\ByteFormatter;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Environment;
use Xibo\Helper\HttpsDetect;
use Xibo\Helper\Random;
use Xibo\Helper\WakeOnLan;
use Xibo\Service\PlayerActionServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Support\Sanitizer\SanitizerInterface;
use Xibo\XMR\LicenceCheckAction;
use Xibo\XMR\PurgeAllAction;
use Xibo\XMR\RekeyAction;
use Xibo\XMR\ScreenShotAction;

/**
 * Class Display
 * @package Xibo\Controller
 */
class Display extends Base
{
    use DisplayProfileConfigFields;

    /**
     * @var StorageServiceInterface
     */
    private $store;

    /**
     * @var PoolInterface
     */
    private $pool;

    /**
     * @var PlayerActionServiceInterface
     */
    private $playerAction;

    /**
     * @var DayPartFactory
     */
    private $dayPartFactory;

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

    /**
     * @var DisplayGroupFactory
     */
    private $displayGroupFactory;

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

    /**
     * @var DisplayProfileFactory
     */
    private $displayProfileFactory;

    /**
     * @var DisplayTypeFactory
     */
    private $displayTypeFactory;

    /** @var  DisplayEventFactory */
    private $displayEventFactory;

    /** @var PlayerVersionFactory */
    private $playerVersionFactory;

    /** @var  RequiredFileFactory */
    private $requiredFileFactory;

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

    /** @var NotificationFactory */
    private $notificationFactory;

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

    /**
     * Set common dependencies.
     * @param StorageServiceInterface $store
     * @param PoolInterface $pool
     * @param PlayerActionServiceInterface $playerAction
     * @param DisplayFactory $displayFactory
     * @param DisplayGroupFactory $displayGroupFactory
     * @param DisplayTypeFactory $displayTypeFactory
     * @param LayoutFactory $layoutFactory
     * @param DisplayProfileFactory $displayProfileFactory
     * @param DisplayEventFactory $displayEventFactory
     * @param RequiredFileFactory $requiredFileFactory
     * @param TagFactory $tagFactory
     * @param NotificationFactory $notificationFactory
     * @param UserGroupFactory $userGroupFactory
     * @param PlayerVersionFactory $playerVersionFactory
     * @param DayPartFactory $dayPartFactory
     */
    public function __construct($store, $pool, $playerAction, $displayFactory, $displayGroupFactory, $displayTypeFactory, $layoutFactory, $displayProfileFactory, $displayEventFactory, $requiredFileFactory, $tagFactory, $notificationFactory, $userGroupFactory, $playerVersionFactory, $dayPartFactory)
    {
        $this->store = $store;
        $this->pool = $pool;
        $this->playerAction = $playerAction;
        $this->displayFactory = $displayFactory;
        $this->displayGroupFactory = $displayGroupFactory;
        $this->displayTypeFactory = $displayTypeFactory;
        $this->layoutFactory = $layoutFactory;
        $this->displayProfileFactory = $displayProfileFactory;
        $this->displayEventFactory = $displayEventFactory;
        $this->requiredFileFactory = $requiredFileFactory;
        $this->tagFactory = $tagFactory;
        $this->notificationFactory = $notificationFactory;
        $this->userGroupFactory = $userGroupFactory;
        $this->playerVersionFactory = $playerVersionFactory;
        $this->dayPartFactory = $dayPartFactory;
    }

    /**
     * @SWG\Get(
     *  path="/displayvenue",
     *  summary="Get Display Venues",
     *  tags={"displayVenue"},
     *  operationId="displayVenueSearch",
     *  @SWG\Response(
     *      response=200,
     *      description="a successful response",
     *  )
     * )
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function displayVenue(Request $request, Response $response)
    {
        if (!file_exists(PROJECT_ROOT . '/openooh/specification.json')) {
            throw new GeneralException(__('OpenOOH specification missing'));
        }

        $content = file_get_contents(PROJECT_ROOT . '/openooh/specification.json');
        $data = json_decode($content, true);

        $taxonomy = [];
        $i = 0;
        foreach ($data['openooh_venue_taxonomy']['specification']['categories'] as $categories) {
            $taxonomy[$i]['venueId'] = $categories['enumeration_id'];
            $taxonomy[$i]['venueName'] = $categories['name'];

            $i++;
            foreach ($categories['children'] as $children) {
                $taxonomy[$i]['venueId'] = $children['enumeration_id'];
                $taxonomy[$i]['venueName'] = $categories['name'] . ' -> ' . $children['name'];
                $i++;

                if (isset($children['children'])) {
                    foreach ($children['children'] as $grandchildren) {
                        $taxonomy[$i]['venueId'] = $grandchildren['enumeration_id'] ;
                        $taxonomy[$i]['venueName'] = $categories['name'] . ' -> ' . $children['name'] .  ' -> ' . $grandchildren['name'] ;
                        $i++;
                    }
                }
            }
        }

        $this->getState()->template = 'grid';
        $this->getState()->recordsTotal = count($taxonomy);
        $this->getState()->setData($taxonomy);

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

    /**
     * Include display page template page based on sub page selected
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    function displayPage(Request $request, Response $response)
    {
        // Build a list of display profiles
        $displayProfiles = $this->displayProfileFactory->query();
        $displayProfiles[] = ['displayProfileId' => -1, 'name' => __('Default')];

        // Call to render the template
        $this->getState()->template = 'display-page';

        $mapConfig = [
            'setArea' => [
                'lat' => $this->getConfig()->getSetting('DEFAULT_LAT'),
                'long' => $this->getConfig()->getSetting('DEFAULT_LONG'),
                'zoom' => 7
            ]
        ];

        $this->getState()->setData([
            'mapConfig' => $mapConfig,
            'displayProfiles' => $displayProfiles
        ]);

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

    /**
     * Display Management Page for an Individual Display
     * @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
     */
    function displayManage(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        // Zero out some variables
        $dependencies = [];
        $layouts = [];
        $widgets = [];
        $widgetData = [];
        $media = [];
        $totalCount = 0;
        $completeCount = 0;
        $totalSize = 0;
        $completeSize = 0;

        // Show 4 widgets
        // Dependencies
        $sql = '
          SELECT `requiredfile`.*
              FROM `requiredfile`
           WHERE `requiredfile`.displayId = :displayId 
            AND `requiredfile`.type = :type
          ORDER BY fileType, path
        ';

        foreach ($this->store->select($sql, ['displayId' => $id, 'type' => 'P']) as $row) {
            $totalSize = $totalSize + $row['size'];
            $totalCount++;
            
            if (intval($row['complete']) === 1) {
                $completeSize = $completeSize + $row['size'];
                $completeCount = $completeCount + 1;
            }

            $row = $this->getSanitizer($row);

            $dependencies[] = [
                'path' => $row->getString('path'),
                'fileType' => $row->getString('fileType'),
                'bytesRequested' => $row->getInt('bytesRequested'),
                'complete' => $row->getInt('complete'),
            ];
        }

        // Layouts
        $sql = '
          SELECT layoutId, layout, `requiredfile`.*
              FROM `layout`
                INNER JOIN `requiredfile`
                ON `requiredfile`.itemId = `layout`.layoutId
           WHERE `requiredfile`.displayId = :displayId 
            AND `requiredfile`.type = :type
          ORDER BY layout
        ';

        foreach ($this->store->select($sql, ['displayId' => $id, 'type' => 'L']) as $row) {
            $rf = $this->requiredFileFactory->getByDisplayAndLayout($id, $row['layoutId']);

            $totalCount++;

            if ($rf->complete) {
                $completeCount = $completeCount + 1;
            }

            $rf = $rf->toArray();
            $rf['layout'] = $row['layout'];
            $layouts[] = $rf;
        }

        // Media
        $sql = '
          SELECT mediaId, `name`, fileSize, media.type AS mediaType, storedAs, `requiredfile`.*
              FROM `media`
                INNER JOIN `requiredfile`
                ON `requiredfile`.itemId = `media`.mediaId
           WHERE `requiredfile`.displayId = :displayId 
            AND `requiredfile`.type = :type
          ORDER BY `name`
        ';

        foreach ($this->store->select($sql, ['displayId' => $id, 'type' => 'M']) as $row) {
            $rf = $this->requiredFileFactory->getByDisplayAndMedia($id, $row['mediaId']);

            $totalSize = $totalSize + $row['fileSize'];
            $totalCount++;

            if ($rf->complete) {
                $completeSize = $completeSize + $row['fileSize'];
                $completeCount = $completeCount + 1;
            }

            $rf = $rf->toArray();
            $rf['name'] = $row['name'];
            $rf['type'] = $row['mediaType'];
            $rf['storedAs'] = $row['storedAs'];
            $rf['size'] = $row['fileSize'];
            $media[] = $rf;
        }

        // Widgets
        $sql = '
          SELECT `widget`.`type` AS widgetType,
                `widgetoption`.`value` AS widgetName,
                `widget`.`widgetId`,
                `requiredfile`.*
              FROM `widget`
                INNER JOIN `requiredfile`
                ON `requiredfile`.itemId = `widget`.widgetId
                LEFT OUTER JOIN `widgetoption`
                ON `widgetoption`.widgetId = `widget`.widgetId
                  AND `widgetoption`.option = \'name\'
           WHERE `requiredfile`.`displayId` = :displayId 
            AND `requiredfile`.`type` IN (\'W\', \'D\')
          ORDER BY `widgetoption`.value, `widget`.type, `widget`.widgetId
        ';

        foreach ($this->store->select($sql, ['displayId' => $id]) as $row) {
            $row = $this->getSanitizer($row);
            $entry = [];
            $entry['type'] = $row->getString('widgetType');
            $entry['widgetName'] = $row->getString('widgetName');
            $entry['widgetType'] = $row->getString('widgetType');

            if ($row->getString('type') === 'W') {
                $rf = $this->requiredFileFactory->getByDisplayAndWidget($id, $row->getInt('widgetId'));

                $totalCount++;

                if ($rf->complete) {
                    $completeCount = $completeCount + 1;
                }

                $widgets[] = array_merge($entry, $rf->toArray());
            } else {
                $entry['widgetId'] = $row->getInt('widgetId');
                $entry['bytesRequested'] = $row->getInt('bytesRequested');
                $widgetData[] = $entry;
            }
        }

        // Widget for file status
        // Decide what our units are going to be, based on the size
        $suffixes = array('bytes', 'k', 'M', 'G', 'T');
        $base = (int)floor(log($totalSize) / log(1024));

        if ($base < 0) {
            $base = 0;
        }

        $units = $suffixes[$base] ?? '';
        $this->getLog()->debug(sprintf('Base for size is %d and suffix is %s', $base, $units));


        // Call to render the template
        $this->getState()->template = 'display-page-manage';
        $this->getState()->setData([
            'requiredFiles' => [],
            'display' => $display,
            'timeAgo' => Carbon::createFromTimestamp($display->lastAccessed)->diffForHumans(),
            'errorSearch' => http_build_query([
                'displayId' => $display->displayId,
                'type' => 'ERROR',
                'fromDt' => Carbon::now()->subHours(12)->format(DateFormatHelper::getSystemFormat()),
                'toDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat())
            ]),
            'inventory' => [
                'dependencies' => $dependencies,
                'layouts' => $layouts,
                'media' => $media,
                'widgets' => $widgets,
                'widgetData' => $widgetData,
            ],
            'status' => [
                'units' => $units,
                'countComplete' => $completeCount,
                'countRemaining' => $totalCount - $completeCount,
                'sizeComplete' => round((double)$completeSize / (pow(1024, $base)), 2),
                'sizeRemaining' => round((double)($totalSize - $completeSize) / (pow(1024, $base)), 2),
            ],
            'defaults' => [
                'fromDate' => Carbon::now()->startOfMonth()->format(DateFormatHelper::getSystemFormat()),
                'fromDateOneDay' => Carbon::now()->subDay()->format(DateFormatHelper::getSystemFormat()),
                'toDate' => Carbon::now()->endOfMonth()->format(DateFormatHelper::getSystemFormat())
            ]
        ]);

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

    /**
     * Get display filters
     * @param SanitizerInterface $parsedQueryParams
     * @return array
     */
    public function getFilters(SanitizerInterface $parsedQueryParams): array
    {
        return [
            'displayId' => $parsedQueryParams->getInt('displayId'),
            'display' => $parsedQueryParams->getString('display'),
            'useRegexForName' => $parsedQueryParams->getCheckbox('useRegexForName'),
            'macAddress' => $parsedQueryParams->getString('macAddress'),
            'license' => $parsedQueryParams->getString('hardwareKey'),
            'displayGroupId' => $parsedQueryParams->getInt('displayGroupId'),
            'clientVersion' => $parsedQueryParams->getString('clientVersion'),
            'clientType' => $parsedQueryParams->getString('clientType'),
            'clientCode' => $parsedQueryParams->getString('clientCode'),
            'customId' => $parsedQueryParams->getString('customId'),
            'authorised' => $parsedQueryParams->getInt('authorised'),
            'displayProfileId' => $parsedQueryParams->getInt('displayProfileId'),
            'tags' => $parsedQueryParams->getString('tags'),
            'exactTags' => $parsedQueryParams->getCheckbox('exactTags'),
            'showTags' => true,
            'clientAddress' => $parsedQueryParams->getString('clientAddress'),
            'mediaInventoryStatus' => $parsedQueryParams->getInt('mediaInventoryStatus'),
            'loggedIn' => $parsedQueryParams->getInt('loggedIn'),
            'lastAccessed' => $parsedQueryParams->getDate('lastAccessed')?->format('U'),
            'displayGroupIdMembers' => $parsedQueryParams->getInt('displayGroupIdMembers'),
            'orientation' => $parsedQueryParams->getString('orientation'),
            'commercialLicence' => $parsedQueryParams->getInt('commercialLicence'),
            'folderId' => $parsedQueryParams->getInt('folderId'),
            'logicalOperator' => $parsedQueryParams->getString('logicalOperator'),
            'logicalOperatorName' => $parsedQueryParams->getString('logicalOperatorName'),
            'bounds' => $parsedQueryParams->getString('bounds'),
            'syncGroupId' => $parsedQueryParams->getInt('syncGroupId'),
            'syncGroupIdMembers' => $parsedQueryParams->getInt('syncGroupIdMembers'),
            'xmrRegistered' => $parsedQueryParams->getInt('xmrRegistered'),
            'isPlayerSupported' => $parsedQueryParams->getInt('isPlayerSupported'),
            'displayGroupIds' => $parsedQueryParams->getIntArray('displayGroupIds'),
        ];
    }

    /**
     * Grid of Displays
     *
     * @SWG\Get(
     *  path="/display",
     *  operationId="displaySearch",
     *  tags={"display"},
     *  summary="Display Search",
     *  description="Search Displays for this User",
     *  @SWG\Parameter(
     *      name="displayId",
     *      in="query",
     *      description="Filter by Display Id",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="displayGroupId",
     *      in="query",
     *      description="Filter by DisplayGroup Id",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="display",
     *      in="query",
     *      description="Filter by Display Name",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="tags",
     *      in="query",
     *      description="Filter by tags",
     *      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="macAddress",
     *      in="query",
     *      description="Filter by Mac Address",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="hardwareKey",
     *      in="query",
     *      description="Filter by Hardware Key",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="clientVersion",
     *      in="query",
     *      description="Filter by Client Version",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="clientType",
     *      in="query",
     *      description="Filter by Client Type",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="clientCode",
     *      in="query",
     *      description="Filter by Client Code",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="embed",
     *      in="query",
     *      description="Embed related data, namely displaygroups. A comma separated list of child objects to embed.",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="authorised",
     *      in="query",
     *      description="Filter by authorised flag",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="displayProfileId",
     *      in="query",
     *      description="Filter by Display Profile",
     *      type="integer",
     *      required=false
     *   ),
     *  *  @SWG\Parameter(
     *      name="mediaInventoryStatus",
     *      in="query",
     *      description="Filter by Display Status ( 1 - up to date, 2 - downloading, 3 - Out of date)",
     *      type="integer",
     *      required=false
     *   ),
     *  *  @SWG\Parameter(
     *      name="loggedIn",
     *      in="query",
     *      description="Filter by Logged In flag",
     *      type="integer",
     *      required=false
     *   ),
     *  *  @SWG\Parameter(
     *      name="lastAccessed",
     *      in="query",
     *      description="Filter by Display Last Accessed date, expects date in Y-m-d H:i:s format",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="folderId",
     *      in="query",
     *      description="Filter by Folder ID",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *       name="xmrRegistered",
     *       in="query",
     *       description="Filter by whether XMR is registed (1 or 0)",
     *       type="integer",
     *       required=false
     *    ),
     *  @SWG\Parameter(
     *       name="isPlayerSupported",
     *       in="query",
     *       description="Filter by whether the player is supported (1 or 0)",
     *       type="integer",
     *       required=false
     *    ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(
     *          type="array",
     *          @SWG\Items(ref="#/definitions/Display")
     *      )
     *  )
     * )
     *
     * @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 grid(Request $request, Response $response)
    {
        $parsedQueryParams = $this->getSanitizer($request->getQueryParams());
        // Embed?
        $embed = ($parsedQueryParams->getString('embed') != null)
            ? explode(',', $parsedQueryParams->getString('embed'))
            : [];

        $filter = $this->getFilters($parsedQueryParams);

        // Get a list of displays
        $displays = $this->displayFactory->query(
            $this->gridRenderSort($parsedQueryParams),
            $this->gridRenderFilter($filter, $parsedQueryParams)
        );

        // Get all Display Profiles
        $displayProfiles = [];
        foreach ($this->displayProfileFactory->query() as $displayProfile) {
            $displayProfiles[$displayProfile->displayProfileId] = $displayProfile->name;
        }

        // validate displays so we get a realistic view of the table
        $this->validateDisplays($displays);

        foreach ($displays as $display) {
            /* @var \Xibo\Entity\Display $display */
            if (in_array('displaygroups', $embed)) {
                $display->load();
            } else {
                $display->excludeProperty('displayGroups');
            }

            if (in_array('overrideconfig', $embed)) {
                $display->includeProperty('overrideConfig');
            }

            $display->setUnmatchedProperty(
                'bandwidthLimitFormatted',
                ByteFormatter::format($display->bandwidthLimit * 1024)
            );

            // Current layout from cache
            $display->getCurrentLayoutId($this->pool, $this->layoutFactory);

            if ($this->isApi($request)) {
                $display->lastAccessed =
                    Carbon::createFromTimestamp($display->lastAccessed)->format(DateFormatHelper::getSystemFormat());
                $display->auditingUntil = ($display->auditingUntil == 0)
                    ? 0
                    : Carbon::createFromTimestamp($display->auditingUntil)->format(DateFormatHelper::getSystemFormat());
                $display->storageAvailableSpace = ByteFormatter::format($display->storageAvailableSpace);
                $display->storageTotalSpace = ByteFormatter::format($display->storageTotalSpace);
                continue;
            }

            // use try and catch here to cover scenario
            // when there is no default display profile set for any of the existing display types.
            $displayProfileName = '';
            try {
                $defaultDisplayProfile = $this->displayProfileFactory->getDefaultByType($display->clientType);
                $displayProfileName = $defaultDisplayProfile->name;
            } catch (NotFoundException) {
                $this->getLog()->debug('No default Display Profile set for Display type ' . $display->clientType);
            }

            // Add in the display profile information
            $display->setUnmatchedProperty(
                'displayProfile',
                (!array_key_exists($display->displayProfileId, $displayProfiles))
                    ? $displayProfileName . __(' (Default)')
                    : $displayProfiles[$display->displayProfileId]
            );

            $display->includeProperty('buttons');

            // Format the storage available / total space
            $display->setUnmatchedProperty(
                'storageAvailableSpaceFormatted',
                ByteFormatter::format($display->storageAvailableSpace)
            );
            $display->setUnmatchedProperty(
                'storageTotalSpaceFormatted',
                ByteFormatter::format($display->storageTotalSpace)
            );
            $display->setUnmatchedProperty(
                'storagePercentage',
                ($display->storageTotalSpace == 0)
                    ? 0
                    : round($display->storageAvailableSpace / $display->storageTotalSpace * 100.0, 2)
            );

            // Set some text for the display status
            $display->setUnmatchedProperty('statusDescription', match ($display->mediaInventoryStatus) {
                1 => __('Display is up to date'),
                2 => __('Display is downloading new files'),
                3 => __('Display is out of date but has not yet checked in with the server'),
                default => __('Unknown Display Status'),
            });

            // Commercial Licence
            $display->setUnmatchedProperty('commercialLicenceDescription', match ($display->commercialLicence) {
                1 => __('Display is fully licensed'),
                2 => __('Display is on a trial licence'),
                default => __('Display is not licensed'),
            });

            if ($display->clientCode < 400) {
                $commercialLicenceDescription = $display->getUnmatchedProperty('commercialLicenceDescription');
                $commercialLicenceDescription .= ' ('
                    . __('The status will be updated with each Commercial Licence check') . ')';
                $display->setUnmatchedProperty('commercialLicenceDescription', $commercialLicenceDescription);
            }

            // Thumbnail
            $display->setUnmatchedProperty('thumbnail', '');
            // If we aren't logged in, and we are showThumbnail == 2, then show a circle
            if (file_exists($this->getConfig()->getSetting('LIBRARY_LOCATION') . 'screenshots/'
                . $display->displayId . '_screenshot.jpg')) {
                $display->setUnmatchedProperty(
                    'thumbnail',
                    $this->urlFor($request, 'display.screenShot', [
                        'id' => $display->displayId
                    ]) . '?' . Random::generateString()
                );
            }

            $display->setUnmatchedProperty(
                'teamViewerLink',
                (!empty($display->teamViewerSerial))
                    ? 'https://start.teamviewer.com/' . $display->teamViewerSerial
                    : ''
            );
            $display->setUnmatchedProperty(
                'webkeyLink',
                (!empty($display->webkeySerial))
                    ? 'https://device.webkeyapp.com/phone?publicid=' . $display->webkeySerial
                    : ''
            );

            // Is a transfer to another CMS in progress?
            $display->setUnmatchedProperty('isCmsTransferInProgress', (!empty($display->newCmsAddress)));

            // Edit and Delete buttons first
            if ($this->getUser()->featureEnabled('displays.modify')
                && $this->getUser()->checkEditable($display)
            ) {
                // Manage
                $display->buttons[] = [
                    'id' => 'display_button_manage',
                    'url' => $this->urlFor($request, 'display.manage', ['id' => $display->displayId]),
                    'text' => __('Manage'),
                    'external' => true
                ];

                $display->buttons[] = ['divider' => true];

                // Edit
                $display->buttons[] = [
                    'id' => 'display_button_edit',
                    'url' => $this->urlFor($request, 'display.edit.form', ['id' => $display->displayId]),
                    'text' => __('Edit')
                ];
            }

            // Delete
            if ($this->getUser()->featureEnabled('displays.modify')
                && $this->getUser()->checkDeleteable($display)
            ) {
                $deleteButton = [
                    'id' => 'display_button_delete',
                    'url' => $this->urlFor($request, 'display.delete.form', ['id' => $display->displayId]),
                    'text' => __('Delete')
                ];

                // We only include this in dev mode, because users have complained that it is too powerful a feature
                // to have in the core product.
                if (Environment::isDevMode()) {
                    $deleteButton['multi-select'] = true;
                    $deleteButton['dataAttributes'] = [
                        [
                            'name' => 'commit-url',
                            'value' => $this->urlFor(
                                $request,
                                'display.delete',
                                ['id' => $display->displayId]
                            )
                        ],
                        ['name' => 'commit-method', 'value' => 'delete'],
                        ['name' => 'id', 'value' => 'display_button_delete'],
                        ['name' => 'sort-group', 'value' => 1],
                        ['name' => 'text', 'value' => __('Delete')],
                        ['name' => 'rowtitle', 'value' => $display->display]
                    ];
                }

                $display->buttons[] = $deleteButton;
            }

            if ($this->getUser()->featureEnabled('displays.modify')
                && ($this->getUser()->checkEditable($display) || $this->getUser()->checkDeleteable($display))
            ) {
                $display->buttons[] = ['divider' => true];
            }

            if ($this->getUser()->featureEnabled('displays.modify')
                && $this->getUser()->checkEditable($display)
            ) {
                // Authorise
                $display->buttons[] = [
                    'id' => 'display_button_authorise',
                    'url' => $this->urlFor($request, 'display.authorise.form', ['id' => $display->displayId]),
                    'text' => __('Authorise'),
                    'multi-select' => true,
                    'dataAttributes' => [
                        ['name' => 'auto-submit', 'value' => true],
                        [
                            'name' => 'commit-url',
                            'value' => $this->urlFor(
                                $request,
                                'display.authorise',
                                ['id' => $display->displayId]
                            )
                        ],
                        ['name' => 'commit-method', 'value' => 'put'],
                        ['name' => 'id', 'value' => 'display_button_authorise'],
                        ['name' => 'sort-group', 'value' => 2],
                        ['name' => 'text', 'value' => __('Toggle Authorise')],
                        ['name' => 'rowtitle', 'value' => $display->display]
                    ]
                ];

                // Default Layout
                $display->buttons[] = [
                    'id' => 'display_button_defaultlayout',
                    'url' => $this->urlFor($request, 'display.defaultlayout.form', ['id' => $display->displayId]),
                    'text' => __('Default Layout'),
                    'multi-select' => true,
                    'dataAttributes' => [
                        [
                            'name' => 'commit-url',
                            'value' => $this->urlFor(
                                $request,
                                'display.defaultlayout',
                                ['id' => $display->displayId]
                            )
                        ],
                        ['name' => 'commit-method', 'value' => 'put'],
                        ['name' => 'id', 'value' => 'display_button_defaultlayout'],
                        ['name' => 'sort-group', 'value' => 2],
                        ['name' => 'text', 'value' => __('Set Default Layout')],
                        ['name' => 'rowtitle', 'value' => $display->display],
                        ['name' => 'form-callback', 'value' => 'setDefaultMultiSelectFormOpen']
                    ]
                ];

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

                if (in_array($display->clientType, ['android', 'lg', 'sssp', 'chromeOS'])) {
                    $display->buttons[] = array(
                        'id' => 'display_button_checkLicence',
                        'url' => $this->urlFor($request, 'display.licencecheck.form', ['id' => $display->displayId]),
                        'text' => __('Check Licence'),
                        'multi-select' => true,
                        'dataAttributes' => [
                            ['name' => 'auto-submit', 'value' => true],
                            [
                                'name' => 'commit-url',
                                'value' => $this->urlFor(
                                    $request,
                                    'display.licencecheck',
                                    ['id' => $display->displayId]
                                )
                            ],
                            ['name' => 'commit-method', 'value' => 'put'],
                            ['name' => 'id', 'value' => 'display_button_checkLicence'],
                            ['name' => 'sort-group', 'value' => 2],
                            ['name' => 'text', 'value' => __('Check Licence')],
                            ['name' => 'rowtitle', 'value' => $display->display]
                        ]
                    );
                }

                $display->buttons[] = ['divider' => true];
            }

            // Schedule
            if ($this->getUser()->featureEnabled('schedule.add')
                && ($this->getUser()->checkEditable($display)
                    || $this->getConfig()->getSetting('SCHEDULE_WITH_VIEW_PERMISSION') == 1)
            ) {
                $display->buttons[] = array(
                    'id' => 'display_button_schedule',
                    'url' => $this->urlFor(
                        $request,
                        'schedule.add.form',
                        ['id' => $display->displayGroupId, 'from' => 'DisplayGroup']
                    ),
                    'text' => __('Schedule')
                );
            }

            // Check if limited view access is allowed
            if (($this->getUser()->featureEnabled('displays.modify') && $this->getUser()->checkEditable($display))
                || $this->getUser()->featureEnabled('displays.limitedView')
            ) {
                if ($this->getUser()->checkEditable($display)) {
                    if ($this->getUser()->featureEnabled('layout.view')) {
                        $display->buttons[] = [
                            'id' => 'display_button_layouts_jump',
                            'linkType' => '_self',
                            'external' => true,
                            'url' => $this->urlFor($request, 'layout.view')
                                . '?activeDisplayGroupId=' . $display->displayGroupId,
                            'text' => __('Jump to Scheduled Layouts')
                        ];
                    }

                    // File Associations
                    $display->buttons[] = array(
                        'id' => 'displaygroup_button_fileassociations',
                        'url' => $this->urlFor($request, 'displayGroup.media.form', ['id' => $display->displayGroupId]),
                        'text' => __('Assign Files')
                    );

                    // Layout Assignments
                    $display->buttons[] = array(
                        'id' => 'displaygroup_button_layout_associations',
                        'url' => $this->urlFor($request, 'displayGroup.layout.form', ['id' => $display->displayGroupId]),
                        'text' => __('Assign Layouts')
                    );
                }

                // Screen Shot
                $display->buttons[] = [
                    'id' => 'display_button_requestScreenShot',
                    'url' => $this->urlFor($request, 'display.screenshot.form', ['id' => $display->displayId]),
                    'text' => __('Request Screen Shot'),
                    'multi-select' => true,
                    'dataAttributes' => [
                        ['name' => 'auto-submit', 'value' => true],
                        [
                            'name' => 'commit-url',
                            'value' => $this->urlFor(
                                $request,
                                'display.requestscreenshot',
                                ['id' => $display->displayId]
                            )
                        ],
                        ['name' => 'commit-method', 'value' => 'put'],
                        ['name' => 'sort-group', 'value' => 3],
                        ['name' => 'id', 'value' => 'display_button_requestScreenShot'],
                        ['name' => 'text', 'value' => __('Request Screen Shot')],
                        ['name' => 'rowtitle', 'value' => $display->display]
                    ]
                ];

                // Collect Now
                $display->buttons[] = [
                    'id' => 'display_button_collectNow',
                    'url' => $this->urlFor(
                        $request,
                        'displayGroup.collectNow.form',
                        ['id' => $display->displayGroupId]
                    ),
                    'text' => __('Collect Now'),
                    'multi-select' => true,
                    'dataAttributes' => [
                        ['name' => 'auto-submit', 'value' => true],
                        [
                            'name' => 'commit-url',
                            'value' => $this->urlFor(
                                $request,
                                'displayGroup.action.collectNow',
                                ['id' => $display->displayGroupId]
                            )
                        ],
                        ['name' => 'commit-method', 'value' => 'post'],
                        ['name' => 'sort-group', 'value' => 3],
                        ['name' => 'id', 'value' => 'display_button_collectNow'],
                        ['name' => 'text', 'value' => __('Collect Now')],
                        ['name' => 'rowtitle', 'value' => $display->display]
                    ]
                ];

                if ($this->getUser()->checkEditable($display)) {
                    // Trigger webhook
                    $display->buttons[] = [
                        'id' => 'display_button_trigger_webhook',
                        'url' => $this->urlFor(
                            $request,
                            'displayGroup.trigger.webhook.form',
                            ['id' => $display->displayGroupId]
                        ),
                        'text' => __('Trigger a web hook'),
                        'multi-select' => true,
                        'dataAttributes' => [
                            [
                                'name' => 'commit-url',
                                'value' => $this->urlFor(
                                    $request,
                                    'displayGroup.action.trigger.webhook',
                                    ['id' => $display->displayGroupId]
                                )
                            ],
                            ['name' => 'commit-method', 'value' => 'post'],
                            ['name' => 'id', 'value' => 'display_button_trigger_webhook'],
                            ['name' => 'sort-group', 'value' => 3],
                            ['name' => 'text', 'value' => __('Trigger a web hook')],
                            ['name' => 'rowtitle', 'value' => $display->display],
                            ['name' => 'form-callback', 'value' => 'triggerWebhookMultiSelectFormOpen']
                        ]
                    ];

                    if ($this->getUser()->isSuperAdmin()) {
                        $display->buttons[] = [
                            'id' => 'display_button_purgeAll',
                            'url' => $this->urlFor($request, 'display.purge.all.form', ['id' => $display->displayId]),
                            'text' => __('Purge All')
                        ];
                    }

                    $display->buttons[] = ['divider' => true];
                }
            }

            if ($this->getUser()->featureEnabled('displays.modify')
                && $this->getUser()->checkPermissionsModifyable($display)
            ) {
                // Display Groups
                $display->buttons[] = array(
                    'id' => 'display_button_group_membership',
                    'url' => $this->urlFor($request, 'display.membership.form', ['id' => $display->displayId]),
                    'text' => __('Display Groups')
                );

                // Permissions
                $display->buttons[] = [
                    'id' => 'display_button_group_permissions',
                    'url' => $this->urlFor(
                        $request,
                        'user.permissions.form',
                        ['entity' => 'DisplayGroup', 'id' => $display->displayGroupId]
                    ),
                    'text' => __('Share'),
                    'multi-select' => true,
                    'dataAttributes' => [
                        [
                            'name' => 'commit-url',
                            'value' => $this->urlFor(
                                $request,
                                'user.permissions.multi',
                                ['entity' => 'DisplayGroup', 'id' => $display->displayGroupId]
                            )
                        ],
                        ['name' => 'commit-method', 'value' => 'post'],
                        ['name' => 'id', 'value' => 'display_button_group_permissions'],
                        ['name' => 'text', 'value' => __('Share')],
                        ['name' => 'rowtitle', 'value' => $display->display],
                        ['name' => 'sort-group', 'value' => 4],
                        ['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
                        [
                            'name' => 'custom-handler-url',
                            'value' => $this->urlFor(
                                $request,
                                'user.permissions.multi.form',
                                ['entity' => 'DisplayGroup']
                            )
                        ],
                        ['name' => 'content-id-name', 'value' => 'displayGroupId']
                    ]
                ];
            }

            // Check if limited view access is allowed
            if (($this->getUser()->featureEnabled('displays.modify') && $this->getUser()->checkEditable($display))
                || $this->getUser()->featureEnabled('displays.limitedView')
            ) {
                if ($this->getUser()->checkPermissionsModifyable($display)) {
                    $display->buttons[] = ['divider' => true];
                }

                if ($this->getUser()->checkEditable($display)) {
                    // Wake On LAN
                    $display->buttons[] = array(
                        'id' => 'display_button_wol',
                        'url' => $this->urlFor($request, 'display.wol.form', ['id' => $display->displayId]),
                        'text' => __('Wake on LAN')
                    );
                }

                // Send Command
                $display->buttons[] = [
                    'id' => 'displaygroup_button_command',
                    'url' => $this->urlFor($request, 'displayGroup.command.form', ['id' => $display->displayGroupId]),
                    'text' => __('Send Command'),
                    'multi-select' => true,
                    'dataAttributes' => [
                        [
                            'name' => 'commit-url',
                            'value' => $this->urlFor(
                                $request,
                                'displayGroup.action.command',
                                ['id' => $display->displayGroupId]
                            )
                        ],
                        ['name' => 'commit-method', 'value' => 'post'],
                        ['name' => 'id', 'value' => 'displaygroup_button_command'],
                        ['name' => 'text', 'value' => __('Send Command')],
                        ['name' => 'sort-group', 'value' => 3],
                        ['name' => 'rowtitle', 'value' => $display->display],
                        ['name' => 'form-callback', 'value' => 'sendCommandMultiSelectFormOpen']
                    ]
                ];

                if ($this->getUser()->checkEditable($display)) {
                    $display->buttons[] = ['divider' => true];

                    $display->buttons[] = [
                        'id' => 'display_button_move_cms',
                        'url' => $this->urlFor($request, 'display.moveCms.form', ['id' => $display->displayId]),
                        'text' => __('Transfer to another CMS'),
                        'multi-select' => true,
                        'dataAttributes' => [
                            [
                                'name' => 'commit-url',
                                'value' => $this->urlFor(
                                    $request,
                                    'display.moveCms',
                                    ['id' => $display->displayId]
                                )
                            ],
                            ['name' => 'commit-method', 'value' => 'put'],
                            ['name' => 'id', 'value' => 'display_button_move_cms'],
                            ['name' => 'text', 'value' => __('Transfer to another CMS')],
                            ['name' => 'sort-group', 'value' => 5],
                            ['name' => 'rowtitle', 'value' => $display->display],
                            ['name' => 'form-callback', 'value' => 'setMoveCmsMultiSelectFormOpen']
                        ]
                    ];

                    $display->buttons[] = [
                        'multi-select' => true,
                        'multiSelectOnly' => true, // Show button only on multi-select menu
                        'id' => 'display_button_set_bandwidth',
                        'dataAttributes' => [
                            [
                                'name' => 'commit-url',
                                'value' => $this->urlFor(
                                    $request,
                                    'display.setBandwidthLimitMultiple'
                                )
                            ],
                            ['name' => 'commit-method', 'value' => 'post'],
                            ['name' => 'id', 'value' => 'display_button_set_bandwidth'],
                            ['name' => 'text', 'value' => __('Set Bandwidth')],
                            ['name' => 'rowtitle', 'value' => $display->display],
                            ['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
                            [
                                'name' => 'custom-handler-url',
                                'value' => $this->urlFor($request, 'display.setBandwidthLimitMultiple.form')
                            ],
                            ['name' => 'content-id-name', 'value' => 'displayId']
                        ]
                    ];

                    if ($display->getUnmatchedProperty('isCmsTransferInProgress', false)) {
                        $display->buttons[] = [
                            'id' => 'display_button_move_cancel',
                            'url' => $this->urlFor($request, 'display.moveCmsCancel.form', ['id' => $display->displayId]),
                            'text' => __('Cancel CMS Transfer'),
                        ];
                    }
                }
            }
        }

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

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

    /**
     * Displays on map
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws NotFoundException
     */
    public function displayMap(Request $request, Response $response)
    {
        $parsedQueryParams = $this->getSanitizer($request->getQueryParams());

        $filter = $this->getFilters($parsedQueryParams);

        // Get a list of displays
        $displays = $this->displayFactory->query(null, $filter);
        $results = [];
        $status = [
            '1' => __('Up to date'),
            '2' => __('Downloading'),
            '3' => __('Out of date')
        ];

        // Get all Display Profiles
        $displayProfiles = [];
        foreach ($this->displayProfileFactory->query() as $displayProfile) {
            $displayProfiles[$displayProfile->displayProfileId] = $displayProfile->name;
        }

        foreach ($displays as $display) {
            // use try and catch here to cover scenario when there is no default display profile set for any of the existing display types.
            $displayProfileName = '';
            try {
                $defaultDisplayProfile = $this->displayProfileFactory->getDefaultByType($display->clientType);
                $displayProfileName = $defaultDisplayProfile->name;
            } catch (NotFoundException $e) {
                $this->getLog()->debug('No default Display Profile set for Display type ' . $display->clientType);
            }

            // Add in the display profile information
            $display->setUnmatchedProperty(
                'displayProfile',
                (!array_key_exists($display->displayProfileId, $displayProfiles))
                    ? $displayProfileName . __(' (Default)')
                    : $displayProfiles[$display->displayProfileId]
            );

            $properties = [
                'display' => $display->display,
                'status' => $display->mediaInventoryStatus ? $status[$display->mediaInventoryStatus] : __('Unknown'),
                'mediaInventoryStatus' => $display->mediaInventoryStatus,
                'orientation' => ucwords($display->orientation ?: __('Unknown')),
                'displayId' => $display->getId(),
                'licensed' => $display->licensed,
                'loggedIn' => $display->loggedIn,
                'displayProfile' => $display->getUnmatchedProperty('displayProfile'),
                'resolution' => $display->resolution,
                'lastAccessed' => $display->lastAccessed,
            ];

            if (file_exists($this->getConfig()->getSetting('LIBRARY_LOCATION') . 'screenshots/' . $display->displayId . '_screenshot.jpg')) {
                $properties['thumbnail'] = $this->urlFor($request, 'display.screenShot', ['id' => $display->displayId]) . '?' . Random::generateString();
            }

            $longitude = ($display->longitude) ?: $this->getConfig()->getSetting('DEFAULT_LONG');
            $latitude =  ($display->latitude) ?: $this->getConfig()->getSetting('DEFAULT_LAT');

            $geo = new Point([(double)$longitude, (double)$latitude]);

            $results[] = new Feature($geo, $properties);
        }

        return $response->withJson(new FeatureCollection($results));
    }

    /**
     * Edit Display 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
     */
    function editForm(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id, true);

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

        // We have permission - load
        $display->load();

        // Dates
        $auditingUntilIso = !empty($display->auditingUntil)
            ? Carbon::createFromTimestamp($display->auditingUntil)->format(DateFormatHelper::getSystemFormat())
            : null;
        $display->setUnmatchedProperty('auditingUntilIso', $auditingUntilIso);

        // display profile dates
        $displayProfile = $display->getDisplayProfile();

        // Get the settings from the profile
        $profile = $display->getSettings();
        $displayTypes = $this->displayTypeFactory->query();

        $elevateLogsUntil = $displayProfile->getSetting('elevateLogsUntil');
        $elevateLogsUntilIso = !empty($elevateLogsUntil)
            ? Carbon::createFromTimestamp($elevateLogsUntil)->format(DateFormatHelper::getSystemFormat())
            : null;
        $displayProfile->setUnmatchedProperty('elevateLogsUntilIso', $elevateLogsUntilIso);

        // Get a list of timezones
        $timeZones = [];
        foreach (DateFormatHelper::timezoneList() as $key => $value) {
            $timeZones[] = ['id' => $key, 'value' => $value];
        }

        // Get the currently assigned default layout
        try {
            $layouts = (($display->defaultLayoutId != null) ? [$this->layoutFactory->getById($display->defaultLayoutId)] : []);
        } catch (NotFoundException $notFoundException) {
            $layouts = [];
        }

        // Player Version Setting
        $versionId = $display->getSetting('versionMediaId', null, ['displayOnly' => true]);
        $profileVersionId = $display->getDisplayProfile()->getSetting('versionMediaId');
        $playerVersions = [];

        // Daypart - Operating Hours
        $dayPartId = $display->getSetting('dayPartId', null, ['displayOnly' => true]);
        $profileDayPartId = $display->getDisplayProfile()->getSetting('dayPartId');
        $dayparts = [];

        // Get the Player Version for this display profile type
        if ($versionId !== null) {
            try {
                $playerVersions[] = $this->playerVersionFactory->getById($versionId);
            } catch (NotFoundException $e) {
                $this->getLog()->debug('Unknown versionId set on Display Profile for displayId ' . $display->displayId);
            }
        }

        if ($versionId !== $profileVersionId && $profileVersionId !== null) {
            try {
                $playerVersions[] = $this->playerVersionFactory->getById($profileVersionId);
            } catch (NotFoundException $e) {
                $this->getLog()->debug('Unknown versionId set on Display Profile for displayId ' . $display->displayId);
            }
        }

        if ($dayPartId !== null) {
            try {
                $dayparts[] = $this->dayPartFactory->getById($dayPartId);
            } catch (NotFoundException $e) {
                $this->getLog()->debug('Unknown dayPartId set on Display Profile for displayId ' . $display->displayId);
            }
        }

        if ($dayPartId !== $profileDayPartId && $profileDayPartId !== null) {
            try {
                $dayparts[] = $this->dayPartFactory->getById($profileDayPartId);
            } catch (NotFoundException $e) {
                $this->getLog()->debug('Unknown dayPartId set on Display Profile for displayId ' . $display->displayId);
            }
        }

        // A list of languages
        // Build an array of supported languages
        $languages = [];
        $localeDir = PROJECT_ROOT . '/locale';
        foreach (array_map('basename', glob($localeDir . '/*.mo')) as $lang) {
            // Trim the .mo off the end
            $lang = str_replace('.mo', '', $lang);
            $languages[] = ['id' => $lang, 'value' => $lang];
        }

        $this->getState()->template = 'display-form-edit';
        $this->getState()->setData([
            'display' => $display,
            'displayProfile' => $displayProfile,
            'lockOptions' => json_decode($display->getDisplayProfile()->getSetting('lockOptions', '[]'), true),
            'layouts' => $layouts,
            'profiles' => $this->displayProfileFactory->query(null, array('type' => $display->clientType)),
            'settings' => $profile,
            'timeZones' => $timeZones,
            'displayLockName' => ($this->getConfig()->getSetting('DISPLAY_LOCK_NAME_TO_DEVICENAME') == 1),
            'versions' => $playerVersions,
            'displayTypes' => $displayTypes,
            'dayParts' => $dayparts,
            'languages' => $languages,
            'isWolDisabled' => defined('ACCOUNT_ID'),
        ]);

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

    /**
     * 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
     */
    function deleteForm(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

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

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

    /**
     * Display Edit
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     * @SWG\Put(
     *  path="/display/{displayId}",
     *  operationId="displayEdit",
     *  tags={"display"},
     *  summary="Display Edit",
     *  description="Edit a Display",
     *  @SWG\Parameter(
     *      name="displayId",
     *      in="path",
     *      description="The Display ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="display",
     *      in="formData",
     *      description="The Display Name",
     *      type="string",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="description",
     *      in="formData",
     *      description="A description of the Display",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="tags",
     *      in="formData",
     *      description="A comma separated list of tags for this item",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="auditingUntil",
     *      in="formData",
     *      description="A date this Display records auditing information until.",
     *      type="string",
     *      format="date-time",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="defaultLayoutId",
     *      in="formData",
     *      description="A Layout ID representing the Default Layout for this Display.",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="licensed",
     *      in="formData",
     *      description="Flag indicating whether this display is licensed.",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="license",
     *      in="formData",
     *      description="The hardwareKey to use as the licence key for this Display",
     *      type="string",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="incSchedule",
     *      in="formData",
     *      description="Flag indicating whether the Default Layout should be included in the Schedule",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="emailAlert",
     *      in="formData",
     *      description="Flag indicating whether the Display generates up/down email alerts.",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="alertTimeout",
     *      in="formData",
     *      description="How long in seconds should this display wait before alerting when it hasn't connected. Override for the collection interval.",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="wakeOnLanEnabled",
     *      in="formData",
     *      description="Flag indicating if Wake On LAN is enabled for this Display",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="wakeOnLanTime",
     *      in="formData",
     *      description="A h:i string representing the time that the Display should receive its Wake on LAN command",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="broadCastAddress",
     *      in="formData",
     *      description="The BroadCast Address for this Display - used by Wake On LAN",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="secureOn",
     *      in="formData",
     *      description="The secure on configuration for this Display",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="cidr",
     *      in="formData",
     *      description="The CIDR configuration for this Display",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="latitude",
     *      in="formData",
     *      description="The Latitude of this Display",
     *      type="number",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="longitude",
     *      in="formData",
     *      description="The Longitude of this Display",
     *      type="number",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="timeZone",
     *      in="formData",
     *      description="The timezone for this display, or empty to use the CMS timezone",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="languages",
     *      in="formData",
     *      description="An array of languages supported in this display location",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="displayProfileId",
     *      in="formData",
     *      description="The Display Settings Profile ID",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="displayTypeId",
     *      in="formData",
     *      description="The Display Type ID of this Display",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="screenSize",
     *      in="formData",
     *      description="The screen size of this Display",
     *      type="number",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="venueId",
     *      in="formData",
     *      description="The Venue ID of this Display",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="address",
     *      in="formData",
     *      description="The Location Address of this Display",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="isMobile",
     *      in="formData",
     *      description="Is this Display mobile?",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="isOutdoor",
     *      in="formData",
     *      description="Is this Display Outdoor?",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="costPerPlay",
     *      in="formData",
     *      description="The Cost Per Play of this Display",
     *      type="number",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="impressionsPerPlay",
     *      in="formData",
     *      description="The Impressions Per Play of this Display",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="customId",
     *      in="formData",
     *      description="The custom ID (an Id of any external system) of this Display",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="ref1",
     *      in="formData",
     *      description="Reference 1",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="ref2",
     *      in="formData",
     *      description="Reference 2",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="ref3",
     *      in="formData",
     *      description="Reference 3",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="ref4",
     *      in="formData",
     *      description="Reference 4",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="ref5",
     *      in="formData",
     *      description="Reference 5",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="clearCachedData",
     *      in="formData",
     *      description="Clear all Cached data for this display",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="rekeyXmr",
     *      in="formData",
     *      description="Clear the cached XMR configuration and send a rekey",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="teamViewerSerial",
     *      in="formData",
     *      description="The TeamViewer serial number for this Display, if applicable",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="webkeySerial",
     *      in="formData",
     *      description="The Webkey serial number for this Display, if applicable",
     *      type="string",
     *      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",
     *      @SWG\Schema(ref="#/definitions/Display")
     *  )
     * )
     */
    function edit(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id, true);
        $sanitizedParams = $this->getSanitizer($request->getParams());

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

        // Update properties
        if ($this->getConfig()->getSetting('DISPLAY_LOCK_NAME_TO_DEVICENAME') == 0) {
            $display->display = $sanitizedParams->getString('display');
        }

        $display->load();

        $display->description = $sanitizedParams->getString('description');
        $display->displayTypeId = $sanitizedParams->getInt('displayTypeId');
        $display->venueId = $sanitizedParams->getInt('venueId');
        $display->address = $sanitizedParams->getString('address');
        $display->isMobile = $sanitizedParams->getCheckbox('isMobile');
        $languages = $sanitizedParams->getArray('languages');
        if (empty($languages)) {
            $display->languages = null;
        } else {
            $display->languages = implode(',', $languages);
        }
        $display->screenSize = $sanitizedParams->getInt('screenSize');
        $display->auditingUntil = $sanitizedParams->getDate('auditingUntil')?->format('U');
        $display->defaultLayoutId = $sanitizedParams->getInt('defaultLayoutId');
        $display->licensed = $sanitizedParams->getInt('licensed');
        $display->license = $sanitizedParams->getString('license');
        $display->incSchedule = $sanitizedParams->getInt('incSchedule');
        $display->emailAlert = $sanitizedParams->getInt('emailAlert');
        $display->alertTimeout = $sanitizedParams->getCheckbox('alertTimeout');
        $display->latitude = $sanitizedParams->getDouble('latitude');
        $display->longitude = $sanitizedParams->getDouble('longitude');
        $display->timeZone = $sanitizedParams->getString('timeZone');
        $display->displayProfileId = $sanitizedParams->getInt('displayProfileId');
        $display->bandwidthLimit = $sanitizedParams->getInt('bandwidthLimit', ['default' => 0]);
        $display->teamViewerSerial = $sanitizedParams->getString('teamViewerSerial');
        $display->webkeySerial = $sanitizedParams->getString('webkeySerial');
        $display->folderId = $sanitizedParams->getInt('folderId', ['default' => $display->folderId]);
        $display->isOutdoor = $sanitizedParams->getCheckbox('isOutdoor');
        $display->costPerPlay = $sanitizedParams->getDouble('costPerPlay');
        $display->impressionsPerPlay = $sanitizedParams->getDouble('impressionsPerPlay');
        $display->customId = $sanitizedParams->getString('customId');
        $display->ref1 = $sanitizedParams->getString('ref1');
        $display->ref2 = $sanitizedParams->getString('ref2');
        $display->ref3 = $sanitizedParams->getString('ref3');
        $display->ref4 = $sanitizedParams->getString('ref4');
        $display->ref5 = $sanitizedParams->getString('ref5');

        // Wake on Lan
        if (defined('ACCOUNT_ID')) {
            // WOL is not allowed on a Xibo Cloud CMS
            // Force disable, but leave the other settings as they are.
            $display->wakeOnLanEnabled = 0;
        } else {
            $display->wakeOnLanEnabled = $sanitizedParams->getCheckbox('wakeOnLanEnabled');
            $display->wakeOnLanTime = $sanitizedParams->getString('wakeOnLanTime');
            $display->broadCastAddress = $sanitizedParams->getString('broadCastAddress');
            $display->secureOn = $sanitizedParams->getString('secureOn');
            $display->cidr = $sanitizedParams->getString('cidr');
        }

        // Get the display profile and use that to pull in any overrides
        // start with an empty config
        $display->overrideConfig = $this->editConfigFields(
            $display->getDisplayProfile(),
            $sanitizedParams,
            [],
            $display
        );

        // Tags are stored on the displaygroup, we're just passing through here
        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'));
            }

            $display->tags = $tags;
        }

        // Should we invalidate this display?
        if ($display->hasPropertyChanged('defaultLayoutId')) {
            $display->notify();
        } elseif ($sanitizedParams->getCheckbox('clearCachedData', ['default' => 1]) == 1) {
            // Remove the cache if the display licenced state has changed
            $this->pool->deleteItem($display->getCacheKey());
        }

        // Should we rekey?
        if ($sanitizedParams->getCheckbox('rekeyXmr', ['default' => 0]) == 1) {
            // Queue the rekey action first (before we clear the channel and key)
            $this->playerAction->sendAction($display, new RekeyAction());

            // Clear the config.
            $display->xmrChannel = null;
            $display->xmrPubKey = null;
        }

        $display->save();

        if ($this->isApi($request)) {
            $display->lastAccessed = Carbon::createFromTimestamp($display->lastAccessed)
                ->format(DateFormatHelper::getSystemFormat());
            $display->auditingUntil = ($display->auditingUntil == 0)
                    ? 0
                    : Carbon::createFromTimestamp($display->auditingUntil)->format(DateFormatHelper::getSystemFormat());
        }

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

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

    /**
     * Delete a display
     * @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
     * @SWG\Delete(
     *  path="/display/{displayId}",
     *  operationId="displayDelete",
     *  tags={"display"},
     *  summary="Display Delete",
     *  description="Delete a Display",
     *  @SWG\Parameter(
     *      name="displayId",
     *      in="path",
     *      description="The Display ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=204,
     *      description="successful operation"
     *  )
     * )
     */
    function delete(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        if ($display->isLead()) {
            throw new InvalidArgumentException(
                __('Cannot delete a Lead Display of a Sync Group'),
            );
        }

        $display->delete();

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

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

    /**
     * Member of Display Groups 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 membershipForm(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        // Groups we are assigned to
        $groupsAssigned = $this->displayGroupFactory->getByDisplayId($display->displayId);

        $this->getState()->template = 'display-form-membership';
        $this->getState()->setData([
            'display' => $display,
            'extra' => [
                'displayGroupsAssigned' => $groupsAssigned
            ],
        ]);

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

    /**
     * Set Bandwidth to one or more displays
     * @param Request $request
     * @param Response $response
     * @param $ids
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function setBandwidthLimitMultipleForm(Request $request, Response $response)
    {
        $sanitizedParams = $this->getSanitizer($request->getParams());

        // Check if the array of ids is passed
        if ($sanitizedParams->getString('ids') == '') {
            throw new InvalidArgumentException(__('The array of ids is empty!'));
        }

        // Get array of ids
        $ids = $sanitizedParams->getString('ids');

        $this->getState()->template = 'display-form-set-bandwidth';
        $this->getState()->setData([
            'ids' => $ids,
        ]);

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

        /**
     * Set Bandwidth to one or more displays
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function setBandwidthLimitMultiple(Request $request, Response $response)
    {
        $sanitizedParams = $this->getSanitizer($request->getParams());

        // Get array of ids
        $ids = ($sanitizedParams->getString('ids') != '') ? explode(',', $sanitizedParams->getString('ids')) : [];
        $bandwidthLimit = intval($sanitizedParams->getString('bandwidthLimit'));
        $bandwidthLimitUnits = $sanitizedParams->getString('bandwidthLimitUnits');

        // Check if the array of ids is passed
        if (count($ids) == 0) {
            throw new InvalidArgumentException(__('The array of ids is empty!'));
        }

        // Check if the bandwidth value has something
        if ($bandwidthLimit == '') {
            throw new InvalidArgumentException(__('The array of ids is empty!'));
        }

        // convert bandwidth to kb based on form units
        if ($bandwidthLimitUnits == 'mb') {
            $bandwidthLimit = $bandwidthLimit * 1024;
        } elseif ($bandwidthLimitUnits == 'gb') {
            $bandwidthLimit = $bandwidthLimit * 1024 * 1024;
        }

        // display group ids to be updated
        $displayGroupIds = [];

        foreach ($ids as $id) {
            // get display
            $display = $this->displayFactory->getById($id);

            // check if the display is accessible by user
            if (!$this->getUser()->checkViewable($display)) {
                throw new AccessDeniedException();
            }

            $displayGroupIds[] = $display->displayGroupId;
        }

        // update bandwidth limit to the array of ids
        $this->displayGroupFactory->setBandwidth($bandwidthLimit, $displayGroupIds);

        // Audit Log message
        $this->getLog()->audit('DisplayGroup', 0, 'Batch update of bandwidth limit for ' . count($displayGroupIds) . ' items', [
            'bandwidthLimit' => $bandwidthLimit,
            'displayGroupIds' => $displayGroupIds
        ]);

        // Return
        $this->getState()->hydrate([
            'httpCode' => 204,
            'message' => __('Displays Updated')
        ]);

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


    /**
     * Assign Display to Display Groups
     * @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 assignDisplayGroup(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);
        $sanitizedParams = $this->getSanitizer($request->getParams());

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

        // Go through each ID to assign
        foreach ($sanitizedParams->getIntArray('displayGroupId', ['default' => []]) as $displayGroupId) {
            $displayGroup = $this->displayGroupFactory->getById($displayGroupId);
            $displayGroup->load();
            $this->getDispatcher()->dispatch(new DisplayGroupLoadEvent($displayGroup), DisplayGroupLoadEvent::$NAME);

            if (!$this->getUser()->checkEditable($displayGroup)) {
                throw new AccessDeniedException(__('Access Denied to DisplayGroup'));
            }

            $displayGroup->assignDisplay($display);
            $displayGroup->save(['validate' => false]);
        }

        // Have we been provided with unassign id's as well?
        foreach ($sanitizedParams->getIntArray('unassignDisplayGroupId', ['default' => []]) as $displayGroupId) {
            $displayGroup = $this->displayGroupFactory->getById($displayGroupId);
            $displayGroup->load();
            $this->getDispatcher()->dispatch(new DisplayGroupLoadEvent($displayGroup), DisplayGroupLoadEvent::$NAME);

            if (!$this->getUser()->checkEditable($displayGroup)) {
                throw new AccessDeniedException(__('Access Denied to DisplayGroup'));
            }

            $displayGroup->unassignDisplay($display);
            $displayGroup->save(['validate' => false]);
        }

        // Queue display to check for cache updates
        $display->notify();

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 204,
            'message' => sprintf(__('%s assigned to Display Groups'), $display->display),
            'id' => $display->displayId
        ]);

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

    /**
     * Output a screen shot
     * @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 screenShot(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

        // Allow limited view access
        if (!$this->getUser()->checkViewable($display) && !$this->getUser()->featureEnabled('displays.limitedView')) {
            throw new AccessDeniedException();
        }

        // The request will output its own content, disable framework
        $this->setNoOutput(true);

        // Output an image if present, otherwise not found image.
        $file = 'screenshots/' . $id . '_screenshot.jpg';

        // File upload directory.. get this from the settings object
        $library = $this->getConfig()->getSetting('LIBRARY_LOCATION');
        $fileName = $library . $file;

        if (!file_exists($fileName)) {
            $fileName = $this->getConfig()->uri('forms/filenotfound.gif');
        }

        Img::configure(array('driver' => 'gd'));
        $img = Img::make($fileName);

        $date = $display->getCurrentScreenShotTime($this->pool);

        if ($date != '') {
            $img
                ->rectangle(0, 0, 110, 15, function ($draw) {
                    $draw->background('#ffffff');
                })
                ->text($date, 10, 10);
        }

        // Cache headers
        header('Cache-Control: no-store, no-cache, must-revalidate');
        header('Pragma: no-cache');
        header('Expires: 0');

        // Disable any buffering to prevent OOM errors.
        while (ob_get_level() > 0) {
            ob_end_clean();
        }

        $response->write($img->encode());
        $response = $response->withHeader('Content-Type', $img->mime());
        return $this->render($request, $response);
    }

    /**
     * Request ScreenShot 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 requestScreenShotForm(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

        // Allow limited view access
        if (!$this->getUser()->checkViewable($display) && !$this->getUser()->featureEnabled('displays.limitedView')) {
            throw new AccessDeniedException();
        }

        // Work out the next collection time based on the last accessed date/time and the collection interval
        if ($display->lastAccessed == 0) {
            $nextCollect = __('once it has connected for the first time');
        } else {
            $collectionInterval = $display->getSetting('collectInterval', 300);
            $nextCollect = Carbon::createFromTimestamp($display->lastAccessed)
                ->addSeconds($collectionInterval)
                ->diffForHumans();
        }

        $this->getState()->template = 'display-form-request-screenshot';
        $this->getState()->autoSubmit = $this->getAutoSubmit('displayRequestScreenshotForm');
        $this->getState()->setData([
            'display' => $display,
            'nextCollect' => $nextCollect,
        ]);

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

    /**
     * Request ScreenShot
     * @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
     * @SWG\Put(
     *  path="/display/requestscreenshot/{displayId}",
     *  operationId="displayRequestScreenshot",
     *  tags={"display"},
     *  summary="Request Screen Shot",
     *  description="Notify the display that the CMS would like a screen shot to be sent.",
     *  @SWG\Parameter(
     *      name="displayId",
     *      in="path",
     *      description="The Display ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Display")
     *  )
     * )
     */
    public function requestScreenShot(Request $request, Response $response, $id): Response
    {
        $display = $this->displayFactory->getById($id);

        // Allow limited view access
        if (!$this->getUser()->checkViewable($display) && !$this->getUser()->featureEnabled('displays.limitedView')) {
            throw new AccessDeniedException();
        }

        $display->screenShotRequested = 1;
        $display->save(['validate' => false, 'audit' => false]);

        if (!empty($display->xmrChannel)) {
            $this->playerAction->sendAction($display, new ScreenShotAction());
        }

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Request sent for %s'), $display->display),
            'id' => $display->displayId
        ]);

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

    /**
     * Form for wake on Lan
     * @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 wakeOnLanForm(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        if ($display->macAddress == '') {
            throw new InvalidArgumentException(
                __('This display has no mac address recorded against it yet. Make sure the display is running.'),
                'macAddress'
            );
        }

        $this->getState()->template = 'display-form-wakeonlan';
        $this->getState()->setData([
            'display' => $display,
        ]);

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

    /**
     * Wake this display using a WOL command
     * @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
     * @SWG\Post(
     *  path="/display/wol/{displayId}",
     *  operationId="displayWakeOnLan",
     *  tags={"display"},
     *  summary="Issue WOL",
     *  description="Send a Wake On LAN packet to this Display",
     *  @SWG\Parameter(
     *      name="displayId",
     *      in="path",
     *      description="The Display ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=204,
     *      description="successful operation"
     *  )
     * )
     */
    public function wakeOnLan(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        if ($display->macAddress == '' || $display->broadCastAddress == '') {
            throw new InvalidArgumentException(
                __('This display has no mac address recorded against it yet. Make sure the display is running.')
            );
        }

        $this->getLog()->notice(
            'About to send WOL packet to '
            . $display->broadCastAddress . ' with Mac Address ' . $display->macAddress
        );

        WakeOnLan::TransmitWakeOnLan(
            $display->macAddress,
            $display->secureOn,
            $display->broadCastAddress,
            $display->cidr,
            '9',
            $this->getLog()
        );

        $display->lastWakeOnLanCommandSent = Carbon::now()->format('U');
        $display->save(['validate' => false]);

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Wake on Lan sent for %s'), $display->display),
            'id' => $display->displayId
        ]);

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

    /**
     * Validate the display list
     * @param \Xibo\Entity\Display[] $displays
     * @throws \Xibo\Support\Exception\GeneralException
     * @throws \Xibo\Support\Exception\NotFoundException
     */
    public function validateDisplays(array $displays): void
    {
        // Get the global time out (overrides the alert time out on the display if 0)
        $globalTimeout = $this->getConfig()->getSetting('MAINTENANCE_ALERT_TOUT') * 60;
        $emailAlerts = ($this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS') == 1);
        $alwaysAlert = ($this->getConfig()->getSetting('MAINTENANCE_ALWAYS_ALERT') == 1);

        foreach ($displays as $display) {
            // Should we test against the collection interval or the preset alert timeout?
            if ($display->alertTimeout == 0 && $display->clientType != '') {
                $timeoutToTestAgainst = ((double)$display->getSetting('collectInterval', $globalTimeout)) * 1.1;
            } else {
                $timeoutToTestAgainst = $globalTimeout;
            }

            // Store the timeout to test against
            $timeOut = $display->lastAccessed + $timeoutToTestAgainst;

            // If the last time we accessed is less than now minus the timeout
            if ($timeOut < Carbon::now()->format('U')) {
                $this->getLog()->debug('Timed out display. Last Accessed: '
                    . date('Y-m-d h:i:s', $display->lastAccessed) . '. Time out: ' . date('Y-m-d h:i:s', $timeOut));

                // Is this the first time this display has gone "off-line"
                $displayOffline = ($display->loggedIn == 1);

                // If this is the first switch (i.e. the row was logged in before)
                if ($displayOffline) {
                    // Update the display and set it as logged out
                    $display->loggedIn = 0;
                    $display->save(\Xibo\Entity\Display::$saveOptionsMinimum);

                    // Log the down event
                    $event = $this->displayEventFactory->createEmpty();
                    $event->displayId = $display->displayId;
                    $event->start = $display->lastAccessed;
                    // eventTypeId 1 is for Display up/down events.
                    $event->eventTypeId = 1;
                    $event->save();
                }

                $dayPartId = $display->getSetting('dayPartId');
                $operatingHours = true;

                if ($dayPartId !== null) {
                    try {
                        $dayPart = $this->dayPartFactory->getById($dayPartId);

                        $startTimeArray = explode(':', $dayPart->startTime);
                        $startTime = Carbon::now()->setTime(intval($startTimeArray[0]), intval($startTimeArray[1]));

                        $endTimeArray = explode(':', $dayPart->endTime);
                        $endTime = Carbon::now()->setTime(intval($endTimeArray[0]), intval($endTimeArray[1]));

                        $now = Carbon::now();

                        // exceptions
                        foreach ($dayPart->exceptions as $exception) {
                            // check if we are on exception day and if so override the start and endtime accordingly
                            if ($exception['day'] == Carbon::now()->format('D')) {
                                $exceptionsStartTime = explode(':', $exception['start']);
                                $startTime = Carbon::now()->setTime(
                                    intval($exceptionsStartTime[0]),
                                    intval($exceptionsStartTime[1])
                                );

                                $exceptionsEndTime = explode(':', $exception['end']);
                                $endTime = Carbon::now()->setTime(
                                    intval($exceptionsEndTime[0]),
                                    intval($exceptionsEndTime[1])
                                );
                            }
                        }

                        // check if we are inside the operating hours for this display -
                        // we use that flag to decide if we need to create a notification and send an email.
                        if (($now >= $startTime && $now <= $endTime)) {
                            $operatingHours = true;
                        } else {
                            $operatingHours = false;
                        }
                    } catch (NotFoundException) {
                        $this->getLog()->debug(
                            'Unknown dayPartId set on Display Profile for displayId ' . $display->displayId
                        );
                    }
                }

                // Should we create a notification
                if ($emailAlerts && $display->emailAlert == 1 && ($displayOffline || $alwaysAlert)) {
                    // Alerts enabled for this display
                    // Display just gone offline, or always alert
                    // Fields for email

                    // for displays without dayPartId set, this is always true,
                    // otherwise we check if we are inside the operating hours set for this display
                    if ($operatingHours) {
                        $subject = sprintf(__('Alert for Display %s'), $display->display);
                        $body = sprintf(
                            __('Display ID %d is offline since %s.'),
                            $display->displayId,
                            Carbon::createFromTimestamp($display->lastAccessed)
                                ->format(DateFormatHelper::getSystemFormat())
                        );

                        // Add to system
                        $notification = $this->notificationFactory->createSystemNotification(
                            $subject,
                            $body,
                            Carbon::now(),
                            'display'
                        );

                        // Add in any displayNotificationGroups, with permissions
                        foreach ($this->userGroupFactory
                                     ->getDisplayNotificationGroups($display->displayGroupId) as $group) {
                            $notification->assignUserGroup($group);
                        }

                        $notification->save();
                    } else {
                        $this->getLog()->info('Not sending email down alert for Display - ' . $display->display
                            . ' we are outside of its operating hours');
                    }
                } elseif ($displayOffline) {
                    $this->getLog()->info('Not sending an email for offline display - emailAlert = '
                        . $display->emailAlert . ', alwaysAlert = ' . $alwaysAlert);
                }
            }
        }
    }

    /**
     * Show the authorise 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 authoriseForm(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        $this->getState()->template = 'display-form-authorise';
        $this->getState()->autoSubmit = $this->getAutoSubmit('displayAuthoriseForm');
        $this->getState()->setData([
            'display' => $display
        ]);

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

    /**
     * Toggle Authorise on this Display
     * @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
     * @SWG\Put(
     *  path="/display/authorise/{displayId}",
     *  operationId="displayToggleAuthorise",
     *  tags={"display"},
     *  summary="Toggle authorised",
     *  description="Toggle authorised for the Display.",
     *  @SWG\Parameter(
     *      name="displayId",
     *      in="path",
     *      description="The Display ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=204,
     *      description="successful operation"
     *  )
     * )
     */
    public function toggleAuthorise(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        $display->licensed = ($display->licensed == 1) ? 0 : 1;
        $display->save(['validate' => false]);

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Authorised set to %d for %s'), $display->licensed, $display->display),
            'id' => $display->displayId
        ]);

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

    /**
     * @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 defaultLayoutForm(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        // Get the currently assigned default layout
        try {
            $layouts = (($display->defaultLayoutId != null) ? [$this->layoutFactory->getById($display->defaultLayoutId)] : []);
        } catch (NotFoundException $notFoundException) {
            $layouts = [];
        }

        $this->getState()->template = 'display-form-defaultlayout';
        $this->getState()->setData([
            'display' => $display,
            'layouts' => $layouts
        ]);

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

    /**
     * Set the Default Layout for this Display
     * @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
     * @SWG\Put(
     *  path="/display/defaultlayout/{displayId}",
     *  operationId="displayDefaultLayout",
     *  tags={"display"},
     *  summary="Set Default Layout",
     *  description="Set the default Layout on this Display",
     *  @SWG\Parameter(
     *      name="displayId",
     *      in="path",
     *      description="The Display ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="layoutId",
     *      in="formData",
     *      description="The Layout ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=204,
     *      description="successful operation"
     *  )
     * )
     */
    public function setDefaultLayout(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        $layoutId = $this->getSanitizer($request->getParams())->getInt('layoutId');

        $layout = $this->layoutFactory->getById($layoutId);

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

        $display->defaultLayoutId = $layoutId;
        $display->save(['validate' => false]);
        if ($display->hasPropertyChanged('defaultLayoutId')) {
            $display->notify();
        }

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Default Layout with name %s set for %s'), $layout->layout, $display->display),
            'id' => $display->displayId
        ]);

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

    /**
     * @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 moveCmsForm(Request $request, Response $response, $id)
    {
        if ($this->getUser()->twoFactorTypeId != 2) {
            throw new AccessDeniedException('This action requires active Google Authenticator Two Factor authentication');
        }

        $display = $this->displayFactory->getById($id);

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

        $this->getState()->template = 'display-form-moveCms';
        $this->getState()->setData([
            'display' => $display,
            'newCmsAddress' => $display->newCmsAddress,
            'newCmsKey' => $display->newCmsKey
        ]);

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

    /**
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \RobThree\Auth\TwoFactorAuthException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function moveCms(Request $request, Response $response, $id)
    {
        if ($this->getUser()->twoFactorTypeId != 2) {
            throw new AccessDeniedException('This action requires active Google Authenticator Two Factor authentication');
        }

        $display = $this->displayFactory->getById($id);
        $sanitizedParams = $this->getSanitizer($request->getParams());

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

        // Two Factor Auth
        $issuerSettings = $this->getConfig()->getSetting('TWOFACTOR_ISSUER');
        $appName = $this->getConfig()->getThemeConfig('app_name');

        if ($issuerSettings !== '') {
            $issuer = $issuerSettings;
        } else {
            $issuer = $appName;
        }

        $authenticationCode = $sanitizedParams->getString('twoFactorCode');

        $tfa = new TwoFactorAuth($issuer);
        $result = $tfa->verifyCode($this->getUser()->twoFactorSecret, $authenticationCode, 3);

        if ($result) {
            // get the new CMS Address and Key from the form.
            $newCmsAddress = $sanitizedParams->getString('newCmsAddress');
            $newCmsKey = $sanitizedParams->getString('newCmsKey');

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

            if (!v::stringType()->length(1, 1000)->validate($newCmsAddress)) {
                throw new InvalidArgumentException(__('New CMS URL can have maximum of 1000 characters'), 'newCmsUrl');
            }

            if ($newCmsKey == '') {
                throw new InvalidArgumentException(__('Provided CMS Key is invalid'), 'newCmsKey');
            }

            // we are successfully authenticated, get new CMS address and Key and save the Display record.
            $display->newCmsAddress = $newCmsAddress;
            $display->newCmsKey = $newCmsKey;
            $display->save();
        } else {
            throw new InvalidArgumentException(__('Invalid Two Factor Authentication Code'), 'twoFactorCode');
        }

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

    /**
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws NotFoundException
     */
    public function moveCmsCancelForm(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        $this->getState()->template = 'display-form-moveCmsCancel';
        $this->getState()->setData([
            'display' => $display
        ]);

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

    /**
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @param $id
     * @throws NotFoundException
     * @throws GeneralException
     */
    public function moveCmsCancel(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        $display->newCmsAddress = '';
        $display->newCmsKey = '';
        $display->save();

        $this->getState()->hydrate([
            'message' => sprintf(__('Cancelled CMS Transfer for %s'), $display->display),
            'id' => $display->displayId
        ]);

        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 addViaCodeForm(Request $request, Response $response)
    {
        $this->getState()->template = 'display-form-addViaCode';

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

    /**
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function addViaCode(Request $request, Response $response)
    {
        $sanitizedParams = $this->getSanitizer($request->getParams());

        $user_code = $sanitizedParams->getString('user_code');
        $cmsAddress = (new HttpsDetect())->getBaseUrl($request);
        $cmsKey = $this->getConfig()->getSetting('SERVER_KEY');

        if ($user_code == '') {
            throw new InvalidArgumentException(__('Code cannot be empty'), 'code');
        }

        $guzzle = new Client();

        try {
            // When the valid code is submitted, it will be sent along with CMS Address and Key to Authentication Service maintained by Xibo Signage Ltd.
            // The Player will then call the service with the same code to retrieve the CMS details.
            // On success, the details will be removed from the Authentication Service.
            $guzzleRequest = $guzzle->request(
                'POST',
                'https://auth.signlicence.co.uk/addDetails',
                $this->getConfig()->getGuzzleProxy([
                    'form_params' => [
                        'user_code' => $user_code,
                        'cmsAddress' => $cmsAddress,
                        'cmsKey' => $cmsKey,
                    ]
                ])
            );

            $data = json_decode($guzzleRequest->getBody(), true);

            $this->getState()->hydrate([
                'message' => $data['message']
            ]);
        } catch (\Exception $e) {
            $this->getLog()->debug($e->getMessage());
            throw new InvalidArgumentException(__('The code provided does not match. Please double-check the code shown on the device you are trying to connect.'), 'user_code');
        }

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

    /**
     * Check commercial licence 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 checkLicenceForm(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        $this->getState()->template = 'display-form-licence-check';
        $this->getState()->autoSubmit = $this->getAutoSubmit('displayLicenceCheckForm');
        $this->getState()->setData([
            'display' => $display
        ]);

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

    /**
     * Check commercial licence
     *
     * @SWG\Put(
     *  summary="Licence Check",
     *  path="/display/licenceCheck/{displayId}",
     *  operationId="displayLicenceCheck",
     *  tags={"display"},
     *  description="Ask this Player to check its Commercial Licence",
     *  @SWG\Parameter(
     *      name="displayId",
     *      in="path",
     *      description="The Display ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=204,
     *      description="successful operation"
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ControllerNotImplemented
     */
    public function checkLicence(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        if (empty($display->xmrChannel)) {
            throw new InvalidArgumentException(__('XMR is not configured for this Display'), 'xmrChannel');
        }

        $this->playerAction->sendAction($display, new LicenceCheckAction());

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Request sent for %s'), $display->display),
            'id' => $display->displayId
        ]);

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

    /**
     * @SWG\Get(
     *  path="/display/status/{id}",
     *  operationId="displayStatus",
     *  tags={"display"},
     *  summary="Display Status",
     *  description="Get the display status window for this Display.",
     *  @SWG\Parameter(
     *      name="id",
     *      in="path",
     *      description="Display Id",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(
     *          type="array",
     *          @SWG\Items(type="string")
     *      )
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param int $id displayId
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws \Xibo\Support\Exception\AccessDeniedException
     * @throws \Xibo\Support\Exception\InvalidArgumentException
     * @throws \Xibo\Support\Exception\NotFoundException
     */
    public function statusWindow(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        return $response->withJson($display->getStatusWindow($this->pool));
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     */
    public function purgeAllForm(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        $this->getState()->template = 'display-form-purge-all';
        $this->getState()->setData([
            'display' => $display
        ]);

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


    /**
     * Purge All
     *
     * @SWG\Put(
     *  summary="Purge All",
     *  path="/display/purgeAll/{displayId}",
     *  operationId="displayPurgeAll",
     *  tags={"display"},
     *  description="Ask this Player to purge all Media from its local storage and request fresh files from CMS.",
     *  @SWG\Parameter(
     *      name="displayId",
     *      in="path",
     *      description="The Display ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=204,
     *      description="successful operation"
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     */
    public function purgeAll(Request $request, Response $response, $id)
    {
        $display = $this->displayFactory->getById($id);

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

        if (empty($display->xmrChannel)) {
            throw new InvalidArgumentException(__('XMR is not configured for this Display'), 'xmrChannel');
        }

        $this->playerAction->sendAction($display, new PurgeAllAction());

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Request sent for %s'), $display->display),
            'id' => $display->displayId
        ]);

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