<?php

declare(strict_types=1);
/**
 * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
 * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
 *
 * @author Lukas Reschke <lukas@statuscode.ch>
 * @author Joas Schilling <coding@schilljs.com>
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program 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 (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

namespace OCA\Talk;

use OCA\Talk\Events\ModifyRoomEvent;
use OCA\Talk\Events\RoomEvent;
use OCA\Talk\Events\SignalingRoomPropertiesEvent;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\SelectHelper;
use OCA\Talk\Model\Session;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\RoomService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IDBConnection;
use OCP\Log\Audit\CriticalActionPerformedEvent;
use OCP\Security\IHasher;
use OCP\Server;

class Room {
	/**
	 * Regex that matches SIP incompatible rooms:
	 * 1. duplicate digit: …11…
	 * 2. leading zero: 0…
	 * 3. non-digit: …a…
	 */
	public const SIP_INCOMPATIBLE_REGEX = '/((\d)(?=\2+)|^0|\D)/';

	public const TYPE_UNKNOWN = -1;
	public const TYPE_ONE_TO_ONE = 1;
	public const TYPE_GROUP = 2;
	public const TYPE_PUBLIC = 3;
	public const TYPE_CHANGELOG = 4;

	/** @deprecated Use self::TYPE_UNKNOWN */
	public const UNKNOWN_CALL = self::TYPE_UNKNOWN;
	/** @deprecated Use self::TYPE_ONE_TO_ONE */
	public const ONE_TO_ONE_CALL = self::TYPE_ONE_TO_ONE;
	/** @deprecated Use self::TYPE_GROUP */
	public const GROUP_CALL = self::TYPE_GROUP;
	/** @deprecated Use self::TYPE_PUBLIC */
	public const PUBLIC_CALL = self::TYPE_PUBLIC;
	/** @deprecated Use self::TYPE_CHANGELOG */
	public const CHANGELOG_CONVERSATION = self::TYPE_CHANGELOG;

	public const READ_WRITE = 0;
	public const READ_ONLY = 1;

	/**
	 * Only visible when joined
	 */
	public const LISTABLE_NONE = 0;

	/**
	 * Searchable by all regular users and moderators, even when not joined, excluding users from the guest app
	 */
	public const LISTABLE_USERS = 1;

	/**
	 * Searchable by everyone, which includes guest users (from guest app), even when not joined
	 */
	public const LISTABLE_ALL = 2;

	public const START_CALL_EVERYONE = 0;
	public const START_CALL_USERS = 1;
	public const START_CALL_MODERATORS = 2;
	public const START_CALL_NOONE = 3;

	public const PARTICIPANT_REMOVED = 'remove';
	public const PARTICIPANT_LEFT = 'leave';

	public const EVENT_AFTER_ROOM_CREATE = self::class . '::createdRoom';
	public const EVENT_BEFORE_ROOM_DELETE = self::class . '::preDeleteRoom';
	public const EVENT_AFTER_ROOM_DELETE = self::class . '::postDeleteRoom';
	public const EVENT_BEFORE_NAME_SET = self::class . '::preSetName';
	public const EVENT_AFTER_NAME_SET = self::class . '::postSetName';
	public const EVENT_BEFORE_DESCRIPTION_SET = self::class . '::preSetDescription';
	public const EVENT_AFTER_DESCRIPTION_SET = self::class . '::postSetDescription';
	public const EVENT_BEFORE_PASSWORD_SET = self::class . '::preSetPassword';
	public const EVENT_AFTER_PASSWORD_SET = self::class . '::postSetPassword';
	public const EVENT_BEFORE_TYPE_SET = self::class . '::preSetType';
	public const EVENT_AFTER_TYPE_SET = self::class . '::postSetType';
	public const EVENT_BEFORE_READONLY_SET = self::class . '::preSetReadOnly';
	public const EVENT_AFTER_READONLY_SET = self::class . '::postSetReadOnly';
	public const EVENT_BEFORE_LISTABLE_SET = self::class . '::preSetListable';
	public const EVENT_AFTER_LISTABLE_SET = self::class . '::postSetListable';
	public const EVENT_BEFORE_LOBBY_STATE_SET = self::class . '::preSetLobbyState';
	public const EVENT_AFTER_LOBBY_STATE_SET = self::class . '::postSetLobbyState';
	public const EVENT_BEFORE_END_CALL_FOR_EVERYONE = self::class . '::preEndCallForEveryone';
	public const EVENT_AFTER_END_CALL_FOR_EVERYONE = self::class . '::postEndCallForEveryone';
	public const EVENT_BEFORE_SIP_ENABLED_SET = self::class . '::preSetSIPEnabled';
	public const EVENT_AFTER_SIP_ENABLED_SET = self::class . '::postSetSIPEnabled';
	public const EVENT_BEFORE_PERMISSIONS_SET = self::class . '::preSetPermissions';
	public const EVENT_AFTER_PERMISSIONS_SET = self::class . '::postSetPermissions';
	public const EVENT_BEFORE_USERS_ADD = self::class . '::preAddUsers';
	public const EVENT_AFTER_USERS_ADD = self::class . '::postAddUsers';
	public const EVENT_BEFORE_PARTICIPANT_TYPE_SET = self::class . '::preSetParticipantType';
	public const EVENT_AFTER_PARTICIPANT_TYPE_SET = self::class . '::postSetParticipantType';
	public const EVENT_BEFORE_PARTICIPANT_PERMISSIONS_SET = self::class . '::preSetParticipantPermissions';
	public const EVENT_AFTER_PARTICIPANT_PERMISSIONS_SET = self::class . '::postSetParticipantPermissions';
	public const EVENT_BEFORE_USER_REMOVE = self::class . '::preRemoveUser';
	public const EVENT_AFTER_USER_REMOVE = self::class . '::postRemoveUser';
	public const EVENT_BEFORE_PARTICIPANT_REMOVE = self::class . '::preRemoveBySession';
	public const EVENT_AFTER_PARTICIPANT_REMOVE = self::class . '::postRemoveBySession';
	public const EVENT_BEFORE_ROOM_CONNECT = self::class . '::preJoinRoom';
	public const EVENT_AFTER_ROOM_CONNECT = self::class . '::postJoinRoom';
	public const EVENT_BEFORE_ROOM_DISCONNECT = self::class . '::preUserDisconnectRoom';
	public const EVENT_AFTER_ROOM_DISCONNECT = self::class . '::postUserDisconnectRoom';
	public const EVENT_BEFORE_GUEST_CONNECT = self::class . '::preJoinRoomGuest';
	public const EVENT_AFTER_GUEST_CONNECT = self::class . '::postJoinRoomGuest';
	public const EVENT_PASSWORD_VERIFY = self::class . '::verifyPassword';
	public const EVENT_BEFORE_GUESTS_CLEAN = self::class . '::preCleanGuests';
	public const EVENT_AFTER_GUESTS_CLEAN = self::class . '::postCleanGuests';
	public const EVENT_BEFORE_SESSION_JOIN_CALL = self::class . '::preSessionJoinCall';
	public const EVENT_AFTER_SESSION_JOIN_CALL = self::class . '::postSessionJoinCall';
	public const EVENT_BEFORE_SESSION_UPDATE_CALL_FLAGS = self::class . '::preSessionUpdateCallFlags';
	public const EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS = self::class . '::postSessionUpdateCallFlags';
	public const EVENT_BEFORE_SESSION_LEAVE_CALL = self::class . '::preSessionLeaveCall';
	public const EVENT_AFTER_SESSION_LEAVE_CALL = self::class . '::postSessionLeaveCall';
	public const EVENT_BEFORE_SIGNALING_PROPERTIES = self::class . '::beforeSignalingProperties';
	public const EVENT_BEFORE_SET_MESSAGE_EXPIRATION = self::class . '::beforeSetMessageExpiration';
	public const EVENT_AFTER_SET_MESSAGE_EXPIRATION = self::class . '::afterSetMessageExpiration';

	public const DESCRIPTION_MAXIMUM_LENGTH = 500;

	private Manager $manager;
	private IDBConnection $db;
	private IEventDispatcher $dispatcher;
	private ITimeFactory $timeFactory;
	private IHasher $hasher;

	private int $id;
	private int $type;
	private int $readOnly;
	private int $listable;
	private int $messageExpiration;
	private int $lobbyState;
	private int $sipEnabled;
	private ?int $assignedSignalingServer;
	private ?\DateTime $lobbyTimer;
	private string $token;
	private string $name;
	private string $description;
	private string $password;
	private string $remoteServer;
	private string $remoteToken;
	private int $activeGuests;
	private int $defaultPermissions;
	private int $callPermissions;
	private int $callFlag;
	private ?\DateTime $activeSince;
	private ?\DateTime $lastActivity;
	private int $lastMessageId;
	private ?IComment $lastMessage;
	private string $objectType;
	private string $objectId;

	protected ?string $currentUser = null;
	protected ?Participant $participant = null;

	public function __construct(Manager $manager,
								IDBConnection $db,
								IEventDispatcher $dispatcher,
								ITimeFactory $timeFactory,
								IHasher $hasher,
								int $id,
								int $type,
								int $readOnly,
								int $listable,
								int $messageExpiration,
								int $lobbyState,
								int $sipEnabled,
								?int $assignedSignalingServer,
								string $token,
								string $name,
								string $description,
								string $password,
								string $remoteServer,
								string $remoteToken,
								int $activeGuests,
								int $defaultPermissions,
								int $callPermissions,
								int $callFlag,
								?\DateTime $activeSince,
								?\DateTime $lastActivity,
								int $lastMessageId,
								?IComment $lastMessage,
								?\DateTime $lobbyTimer,
								string $objectType,
								string $objectId) {
		$this->manager = $manager;
		$this->db = $db;
		$this->dispatcher = $dispatcher;
		$this->timeFactory = $timeFactory;
		$this->hasher = $hasher;
		$this->id = $id;
		$this->type = $type;
		$this->readOnly = $readOnly;
		$this->listable = $listable;
		$this->messageExpiration = $messageExpiration;
		$this->lobbyState = $lobbyState;
		$this->sipEnabled = $sipEnabled;
		$this->assignedSignalingServer = $assignedSignalingServer;
		$this->token = $token;
		$this->name = $name;
		$this->description = $description;
		$this->password = $password;
		$this->remoteServer = $remoteServer;
		$this->remoteToken = $remoteToken;
		$this->activeGuests = $activeGuests;
		$this->defaultPermissions = $defaultPermissions;
		$this->callPermissions = $callPermissions;
		$this->callFlag = $callFlag;
		$this->activeSince = $activeSince;
		$this->lastActivity = $lastActivity;
		$this->lastMessageId = $lastMessageId;
		$this->lastMessage = $lastMessage;
		$this->lobbyTimer = $lobbyTimer;
		$this->objectType = $objectType;
		$this->objectId = $objectId;
	}

	public function getId(): int {
		return $this->id;
	}

	public function getType(): int {
		return $this->type;
	}

	public function setType(int $type): void {
		$this->type = $type;
	}

	public function getReadOnly(): int {
		return $this->readOnly;
	}

	/**
	 * @param int $readOnly Currently it is only allowed to change between
	 * 						`self::READ_ONLY` and `self::READ_WRITE`
	 * 						Also it's only allowed on rooms of type
	 * 						`self::TYPE_GROUP` and `self::TYPE_PUBLIC`
	 */
	public function setReadOnly(int $readOnly): void {
		$this->readOnly = $readOnly;
	}

	public function getListable(): int {
		return $this->listable;
	}

	/**
	 * @param int $newState New listable scope from self::LISTABLE_*
	 * 						Also it's only allowed on rooms of type
	 * 						`self::TYPE_GROUP` and `self::TYPE_PUBLIC`
	 */
	public function setListable(int $newState): void {
		$this->listable = $newState;
	}

	public function getMessageExpiration(): int {
		return $this->messageExpiration;
	}

	public function setMessageExpiration(int $messageExpiration): void {
		$this->messageExpiration = $messageExpiration;
	}

	public function getLobbyState(bool $validateTime = true): int {
		if ($validateTime) {
			$this->validateTimer();
		}
		return $this->lobbyState;
	}

	public function setLobbyState(int $lobbyState): void {
		$this->lobbyState = $lobbyState;
	}

	public function getLobbyTimer(bool $validateTime = true): ?\DateTime {
		if ($validateTime) {
			$this->validateTimer();
		}
		return $this->lobbyTimer;
	}

	public function setLobbyTimer(?\DateTime $lobbyTimer): void {
		$this->lobbyTimer = $lobbyTimer;
	}

	protected function validateTimer(): void {
		if ($this->lobbyTimer !== null && $this->lobbyTimer < $this->timeFactory->getDateTime()) {
			/** @var RoomService $roomService */
			$roomService = Server::get(RoomService::class);
			$roomService->setLobby($this, Webinary::LOBBY_NONE, null, true);
		}
	}

	public function getSIPEnabled(): int {
		return $this->sipEnabled;
	}

	public function setSIPEnabled(int $sipEnabled): void {
		$this->sipEnabled = $sipEnabled;
	}

	public function getAssignedSignalingServer(): ?int {
		return $this->assignedSignalingServer;
	}

	public function setAssignedSignalingServer(?int $assignedSignalingServer): void {
		$this->assignedSignalingServer = $assignedSignalingServer;
	}

	public function getToken(): string {
		return $this->token;
	}

	public function getName(): string {
		if ($this->type === self::TYPE_ONE_TO_ONE) {
			if ($this->name === '') {
				// TODO use DI
				$participantService = Server::get(ParticipantService::class);
				// Fill the room name with the participants for 1-to-1 conversations
				$users = $participantService->getParticipantUserIds($this);
				sort($users);
				$this->setName(json_encode($users), '');
			} elseif (strpos($this->name, '["') !== 0) {
				// TODO use DI
				$participantService = Server::get(ParticipantService::class);
				// Not the json array, but the old fallback when someone left
				$users = $participantService->getParticipantUserIds($this);
				if (count($users) !== 2) {
					$users[] = $this->name;
				}
				sort($users);
				$this->setName(json_encode($users), '');
			}
		}
		return $this->name;
	}

	public function getSecondParticipant(string $userId): string {
		if ($this->getType() !== self::TYPE_ONE_TO_ONE) {
			throw new \InvalidArgumentException('Not a one-to-one room');
		}
		$participants = json_decode($this->getName(), true);

		foreach ($participants as $uid) {
			if ($uid !== $userId) {
				return $uid;
			}
		}

		return $this->getName();
	}

	public function getDisplayName(string $userId): string {
		return $this->manager->resolveRoomDisplayName($this, $userId);
	}

	public function getDescription(): string {
		return $this->description;
	}

	public function setDescription(string $description): void {
		$this->description = $description;
	}

	/**
	 * @deprecated Use ParticipantService::getGuestCount() instead
	 * @return int
	 */
	public function getActiveGuests(): int {
		return $this->activeGuests;
	}

	public function getDefaultPermissions(): int {
		return $this->defaultPermissions;
	}

	public function setDefaultPermissions(int $defaultPermissions): void {
		$this->defaultPermissions = $defaultPermissions;
	}

	public function getCallPermissions(): int {
		return $this->callPermissions;
	}

	public function setCallPermissions(int $callPermissions): void {
		$this->callPermissions = $callPermissions;
	}

	public function getCallFlag(): int {
		return $this->callFlag;
	}

	public function getActiveSince(): ?\DateTime {
		return $this->activeSince;
	}

	public function getLastActivity(): ?\DateTime {
		return $this->lastActivity;
	}

	public function getLastMessage(): ?IComment {
		if ($this->lastMessageId && $this->lastMessage === null) {
			$this->lastMessage = $this->manager->loadLastCommentInfo($this->lastMessageId);
			if ($this->lastMessage === null) {
				$this->lastMessageId = 0;
			}
		}

		return $this->lastMessage;
	}

	public function getObjectType(): string {
		return $this->objectType;
	}

	public function getObjectId(): string {
		return $this->objectId;
	}

	public function hasPassword(): bool {
		return $this->password !== '';
	}

	public function getPassword(): string {
		return $this->password;
	}

	public function setPassword(string $password): void {
		$this->password = $password;
	}

	public function getRemoteServer(): string {
		return $this->remoteServer;
	}

	public function getRemoteToken(): string {
		return $this->remoteToken;
	}

	public function isFederatedRemoteRoom(): bool {
		return $this->remoteServer !== '';
	}

	public function setParticipant(?string $userId, Participant $participant): void {
		$this->currentUser = $userId;
		$this->participant = $participant;
	}

	/**
	 * Return the room properties to send to the signaling server.
	 *
	 * @param string $userId
	 * @param bool $roomModified
	 * @return array
	 */
	public function getPropertiesForSignaling(string $userId, bool $roomModified = true): array {
		$properties = [
			'name' => $this->getDisplayName($userId),
			'type' => $this->getType(),
			'lobby-state' => $this->getLobbyState(),
			'lobby-timer' => $this->getLobbyTimer(),
			'read-only' => $this->getReadOnly(),
			'listable' => $this->getListable(),
			'active-since' => $this->getActiveSince(),
			'sip-enabled' => $this->getSIPEnabled(),
		];

		if ($roomModified) {
			$properties['description'] = $this->getDescription();
		} else {
			$properties['participant-list'] = 'refresh';
		}

		$event = new SignalingRoomPropertiesEvent($this, $userId, $properties);
		$this->dispatcher->dispatch(self::EVENT_BEFORE_SIGNALING_PROPERTIES, $event);
		return $event->getProperties();
	}

	/**
	 * @param string|null $userId
	 * @param string|null|false $sessionId Set to false if you don't want to load a session (and save resources),
	 *                                     string to try loading a specific session
	 *                                     null to try loading "any"
	 * @return Participant
	 * @throws ParticipantNotFoundException When the user is not a participant
	 */
	public function getParticipant(?string $userId, $sessionId = null): Participant {
		if (!is_string($userId) || $userId === '') {
			throw new ParticipantNotFoundException('Not a user');
		}

		if ($this->currentUser === $userId && $this->participant instanceof Participant) {
			if (!$sessionId
				|| ($this->participant->getSession() instanceof Session
					&& $this->participant->getSession()->getSessionId() === $sessionId)) {
				return $this->participant;
			}
		}

		$query = $this->db->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$query->from('talk_attendees', 'a')
			->where($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)))
			->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)))
			->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId())))
			->setMaxResults(1);

		if ($sessionId !== false) {
			if ($sessionId !== null) {
				$helper->selectSessionsTable($query);
				$query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX(
					$query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)),
					$query->expr()->eq('a.id', 's.attendee_id')
				));
			} else {
				$helper->selectSessionsTable($query); // FIXME PROBLEM
				$query->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id'));
			}
		}

		$result = $query->executeQuery();
		$row = $result->fetch();
		$result->closeCursor();

		if ($row === false) {
			throw new ParticipantNotFoundException('User is not a participant');
		}

		if ($this->currentUser === $userId) {
			$this->participant = $this->manager->createParticipantObject($this, $row);
			return $this->participant;
		}

		return $this->manager->createParticipantObject($this, $row);
	}

	/**
	 * @param string|null $sessionId
	 * @return Participant
	 * @throws ParticipantNotFoundException When the user is not a participant
	 */
	public function getParticipantBySession(?string $sessionId): Participant {
		if (!is_string($sessionId) || $sessionId === '' || $sessionId === '0') {
			throw new ParticipantNotFoundException('Not a user');
		}

		$query = $this->db->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$helper->selectSessionsTable($query);
		$query->from('talk_sessions', 's')
			->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('a.id', 's.attendee_id'))
			->where($query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)))
			->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId())))
			->setMaxResults(1);
		$result = $query->executeQuery();
		$row = $result->fetch();
		$result->closeCursor();

		if ($row === false) {
			throw new ParticipantNotFoundException('User is not a participant');
		}

		return $this->manager->createParticipantObject($this, $row);
	}

	/**
	 * @param string $pin
	 * @return Participant
	 * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned)
	 */
	public function getParticipantByPin(string $pin): Participant {
		$query = $this->db->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$query->from('talk_attendees', 'a')
			->where($query->expr()->eq('a.pin', $query->createNamedParameter($pin)))
			->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId())))
			->setMaxResults(1);
		$result = $query->executeQuery();
		$row = $result->fetch();
		$result->closeCursor();

		if ($row === false) {
			throw new ParticipantNotFoundException('User is not a participant');
		}

		return $this->manager->createParticipantObject($this, $row);
	}

	/**
	 * @param int $attendeeId
	 * @return Participant
	 * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned)
	 */
	public function getParticipantByAttendeeId(int $attendeeId): Participant {
		$query = $this->db->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$query->from('talk_attendees', 'a')
			->where($query->expr()->eq('a.id', $query->createNamedParameter($attendeeId, IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId())))
			->setMaxResults(1);

		$result = $query->executeQuery();
		$row = $result->fetch();
		$result->closeCursor();

		if ($row === false) {
			throw new ParticipantNotFoundException('User is not a participant');
		}

		return $this->manager->createParticipantObject($this, $row);
	}

	/**
	 * @param string $actorType
	 * @param string $actorId
	 * @return Participant
	 * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned)
	 */
	public function getParticipantByActor(string $actorType, string $actorId): Participant {
		if ($actorType === Attendee::ACTOR_USERS) {
			return $this->getParticipant($actorId, false);
		}

		$query = $this->db->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$query->from('talk_attendees', 'a')
			->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType)))
			->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId)))
			->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId())))
			->setMaxResults(1);

		$result = $query->executeQuery();
		$row = $result->fetch();
		$result->closeCursor();

		if ($row === false) {
			throw new ParticipantNotFoundException('User is not a participant');
		}

		return $this->manager->createParticipantObject($this, $row);
	}

	public function deleteRoom(): void {
		$event = new RoomEvent($this);
		$this->dispatcher->dispatch(self::EVENT_BEFORE_ROOM_DELETE, $event);
		$delete = $this->db->getQueryBuilder();

		// Delete attendees
		$delete->delete('talk_attendees')
			->where($delete->expr()->eq('room_id', $delete->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
		$delete->executeStatement();

		// Delete room
		$delete->delete('talk_rooms')
			->where($delete->expr()->eq('id', $delete->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
		$delete->executeStatement();

		$this->dispatcher->dispatch(self::EVENT_AFTER_ROOM_DELETE, $event);
		if (class_exists(CriticalActionPerformedEvent::class)) {
			$this->dispatcher->dispatchTyped(new CriticalActionPerformedEvent(
				'Conversation "%s" deleted',
				['name' => $this->getName()],
			));
		}
	}

	/**
	 * @param string $newName Currently it is only allowed to rename: self::TYPE_GROUP, self::TYPE_PUBLIC
	 * @param string|null $oldName
	 * @return bool True when the change was valid, false otherwise
	 */
	public function setName(string $newName, ?string $oldName = null): bool {
		$oldName = $oldName !== null ? $oldName : $this->getName();
		if ($newName === $oldName) {
			return false;
		}

		$event = new ModifyRoomEvent($this, 'name', $newName, $oldName);
		$this->dispatcher->dispatch(self::EVENT_BEFORE_NAME_SET, $event);

		$update = $this->db->getQueryBuilder();
		$update->update('talk_rooms')
			->set('name', $update->createNamedParameter($newName))
			->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
		$update->executeStatement();
		$this->name = $newName;

		$this->dispatcher->dispatch(self::EVENT_AFTER_NAME_SET, $event);

		return true;
	}

	/**
	 * @param \DateTime $now
	 * @return bool
	 */
	public function setLastActivity(\DateTime $now): bool {
		$update = $this->db->getQueryBuilder();
		$update->update('talk_rooms')
			->set('last_activity', $update->createNamedParameter($now, 'datetime'))
			->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
		$update->executeStatement();

		$this->lastActivity = $now;

		return true;
	}

	/**
	 * @param \DateTime $since
	 * @param int $callFlag
	 * @param bool $isGuest
	 * @return bool
	 */
	public function setActiveSince(\DateTime $since, int $callFlag, bool $isGuest): bool {
		if ($isGuest && $this->getType() === self::TYPE_PUBLIC) {
			$update = $this->db->getQueryBuilder();
			$update->update('talk_rooms')
				->set('active_guests', $update->createFunction($update->getColumnName('active_guests') . ' + 1'))
				->set(
					'call_flag',
					$update->expr()->bitwiseOr('call_flag', $callFlag)
				)
				->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
			$update->executeStatement();

			$this->activeGuests++;
		} elseif (!$isGuest) {
			$update = $this->db->getQueryBuilder();
			$update->update('talk_rooms')
				->set(
					'call_flag',
					$update->expr()->bitwiseOr('call_flag', $callFlag)
				)
				->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
			$update->executeStatement();
		}

		if ($this->activeSince instanceof \DateTime) {
			return false;
		}

		$update = $this->db->getQueryBuilder();
		$update->update('talk_rooms')
			->set('active_since', $update->createNamedParameter($since, IQueryBuilder::PARAM_DATE))
			->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($update->expr()->isNull('active_since'));
		$update->executeStatement();

		$this->activeSince = $since;

		return true;
	}

	public function setLastMessage(IComment $message): void {
		$update = $this->db->getQueryBuilder();
		$update->update('talk_rooms')
			->set('last_message', $update->createNamedParameter((int) $message->getId()))
			->set('last_activity', $update->createNamedParameter($message->getCreationDateTime(), 'datetime'))
			->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
		$update->executeStatement();

		$this->lastMessage = $message;
		$this->lastMessageId = (int) $message->getId();
		$this->lastActivity = $message->getCreationDateTime();
	}

	public function resetActiveSince(): bool {
		$update = $this->db->getQueryBuilder();
		$update->update('talk_rooms')
			->set('active_guests', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT))
			->set('active_since', $update->createNamedParameter(null, IQueryBuilder::PARAM_DATE))
			->set('call_flag', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT))
			->set('call_permissions', $update->createNamedParameter(Attendee::PERMISSIONS_DEFAULT, IQueryBuilder::PARAM_INT))
			->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($update->expr()->isNotNull('active_since'));

		$this->activeGuests = 0;
		$this->activeSince = null;

		return (bool) $update->executeStatement();
	}
}
