<?php

error_reporting(E_ALL);

/**
 * Класс Тревога
 */
class Alarm
{

    const MONDAY = 'Monday';
    const TUESDAY = 'Tuesday';
    const WEDNESDAY = 'Wednesday';
    const THURSDAY = 'Thursday';
    const FRIDAY = 'Friday';
    const SATURDAY = 'Saturday';
    const SUNDAY = 'Sunday';
    const EVERYDAY = 'Everyday';

    const TYPE_REPEATING = 1;
    const TYPE_NON_REPEATING = 2;

    const STATUS_ACTIVE = true;
    const STATUS_INACTIVE = false;


    /** @var int */
    private $id;
    /** @var DateTime */
    private $time;
    /** @var array */
    private $daysOfWeek = [self::EVERYDAY];
    /** @var int */
    private $type = self::TYPE_REPEATING;
    /** @var bool */
    private $status = self::STATUS_ACTIVE;

    /**
     * Конструктор тревоги.
     *
     * @param int $id
     * @param DateTime $alarmTime
     * @param DateTime $currentTime
     *
     * @throws InvalidArgumentException
     */
    public function __construct(int $id, DateTime $alarmTime, DateTime $currentTime)
    {
        $this->id = $id;
        $this->setTime($alarmTime, $currentTime);
    }

    /**
     * Устанавливает время тревоги.
     *
     * @param DateTime $alarmTime
     * @param DateTime $currentTime
     *
     * @throws InvalidArgumentException
     */
    public function setTime(DateTime $alarmTime, DateTime $currentTime): void
    {

        if ($alarmTime->diff($currentTime)->format('%d') > 7) {
            throw new InvalidArgumentException('Время тревоги больше текущего на неделю');
        }
        if ($alarmTime->format('Ymd') < $currentTime->format('Ymd')) {
            throw new InvalidArgumentException('Нельзя устанавливать прошедшую дату');
        }

        $this->time = $alarmTime;
    }

    /**
     * Устанавливает дни недели по которым срабатывает тревога.
     *
     * @param array $daysOfWeek
     *
     * @throws InvalidArgumentException
     */
    public function setDaysOfWeek(array $daysOfWeek): void
    {

        $namesOfDaysOfWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];

        foreach ($daysOfWeek as $dayOfWeek) {

            if (count($daysOfWeek) > 1 && $dayOfWeek == Alarm::EVERYDAY) {
                throw new InvalidArgumentException('К ежедневному параметру нельзя добавлять другие');
            }
            if (!in_array($dayOfWeek, $namesOfDaysOfWeek) && $dayOfWeek != Alarm::EVERYDAY) {
                throw new InvalidArgumentException('Неправильное значение в массиве с днями недели');
            }

        }

        $this->daysOfWeek = $daysOfWeek;
    }

    /**
     * Устанавливает тип тревоги - одноразовый или многоразовый.
     *
     * @param int $type
     *
     * @throws InvalidArgumentException
     */
    public function setType(int $type): void
    {

        if ($type != self::TYPE_REPEATING && $type != self::TYPE_NON_REPEATING) {
            throw new InvalidArgumentException('Неправильный тип, используйте константы');
        }
        if ($this->status == Alarm::STATUS_INACTIVE && $type == Alarm::TYPE_NON_REPEATING) {
            throw new InvalidArgumentException('Нельзя делать одноразовой неактивную тревогу');
        }

        $this->type = $type;
    }

    /**
     * Устанавливает статус тревоги.
     *
     * @param bool $status
     *
     * @throws InvalidArgumentException
     */
    public function setStatus(bool $status): void
    {

        if ($status == self::STATUS_INACTIVE && $this->type == self::TYPE_NON_REPEATING) {
            throw new InvalidArgumentException('Нельзя изменять статус одноразовой тревоги');
        }

        $this->status = $status;
    }

    /**
     * Возвращает идентификатор тревоги.
     *
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * Возвращает время тревоги.
     *
     * @return DateTime
     */
    public function getTime(): DateTime
    {
        return clone $this->time;
    }

    /**
     * Возвращает дни недели по которым срабатывает тревога.
     *
     * @return array
     */
    public function getDaysOfWeek(): array
    {
        return $this->daysOfWeek;
    }

    /**
     * Возвращает тип тревоги.
     *
     * @return int
     */
    public function getType(): int
    {
        return $this->type;
    }

    /**
     * Возвращает статус тревоги.
     *
     * @return bool
     */
    public function getStatus(): bool
    {
        return $this->status;
    }
}

/**
 * Класс Будильник
 */
class AlarmClock
{
    /** @var int */
    private $id;
    /** @var array */
    private $alarms;

    /**
     * Добавляет новую тревогу
     *
     * @param DateTime $alarmTime
     * @param DateTime $currentTime
     *
     * @throws InvalidArgumentException
     */
    public function addAlarm(DateTime $alarmTime, DateTime $currentTime): void
    {
        $this->id++;
        $this->alarms[] = new Alarm($this->id, $alarmTime, $currentTime);
    }

    /**
     * Удаляет тревогу
     *
     * @param int $id
     *
     * @throws Exception|InvalidArgumentException
     */
    public function deleteAlarm(int $id): void
    {
        if (!$this->alarms) {
            throw new Exception('Нет тревог');
        }

        $yesOrNot = null;

        foreach ($this->alarms as $key => $alarm) {

            if ($alarm->getId() == $id) {

                unset($this->alarms[$key]);
                $yesOrNot = true;
                break;

            }

        }

        if (!$yesOrNot) {
            throw new InvalidArgumentException('Неправильный номер тревоги');
        }

    }

    /**
     * Настраивает тревогу
     *
     * @param int $id
     * @param DateTime|null $alarmTime
     * @param array|null $daysOfWeek
     * @param int|null $type
     * @param bool|null $status
     *
     * @throws Exception|InvalidArgumentException
     */
    public function setAlarm(int $id, ?DateTime $alarmTime, ?array $daysOfWeek, ?int $type, ?bool $status): void
    {

        if (!$this->alarms) {
            throw new Exception('Нет тревог');
        }

        $yesOrNot = null;

        foreach ($this->alarms as $alarm) {

            if ($alarm->getId() == $id) {

                if ($alarmTime) {
                    $alarm->setTime($alarmTime, new DateTime);
                }
                if ($daysOfWeek) {
                    $alarm->setDaysOfWeek($daysOfWeek);
                }
                if ($status !== null) {
                    $alarm->setStatus($status);
                }
                if ($type) {
                    $alarm->setType($type);
                }

                $yesOrNot = true;
                break;
            }

        }

        if (!$yesOrNot) {
            throw new InvalidArgumentException('Неправильный номер тревоги');
        }
    }

    /**
     * Находит ближайшее время тревоги
     *
     * @param Alarm $alarm
     * @param DateTime $currentTime
     *
     * @return DateTime
     *
     * @throws InvalidArgumentException
     */
    public function findNearestAlarmTime(Alarm $alarm, DateTime $currentTime): DateTime
    {

        $alarmTimeIsLess = $alarm->getTime()->format('Ymd H:i') < $currentTime->format('Ymd H:i');
        $isEveryday = $alarm->getDaysOfWeek()[0] == Alarm::EVERYDAY;

        if ($alarmTimeIsLess && $isEveryday) {

            /*
             * Если время тревоги меньше, чем текущее и
             * тревога ежедневная - добавляем к объекту
             * времени тревоги интервал, передвигающий её
             * на следующий день
             */
            $alarmTime = $alarm->getTime()->add(
                DateInterval::createFromDateString('tomorrow')
            );
            // Обновляем время тревоги
            $alarm->setTime($alarmTime, new DateTime);

            return $alarmTime;

        } elseif (!$isEveryday) {

            /*
             * Если у тревоги указаны дни недели в $daysOfWeek
             * - получаем время тревоги для каждого дня недели.
             * Полученные объекты кладем в массив $datesAndTimes
             */
            foreach ($alarm->getDaysOfWeek() as $dayOfWeek) {

                $datesAndTimes[] = $alarm->getTime()->add(
                    (
                    DateInterval::createFromDateString(($alarmTimeIsLess) ? "next $dayOfWeek" : "$dayOfWeek")
                    )
                );

            }

            // Сортируем DatesTimes по возрастанию к текущей дате
            sort($datesAndTimes);
            // Ближайшем временем тревоги становится первостоящая в массиве
            $alarmTime = array_shift($datesAndTimes);
            // Также, не забываем перезаписать время тревоги
            $alarm->setTime($alarmTime, new DateTime);

            return $alarmTime;

        } else {

            // В остальных случаях, просто возвращаем объект даты-времени тревоги
            return $alarm->getTime();

        }

    }

    /**
     * Находит ближайшую к текущему времени тревогу
     *
     * @param DateTime $currentTime
     *
     * @return Alarm
     *
     * @throws Exception|InvalidArgumentException
     */
    public function findNearestAlarm(DateTime $currentTime): Alarm
    {

        if (!$this->alarms) {
            throw new Exception('Нет тревог');
        }

        $alarms = [];

        foreach ($this->alarms as $alarm) {

            $alarmTime = $this->findNearestAlarmTime($alarm, $currentTime);
            $alarms[] = [$alarmTime, $alarm];

        }

        sort($alarms);

        return $alarms[0][1];

    }

    /**
     * Находит тревогу равную текущему времени
     *
     * @param $currentTime
     *
     * @return Alarm|null
     *
     * @throws Exception|InvalidArgumentException
     */
    public function findTriggeredAlarm(DateTime $currentTime): ?Alarm
    {

        if (!$this->alarms) {
            throw new Exception('Нет тревог');
        }

        foreach ($this->alarms as $alarm) {

            // На всякий случай обновим время срабатывания тревоги
            $alarmTime = $this->findNearestAlarmTime($alarm, new DateTime);

            if ($alarmTime->format('Ymd H:i') == $currentTime->format('Ymd H:i')) {
                return $alarm;
            }

        }

        return null;

    }

    /**
     * Удаляет активные, сработавшие, одноразовые тревоги для текущего времени
     *
     * @param DateTime $currentTime
     *
     * @throws Exception
     */
    public function deleteProcessedNonRepeatingAlarms(DateTime $currentTime): void
    {

        if (!$this->alarms) {
            throw new Exception('Нет тревог');
        }

        foreach ($this->alarms as $alarm) {

            // Определяем равно ли время тревоги текущему
            $alarmTimeIsEqualTo = $currentTime->format('N H:i') == $alarm->getTime()->format('N H:i');

            /*
             * Если время срабатывания тревоги равно текущему,
             * а тип тревоги одноразовый - удаляем её
             */
            if ($alarmTimeIsEqualTo && $alarm->getType() == Alarm::TYPE_NON_REPEATING) {
                $this->deleteAlarm($alarm->getId());
            }

        }

    }

}

/**
 * Проигрывает мелодию
 *
 * @param AlarmClock $alarmClock
 * @param DateTime $currentTime
 *
 * @throws Exception
 */
function playMelody(AlarmClock $alarmClock, DateTime $currentTime)
{

    $nearestAlarm = $alarmClock->findNearestAlarm(new DateTime);

    echo "Ближайшая тревога №{$nearestAlarm->getId()}. Время тревоги {$nearestAlarm->getTime()->format('Y-m-d H:i')}\n";

    // Получаем разницу в секундах между текущим временем и временем тревоги
    $waitingTime = $nearestAlarm->getTime()->getTimestamp() - $currentTime->getTimestamp();

    // Ожидаем время тревоги
    if ($waitingTime > 0) {
        sleep($waitingTime);
    }

    // Находим тревогу срабатывающую ровно в это время
    $triggeredAlarm = $alarmClock->findTriggeredAlarm(new DateTime);

    if ($triggeredAlarm && $triggeredAlarm->getStatus()) {

        // Проигрываем мелодию {}

        echo "Сработала тревога №{$triggeredAlarm->getId()}\n";

        // Удаляем сработавшие одноразовые тревоги
        $alarmClock->deleteProcessedNonRepeatingAlarms(new DateTime);
    }

    // Ожидаем минуту, из-за особенностей работы метода поиска ближайшей тревоги
    sleep(60);
}

$alarmClock = new AlarmClock;

$alarmClock->addAlarm(new DateTime('18:24'), new DateTime);
$alarmClock->addAlarm(new DateTime('18:25'), new DateTime);
$alarmClock->setAlarm(1, null, [Alarm::SUNDAY], Alarm::TYPE_NON_REPEATING, null);
$alarmClock->setAlarm(2, null, [Alarm::SUNDAY, Alarm::MONDAY], Alarm::TYPE_NON_REPEATING, null);

// Код будет выполнятся пока не закончатся тревоги
while ($alarmClock->findNearestAlarm(new DateTime)) {
    playMelody($alarmClock, new DateTime);
}

