<?php

assert_options(ASSERT_ACTIVE,   true);
assert_options(ASSERT_WARNING,  true);

class Alarm
{
    const MONDAY = 1;
    const TUESDAY = 2;
    const WEDNESDAY = 3;
    const THURSDAY = 4;
    const FRIDAY = 5;
    const SATURDAY = 6;
    const SUNDAY = 7;

    private const ALL_DAYS = [
        self::MONDAY,
        self::TUESDAY,
        self::WEDNESDAY,
        self::THURSDAY,
        self::FRIDAY,
        self::SATURDAY,
        self::SUNDAY
    ];

    const TYPE_REPEATING = true;
    const TYPE_NON_REPEATING = false;

    const STATUS_ACTIVE = true;
    const STATUS_INACTIVE = false;

    private $time;
    private $daysOfWeek;
    private $type;
    private $status;

    public function __construct(int $hour, int $minute)
    {
        $this->setTime($hour, $minute);
        $this->setDaysOfWeek(self::ALL_DAYS);
        $this->setType(self::TYPE_REPEATING);
        $this->setStatus(self::STATUS_ACTIVE);
    }

    public function setTime(int $hour, int $minute): void
    {
        if ($hour > 23 || $hour < 0 || $minute > 60 || $minute < 0) {
            throw new InvalidArgumentException('Неправильное время');
        }

        if ($hour < 10) {
            $hour = 0..$hour;
        }

        if ($minute < 10) {
            $minute = 0..$minute;
        }

        $this->time = $hour.':'.$minute;
    }

    public function setDaysOfWeek(array $daysOfWeek): void
    {
        foreach ($daysOfWeek as $dayOfWeek) {
            if ($dayOfWeek > 7 || $dayOfWeek < 1) {
                throw new InvalidArgumentException('Неправильное значение в массиве с днями недели');
            }
        }

        $this->daysOfWeek = $daysOfWeek;
    }

    public function setType(bool $type): void
    {
        if ($this->status == Alarm::STATUS_INACTIVE && $type == Alarm::TYPE_NON_REPEATING) {
            throw new InvalidArgumentException('Нельзя делать одноразовой неактивную тревогу');
        }

        $this->type = $type;
    }

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

        $this->status = $status;
    }

    public function getTime(): string
    {
        return $this->time;
    }

    public function getDaysOfWeek(): array
    {
        return $this->daysOfWeek;
    }

    public function isRepeating(): bool
    {
        return $this->type;
    }

    public function isActive(): bool
    {
        return $this->status;
    }

    public function findNearestAlarmTime(DateTimeImmutable $currentTime): DateTimeImmutable
    {
        $spelling = [
            '', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'
        ];

        foreach ($this->daysOfWeek as $dayOfWeek) {

            if ($dayOfWeek > 7 || $dayOfWeek < 1) {
                throw new InvalidArgumentException('Неправильное значение в массиве с днями недели');
            }

            $ordinal = ($currentTime->format('H:i') > $this->time) ? 'next' : '';
            $datesAndTimes[] = $currentTime->modify($ordinal.' '.$spelling[$dayOfWeek].' '.$this->time);

        }

        sort($datesAndTimes);

        return $datesAndTimes[0];
    }
}

class AlarmClock
{
    private $alarms;

    public function addAlarm(Alarm $alarm): void
    {
        $this->alarms[] = $alarm;
    }

    public function setAlarm(int $id, Alarm $alarm): void
    {
        if (!$this->alarms) {
            throw new Exception('Нет тревог');
        }
        if (!$this->alarms[$id]) {
            throw new InvalidArgumentException('Несуществующая тревога');
        }

        $this->alarms[$id] = $alarm;
    }

    public function getAlarm(int $id): ?Alarm
    {
        return $this->alarms[$id];
    }

    public function deleteAlarm(Alarm $alarm): void
    {
        if (!$this->alarms) {
            throw new Exception('Нет тревог');
        }

        foreach ($this->alarms as $key => $object) {
            if ($object == $alarm) {
                unset($this->alarms[$key]);
            }
        }
    }

    public function findNearestAlarm(DateTimeImmutable $currentTime): Alarm
    {
        if (!$this->alarms) {
            throw new Exception('Нет тревог');
        }

        $alarms = [];

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

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

        }

        sort($alarms);

        return $alarms[0][1];
    }

    public function findTriggeredAlarm(DateTimeImmutable $currentTime): ?Alarm
    {
        if (!$this->alarms) {
            throw new Exception('Нет тревог');
        }

        foreach ($this->alarms as $alarm) {
            if ($this->isTriggeredAlarm($alarm, $currentTime)
                && $alarm->isActive() == Alarm::STATUS_ACTIVE) {
                return $alarm;
            }
        }

        return null;
    }

    public function deleteProcessedNonRepeatingAlarms(DateTimeImmutable $currentTime): void
    {
        if (!$this->alarms) {
            throw new Exception('Нет тревог');
        }

        foreach ($this->alarms as $alarm) {
            if ($this->isTriggeredAlarm($alarm, $currentTime)
                && $alarm->isRepeating() == Alarm::TYPE_NON_REPEATING) {
                $this->deleteAlarm($alarm);
            }
        }
    }

    private function isTriggeredAlarm(Alarm $alarm, DateTimeImmutable $currentTime): bool
    {
        $alarmTime = $alarm->findNearestAlarmTime($currentTime);
        return ($alarmTime->format('Y-m-d H:i') == $currentTime->format('Y-m-d H:i'));
    }
}

/*
 * Тревога срабатывает только в установленное время
 */
$alarm = new Alarm(13, 0);

$alarmClock = new AlarmClock();
$alarmClock->addAlarm($alarm);

$currentTime = new DateTimeImmutable('13:00');
$ringingAlarm = $alarmClock->findTriggeredAlarm($currentTime);

assert($ringingAlarm !== null);
assert($alarm === $ringingAlarm);

$currentTime2 = new DateTimeImmutable('14:00');
$ringingAlarm2 = $alarmClock->findTriggeredAlarm($currentTime2);

assert($ringingAlarm2 === null);

echo "Тест пройден: Тревога срабатывает только в установленное время\n";

/*
 * Многократная тревога сработает неограниченное число раз
 */
$alarm = new Alarm(13, 0);

$alarmClock = new AlarmClock();
$alarmClock->addAlarm($alarm);

$currentTime = new DateTimeImmutable('13:00');
$ringingAlarm = $alarmClock->findTriggeredAlarm($currentTime);
assert($ringingAlarm === $alarm);

$currentTime2 = new DateTimeImmutable('+1 day 13:00');
$ringingAlarm2 = $alarmClock->findTriggeredAlarm($currentTime2);
assert($ringingAlarm2 === $alarm);

echo "Тест пройден: Многократная тревога срабатывает неограниченное число раз\n";

/*
 * Если в Будильник добавить одноразовую Тревогу на будущее, то она сработает однократно
 */
$alarm = new Alarm(13, 0);
$alarm->setType(Alarm::TYPE_NON_REPEATING);
$alarm->setDaysOfWeek([Alarm::SUNDAY]);

$alarm2 = new Alarm(13, 30);

$alarmClock = new AlarmClock();
$alarmClock->addAlarm($alarm);
$alarmClock->addAlarm($alarm2);

$currentTime = new DateTimeImmutable('Sunday 13:00');
$ringingAlarm = $alarmClock->findTriggeredAlarm($currentTime);
assert($ringingAlarm === $alarm);

$alarmClock->deleteProcessedNonRepeatingAlarms($currentTime);

$currentTime2 = $currentTime->modify('next Sunday 13:00');
$ringingAlarm2 = $alarmClock->findTriggeredAlarm($currentTime2);
assert($ringingAlarm2 === null);

echo "Тест пройден: Одноразовая тревога добавленная на будущее срабатывает однократно\n";

/* Тревога срабатывает только в установленные дни */
$alarm = new Alarm(13, 0);
$alarm->setDaysOfWeek([Alarm::SATURDAY, Alarm::SUNDAY]);

$alarmClock = new AlarmClock();
$alarmClock->addAlarm($alarm);

$currentTime = new DateTimeImmutable('Saturday 13:00');
$ringingAlarm = $alarmClock->findTriggeredAlarm($currentTime);
assert($ringingAlarm === $alarm);

$currentTime2 = new DateTimeImmutable('Sunday 13:00');
$ringingAlarm2 = $alarmClock->findTriggeredAlarm($currentTime2);
assert($ringingAlarm2 === $alarm);

$currentTime3 = new DateTimeImmutable('Monday 13:00');
$ringingAlarm3 = $alarmClock->findTriggeredAlarm($currentTime3);
assert($ringingAlarm3 === null);

echo "Тест пройден: Тревога срабатывает в установленные дни\n";

/* Неактивная тревога не срабатывает */
$alarm = new Alarm(13, 0);
$alarm->setStatus(Alarm::STATUS_INACTIVE);

$alarmClock = new AlarmClock();
$alarmClock->addAlarm($alarm);

$currentTime = new DateTimeImmutable('13:00');
$ringingAlarm = $alarmClock->findTriggeredAlarm($currentTime);
assert($ringingAlarm === null);

echo "Тест пройден: Неактивная тревога не срабатывает\n";

/* Можно определить ближайшее время срабатывания даже для неактивной тревоги */
$alarm = new Alarm(13, 0);
$alarm2 = new Alarm(13, 30);
$alarm2->setStatus(Alarm::STATUS_INACTIVE);

$currentTime = new DateTimeImmutable();
$alarmTime = $alarm->findNearestAlarmTime($currentTime);
assert(get_class($alarmTime) == DateTimeImmutable::class);

$currentTime2 = new DateTimeImmutable();
$alarmTime2 = $alarm2->findNearestAlarmTime($currentTime2);
assert(get_class($alarmTime2) == DateTimeImmutable::class);

echo "Тест пройден: Можно определить время срабатывания, в том числе для неактивной тревоги\n";

/* Можно удалить сработавшие одноразовые тревоги */
$alarm = new Alarm(13, 0);
$alarm->setType(Alarm::TYPE_NON_REPEATING);
$alarm->setDaysOfWeek([Alarm::SUNDAY]);

$alarmClock = new AlarmClock();
$alarmClock->addAlarm($alarm);

$currentTime = new DateTimeImmutable('Sunday 13:00');
$alarmClock->deleteProcessedNonRepeatingAlarms($currentTime);
assert($alarmClock->getAlarm(0) === null);

echo "Тест пройден: Можно удалить сработавшие одноразовые тревоги\n";

/* Можно узнать ближайшую сработающую Тревогу, даже если их несколько */
$alarm = new Alarm(15, 50);
$alarm2 = new Alarm(19, 0);

$alarmClock = new AlarmClock();
$alarmClock->addAlarm($alarm);
$alarmClock->addAlarm($alarm2);

$currentTime = new DateTimeImmutable();
$nearestAlarm = $alarmClock->findNearestAlarm($currentTime);
assert($nearestAlarm === $alarm || $nearestAlarm === $alarm2);

echo "Тест пройден: Можно узнать ближайщую тревогу, даже если их несколько\n";

/* Можно добавить Тревогу в Будильник */
$alarm = new Alarm(13, 0);

$alarmClock = new AlarmClock();

$alarmClock->addAlarm($alarm);
assert($alarmClock->getAlarm(0) === $alarm);

echo "Тест пройден: Можно добавить тревогу в будильник\n";

/* Можно удалить Тревогу из Будильника */
$alarm = new Alarm(13, 0);

$alarmClock = new AlarmClock();
$alarmClock->addAlarm($alarm);

$alarmClock->deleteAlarm($alarm);
assert($alarmClock->getAlarm(0) === null);

echo "Тест пройден: Можно удалить тревогу из будильника\n";

/* Можно менять настройки Тревоги после добавления */

$alarm = new Alarm(13, 0);

$alarmClock = new AlarmClock();
$alarmClock->addAlarm($alarm);

$alarm2 = new Alarm(14, 0);
$alarm2->setType(Alarm::TYPE_NON_REPEATING);

$alarmClock->setAlarm(0, $alarm2);

assert($alarmClock->getAlarm(0) === $alarm2);

echo "Тест пройден: Можно менять настройки тревоги после добавления\n";