<?php
/*
 * Copyright (C) 2024 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 Illuminate\Support\Str;
use Psr\Http\Message\ResponseInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Xibo\Entity\ScheduleReminder;
use Xibo\Event\ScheduleCriteriaRequestEvent;
use Xibo\Factory\CampaignFactory;
use Xibo\Factory\CommandFactory;
use Xibo\Factory\DayPartFactory;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\ScheduleCriteriaFactory;
use Xibo\Factory\ScheduleExclusionFactory;
use Xibo\Factory\ScheduleFactory;
use Xibo\Factory\ScheduleReminderFactory;
use Xibo\Factory\SyncGroupFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Session;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;

/**
 * Class Schedule
 * @package Xibo\Controller
 */
class Schedule extends Base
{
    /**
     * @var Session
     */
    private $session;

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

    /**
     * @var ScheduleReminderFactory
     */
    private $scheduleReminderFactory;

    /**
     * @var ScheduleExclusionFactory
     */
    private $scheduleExclusionFactory;

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

    /**
     * @var CampaignFactory
     */
    private $campaignFactory;

    /**
     * @var CommandFactory
     */
    private $commandFactory;

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

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

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

    private SyncGroupFactory $syncGroupFactory;

    /**
     * Set common dependencies.
     * @param Session $session
     * @param ScheduleFactory $scheduleFactory
     * @param DisplayGroupFactory $displayGroupFactory
     * @param CampaignFactory $campaignFactory
     * @param CommandFactory $commandFactory
     * @param DisplayFactory $displayFactory
     * @param LayoutFactory $layoutFactory
     * @param DayPartFactory $dayPartFactory
     * @param ScheduleReminderFactory $scheduleReminderFactory
     * @param ScheduleExclusionFactory $scheduleExclusionFactory
     */

    public function __construct(
        $session,
        $scheduleFactory,
        $displayGroupFactory,
        $campaignFactory,
        $commandFactory,
        $displayFactory,
        $layoutFactory,
        $dayPartFactory,
        $scheduleReminderFactory,
        $scheduleExclusionFactory,
        SyncGroupFactory $syncGroupFactory,
        private readonly ScheduleCriteriaFactory $scheduleCriteriaFactory
    ) {
        $this->session = $session;
        $this->scheduleFactory = $scheduleFactory;
        $this->displayGroupFactory = $displayGroupFactory;
        $this->campaignFactory = $campaignFactory;
        $this->commandFactory = $commandFactory;
        $this->displayFactory = $displayFactory;
        $this->layoutFactory = $layoutFactory;
        $this->dayPartFactory = $dayPartFactory;
        $this->scheduleReminderFactory = $scheduleReminderFactory;
        $this->scheduleExclusionFactory = $scheduleExclusionFactory;
        $this->syncGroupFactory = $syncGroupFactory;
    }

    /**
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws GeneralException
     * @throws ControllerNotImplemented
     */
    function displayPage(Request $request, Response $response)
    {
        // get the default longitude and latitude from CMS options
        $defaultLat = (float)$this->getConfig()->getSetting('DEFAULT_LAT');
        $defaultLong = (float)$this->getConfig()->getSetting('DEFAULT_LONG');

        $data = [
            'defaultLat' => $defaultLat,
            'defaultLong' => $defaultLong,
            'eventTypes' => \Xibo\Entity\Schedule::getEventTypes(),
        ];

        // Render the Theme and output
        $this->getState()->template = 'schedule-page';
        $this->getState()->setData($data);

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

    /**
     * Generates the calendar that we draw events on
     * @deprecated - Deprecated API: This endpoint will be removed in v5.0
     * @SWG\Get(
     *  path="/schedule/data/events",
     *  operationId="scheduleCalendarData",
     *  description="⚠️ This endpoint is deprecated and will be removed in v5.0.",
     *  tags={"schedule"},
     *  deprecated=true,
     *  @SWG\Parameter(
     *      name="displayGroupIds",
     *      description="The DisplayGroupIds to return the schedule for. [-1] for All.",
     *      in="query",
     *      type="array",
     *      required=true,
     *      @SWG\Items(
     *          type="integer"
     *      )
     *  ),
     *  @SWG\Parameter(
     *      name="from",
     *      in="query",
     *      required=false,
     *      type="string",
     *      description="From Date in Y-m-d H:i:s format, if not provided defaults to start of the current month"
     *  ),
     *  @SWG\Parameter(
     *      name="to",
     *      in="query",
     *      required=false,
     *      type="string",
     *      description="To Date in Y-m-d H:i:s format, if not provided defaults to start of the next month"
     *  ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful response",
     *      @SWG\Schema(
     *          type="array",
     *          @SWG\Items(ref="#/definitions/ScheduleCalendarData")
     *      )
     *  )
     * )
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws InvalidArgumentException
     * @throws NotFoundException
     */
    public function eventData(Request $request, Response $response)
    {
        $response = $response
            ->withHeader(
                'Warning',
                '299 - "Deprecated API: /schedule/data/events will be removed in v5.0"'
            );

        $this->getLog()->error('Deprecated API called: /schedule/data/events');

        $this->setNoOutput();
        $sanitizedParams = $this->getSanitizer($request->getParams());

        $displayGroupIds = $sanitizedParams->getIntArray('displayGroupIds', ['default' => []]);
        $displaySpecificDisplayGroupIds = $sanitizedParams->getIntArray('displaySpecificGroupIds', ['default' => []]);
        $originalDisplayGroupIds = array_merge($displayGroupIds, $displaySpecificDisplayGroupIds);
        $campaignId = $sanitizedParams->getInt('campaignId');

        $start = $sanitizedParams->getDate('from', ['default' => Carbon::now()->startOfMonth()]);
        $end = $sanitizedParams->getDate('to', ['default' => Carbon::now()->addMonth()->startOfMonth()]);

        if (count($originalDisplayGroupIds) <= 0) {
            return $response->withJson(['success' => 1, 'result' => []]);
        }

        // Setting for whether we show Layouts without permissions
        $showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1);

        // Permissions check the list of display groups with the user accessible list of display groups
        $resolvedDisplayGroupIds = array_diff($originalDisplayGroupIds, [-1]);

        if (!$this->getUser()->isSuperAdmin()) {
            $userDisplayGroupIds = array_map(function ($element) {
                /** @var \Xibo\Entity\DisplayGroup $element */
                return $element->displayGroupId;
            }, $this->displayGroupFactory->query(null, ['isDisplaySpecific' => -1]));

            // Reset the list to only those display groups that intersect and if 0 have been provided, only those from
            // the user list
            $resolvedDisplayGroupIds = (count($originalDisplayGroupIds) > 0) ? array_intersect($originalDisplayGroupIds, $userDisplayGroupIds) : $userDisplayGroupIds;

            $this->getLog()->debug('Resolved list of display groups ['
                . json_encode($resolvedDisplayGroupIds) . '] from provided list ['
                . json_encode($originalDisplayGroupIds) . '] and user list ['
                . json_encode($userDisplayGroupIds) . ']');

            // If we have none, then we do not return any events.
            if (count($resolvedDisplayGroupIds) <= 0) {
                return $response->withJson(['success' => 1, 'result' => []]);
            }
        }

        $events = [];
        $filter = [
            'futureSchedulesFrom' => $start->format('U'),
            'futureSchedulesTo' => $end->format('U'),
            'displayGroupIds' => $resolvedDisplayGroupIds,
            'geoAware' => $sanitizedParams->getInt('geoAware'),
            'recurring' => $sanitizedParams->getInt('recurring'),
            'eventTypeId' => $sanitizedParams->getInt('eventTypeId'),
            'name' => $sanitizedParams->getString('name'),
            'useRegexForName' => $sanitizedParams->getCheckbox('useRegexForName'),
            'logicalOperatorName' => $sanitizedParams->getString('logicalOperatorName'),
        ];

        if ($campaignId != null) {
            // Is this an ad campaign?
            $campaign = $this->campaignFactory->getById($campaignId);
            if ($campaign->type === 'ad') {
                $filter['parentCampaignId'] = $campaignId;
            } else {
                $filter['campaignId'] = $campaignId;
            }
        }

        foreach ($this->scheduleFactory->query(['FromDT'], $filter) as $row) {
            /* @var \Xibo\Entity\Schedule $row */

            // Generate this event
            try {
                $scheduleEvents = $row->getEvents($start, $end);
            } catch (GeneralException $e) {
                $this->getLog()->error('Unable to getEvents for ' . $row->eventId);
                continue;
            }

            if (count($scheduleEvents) <= 0) {
                continue;
            }

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

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

            $displayGroupList = '';

            if (count($row->displayGroups) >= 0) {
                $array = array_map(function ($object) {
                    return $object->displayGroup;
                }, $row->displayGroups);
                $displayGroupList = implode(', ', $array);
            }

            // Event Permissions
            $editable = $this->getUser()->featureEnabled('schedule.modify')
                && $this->isEventEditable($row);

            // Event Title
            if ($this->isSyncEvent($row->eventTypeId)) {
                $title = sprintf(
                    __('%s scheduled on sync group %s'),
                    $row->getSyncTypeForEvent(),
                    $row->getUnmatchedProperty('syncGroupName'),
                );
            } else if ($row->campaignId == 0) {
                // Command
                $title = __('%s scheduled on %s', $row->command, $displayGroupList);
            } else {
                // Should we show the Layout name, or not (depending on permission)
                // Make sure we only run the below code if we have to, it's quite expensive
                if (!$showLayoutName && !$this->getUser()->isSuperAdmin()) {
                    // Campaign
                    $campaign = $this->campaignFactory->getById($row->campaignId);

                    if (!$this->getUser()->checkViewable($campaign)) {
                        $row->campaign = __('Private Item');
                    }
                }

                $title = sprintf(
                    __('%s scheduled on %s'),
                    $row->getUnmatchedProperty('parentCampaignName', $row->campaign),
                    $displayGroupList
                );

                if ($row->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) {
                    $title .= __(' with Share of Voice %d seconds per hour', $row->shareOfVoice);
                }
            }

            // Day diff from start date to end date
            $diff = $end->diff($start)->days;

            // Show all Hourly repeats on the day view
            if ($row->recurrenceType == 'Minute' || ($diff > 1 && $row->recurrenceType == 'Hour')) {
                $title .= __(', Repeats every %s %s', $row->recurrenceDetail, $row->recurrenceType);
            }

            // Event URL
            $editUrlWeb = 'schedule.edit.form';
            $editUrl = ($this->isApi($request)) ? 'schedule.edit' : $editUrlWeb;
            $url = ($editable) ? $this->urlFor($request, $editUrl, ['id' => $row->eventId]) : '#';

            $days = [];

            // Event scheduled events
            foreach ($scheduleEvents as $scheduleEvent) {
                $this->getLog()->debug(sprintf('Parsing event dates from %s and %s', $scheduleEvent->fromDt, $scheduleEvent->toDt));

                // Get the day of schedule start
                $fromDtDay = Carbon::createFromTimestamp($scheduleEvent->fromDt)->format('Y-m-d');

                // Handle command events which do not have a toDt
                if ($row->eventTypeId == \Xibo\Entity\Schedule::$COMMAND_EVENT) {
                    $scheduleEvent->toDt = $scheduleEvent->fromDt;
                }

                // Parse our dates into a Date object, so that we convert to local time correctly.
                $fromDt = Carbon::createFromTimestamp($scheduleEvent->fromDt);
                $toDt = Carbon::createFromTimestamp($scheduleEvent->toDt);

                // Set the row from/to date to be an ISO date for display
                $scheduleEvent->fromDt =
                    Carbon::createFromTimestamp($scheduleEvent->fromDt)
                        ->format(DateFormatHelper::getSystemFormat());
                $scheduleEvent->toDt =
                    Carbon::createFromTimestamp($scheduleEvent->toDt)
                        ->format(DateFormatHelper::getSystemFormat());

                $this->getLog()->debug(sprintf('Start date is ' . $fromDt->toRssString() . ' ' . $scheduleEvent->fromDt));
                $this->getLog()->debug(sprintf('End date is ' . $toDt->toRssString() . ' ' . $scheduleEvent->toDt));

                // For a minute/hourly repeating events show only 1 event per day
                if ($row->recurrenceType == 'Minute' || ($diff > 1 && $row->recurrenceType == 'Hour')) {
                    if (array_key_exists($fromDtDay, $days)) {
                        continue;
                    } else {
                        $days[$fromDtDay] = $scheduleEvent->fromDt;
                    }
                }

                /**
                 * @SWG\Definition(
                 *  definition="ScheduleCalendarData",
                 *  @SWG\Property(
                 *      property="id",
                 *      type="integer",
                 *      description="Event ID"
                 *  ),
                 *  @SWG\Property(
                 *      property="title",
                 *      type="string",
                 *      description="Event Title"
                 *  ),
                 *  @SWG\Property(
                 *      property="sameDay",
                 *      type="integer",
                 *      description="Does this event happen only on 1 day"
                 *  ),
                 *  @SWG\Property(
                 *      property="event",
                 *      ref="#/definitions/Schedule"
                 *  )
                 * )
                 */
                $events[] = [
                    'id' => $row->eventId,
                    'title' => $title,
                    'url' => ($editable) ? $url : null,
                    'start' => $fromDt->format('U') * 1000,
                    'end' => $toDt->format('U') * 1000,
                    'sameDay' => ($fromDt->day == $toDt->day && $fromDt->month == $toDt->month && $fromDt->year == $toDt->year),
                    'editable' => $editable,
                    'event' => $row,
                    'scheduleEvent' => $scheduleEvent,
                    'recurringEvent' => $row->recurrenceType != ''
                ];
            }
        }

        return $response->withJson(['success' => 1, 'result' => $events]);
    }

    /**
     * Event List
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws ControllerNotImplemented
     * @SWG\Get(
     *  path="/schedule/{displayGroupId}/events",
     *  operationId="scheduleCalendarDataDisplayGroup",
     *  tags={"schedule"},
     *  @SWG\Parameter(
     *      name="displayGroupId",
     *      description="The DisplayGroupId to return the event list for.",
     *      in="path",
     *      type="integer",
     *      required=true
     *  ),
     *  @SWG\Parameter(
     *      name="singlePointInTime",
     *      in="query",
     *      required=false,
     *      type="integer",
     *  ),
     *  @SWG\Parameter(
     *      name="date",
     *      in="query",
     *      required=false,
     *      type="string",
     *      description="Date in Y-m-d H:i:s"
     *  ),
     *  @SWG\Parameter(
     *      name="startDate",
     *      in="query",
     *      required=false,
     *      type="string",
     *      description="Date in Y-m-d H:i:s"
     *  ),
     *  @SWG\Parameter(
     *      name="endDate",
     *      in="query",
     *      required=false,
     *      type="string",
     *      description="Date in Y-m-d H:i:s"
     *  ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful response"
     *  )
     * )
     */
    public function eventList(Request $request, Response $response, $id)
    {
        $displayGroup = $this->displayGroupFactory->getById($id);
        $sanitizedParams = $this->getSanitizer($request->getParams());

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

        // Setting for whether we show Layouts with out permissions
        $showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1);

        $singlePointInTime = $sanitizedParams->getInt('singlePointInTime');
        if ($singlePointInTime == 1) {
            $startDate = $sanitizedParams->getDate('date');
            $endDate = $sanitizedParams->getDate('date');
        } else {
            $startDate = $sanitizedParams->getDate('startDate');
            $endDate = $sanitizedParams->getDate('endDate');
        }

        // Reset the seconds
        $startDate->second(0);
        $endDate->second(0);

        $this->getLog()->debug(
            sprintf(
                'Generating eventList for DisplayGroupId ' . $id . ' from date '
                . $startDate->format(DateFormatHelper::getSystemFormat()) . ' to '
                . $endDate->format(DateFormatHelper::getSystemFormat())
            )
        );

        // Get a list of scheduled events
        $events = [];
        $displayGroups = [];
        $layouts = [];
        $campaigns = [];

        // Add the displayGroupId I am filtering for to the displayGroup object
        $displayGroups[$displayGroup->displayGroupId] = $displayGroup;

        // Is this group a display specific group, or a standalone?
        $options = [];
        /** @var \Xibo\Entity\Display $display */
        $display = null;
        if ($displayGroup->isDisplaySpecific == 1) {
            // We should lookup the displayId for this group.
            $display = $this->displayFactory->getByDisplayGroupId($id)[0];
        } else {
            $options['useGroupId'] = true;
            $options['displayGroupId'] = $id;
        }

        // Get list of events
        $scheduleForXmds = $this->scheduleFactory->getForXmds(
            ($display === null) ? null : $display->displayId,
            $startDate,
            $endDate,
            $options
        );

        $this->getLog()->debug(count($scheduleForXmds) . ' events returned for displaygroup and date');

        foreach ($scheduleForXmds as $event) {

            // Ignore command events
            if ($event['eventTypeId'] == \Xibo\Entity\Schedule::$COMMAND_EVENT)
                continue;

            // Ignore events that have a campaignId, but no layoutId (empty Campaigns)
            if ($event['layoutId'] == 0 && $event['campaignId'] != 0)
                continue;

            // Assess schedules
            $schedule = $this->scheduleFactory->createEmpty()->hydrate($event, ['intProperties' => ['isPriority', 'syncTimezone', 'displayOrder', 'fromDt', 'toDt']]);
            $schedule->load();

            $this->getLog()->debug('EventId ' . $schedule->eventId . ' exists in the schedule window, checking its instances for activity');

            // Get scheduled events based on recurrence
            try {
                $scheduleEvents = $schedule->getEvents($startDate, $endDate);
            } catch (GeneralException $e) {
                $this->getLog()->error('Unable to getEvents for ' . $schedule->eventId);
                continue;
            }

            // If this event is active, collect extra information and add to the events list
            if (count($scheduleEvents) > 0) {
                // Add the link to the schedule
                if (!$this->isApi($request)) {
                    $route = 'schedule.edit.form';
                    $schedule->setUnmatchedProperty(
                        'link',
                        $this->urlFor($request, $route, ['id' => $schedule->eventId])
                    );
                }

                // Add the Layout
                if ($event['eventTypeId'] == \Xibo\Entity\Schedule::$SYNC_EVENT) {
                    $layoutId = $event['syncLayoutId'];
                } else {
                    $layoutId = $event['layoutId'];
                }

                $this->getLog()->debug('Adding this events layoutId [' . $layoutId . '] to list');

                if ($layoutId != 0 && !array_key_exists($layoutId, $layouts)) {
                    // Look up the layout details
                    $layout = $this->layoutFactory->getById($layoutId);

                    // Add the link to the layout
                    if (!$this->isApi($request)) {
                        // do not link to Layout Designer for Full screen Media/Playlist Layout.
                        $link = (in_array($event['eventTypeId'], [7, 8]))
                            ? ''
                            : $this->urlFor($request, 'layout.designer', ['id' => $layout->layoutId]);

                        $layout->setUnmatchedProperty(
                            'link',
                            $link
                        );
                    }
                    if ($showLayoutName || $this->getUser()->checkViewable($layout)) {
                        $layouts[$layoutId] = $layout;
                    } else {
                        $layouts[$layoutId] = [
                            'layout' => __('Private Item')
                        ];
                    }

                    // Add the Campaign
                    $layout->campaigns = $this->campaignFactory->getByLayoutId($layout->layoutId);

                    if (count($layout->campaigns) > 0) {
                        // Add to the campaigns array
                        foreach ($layout->campaigns as $campaign) {
                            if (!array_key_exists($campaign->campaignId, $campaigns)) {
                                $campaigns[$campaign->campaignId] = $campaign;
                            }
                        }
                    }
                }

                $event['campaign'] = is_object($layouts[$layoutId]) ? $layouts[$layoutId]->layout : $layouts[$layoutId];

                // Display Group details
                $this->getLog()->debug('Adding this events displayGroupIds to list');
                $schedule->excludeProperty('displayGroups');

                foreach ($schedule->displayGroups as $scheduleDisplayGroup) {
                    if (!array_key_exists($scheduleDisplayGroup->displayGroupId, $displayGroups)) {
                        $displayGroups[$scheduleDisplayGroup->displayGroupId] = $scheduleDisplayGroup;
                    }
                }

                // Determine the intermediate display groups
                $this->getLog()->debug('Adding this events intermediateDisplayGroupIds to list');
                $schedule->setUnmatchedProperty(
                    'intermediateDisplayGroupIds',
                    $this->calculateIntermediates($display, $displayGroup, $event['displayGroupId'])
                );

                foreach ($schedule->getUnmatchedProperty('intermediateDisplayGroupIds') as $intermediate) {
                    if (!array_key_exists($intermediate, $displayGroups)) {
                        $displayGroups[$intermediate] = $this->displayGroupFactory->getById($intermediate);
                    }
                }

                $this->getLog()->debug(sprintf('Adding scheduled events: ' . json_encode($scheduleEvents)));

                // We will never save this and we need the eventId on the agenda view
                $eventId = $schedule->eventId;

                foreach ($scheduleEvents as $scheduleEvent) {
                    $schedule = clone $schedule;
                    $schedule->eventId = $eventId;
                    $schedule->fromDt = $scheduleEvent->fromDt;
                    $schedule->toDt = $scheduleEvent->toDt;
                    $schedule->setUnmatchedProperty('layoutId', intval($layoutId));
                    $schedule->setUnmatchedProperty('displayGroupId', intval($event['displayGroupId']));

                    $events[] = $schedule;
                }
            } else {
                $this->getLog()->debug('No activity inside window');
            }
        }

        $this->getState()->hydrate([
             'data' => [
                 'events' => $events,
                 'displayGroups' => $displayGroups,
                 'layouts' => $layouts,
                 'campaigns' => $campaigns
             ]
        ]);

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

    /**
     * @param \Xibo\Entity\Display $display
     * @param \Xibo\Entity\DisplayGroup $displayGroup
     * @param int $eventDisplayGroupId
     * @return array
     * @throws NotFoundException
     */
    private function calculateIntermediates($display, $displayGroup, $eventDisplayGroupId)
    {
        $this->getLog()->debug('Calculating intermediates for events displayGroupId ' . $eventDisplayGroupId . ' viewing displayGroupId ' . $displayGroup->displayGroupId);

        $intermediates = [];
        $eventDisplayGroup = $this->displayGroupFactory->getById($eventDisplayGroupId);

        // Is the event scheduled directly on the displayGroup in question?
        if ($displayGroup->displayGroupId == $eventDisplayGroupId)
            return $intermediates;

        // Is the event scheduled directly on the display in question?
        if ($eventDisplayGroup->isDisplaySpecific == 1)
            return $intermediates;

        $this->getLog()->debug('Event isnt directly scheduled to a display or to the current displaygroup ');

        // There are nested groups involved, so we need to trace the relationship tree.
        if ($display === null) {
            $this->getLog()->debug('We are looking at a DisplayGroup');
            // We are on a group.

            // Get the relationship tree for this display group
            $tree = $this->displayGroupFactory->getRelationShipTree($displayGroup->displayGroupId);

            foreach ($tree as $branch) {
                $this->getLog()->debug(
                    'Branch found: ' . $branch->displayGroup .
                    ' [' . $branch->displayGroupId . '], ' .
                    $branch->getUnmatchedProperty('depth') . '-' .
                    $branch->getUnmatchedProperty('level')
                );

                if ($branch->getUnmatchedProperty('depth') < 0 &&
                    $branch->displayGroupId != $eventDisplayGroup->displayGroupId
                ) {
                    $intermediates[] = $branch->displayGroupId;
                }
            }
        } else {
            // We are on a display.
            $this->getLog()->debug('We are looking at a Display');

            // We will need to get all of this displays groups and then add only those ones that give us an eventual
            // match on the events display group (complicated or what!)
            $display->load();

            foreach ($display->displayGroups as $displayDisplayGroup) {

                // Ignore the display specific group
                if ($displayDisplayGroup->isDisplaySpecific == 1)
                    continue;

                // Get the relationship tree for this display group
                $tree = $this->displayGroupFactory->getRelationShipTree($displayDisplayGroup->displayGroupId);

                $found = false;
                $possibleIntermediates = [];

                foreach ($tree as $branch) {
                    $this->getLog()->debug(
                        'Branch found: ' . $branch->displayGroup .
                        ' [' . $branch->displayGroupId . '], ' .
                        $branch->getUnmatchedProperty('depth') . '-' .
                        $branch->getUnmatchedProperty('level')
                    );

                    if ($branch->displayGroupId != $eventDisplayGroup->displayGroupId) {
                        $possibleIntermediates[] = $branch->displayGroupId;
                    }

                    if ($branch->displayGroupId != $eventDisplayGroup->displayGroupId && count($possibleIntermediates) > 0)
                        $found = true;
                }

                if ($found) {
                    $this->getLog()->debug('We have found intermediates ' . json_encode($possibleIntermediates) . ' for display when looking at displayGroupId ' . $displayDisplayGroup->displayGroupId);
                    $intermediates = array_merge($intermediates, $possibleIntermediates);
                }
            }
        }

        $this->getLog()->debug('Returning intermediates: ' . json_encode($intermediates));

        return $intermediates;
    }

    /**
     * Shows a form to add an event
     * @param Request $request
     * @param Response $response
     * @param string|null $from
     * @param int|null $id
     * @return ResponseInterface|Response
     * @throws ControllerNotImplemented
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     */
    public function addForm(Request $request, Response $response, ?string $from, ?int $id): Response|ResponseInterface
    {
        $sanitizedParams = $this->getSanitizer($request->getParams());

        // get the default longitude and latitude from CMS options
        $defaultLat = (float)$this->getConfig()->getSetting('DEFAULT_LAT');
        $defaultLong = (float)$this->getConfig()->getSetting('DEFAULT_LONG');

        // Dispatch an event to initialize schedule criteria
        $event = new ScheduleCriteriaRequestEvent();
        $this->getDispatcher()->dispatch($event, ScheduleCriteriaRequestEvent::$NAME);

        // Retrieve the criteria data from the event
        $criteria = $event->getCriteria();
        $criteriaDefaultCondition = $event->getCriteriaDefaultCondition();

        $addFormData = [
            'dayParts' => $this->dayPartFactory->allWithSystem(['isRetired' => 0]),
            'reminders' => [],
            'defaultLat' => $defaultLat,
            'defaultLong' => $defaultLong,
            'eventTypes' => \Xibo\Entity\Schedule::getEventTypes(),
            'addForm' => true,
            'relativeTime' => 0,
            'setDisplaysFromFilter' => true,
            'scheduleCriteria' => $criteria,
            'criteriaDefaultCondition' => $criteriaDefaultCondition
        ];
        $formNowData = [];

        if (!empty($from) && !empty($id)) {
            $formNowData = [
                'event' => [
                    'eventTypeId' => $this->getEventTypeId($from),
                ],
                'campaign' => (
                    ($from == 'Campaign' || $from == 'Layout')
                    ? $this->campaignFactory->getById($id)
                    : null
                ),
                'displayGroups' => (($from == 'DisplayGroup') ? [$this->displayGroupFactory->getById($id)] : null),
                'displayGroupIds' => (($from == 'DisplayGroup') ? [$id] : [0]),
                'mediaId' => (($from === 'Library') ? $id : null),
                'playlistId' => (($from === 'Playlist') ? $id : null),
                // Lock for layout editor only
                'readonlySelect' => ($from == 'Layout' && $sanitizedParams->getString('fromLayoutEditor') === '1'),
                // Hide event type, except for Display Groups
                'hideEventType' => !($from == 'DisplayGroup'),
                // Skip first step, except for Display Groups
                'skipFirstStep' => !($from == 'DisplayGroup'),
                // If coming from display page, don't show syncEvent type
                'eventTypes' => \Xibo\Entity\Schedule::getEventTypes((($from === 'DisplayGroup') ? [9] : [])),
                'addForm' => true,
                'fromCampaign' => ($from == 'Campaign'),
                'relativeTime' => 1,
                'setDisplaysFromFilter' => false,
            ];
        }

        $formData = array_merge($addFormData, $formNowData);

        $this->getState()->template = 'schedule-form-edit';
        $this->getState()->setData($formData);

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

    /**
     * Model to use for supplying key/value pairs to arrays
     * @SWG\Definition(
     *  definition="ScheduleReminderArray",
     *  @SWG\Property(
     *      property="reminder_value",
     *      type="integer"
     *  ),
     *  @SWG\Property(
     *      property="reminder_type",
     *      type="integer"
     *  ),
     *  @SWG\Property(
     *      property="reminder_option",
     *      type="integer"
     *  ),
     *  @SWG\Property(
     *      property="reminder_isEmailHidden",
     *      type="integer"
     *  )
     * )
     */

    /**
     * Add Event
     * @SWG\Post(
     *  path="/schedule",
     *  operationId="scheduleAdd",
     *  tags={"schedule"},
     *  summary="Add Schedule Event",
     *  description="Add a new scheduled event for a Campaign/Layout to be shown on a Display Group/Display.",
     *  @SWG\Parameter(
     *      name="eventTypeId",
     *      in="formData",
     *      description="The Event Type Id to use for this Event.
     * 1=Layout, 2=Command, 3=Overlay, 4=Interrupt, 5=Campaign, 6=Action, 7=Media Library, 8=Playlist",
     *      type="integer",
     *      required=true
     *  ),
     *  @SWG\Parameter(
     *      name="campaignId",
     *      in="formData",
     *      description="The Campaign ID to use for this Event.
     * If a Layout is needed then the Campaign specific ID for that Layout should be used.",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="fullScreenCampaignId",
     *      in="formData",
     *      description="For Media or Playlist event Type. The Layout specific Campaign ID to use for this Event.
     * This needs to be the Layout created with layout/fullscreen function",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="commandId",
     *      in="formData",
     *      description="The Command ID to use for this Event.",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="mediaId",
     *      in="formData",
     *      description="The Media ID to use for this Event.",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="displayOrder",
     *      in="formData",
     *      description="The display order for this event. ",
     *      type="integer",
     *      required=true
     *  ),
     *  @SWG\Parameter(
     *      name="isPriority",
     *      in="formData",
     *      description="An integer indicating the priority of this event. Normal events have a priority of 0.",
     *      type="integer",
     *      required=true
     *   ),
     *   @SWG\Parameter(
     *      name="displayGroupIds",
     *      in="formData",
     *      description="The Display Group IDs for this event. Display specific Group IDs should be used to schedule on single displays.",
     *      type="array",
     *      required=true,
     *      @SWG\Items(type="integer")
     *   ),
     *   @SWG\Parameter(
     *      name="dayPartId",
     *      in="formData",
     *      description="The Day Part for this event. Overrides supported are 0(custom) and 1(always). Defaulted to 0.",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="syncTimezone",
     *      in="formData",
     *      description="Should this schedule be synced to the resulting Display timezone?",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="fromDt",
     *      in="formData",
     *      description="The from date for this event.",
     *      type="string",
     *      format="date-time",
     *      required=true
     *   ),
     *   @SWG\Parameter(
     *      name="toDt",
     *      in="formData",
     *      description="The to date for this event.",
     *      type="string",
     *      format="date-time",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="recurrenceType",
     *      in="formData",
     *      description="The type of recurrence to apply to this event.",
     *      type="string",
     *      required=false,
     *      enum={"", "Minute", "Hour", "Day", "Week", "Month", "Year"}
     *   ),
     *   @SWG\Parameter(
     *      name="recurrenceDetail",
     *      in="formData",
     *      description="The interval for the recurrence.",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="recurrenceRange",
     *      in="formData",
     *      description="The end date for this events recurrence.",
     *      type="string",
     *      format="date-time",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="recurrenceRepeatsOn",
     *      in="formData",
     *      description="The days of the week that this event repeats - weekly only",
     *      type="string",
     *      format="array",
     *      required=false,
     *      @SWG\Items(type="integer")
     *   ),
     *   @SWG\Parameter(
     *      name="scheduleReminders",
     *      in="formData",
     *      description="Array of Reminders for this event",
     *      type="array",
     *      required=false,
     *      @SWG\Items(
     *          ref="#/definitions/ScheduleReminderArray"
     *      )
     *   ),
     *   @SWG\Parameter(
     *      name="isGeoAware",
     *      in="formData",
     *      description="Flag (0-1), whether this event is using Geo Location",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="geoLocation",
     *      in="formData",
     *      description="Array of comma separated strings each with comma separated pair of coordinates",
     *      type="array",
     *      required=false,
     *      @SWG\Items(type="string")
     *   ),
     *   @SWG\Parameter(
     *      name="geoLocationJson",
     *      in="formData",
     *      description="Valid GeoJSON string, use as an alternative to geoLocation parameter",
     *      type="string",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="actionType",
     *      in="formData",
     *      description="For Action eventTypeId, the type of the action - command or navLayout",
     *      type="string",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="actionTriggerCode",
     *      in="formData",
     *      description="For Action eventTypeId, the webhook trigger code for the Action",
     *      type="string",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="actionLayoutCode",
     *      in="formData",
     *      description="For Action eventTypeId and navLayout actionType, the Layout Code identifier",
     *      type="string",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="dataSetId",
     *      in="formData",
     *      description="For Data Connector eventTypeId, the DataSet ID",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="dataSetParams",
     *      in="formData",
     *      description="For Data Connector eventTypeId, the DataSet params",
     *      type="string",
     *      required=false
     *   ),
     *   @SWG\Response(
     *      response=201,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Schedule"),
     *      @SWG\Header(
     *          header="Location",
     *          description="Location of the new record",
     *          type="string"
     *      )
     *  )
     * )
     *
     * @param Request $request
     * @param Response $response
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws ControllerNotImplemented
     * @throws GeneralException
     * @throws NotFoundException
     */
    public function add(Request $request, Response $response)
    {
        $this->getLog()->debug('Add Schedule');
        $sanitizedParams = $this->getSanitizer($request->getParams());

        $embed = ($sanitizedParams->getString('embed') != null)
            ? explode(',', $sanitizedParams->getString('embed'))
            : [];

        // Get the custom day part to use as a default day part
        $customDayPart = $this->dayPartFactory->getCustomDayPart();

        $schedule = $this->scheduleFactory->createEmpty();
        $schedule->userId = $this->getUser()->userId;
        $schedule->eventTypeId = $sanitizedParams->getInt('eventTypeId');
        $schedule->campaignId = $this->isFullScreenSchedule($schedule->eventTypeId)
            ? $sanitizedParams->getInt('fullScreenCampaignId')
            : $sanitizedParams->getInt('campaignId');
        $schedule->commandId = $sanitizedParams->getInt('commandId');
        $schedule->displayOrder = $sanitizedParams->getInt('displayOrder', ['default' => 0]);
        $schedule->isPriority = $sanitizedParams->getInt('isPriority', ['default' => 0]);
        $schedule->dayPartId = $sanitizedParams->getInt('dayPartId', ['default' => $customDayPart->dayPartId]);
        $schedule->isGeoAware = $sanitizedParams->getCheckbox('isGeoAware');
        $schedule->actionType = $sanitizedParams->getString('actionType');
        $schedule->actionTriggerCode = $sanitizedParams->getString('actionTriggerCode');
        $schedule->actionLayoutCode = $sanitizedParams->getString('actionLayoutCode');
        $schedule->maxPlaysPerHour = $sanitizedParams->getInt('maxPlaysPerHour', ['default' => 0]);
        $schedule->syncGroupId = $sanitizedParams->getInt('syncGroupId');
        $schedule->name = $sanitizedParams->getString('name');

        // Set the parentCampaignId for campaign events
        if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$CAMPAIGN_EVENT) {
            $schedule->parentCampaignId = $schedule->campaignId;

            // Make sure we're not directly scheduling an ad campaign
            $campaign = $this->campaignFactory->getById($schedule->campaignId);
            if ($campaign->type === 'ad') {
                throw new InvalidArgumentException(
                    __('Direct scheduling of an Ad Campaign is not allowed'),
                    'campaignId'
                );
            }
        }

        // Fields only collected for interrupt events
        if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) {
            $schedule->shareOfVoice = $sanitizedParams->getInt('shareOfVoice', [
                'throw' => function () {
                    new InvalidArgumentException(
                        __('Share of Voice must be a whole number between 0 and 3600'),
                        'shareOfVoice'
                    );
                }
            ]);
        } else {
            $schedule->shareOfVoice = null;
        }

        // Fields only collected for data connector events
        if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$DATA_CONNECTOR_EVENT) {
            $schedule->dataSetId = $sanitizedParams->getInt('dataSetId', [
                'throw' => function () {
                    new InvalidArgumentException(
                        __('Please select a DataSet'),
                        'dataSetId'
                    );
                }
            ]);
            $schedule->dataSetParams = $sanitizedParams->getString('dataSetParams');
        }

        // Create fullscreen layout for media/playlist events
        if ($this->isFullScreenSchedule($schedule->eventTypeId)) {
            $type = $schedule->eventTypeId === \Xibo\Entity\Schedule::$MEDIA_EVENT ? 'media' : 'playlist';
            $id = ($type === 'media') ? $sanitizedParams->getInt('mediaId') : $sanitizedParams->getInt('playlistId');

            if (!$id) {
                throw new InvalidArgumentException(
                    sprintf('%sId is required when scheduling %s events.', ucfirst($type), $type)
                );
            }

            $fsLayout = $this->layoutFactory->createFullScreenLayout(
                $type,
                $id,
                $sanitizedParams->getInt('resolutionId'),
                $sanitizedParams->getString('backgroundColor'),
                $sanitizedParams->getInt('layoutDuration'),
            );

            $schedule->campaignId = $this->layoutFactory->getCampaignIdFromLayoutHistory($fsLayout->layoutId);
            $schedule->parentCampaignId = $schedule->campaignId;
        }

        // API request can provide an array of coordinates or valid GeoJSON, handle both cases here.
        if ($this->isApi($request) && $schedule->isGeoAware === 1) {
            if ($sanitizedParams->getArray('geoLocation') != null) {
                // get string array from API
                $coordinates = $sanitizedParams->getArray('geoLocation');
                // generate GeoJSON and assign to Schedule
                $schedule->geoLocation = $this->createGeoJson($coordinates);
            } else {
                // we were provided with GeoJSON
                $schedule->geoLocation = $sanitizedParams->getString('geoLocationJson');
            }
        } else {
            // if we are not using API, then valid GeoJSON is created in the front end.
            $schedule->geoLocation = $sanitizedParams->getString('geoLocation');
        }

        // Workaround for cases where we're supplied 0 as the dayPartId (legacy custom dayPart)
        if ($schedule->dayPartId === 0) {
            $schedule->dayPartId = $customDayPart->dayPartId;
        }

        $schedule->syncTimezone = $sanitizedParams->getCheckbox('syncTimezone');
        $schedule->syncEvent = $this->isSyncEvent($schedule->eventTypeId);
        $schedule->recurrenceType = $sanitizedParams->getString('recurrenceType');
        $schedule->recurrenceDetail = $sanitizedParams->getInt('recurrenceDetail');
        $recurrenceRepeatsOn = $sanitizedParams->getIntArray('recurrenceRepeatsOn');
        $schedule->recurrenceRepeatsOn = (empty($recurrenceRepeatsOn)) ? null : implode(',', $recurrenceRepeatsOn);
        $schedule->recurrenceMonthlyRepeatsOn = $sanitizedParams->getInt(
            'recurrenceMonthlyRepeatsOn',
            ['default' => 0]
        );

        foreach ($sanitizedParams->getIntArray('displayGroupIds', ['default' => []]) as $displayGroupId) {
            $schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId));
        }

        if (!$schedule->isAlwaysDayPart()) {
            // Handle the dates
            $fromDt = $sanitizedParams->getDate('fromDt');
            $toDt = $sanitizedParams->getDate('toDt');
            $recurrenceRange = $sanitizedParams->getDate('recurrenceRange');

            if ($fromDt === null) {
                throw new InvalidArgumentException(__('Please enter a from date'), 'fromDt');
            }

            $logToDt = $toDt?->format(DateFormatHelper::getSystemFormat());
            $logRecurrenceRange = $recurrenceRange?->format(DateFormatHelper::getSystemFormat());
            $this->getLog()->debug(
                'Times received are: FromDt=' . $fromDt->format(DateFormatHelper::getSystemFormat())
                . '. ToDt=' . $logToDt . '. recurrenceRange=' . $logRecurrenceRange
            );

            if (!$schedule->isCustomDayPart() && !$schedule->isAlwaysDayPart()) {
                // Daypart selected
                // expect only a start date (no time)
                $schedule->fromDt = $fromDt->startOfDay()->format('U');
                $schedule->toDt = null;

                if ($recurrenceRange != null) {
                    $schedule->recurrenceRange = $recurrenceRange->format('U');
                }

            } else if (!($this->isApi($request) || Str::contains($this->getConfig()->getSetting('DATE_FORMAT'), 's'))) {
                // In some circumstances we want to trim the seconds from the provided dates.
                // this happens when the date format provided does not include seconds and when the add
                // event comes from the UI.
                $this->getLog()->debug('Date format does not include seconds, removing them');
                $schedule->fromDt = $fromDt->setTime($fromDt->hour, $fromDt->minute, 0)->format('U');

                if ($toDt !== null) {
                    $schedule->toDt = $toDt->setTime($toDt->hour, $toDt->minute, 0)->format('U');
                }

                if ($recurrenceRange != null) {
                    $schedule->recurrenceRange =
                        $recurrenceRange->setTime(
                            $recurrenceRange->hour,
                            $recurrenceRange->minute,
                            0
                        )->format('U');
                }
            } else {
                $schedule->fromDt = $fromDt->format('U');

                if ($toDt !== null) {
                    $schedule->toDt = $toDt->format('U');
                }

                if ($recurrenceRange != null) {
                    $schedule->recurrenceRange = $recurrenceRange->format('U');
                }
            }

            $logToDt = $toDt?->format(DateFormatHelper::getSystemFormat());
            $logRecurrenceRange = $recurrenceRange?->format(DateFormatHelper::getSystemFormat());
            $this->getLog()->debug(
                'Processed times are: FromDt=' . $fromDt->format(DateFormatHelper::getSystemFormat())
                . '. ToDt=' . $logToDt . '. recurrenceRange=' . $logRecurrenceRange
            );
        }

        // Schedule Criteria
        $criteria = $sanitizedParams->getArray('criteria');
        if (is_array($criteria) && count($criteria) > 0) {
            foreach ($criteria as $item) {
                $itemParams = $this->getSanitizer($item);
                $criterion = $this->scheduleCriteriaFactory->createEmpty();
                $criterion->metric = $itemParams->getString('metric');
                $criterion->type = $itemParams->getString('type');
                $criterion->condition = $itemParams->getString('condition');
                $criterion->value = $itemParams->getString('value');
                $schedule->addOrUpdateCriteria($criterion);
            }
        }

        // Ready to do the add
        $schedule->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService());
        if ($schedule->campaignId != null) {
            $schedule->setCampaignFactory($this->campaignFactory);
        }
        $schedule->save();

        $this->getLog()->debug('Add Schedule Reminder');

        // API Request
        $rows = [];
        if ($this->isApi($request)) {
            $reminders =  $sanitizedParams->getArray('scheduleReminders', ['default' => []]);
            foreach ($reminders as $i => $reminder) {
                $rows[$i]['reminder_value'] = (int) $reminder['reminder_value'];
                $rows[$i]['reminder_type'] = (int) $reminder['reminder_type'];
                $rows[$i]['reminder_option'] = (int) $reminder['reminder_option'];
                $rows[$i]['reminder_isEmailHidden'] = (int) $reminder['reminder_isEmailHidden'];
            }
        } else {
            for ($i=0; $i < count($sanitizedParams->getIntArray('reminder_value', ['default' => []])); $i++) {
                $rows[$i]['reminder_value'] = $sanitizedParams->getIntArray('reminder_value')[$i];
                $rows[$i]['reminder_type'] = $sanitizedParams->getIntArray('reminder_type')[$i];
                $rows[$i]['reminder_option'] = $sanitizedParams->getIntArray('reminder_option')[$i];
                $rows[$i]['reminder_isEmailHidden'] = $sanitizedParams->getIntArray('reminder_isEmailHidden')[$i];
            }
        }

        // Save new reminders
        foreach ($rows as $reminder) {
            // Do not add reminder if empty value provided for number of minute/hour
            if ($reminder['reminder_value'] == 0) {
                continue;
            }

            $scheduleReminder = $this->scheduleReminderFactory->createEmpty();
            $scheduleReminder->scheduleReminderId = null;
            $scheduleReminder->eventId = $schedule->eventId;
            $scheduleReminder->value = $reminder['reminder_value'];
            $scheduleReminder->type = $reminder['reminder_type'];
            $scheduleReminder->option = $reminder['reminder_option'];
            $scheduleReminder->isEmail = $reminder['reminder_isEmailHidden'];

            $this->saveReminder($schedule, $scheduleReminder);
        }

        // We can get schedule reminders in an array
        if ($this->isApi($request)) {
            $schedule = $this->scheduleFactory->getById($schedule->eventId);
            $schedule->load([
                'loadScheduleReminders' => in_array('scheduleReminders', $embed),
            ]);
        }

        if ($this->isSyncEvent($schedule->eventTypeId)) {
            $syncGroup = $this->syncGroupFactory->getById($schedule->syncGroupId);
            $syncGroup->validateForSchedule($sanitizedParams);
            $schedule->updateSyncLinks($syncGroup, $sanitizedParams);
        }

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 201,
            'message' => __('Added Event'),
            'id' => $schedule->eventId,
            'data' => $schedule
        ]);

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

    /**
     * Shows a form to edit an event
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws ControllerNotImplemented
     */
    function editForm(Request $request, Response $response, $id)
    {
        $sanitizedParams = $this->getSanitizer($request->getParams());
        // Recurring event start/end
        $eventStart = $sanitizedParams->getInt('eventStart', ['default' => 1000]) / 1000;
        $eventEnd = $sanitizedParams->getInt('eventEnd', ['default' => 1000]) / 1000;

        $schedule = $this->scheduleFactory->getById($id);
        $schedule->load();

        // Dispatch an event to retrieve criteria for scheduling from the OpenWeatherMap connector.
        $event = new ScheduleCriteriaRequestEvent();
        $this->getDispatcher()->dispatch($event, ScheduleCriteriaRequestEvent::$NAME);

        // Retrieve the data from the event
        $criteria = $event->getCriteria();
        $criteriaDefaultCondition = $event->getCriteriaDefaultCondition();

        if (!$this->isEventEditable($schedule)) {
            throw new AccessDeniedException();
        }

        // Fix the event dates for display
        if ($schedule->isAlwaysDayPart()) {
            $schedule->fromDt = '';
            $schedule->toDt = '';
        } else {
            $schedule->fromDt =
                Carbon::createFromTimestamp($schedule->fromDt)
                    ->format(DateFormatHelper::getSystemFormat());
            if ($schedule->toDt != null) {
                $schedule->toDt =
                    Carbon::createFromTimestamp($schedule->toDt)
                        ->format(DateFormatHelper::getSystemFormat());
            }
        }

        if ($schedule->recurrenceRange != null) {
            $schedule->recurrenceRange =
                Carbon::createFromTimestamp($schedule->recurrenceRange)
                    ->format(DateFormatHelper::getSystemFormat());
        }
        // Get all reminders
        $scheduleReminders = $this->scheduleReminderFactory->query(null, ['eventId' => $id]);

        // get the default longitude and latitude from CMS options
        $defaultLat = (float)$this->getConfig()->getSetting('DEFAULT_LAT');
        $defaultLong = (float)$this->getConfig()->getSetting('DEFAULT_LONG');

        if ($this->isFullScreenSchedule($schedule->eventTypeId)) {
            $schedule->setUnmatchedProperty('fullScreenCampaignId', $schedule->campaignId);

            if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$MEDIA_EVENT) {
                $schedule->setUnmatchedProperty(
                    'mediaId',
                    $this->layoutFactory->getLinkedFullScreenMediaId($schedule->campaignId)
                );
            } else if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$PLAYLIST_EVENT) {
                $schedule->setUnmatchedProperty(
                    'playlistId',
                    $this->layoutFactory->getLinkedFullScreenPlaylistId($schedule->campaignId)
                );
            }

            // Get the associated fullscreen layout
            $fsLayout = $this->layoutFactory->getById(
                $this->campaignFactory->getLinkedLayouts($schedule->campaignId)[0]->layoutId
            );

            // Set the layout properties
            $schedule->backgroundColor = $fsLayout->backgroundColor;
            $schedule->layoutDuration = $fsLayout->duration;
            $schedule->resolutionId = $this->layoutFactory->getLayoutResolutionId($fsLayout)->resolutionId;
        }

        $this->getState()->template = 'schedule-form-edit';
        $this->getState()->setData([
            'event' => $schedule,
            'dayParts' => $this->dayPartFactory->allWithSystem(['isRetired' => 0]),
            'displayGroups' => $schedule->displayGroups,
            'campaign' => !empty($schedule->campaignId) ? $this->campaignFactory->getById($schedule->campaignId) : null,
            'displayGroupIds' => array_map(function ($element) {
                return $element->displayGroupId;
            }, $schedule->displayGroups),
            'addForm' => false,
            'reminders' => $scheduleReminders,
            'defaultLat' => $defaultLat,
            'defaultLong' => $defaultLong,
            'recurringEvent' => $schedule->recurrenceType != '',
            'eventStart' => $eventStart,
            'eventEnd' => $eventEnd,
            'eventTypes' => \Xibo\Entity\Schedule::getEventTypes(),
            'scheduleCriteria' => $criteria,
            'criteriaDefaultCondition' => $criteriaDefaultCondition
        ]);

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

    /**
     * Shows the Delete a Recurring Event form
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws ControllerNotImplemented
     */
    public function deleteRecurrenceForm(Request $request, Response $response, $id)
    {
        $sanitizedParams = $this->getSanitizer($request->getParams());
        // Recurring event start/end
        $eventStart = $sanitizedParams->getInt('eventStart', ['default' => 1000]);
        $eventEnd = $sanitizedParams->getInt('eventEnd', ['default' => 1000]);

        $schedule = $this->scheduleFactory->getById($id);
        $schedule->load();

        if (!$this->isEventEditable($schedule)) {
            throw new AccessDeniedException();
        }

        $this->getState()->template = 'schedule-recurrence-form-delete';
        $this->getState()->setData([
            'event' => $schedule,
            'eventStart' => $eventStart,
            'eventEnd' => $eventEnd,
        ]);

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

    /**
     * Deletes a recurring Event from all displays
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws ControllerNotImplemented
     * @SWG\Delete(
     *  path="/schedulerecurrence/{eventId}",
     *  operationId="schedulerecurrenceDelete",
     *  tags={"schedule"},
     *  summary="Delete a Recurring Event",
     *  description="Delete a Recurring Event of a Scheduled Event",
     *  @SWG\Parameter(
     *      name="eventId",
     *      in="path",
     *      description="The Scheduled Event ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=204,
     *      description="successful operation"
     *  )
     * )
     */
    public function deleteRecurrence(Request $request, Response $response, $id)
    {
        $schedule = $this->scheduleFactory->getById($id);
        $schedule->load();

        if (!$this->isEventEditable($schedule)) {
            throw new AccessDeniedException();
        }

        $sanitizedParams = $this->getSanitizer($request->getParams());
        // Recurring event start/end
        $eventStart = $sanitizedParams->getInt('eventStart', ['default' => 1000]);
        $eventEnd = $sanitizedParams->getInt('eventEnd', ['default' => 1000]);
        $scheduleExclusion = $this->scheduleExclusionFactory->create($schedule->eventId, $eventStart, $eventEnd);

        $this->getLog()->debug('Create a schedule exclusion record');
        $scheduleExclusion->save();

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 204,
            'message' => __('Deleted Event')
        ]);

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

    /**
     * Edits an event
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws ControllerNotImplemented
     * @SWG\Put(
     *  path="/schedule/{eventId}",
     *  operationId="scheduleEdit",
     *  tags={"schedule"},
     *  summary="Edit Schedule Event",
     *  description="Edit a scheduled event for a Campaign/Layout to be shown on a Display Group/Display.",
     *  @SWG\Parameter(
     *      name="eventId",
     *      in="path",
     *      description="The Scheduled Event ID",
     *      type="integer",
     *      required=true
     *  ),
     *  @SWG\Parameter(
     *      name="eventTypeId",
     *      in="formData",
     *      description="The Event Type Id to use for this Event.
     * 1=Layout, 2=Command, 3=Overlay, 4=Interrupt, 5=Campaign, 6=Action",
     *      type="integer",
     *      required=true
     *  ),
     *  @SWG\Parameter(
     *      name="campaignId",
     *      in="formData",
     *      description="The Campaign ID to use for this Event.
     * If a Layout is needed then the Campaign specific ID for that Layout should be used.",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="commandId",
     *      in="formData",
     *      description="The Command ID to use for this Event.",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="mediaId",
     *      in="formData",
     *      description="The Media ID to use for this Event.",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="displayOrder",
     *      in="formData",
     *      description="The display order for this event. ",
     *      type="integer",
     *      required=true
     *  ),
     *  @SWG\Parameter(
     *      name="isPriority",
     *      in="formData",
     *      description="An integer indicating the priority of this event. Normal events have a priority of 0.",
     *      type="integer",
     *      required=true
     *   ),
     *   @SWG\Parameter(
     *      name="displayGroupIds",
     *      in="formData",
     *      description="The Display Group IDs for this event.
     * Display specific Group IDs should be used to schedule on single displays.",
     *      type="array",
     *      required=true,
     *      @SWG\Items(type="integer")
     *   ),
     *   @SWG\Parameter(
     *      name="dayPartId",
     *      in="formData",
     *      description="The Day Part for this event. Overrides supported are 0(custom) and 1(always). Defaulted to 0.",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="syncTimezone",
     *      in="formData",
     *      description="Should this schedule be synced to the resulting Display timezone?",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="fromDt",
     *      in="formData",
     *      description="The from date for this event.",
     *      type="string",
     *      format="date-time",
     *      required=true
     *   ),
     *   @SWG\Parameter(
     *      name="toDt",
     *      in="formData",
     *      description="The to date for this event.",
     *      type="string",
     *      format="date-time",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="recurrenceType",
     *      in="formData",
     *      description="The type of recurrence to apply to this event.",
     *      type="string",
     *      required=false,
     *      enum={"", "Minute", "Hour", "Day", "Week", "Month", "Year"}
     *   ),
     *   @SWG\Parameter(
     *      name="recurrenceDetail",
     *      in="formData",
     *      description="The interval for the recurrence.",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="recurrenceRange",
     *      in="formData",
     *      description="The end date for this events recurrence.",
     *      type="string",
     *      format="date-time",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="recurrenceRepeatsOn",
     *      in="formData",
     *      description="The days of the week that this event repeats - weekly only",
     *      type="string",
     *      format="array",
     *      required=false,
     *      @SWG\Items(type="integer")
     *   ),
     *   @SWG\Parameter(
     *      name="scheduleReminders",
     *      in="formData",
     *      description="Array of Reminders for this event",
     *      type="array",
     *      required=false,
     *      @SWG\Items(
     *          ref="#/definitions/ScheduleReminderArray"
     *      )
     *   ),
     *   @SWG\Parameter(
     *      name="isGeoAware",
     *      in="formData",
     *      description="Flag (0-1), whether this event is using Geo Location",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="geoLocation",
     *      in="formData",
     *      description="Array of comma separated strings each with comma separated pair of coordinates",
     *      type="array",
     *      required=false,
     *      @SWG\Items(type="string")
     *   ),
     *   @SWG\Parameter(
     *      name="geoLocationJson",
     *      in="formData",
     *      description="Valid GeoJSON string, use as an alternative to geoLocation parameter",
     *      type="string",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="actionType",
     *      in="formData",
     *      description="For Action eventTypeId, the type of the action - command or navLayout",
     *      type="string",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="actionTriggerCode",
     *      in="formData",
     *      description="For Action eventTypeId, the webhook trigger code for the Action",
     *      type="string",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="actionLayoutCode",
     *      in="formData",
     *      description="For Action eventTypeId and navLayout actionType, the Layout Code identifier",
     *      type="string",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="dataSetId",
     *      in="formData",
     *      description="For Data Connector eventTypeId, the DataSet ID",
     *      type="integer",
     *      required=false
     *   ),
     *   @SWG\Parameter(
     *      name="dataSetParams",
     *      in="formData",
     *      description="For Data Connector eventTypeId, the DataSet params",
     *      type="string",
     *      required=false
     *   ),
     *   @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Schedule")
     *  )
     * )
     */
    public function edit(Request $request, Response $response, $id)
    {
        $sanitizedParams = $this->getSanitizer($request->getParams());
        $embed = ($sanitizedParams->getString('embed') != null) ? explode(',', $sanitizedParams->getString('embed')) : [];

        $schedule = $this->scheduleFactory->getById($id);
        $oldSchedule = clone $schedule;

        $schedule->load([
            'loadScheduleReminders' => in_array('scheduleReminders', $embed),
        ]);

        if (!$this->isEventEditable($schedule)) {
            throw new AccessDeniedException();
        }

        $schedule->eventTypeId = $sanitizedParams->getInt('eventTypeId');
        $schedule->campaignId = $this->isFullScreenSchedule($schedule->eventTypeId)
            ? $sanitizedParams->getInt('fullScreenCampaignId')
            : $sanitizedParams->getInt('campaignId');
        // displayOrder and isPriority: if present but empty (""): set to 0
        // if missing from form: keep existing value (fallback to 0 if unset)
        $schedule->displayOrder = $sanitizedParams->hasParam('displayOrder')
            ? $sanitizedParams->getInt('displayOrder', ['default' => 0])
            : ($schedule->displayOrder ?? 0);

        $schedule->isPriority = $sanitizedParams->hasParam('isPriority')
            ? $sanitizedParams->getInt('isPriority', ['default' => 0])
            : ($schedule->isPriority ?? 0);

        $schedule->dayPartId = $sanitizedParams->getInt('dayPartId', ['default' => $schedule->dayPartId]);
        $schedule->syncTimezone = $sanitizedParams->getCheckbox('syncTimezone');
        $schedule->syncEvent = $this->isSyncEvent($schedule->eventTypeId);
        $schedule->recurrenceType = $sanitizedParams->getString('recurrenceType');
        $schedule->recurrenceDetail = $sanitizedParams->getInt('recurrenceDetail');
        $recurrenceRepeatsOn = $sanitizedParams->getIntArray('recurrenceRepeatsOn');
        $schedule->recurrenceRepeatsOn = (empty($recurrenceRepeatsOn)) ? null : implode(',', $recurrenceRepeatsOn);
        $schedule->recurrenceMonthlyRepeatsOn = $sanitizedParams->getInt(
            'recurrenceMonthlyRepeatsOn',
            ['default' => 0]
        );
        $schedule->displayGroups = [];
        $schedule->isGeoAware = $sanitizedParams->getCheckbox('isGeoAware');
        $schedule->maxPlaysPerHour = $sanitizedParams->getInt('maxPlaysPerHour', ['default' => 0]);
        $schedule->syncGroupId = $sanitizedParams->getInt('syncGroupId');
        $schedule->name = $sanitizedParams->getString('name');
        $schedule->modifiedBy = $this->getUser()->getId();

        // collect action event relevant properties only on action event
        // null these properties otherwise
        if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$ACTION_EVENT) {
            $schedule->actionType = $sanitizedParams->getString('actionType');
            $schedule->actionTriggerCode = $sanitizedParams->getString('actionTriggerCode');
            $schedule->commandId = $sanitizedParams->getInt('commandId');
            $schedule->actionLayoutCode = $sanitizedParams->getString('actionLayoutCode');
            $schedule->campaignId = null;
        } else {
            $schedule->actionType = null;
            $schedule->actionTriggerCode = null;
            $schedule->commandId = null;
            $schedule->actionLayoutCode = null;
        }

        // collect commandId on Command event
        // Retain existing commandId value otherwise
        if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$COMMAND_EVENT) {
            $schedule->commandId = $sanitizedParams->getInt('commandId');
            $schedule->campaignId = null;
        }

        // Set the parentCampaignId for campaign events
        // null parentCampaignId on other events
        // make sure correct Layout/Campaign is selected for relevant event.
        if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$CAMPAIGN_EVENT) {
            $schedule->parentCampaignId = $schedule->campaignId;

            // Make sure we're not directly scheduling an ad campaign
            $campaign = $this->campaignFactory->getById($schedule->campaignId);
            if ($campaign->type === 'ad') {
                throw new InvalidArgumentException(
                    __('Direct scheduling of an Ad Campaign is not allowed'),
                    'campaignId'
                );
            }

            if ($campaign->isLayoutSpecific === 1) {
                throw new InvalidArgumentException(
                    __('Cannot schedule Layout as a Campaign, please select a Campaign instead.'),
                    'campaignId'
                );
            }
        } else {
            $schedule->parentCampaignId = null;
            if (!empty($schedule->campaignId)) {
                $campaign = $this->campaignFactory->getById($schedule->campaignId);
                if ($campaign->isLayoutSpecific === 0) {
                    throw new InvalidArgumentException(
                        __('Cannot schedule Campaign in selected event type, please select a Layout instead.'),
                        'campaignId'
                    );
                }
            }
        }

        // Fields only collected for interrupt events
        if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) {
            $schedule->shareOfVoice = $sanitizedParams->getInt('shareOfVoice', [
                'throw' => function () {
                    new InvalidArgumentException(
                        __('Share of Voice must be a whole number between 0 and 3600'),
                        'shareOfVoice'
                    );
                }
            ]);
        } else {
            $schedule->shareOfVoice = null;
        }

        // Fields only collected for data connector events
        if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$DATA_CONNECTOR_EVENT) {
            $schedule->dataSetId = $sanitizedParams->getInt('dataSetId', [
                'throw' => function () {
                    new InvalidArgumentException(
                        __('Please select a DataSet'),
                        'dataSetId'
                    );
                }
            ]);
            $schedule->dataSetParams = $sanitizedParams->getString('dataSetParams');
        }

        // Get the campaignId for media/playlist events
        if ($this->isFullScreenSchedule($schedule->eventTypeId)) {
            $type = $schedule->eventTypeId === \Xibo\Entity\Schedule::$MEDIA_EVENT ? 'media' : 'playlist';
            $id = ($type === 'media') ? $sanitizedParams->getInt('mediaId') : $sanitizedParams->getInt('playlistId');

            if (!$id) {
                throw new InvalidArgumentException(
                    sprintf('%sId is required when scheduling %s events.', ucfirst($type), $type)
                );
            }

            // Create a full screen layout for this event
            $fsLayout = $this->layoutFactory->createFullScreenLayout(
                $type,
                $id,
                $sanitizedParams->getInt('resolutionId'),
                $sanitizedParams->getString('backgroundColor'),
                $sanitizedParams->getInt('layoutDuration'),
            );

            $schedule->campaignId = $this->layoutFactory->getCampaignIdFromLayoutHistory($fsLayout->layoutId);
            $schedule->parentCampaignId = $schedule->campaignId;
        }

        // API request can provide an array of coordinates or valid GeoJSON, handle both cases here.
        if ($this->isApi($request) && $schedule->isGeoAware === 1) {
            if ($sanitizedParams->getArray('geoLocation') != null) {
                // get string array from API
                $coordinates = $sanitizedParams->getArray('geoLocation');
                // generate GeoJSON and assign to Schedule
                $schedule->geoLocation = $this->createGeoJson($coordinates);
            } else {
                // we were provided with GeoJSON
                $schedule->geoLocation = $sanitizedParams->getString('geoLocationJson');
            }
        } else {
            // if we are not using API, then valid GeoJSON is created in the front end.
            $schedule->geoLocation = $sanitizedParams->getString('geoLocation');
        }

        // if we are editing Layout/Campaign event that was set with Always daypart and change it to Command event type
        // the daypartId will remain as always, which will then cause the event to "disappear" from calendar
        // https://github.com/xibosignage/xibo/issues/1982
        if ($schedule->eventTypeId == \Xibo\Entity\Schedule::$COMMAND_EVENT) {
            $schedule->dayPartId = $this->dayPartFactory->getCustomDayPart()->dayPartId;
        }

        foreach ($sanitizedParams->getIntArray('displayGroupIds', ['default' => []]) as $displayGroupId) {
            $schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId));
        }

        if (!$schedule->isAlwaysDayPart()) {
            // Handle the dates
            $fromDt = $sanitizedParams->getDate('fromDt');
            $toDt = $sanitizedParams->getDate('toDt');
            $recurrenceRange = $sanitizedParams->getDate('recurrenceRange');

            if ($fromDt === null) {
                throw new InvalidArgumentException(__('Please enter a from date'). 'fromDt');
            }

            $logToDt = $toDt?->format(DateFormatHelper::getSystemFormat());
            $logRecurrenceRange = $recurrenceRange?->format(DateFormatHelper::getSystemFormat());
            $this->getLog()->debug(
                'Times received are: FromDt=' . $fromDt->format(DateFormatHelper::getSystemFormat())
                . '. ToDt=' . $logToDt . '. recurrenceRange=' . $logRecurrenceRange
            );

            if (!$schedule->isCustomDayPart() && !$schedule->isAlwaysDayPart()) {
                // Daypart selected
                // expect only a start date (no time)
                $schedule->fromDt = $fromDt->startOfDay()->format('U');
                $schedule->toDt = null;
                $schedule->recurrenceRange = ($recurrenceRange === null) ? null : $recurrenceRange->format('U');

            } else if (!($this->isApi($request) || Str::contains($this->getConfig()->getSetting('DATE_FORMAT'), 's'))) {
                // In some circumstances we want to trim the seconds from the provided dates.
                // this happens when the date format provided does not include seconds and when the add
                // event comes from the UI.
                $this->getLog()->debug('Date format does not include seconds, removing them');
                $schedule->fromDt = $fromDt->setTime($fromDt->hour, $fromDt->minute, 0)->format('U');

                // If we have a toDt
                if ($toDt !== null) {
                    $schedule->toDt = $toDt->setTime($toDt->hour, $toDt->minute, 0)->format('U');
                }

                $schedule->recurrenceRange = ($recurrenceRange === null)
                    ? null
                    : $recurrenceRange->setTime($recurrenceRange->hour, $recurrenceRange->minute, 0)->format('U');
            } else {
                $schedule->fromDt = $fromDt->format('U');

                if ($toDt !== null) {
                    $schedule->toDt = $toDt->format('U');
                }

                $schedule->recurrenceRange = ($recurrenceRange === null) ? null : $recurrenceRange->format('U');
            }

            $this->getLog()->debug('Processed start is: FromDt=' . $fromDt->toRssString());
        } else {
            // This is an always day part, which cannot be recurring, make sure we clear the recurring type if it has been set
            $schedule->recurrenceType = null;
        }

        // Schedule Criteria
        $schedule->criteria = [];
        $criteria = $sanitizedParams->getArray('criteria');
        if (is_array($criteria)) {
            foreach ($criteria as $item) {
                $itemParams = $this->getSanitizer($item);
                $criterion = $this->scheduleCriteriaFactory->createEmpty();
                $criterion->metric = $itemParams->getString('metric');
                $criterion->type = $itemParams->getString('type');
                $criterion->condition = $itemParams->getString('condition');
                $criterion->value = $itemParams->getString('value');
                $schedule->addOrUpdateCriteria($criterion, $itemParams->getInt('id'));
            }
        }

        // Ready to do the add
        $schedule->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService());
        if ($schedule->campaignId != null) {
            $schedule->setCampaignFactory($this->campaignFactory);
        }
        $schedule->save();

        if ($this->isSyncEvent($schedule->eventTypeId)) {
            $syncGroup = $this->syncGroupFactory->getById($schedule->syncGroupId);
            $syncGroup->validateForSchedule($sanitizedParams);
            $schedule->updateSyncLinks($syncGroup, $sanitizedParams);
        }

        // Get form reminders
        $rows = [];
        for ($i=0; $i < count($sanitizedParams->getIntArray('reminder_value',['default' => []])); $i++) {
            $entry = [];

            if ($sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i] == null) {
                continue;
            }

            $entry['reminder_scheduleReminderId'] = $sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i];
            $entry['reminder_value'] = $sanitizedParams->getIntArray('reminder_value')[$i];
            $entry['reminder_type'] = $sanitizedParams->getIntArray('reminder_type')[$i];
            $entry['reminder_option'] = $sanitizedParams->getIntArray('reminder_option')[$i];
            $entry['reminder_isEmail'] = $sanitizedParams->getIntArray('reminder_isEmailHidden')[$i];

            $rows[$sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i]] = $entry;
        }
        $formReminders = $rows;

        // Compare to delete
        // Get existing db reminders
        $scheduleReminders = $this->scheduleReminderFactory->query(null, ['eventId' => $id]);

        $rows = [];
        foreach ($scheduleReminders as $reminder) {

            $entry = [];
            $entry['reminder_scheduleReminderId'] = $reminder->scheduleReminderId;
            $entry['reminder_value'] = $reminder->value;
            $entry['reminder_type'] = $reminder->type;
            $entry['reminder_option'] = $reminder->option;
            $entry['reminder_isEmail'] = $reminder->isEmail;

            $rows[$reminder->scheduleReminderId] = $entry;
        }
        $dbReminders = $rows;

        $deleteReminders = $schedule->compareMultidimensionalArrays($dbReminders, $formReminders, false);
        foreach ($deleteReminders as $reminder) {
            $reminder = $this->scheduleReminderFactory->getById($reminder['reminder_scheduleReminderId']);
            $reminder->delete();
        }

        // API Request
        $rows = [];
        if ($this->isApi($request)) {

            $reminders =  $sanitizedParams->getArray('scheduleReminders', ['default' => []]);
            foreach ($reminders as $i => $reminder) {

                $rows[$i]['reminder_scheduleReminderId'] = isset($reminder['reminder_scheduleReminderId']) ? (int) $reminder['reminder_scheduleReminderId'] : null;
                $rows[$i]['reminder_value'] = (int) $reminder['reminder_value'];
                $rows[$i]['reminder_type'] = (int) $reminder['reminder_type'];
                $rows[$i]['reminder_option'] = (int) $reminder['reminder_option'];
                $rows[$i]['reminder_isEmailHidden'] = (int) $reminder['reminder_isEmailHidden'];
            }
        } else {

            for ($i=0; $i < count($sanitizedParams->getIntArray('reminder_value', ['default' => []])); $i++) {
                $rows[$i]['reminder_scheduleReminderId'] = $sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i];
                $rows[$i]['reminder_value'] = $sanitizedParams->getIntArray('reminder_value')[$i];
                $rows[$i]['reminder_type'] = $sanitizedParams->getIntArray('reminder_type')[$i];
                $rows[$i]['reminder_option'] = $sanitizedParams->getIntArray('reminder_option')[$i];
                $rows[$i]['reminder_isEmailHidden'] = $sanitizedParams->getIntArray('reminder_isEmailHidden')[$i];
            }

        }

        // Save rest of the reminders
        foreach ($rows as $reminder) {

            // Do not add reminder if empty value provided for number of minute/hour
            if ($reminder['reminder_value'] == 0) {
                continue;
            }

            $scheduleReminderId = isset($reminder['reminder_scheduleReminderId']) ? $reminder['reminder_scheduleReminderId'] : null;

            try {
                $scheduleReminder = $this->scheduleReminderFactory->getById($scheduleReminderId);
                $scheduleReminder->load();
            } catch (NotFoundException $e) {
                $scheduleReminder = $this->scheduleReminderFactory->createEmpty();
                $scheduleReminder->scheduleReminderId = null;
                $scheduleReminder->eventId = $id;
            }

            $scheduleReminder->value = $reminder['reminder_value'];
            $scheduleReminder->type = $reminder['reminder_type'];
            $scheduleReminder->option = $reminder['reminder_option'];
            $scheduleReminder->isEmail = $reminder['reminder_isEmailHidden'];

            $this->saveReminder($schedule, $scheduleReminder);
        }

        // If this is a recurring event delete all schedule exclusions
        if ($schedule->recurrenceType != '') {
            // Delete schedule exclusions
            $scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $schedule->eventId]);
            foreach ($scheduleExclusions as $exclusion) {
                $exclusion->delete();
            }
        }

        // Return
        $this->getState()->hydrate([
            'message' => __('Edited Event'),
            'id' => $schedule->eventId,
            'data' => $schedule
        ]);

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

    /**
     * Shows the DeleteEvent form
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws ControllerNotImplemented
     */
    function deleteForm(Request $request, Response $response, $id)
    {
        $schedule = $this->scheduleFactory->getById($id);
        $schedule->load();

        if (!$this->isEventEditable($schedule)) {
            throw new AccessDeniedException();
        }

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

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

    /**
     * Deletes an Event from all displays
     * @param Request $request
     * @param Response $response
     * @param $id
     * @return \Psr\Http\Message\ResponseInterface|Response
     * @throws AccessDeniedException
     * @throws GeneralException
     * @throws NotFoundException
     * @throws ControllerNotImplemented
     * @SWG\Delete(
     *  path="/schedule/{eventId}",
     *  operationId="scheduleDelete",
     *  tags={"schedule"},
     *  summary="Delete Event",
     *  description="Delete a Scheduled Event",
     *  @SWG\Parameter(
     *      name="eventId",
     *      in="path",
     *      description="The Scheduled Event ID",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Response(
     *      response=204,
     *      description="successful operation"
     *  )
     * )
     */
    public function delete(Request $request, Response $response, $id)
    {
        $schedule = $this->scheduleFactory->getById($id);
        $schedule->load();

        if (!$this->isEventEditable($schedule)) {
            throw new AccessDeniedException();
        }

        $schedule
            ->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService())
            ->delete();

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 204,
            'message' => __('Deleted Event')
        ]);

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

    /**
     * Is this event editable?
     * @param \Xibo\Entity\Schedule $event
     * @return bool
     * @throws \Xibo\Support\Exception\InvalidArgumentException
     */
    private function isEventEditable(\Xibo\Entity\Schedule $event): bool
    {
        if (!$this->getUser()->featureEnabled('schedule.modify')) {
            return false;
        }

        // Is this an event coming from an ad campaign?
        if (!empty($event->parentCampaignId) && $event->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) {
            return false;
        }

        $scheduleWithView = ($this->getConfig()->getSetting('SCHEDULE_WITH_VIEW_PERMISSION') == 1);

        // Work out if this event is editable or not. To do this we need to compare the permissions
        // of each display group this event is associated with
        foreach ($event->displayGroups as $displayGroup) {
            // Can schedule with view, but no view permissions
            if ($scheduleWithView && !$this->getUser()->checkViewable($displayGroup)) {
                return false;
            }

            // Can't schedule with view, but no edit permissions
            if (!$scheduleWithView && !$this->getUser()->checkEditable($displayGroup)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Return Schedule eventTypeId based on the grid the requests is coming from
     * @param $from
     * @return int
     */
    private function getEventTypeId($from)
    {
        return match ($from) {
            'Campaign' => \Xibo\Entity\Schedule::$CAMPAIGN_EVENT,
            'Library' => \Xibo\Entity\Schedule::$MEDIA_EVENT,
            'Playlist' => \Xibo\Entity\Schedule::$PLAYLIST_EVENT,
            default => \Xibo\Entity\Schedule::$LAYOUT_EVENT
        };
    }

    /**
     * Generates the Schedule events grid
     *
     * @SWG\Get(
     *  path="/schedule",
     *  operationId="scheduleSearch",
     *  tags={"schedule"},
     *  @SWG\Parameter(
     *      name="eventTypeId",
     *      in="query",
     *      required=false,
     *      type="integer",
     *      description="Filter grid by eventTypeId.
     * 1=Layout, 2=Command, 3=Overlay, 4=Interrupt, 5=Campaign, 6=Action, 7=Media Library, 8=Playlist"
     *  ),
     *  @SWG\Parameter(
     *      name="fromDt",
     *      in="query",
     *      required=false,
     *      type="string",
     *      description="From Date in Y-m-d H:i:s format"
     *  ),
     *  @SWG\Parameter(
     *      name="toDt",
     *      in="query",
     *      required=false,
     *      type="string",
     *      description="To Date in Y-m-d H:i:s format"
     *  ),
     *  @SWG\Parameter(
     *      name="geoAware",
     *      in="query",
     *      required=false,
     *      type="integer",
     *      description="Flag (0-1), whether to return events using Geo Location"
     *  ),
     *  @SWG\Parameter(
     *      name="recurring",
     *      in="query",
     *      required=false,
     *      type="integer",
     *      description="Flag (0-1), whether to return Recurring events"
     *  ),
     *  @SWG\Parameter(
     *      name="campaignId",
     *      in="query",
     *      required=false,
     *      type="integer",
     *      description="Filter events by specific campaignId"
     *  ),
     *  @SWG\Parameter(
     *      name="displayGroupIds",
     *      in="query",
     *      required=false,
     *      type="array",
     *      description="Filter events by an array of Display Group Ids",
     *      @SWG\Items(type="integer")
     *  ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(
     *          type="array",
     *          @SWG\Items(ref="#/definitions/Schedule")
     *      )
     *  )
     * )
     * @param Request $request
     * @param Response $response
     * @return ResponseInterface|Response
     * @throws ControllerNotImplemented
     * @throws GeneralException
     * @throws InvalidArgumentException
     * @throws NotFoundException
     */
    public function grid(Request $request, Response $response)
    {
        $params = $this->getSanitizer($request->getParams());

        $displayGroupIds = $params->getIntArray('displayGroupIds', ['default' => []]);
        $displaySpecificDisplayGroupIds = $params->getIntArray('displaySpecificGroupIds', ['default' => []]);
        $originalDisplayGroupIds = array_merge($displayGroupIds, $displaySpecificDisplayGroupIds);

        if (!$this->getUser()->isSuperAdmin()) {
            $userDisplayGroupIds = array_map(function ($element) {
                /** @var \Xibo\Entity\DisplayGroup $element */
                return $element->displayGroupId;
            }, $this->displayGroupFactory->query(null, ['isDisplaySpecific' => -1]));

            // Reset the list to only those display groups that intersect and if 0 have been provided, only those from
            // the user list
            $resolvedDisplayGroupIds = (count($originalDisplayGroupIds) > 0)
                    ? array_intersect($originalDisplayGroupIds, $userDisplayGroupIds)
                    : $userDisplayGroupIds;

            $this->getLog()->debug('Resolved list of display groups ['
                . json_encode($displayGroupIds) . '] from provided list ['
                . json_encode($originalDisplayGroupIds) . '] and user list ['
                . json_encode($userDisplayGroupIds) . ']');

            // If we have none, then we do not return any events.
            if (count($resolvedDisplayGroupIds) <= 0) {
                $this->getState()->template = 'grid';
                $this->getState()->recordsTotal = $this->scheduleFactory->countLast();
                $this->getState()->setData([]);

                return $this->render($request, $response);
            }
        } else {
            $resolvedDisplayGroupIds = $originalDisplayGroupIds;
        }

        $events = $this->scheduleFactory->query(
            $this->gridRenderSort($params),
            $this->gridRenderFilter([
                'eventTypeId' => $params->getInt('eventTypeId'),
                'futureSchedulesFrom' =>  $params->getDate('fromDt')?->format('U'),
                'futureSchedulesTo' => $params->getDate('toDt')?->format('U'),
                'geoAware' => $params->getInt('geoAware'),
                'recurring' => $params->getInt('recurring'),
                'campaignId' => $params->getInt('campaignId'),
                'displayGroupIds' => $resolvedDisplayGroupIds,
                'name' => $params->getString('name'),
                'useRegexForName' => $params->getCheckbox('useRegexForName'),
                'logicalOperatorName' => $params->getString('logicalOperatorName'),
                'directSchedule' => $params->getCheckbox('directSchedule'),
                'sharedSchedule' => $params->getCheckbox('sharedSchedule'),
                'gridFilter' => 1,
            ], $params)
        );

        // Grab some settings which determine how events are displayed.
        $showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1);
        $defaultTimezone = $this->getConfig()->getSetting('defaultTimezone');

        foreach ($events as $event) {
            $event->load();

            if (count($event->displayGroups) > 0) {
                $array = array_map(function ($object) {
                    return $object->displayGroup;
                }, $event->displayGroups);
                $displayGroupList = implode(', ', $array);
            } else {
                $displayGroupList = '';
            }

            $eventTypes = \Xibo\Entity\Schedule::getEventTypes();
            foreach ($eventTypes as $eventType) {
                if ($eventType['eventTypeId'] === $event->eventTypeId) {
                    $event->setUnmatchedProperty('eventTypeName', $eventType['eventTypeName']);
                }
            }

            $event->setUnmatchedProperty('displayGroupList', $displayGroupList);
            $event->setUnmatchedProperty('recurringEvent', !empty($event->recurrenceType));

            if ($this->isSyncEvent($event->eventTypeId)) {
                $event->setUnmatchedProperty(
                    'displayGroupList',
                    $event->getUnmatchedProperty('syncGroupName')
                );
                $event->setUnmatchedProperty(
                    'syncType',
                    $event->getSyncTypeForEvent()
                );
            }

            if (!$showLayoutName && !$this->getUser()->isSuperAdmin() && !empty($event->campaignId)) {
                // Campaign
                $campaign = $this->campaignFactory->getById($event->campaignId);

                if (!$this->getUser()->checkViewable($campaign)) {
                    $event->campaign = __('Private Item');
                }
            }

            if (!empty($event->recurrenceType)) {
                $repeatsOn = '';
                $repeatsUntil = '';

                if ($event->recurrenceType === 'Week' && !empty($event->recurrenceRepeatsOn)) {
                    $weekdays = Carbon::getDays();
                    $repeatDays = explode(',', $event->recurrenceRepeatsOn);
                    $i = 0;
                    foreach ($repeatDays as $repeatDay) {
                        // Carbon getDays starts with Sunday,
                        // return first element from that array if in our array we have 7 (Sunday)
                        $repeatDay = ($repeatDay == 7) ? 0 : $repeatDay;
                        $repeatsOn .= $weekdays[$repeatDay];
                        if ($i < count($repeatDays) - 1) {
                            $repeatsOn .= ', ';
                        }
                        $i++;
                    }
                } else if ($event->recurrenceType === 'Month') {
                    // Force the timezone for this date (schedule from/to dates are timezone agnostic, but this
                    // date still has timezone information, which could lead to use formatting as the wrong day)
                    $date = Carbon::parse($event->fromDt)->tz($defaultTimezone);
                    $this->getLog()->debug('grid: setting description for monthly event with date: '
                        . $date->toAtomString());

                    if ($event->recurrenceMonthlyRepeatsOn === 0) {
                        $repeatsOn = 'the ' . $date->format('jS') . ' day of the month';
                    } else {
                        // Which day of the month is this?
                        $firstDay = Carbon::parse('first ' . $date->format('l') . ' of ' . $date->format('F'));

                        $this->getLog()->debug('grid: the first day of the month for this date is: '
                            . $firstDay->toAtomString());

                        $nth = $firstDay->diffInDays($date) / 7 + 1;
                        $repeatWeekDayDate = $date->copy()->setDay($nth)->format('jS');
                        $repeatsOn = 'the ' . $repeatWeekDayDate . ' '
                            . $date->format('l')
                            . ' of the month';
                    }
                }

                if (!empty($event->recurrenceRange)) {
                    $repeatsUntil = Carbon::createFromTimestamp($event->recurrenceRange)
                            ->format(DateFormatHelper::getSystemFormat());
                }

                $event->setUnmatchedProperty(
                    'recurringEventDescription',
                    __(sprintf(
                        'Repeats every %d %s %s %s',
                        $event->recurrenceDetail,
                        $event->recurrenceType . ($event->recurrenceDetail > 1 ? 's' : ''),
                        !empty($repeatsOn) ? 'on ' . $repeatsOn : '',
                        !empty($repeatsUntil) ? ' until ' . $repeatsUntil : ''
                    ))
                );
            } else {
                $event->setUnmatchedProperty('recurringEventDescription', '');
            }

            if (!$event->isAlwaysDayPart() && !$event->isCustomDayPart()) {
                $dayPart = $this->dayPartFactory->getById($event->dayPartId);
                $dayPart->adjustForDate(Carbon::createFromTimestamp($event->fromDt));
                $event->fromDt = $dayPart->adjustedStart->format('U');
                $event->toDt = $dayPart->adjustedEnd->format('U');
            }

            if ($event->eventTypeId == \Xibo\Entity\Schedule::$COMMAND_EVENT) {
                $event->toDt = $event->fromDt;
            }

            // Set the row from/to date to be an ISO date for display (no timezone)
            $event->setUnmatchedProperty(
                'displayFromDt',
                Carbon::createFromTimestamp($event->fromDt)->format(DateFormatHelper::getSystemFormat())
            );
            $event->setUnmatchedProperty(
                'displayToDt',
                Carbon::createFromTimestamp($event->toDt)->format(DateFormatHelper::getSystemFormat())
            );

            if ($this->isApi($request)) {
                continue;
            }

            $event->includeProperty('buttons');
            $event->setUnmatchedProperty('isEditable', $this->isEventEditable($event));
            if ($this->isEventEditable($event)) {
                $event->buttons[] = [
                    'id' => 'schedule_button_edit',
                    'url' => $this->urlFor(
                        $request,
                        'schedule.edit.form',
                        ['id' => $event->eventId]
                    ),
                    'dataAttributes' => [
                        ['name' => 'event-id', 'value' => $event->eventId],
                        ['name' => 'event-start', 'value' => $event->fromDt * 1000],
                        ['name' => 'event-end', 'value' => $event->toDt * 1000]
                    ],
                    'text' => __('Edit')
                ];

                $event->buttons[] = [
                    'id' => 'schedule_button_delete',
                    'url' => $this->urlFor($request, 'schedule.delete.form', ['id' => $event->eventId]),
                    'text' => __('Delete'),
                    'multi-select' => true,
                    'dataAttributes' => [
                        [
                            'name' => 'commit-url',
                            'value' => $this->urlFor($request, 'schedule.delete', ['id' => $event->eventId])
                        ],
                        ['name' => 'commit-method', 'value' => 'delete'],
                        ['name' => 'id', 'value' => 'schedule_button_delete'],
                        ['name' => 'text', 'value' => __('Delete')],
                        ['name' => 'sort-group', 'value' => 1],
                        ['name' => 'rowtitle', 'value' => $event->eventId]
                    ]
                ];
            }
        }

        // Store the table rows
        $this->getState()->template = 'grid';
        $this->getState()->recordsTotal = $this->scheduleFactory->countLast();
        $this->getState()->setData($events);

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

    /**
     * @param \Xibo\Entity\Schedule $schedule
     * @param ScheduleReminder $scheduleReminder
     * @throws GeneralException
     * @throws NotFoundException
     * @throws \Xibo\Support\Exception\ConfigurationException
     * @throws \Xibo\Support\Exception\InvalidArgumentException
     */
    private function saveReminder($schedule, $scheduleReminder)
    {
        // if someone changes from custom to always
        // we should keep the definitions, but make sure they don't get executed in the task
        if ($schedule->isAlwaysDayPart()) {
            $scheduleReminder->reminderDt = 0;
            $scheduleReminder->save();
            return;
        }

        switch ($scheduleReminder->type) {
            case ScheduleReminder::$TYPE_MINUTE:
                $type = ScheduleReminder::$MINUTE;
                break;
            case ScheduleReminder::$TYPE_HOUR:
                $type = ScheduleReminder::$HOUR;
                break;
            case ScheduleReminder::$TYPE_DAY:
                $type = ScheduleReminder::$DAY;
                break;
            case ScheduleReminder::$TYPE_WEEK:
                $type = ScheduleReminder::$WEEK;
                break;
            case ScheduleReminder::$TYPE_MONTH:
                $type = ScheduleReminder::$MONTH;
                break;
            default:
                throw new NotFoundException(__('Unknown type'));
        }

        // Remind seconds that we will subtract/add from schedule fromDt/toDt to get reminderDt
        $remindSeconds =  $scheduleReminder->value *  $type;

        // Set reminder date
        if ($scheduleReminder->option == ScheduleReminder::$OPTION_BEFORE_START) {
            $scheduleReminder->reminderDt = $schedule->fromDt - $remindSeconds;
        } elseif ($scheduleReminder->option == ScheduleReminder::$OPTION_AFTER_START) {
            $scheduleReminder->reminderDt = $schedule->fromDt + $remindSeconds;
        } elseif ($scheduleReminder->option == ScheduleReminder::$OPTION_BEFORE_END) {
            $scheduleReminder->reminderDt = $schedule->toDt - $remindSeconds;
        } elseif ($scheduleReminder->option == ScheduleReminder::$OPTION_AFTER_END) {
            $scheduleReminder->reminderDt = $schedule->toDt + $remindSeconds;
        }

        // Is recurring event?
        $now = Carbon::now();
        if ($schedule->recurrenceType != '') {

            // find the next event from now
            try {
                $nextReminderDate = $schedule->getNextReminderDate($now, $scheduleReminder, $remindSeconds);
            } catch (NotFoundException $error) {
                $nextReminderDate = 0;
                $this->getLog()->debug('No next occurrence of reminderDt found. ReminderDt set to 0.');
            }

            if ($nextReminderDate != 0) {

                if ($nextReminderDate < $scheduleReminder->lastReminderDt) {

                    // handle if someone edit in frontend after notifications were created
                    // we cannot have a reminderDt set to below the lastReminderDt
                    // so we make the lastReminderDt 0
                    $scheduleReminder->lastReminderDt = 0;
                    $scheduleReminder->reminderDt = $nextReminderDate;

                } else {
                    $scheduleReminder->reminderDt = $nextReminderDate;
                }

            } else {
                // next event is not found
                // we make the reminderDt and lastReminderDt as 0
                $scheduleReminder->lastReminderDt = 0;
                $scheduleReminder->reminderDt = 0;
            }

            // Save
            $scheduleReminder->save();

        } else { // one off event

            $scheduleReminder->save();

        }
    }

    private function createGeoJson($coordinates)
    {
        $properties = new \StdClass();
        $convertedCoordinates = [];


        // coordinates come as array of strings, we need convert that to array of arrays with float values for the Geo JSON
        foreach ($coordinates as $coordinate) {

            // each $coordinate is a comma separated string with 2 coordinates
            // make it into an array
            $explodedCords = explode(',', $coordinate);

            // prepare a new array, we will add float values to it, need to be cleared for each set of coordinates
            $floatCords = [];

            // iterate through the exploded array, change the type to float store in a new array
            foreach ($explodedCords as $explodedCord) {
                $explodedCord = (float)$explodedCord;
                $floatCords[] = $explodedCord;
            }

            // each set of coordinates will be added to this new array, which we will use in the geo json
            $convertedCoordinates[] = $floatCords;
        }

        $geometry = [
            'type' => 'Polygon',
            'coordinates' => [
                $convertedCoordinates
            ]
        ];

        $geoJson = [
            'type'      => 'Feature',
            'properties' => $properties,
            'geometry'  => $geometry
        ];

        return json_encode($geoJson);
    }

    /**
     * Check if this is event is using full screen schedule with Media or Playlist
     * @param $eventTypeId
     * @return bool
     */
    private function isFullScreenSchedule($eventTypeId)
    {
        return in_array($eventTypeId, [\Xibo\Entity\Schedule::$MEDIA_EVENT, \Xibo\Entity\Schedule::$PLAYLIST_EVENT]);
    }

    /**
     * @param $eventTypeId
     * @return int
     */
    private function isSyncEvent($eventTypeId): int
    {
        return ($eventTypeId === \Xibo\Entity\Schedule::$SYNC_EVENT) ? 1 : 0;
    }
}
