<?php

/**
 * Model class for manipulating user alerts.
 *
 * @author kier
 */
class XenForo_Model_Alert extends XenForo_Model
{
	/**
	 * Fetch alerts viewed in the last options:alertsPopupExpiryHours hours
	 *
	 * @var string
	 */
	const FETCH_MODE_POPUP = 'fetchPopupItems';

	/**
	 * Fetch alerts viewed in the last options:alertExpiryDays days
	 *
	 * @var string
	 */
	const FETCH_MODE_RECENT = 'fetchRecent';

	/**
	 * Fetch alerts regardless of their view_date
	 *
	 * @var string
	 */
	const FETCH_MODE_ALL = 'fetchAll';

	/**
	 * Prevent alerts from being marked as read (debug option);
	 *
	 * @var boolean
	 */
	const PREVENT_MARK_READ = false;

	/**
	 * Array to store alert handler classes
	 *
	 * @var array
	 */
	protected $_handlerCache = array();

	/**
	 * Fetches a single alert using its ID
	 *
	 * @param integer $alertId
	 *
	 * @return array|false
	 */
	public function getAlertById($alertId)
	{
		return $this->_getDb()->fetchRow('
			SELECT *
			FROM xf_user_alert
			WHERE alert_id = ?
		', $alertId);
	}

	/**
	 * Returns alert data for the specified user.
	 *
	 * @param integer $userId
	 * @param string $fetchMode Use one of the FETCH_x constants
	 * @param array $fetchOptions (supports page, perpage)
	 * @param array|null $viewingUser Information about the viewing user (keys: user_id, permission_combination_id, permissions) or null for visitor
	 *
	 * @return array
	 */
	public function getAlertsForUser($userId, $fetchMode, array $fetchOptions = array(), array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);

		$alerts = $this->_getAlertsFromSource($userId, $fetchMode, $fetchOptions);

		$alerts = $this->_getContentForAlerts($alerts, $userId, $viewingUser);
		$alerts = $this->_getViewableAlerts($alerts, $viewingUser);

		$alerts = $this->prepareAlerts($alerts, $viewingUser);

		return array(
			'alerts' => $alerts,
			'alertHandlers' => $this->_handlerCache
		);
	}

	/**
	 * Returns true if the alert passed in is 'unread' - ie: has view_date == 0
	 *
	 * @param array $alert
	 *
	 * @return boolean
	 */
	protected function _isUnread(array $alert)
	{
		return ($alert['view_date'] === 0);
	}

	/**
	 * Returns true if the alert passed in is 'current' for the given date-cut-off.
	 *
	 * Current means unread, or viewed within the specified date cut off.
	 *
	 * @param array $alert
	 * @param integer $dateCut
	 *
	 * @return boolean
	 */
	protected function _isCurrent(array $alert, $dateCut = null)
	{
		if ($this->_isUnread($alert))
		{
			return true;
		}
		else
		{
			if ($dateCut === null)
			{
				$dateCut = $this->_getFetchModeDateCut(self::FETCH_MODE_RECENT);
			}

			if ($alert['view_date'] > XenForo_Application::$time - $dateCut)
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Translates the FETCH_MODE_x constants from this class into a cut-off timestamp
	 *
	 * @param string self::FETCH_MODE_x
	 *
	 * @return integer Unix timestamp
	 */
	protected function _getFetchModeDateCut($fetchMode)
	{
		$timeNow = XenForo_Application::$time;
		$options = XenForo_Application::get('options');

		switch ($fetchMode)
		{
			case self::FETCH_MODE_ALL:
				return 0;

			case self::FETCH_MODE_POPUP:
				return $timeNow - $options->alertsPopupExpiryHours * 3600;

			case self::FETCH_MODE_RECENT:
			default:
				return $timeNow - $options->alertExpiryDays * 86400;
		}
	}

	/**
	 * Fetches raw alert records for the specified user.
	 *
	 * Includes any unviewed alerts plus any alerts that
	 * were viewed within the last $dateCut seconds.
	 *
	 * @param integer $userId User to whom the alerts belong
	 * @param string $fetchMode Fetch viewed alerts read more recently than this timestamp
	 * @param array $fetchOptions (supports page and perpage)
	 */
	protected function _getAlertsFromSource($userId, $fetchMode, array $fetchOptions = array())
	{
		if ($fetchMode == self::FETCH_MODE_POPUP)
		{
			$fetchOptions['page'] = 0;
			$fetchOptions['perPage'] = 25;
		}

		$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);

		return $this->fetchAllKeyed($this->limitQueryResults(
			'
				SELECT
					alert.*,
					user.gender, user.avatar_date, user.gravatar,
					IF (user.user_id IS NULL, alert.username, user.username) AS username
				FROM xf_user_alert AS alert
				LEFT JOIN xf_user AS user ON
					(user.user_id = alert.user_id)
				WHERE alert.alerted_user_id = ?
					AND (alert.view_date = 0 OR alert.view_date > ?)
				ORDER BY event_date DESC
			', $limitOptions['limit'], $limitOptions['offset']
		), 'alert_id', array($userId, $this->_getFetchModeDateCut($fetchMode)));
	}

	public function countAlertsForUser($userId)
	{
		return $this->_getDb()->fetchOne('
			SELECT COUNT(*)
			FROM xf_user_alert
			WHERE alerted_user_id = ?
				AND (view_date = 0 OR view_date > ?)
		', array($userId, $this->_getFetchModeDateCut(self::FETCH_MODE_RECENT)));
	}

	/**
	 * Fetches content data for alerts
	 *
	 * @param array $data Raw alert data
	 * @param integer $userId The user ID the alerts are for
	 * @param array $viewingUser Information about the viewing user (keys: user_id, permission_combination_id, permissions)
	 *
	 * @return array
	 */
	protected function _getContentForAlerts(array $data, $userId, array $viewingUser)
	{
		// group all content ids of each content type...
		$fetchQueue = array();
		foreach ($data AS $id => $item)
		{
			$fetchQueue[$item['content_type']][$item['alert_id']] = $item['content_id'];
		}

		// fetch content for all items of each content type in one go...
		$fetchData = array();
		foreach ($fetchQueue AS $contentType => $contentIds)
		{
			$handler = $this->_getAlertHandlerFromCache($contentType);
			if (!$handler)
			{
				continue;
			}

			$fetchData[$contentType] = $handler->getContentByIds(
				$contentIds, $this, $userId, $viewingUser
			);
		}

		// attach resulting content to each alert
		foreach ($data AS $id => $item)
		{
			if (!isset($fetchData[$item['content_type']][$item['content_id']]))
			{
				// For whatever reason, there was no related content found for this alert,
				// therefore remove it from this user's alerts
				unset($data[$id]);
				continue;
			}

			$data[$id]['content'] = $fetchData[$item['content_type']][$item['content_id']];
		}

		return $data;
	}

	/**
	 * Filters out unviewable alerts and returns only those the user can view.
	 *
	 * @param array $alerts
	 * @param array $viewingUser Information about the viewing user (keys: user_id, permission_combination_id, permissions)
	 *
	 * @return array Filtered items
	 */
	protected function _getViewableAlerts(array $alerts, array $viewingUser)
	{
		foreach ($alerts AS $key => $alert)
		{
			$handler = $this->_getAlertHandlerFromCache($alert['content_type']);
			if (!$handler || !$handler->canViewAlert($alert, $alert['content'], $viewingUser))
			{
				unset($alerts[$key]);
			}
		}

		return $alerts;
	}

	/**
	 * Runs prepareAlert on an array of items
	 *
	 * @param array $alerts
	 * @param array $viewingUser Information about the viewing user (keys: user_id, permission_combination_id, permissions)
	 *
	 * @return array
	 */
	public function prepareAlerts(array $alerts, array $viewingUser)
	{
		foreach ($alerts AS $id => $item)
		{
			$handlerClass = $this->_getAlertHandlerForContent($item['content_type']);
			$alerts[$id] = $this->prepareAlert($item, $handlerClass, $viewingUser);
		}

		return $alerts;
	}

	/**
	 * Wraps around the prepareX functions in the handler class for each content type.
	 * Also does basic setup, moving user info to a sub-array.
	 *
	 * @param array $item
	 * @param string $handlerClassName Name of alert handler class for this item
	 * @param array $viewingUser Information about the viewing user (keys: user_id, permission_combination_id, permissions)
	 *
	 * @return array
	 */
	public function prepareAlert(array $item, $handlerClassName, array $viewingUser)
	{
		$item['user'] = XenForo_Application::arrayFilterKeys($item, array(
			'user_id',
			'username',
			'gender',
			'gravatar',
			'avatar_date',
		));

		unset($item['user_id'], $item['username'], $item['gender'], $item['gravatar'], $item['avatar_date']);

		$item['new'] = ($item['view_date'] === 0 || $item['view_date'] > XenForo_Application::$time - 600);
		$item['unviewed'] = $this->_isUnread($item);

		$handler = $this->_getAlertHandlerFromCache($item['content_type']);
		if ($handler)
		{
			$item = $handler->prepareAlert($item, $viewingUser);
		}

		return $item;
	}

	/**
	 * Marks all of a user's alerts as read.
	 *
	 * @param integer $userId
	 * @param integer|null $time
	 */
	public function markAllAlertsReadForUser($userId, $time = null)
	{
		if (self::PREVENT_MARK_READ)
		{
			return;
		}

		if ($time === null)
		{
			$time = XenForo_Application::$time;
		}

		$db = $this->_getDb();

		$condition = 'alerted_user_id = ' . $db->quote($userId) . ' AND view_date = 0';
		$db->update('xf_user_alert', array('view_date' => $time), $condition);

		$this->resetUnreadAlertsCounter($userId);
	}

	/**
	 * Resets the unviewed alerts counter to 0 for the specified user.
	 *
	 * @param integer $userId
	 */
	public function resetUnreadAlertsCounter($userId)
	{
		if (!self::PREVENT_MARK_READ)
		{
			$db = $this->_getDb();
			$db->update('xf_user', array('alerts_unread' => 0), 'user_id = ' . $db->quote($userId));

			$visitor = XenForo_Visitor::getInstance();
			if ($userId == $visitor['user_id'])
			{
				$visitor['alerts_unread'] = 0;
			}
		}
	}

	/**
	 * Deletes old viewed alerts.
	 *
	 * @param integer|null $dateCut Cut off date; if not specified, defaults to expiry setting
	 */
	public function deleteOldReadAlerts($dateCut = null)
	{
		if ($dateCut === null)
		{
			$expiryTime = XenForo_Application::get('options')->alertExpiryDays * 86400;
			$dateCut = XenForo_Application::$time - $expiryTime;
		}

		$db = $this->_getDb();
		$db->delete('xf_user_alert', 'view_date > 0 AND view_date < '. $db->quote($dateCut));
	}

	/**
	 * Deletes old unviewed alerts. The cut-off here is much longer than viewed ones.
	 *
	 * @param integer|null $dateCut Cut off date; if not specified, defaults to 30 days
	 */
	public function deleteOldUnreadAlerts($dateCut = null)
	{
		if ($dateCut === null)
		{
			$dateCut = XenForo_Application::$time - 30 * 86400;
		}

		$db = $this->_getDb();
		$db->delete('xf_user_alert', 'view_date = 0 AND event_date < '. $db->quote($dateCut));
	}

	/**
	 * Send a user alert
	 *
	 * @param integer $alertUserId
	 * @param integer $userId
	 * @param string $username
	 * @param string $contentType
	 * @param integer $contentId
	 * @param string $action
	 * @param array $extraData
	 */
	public static function alert($alertUserId, $userId, $username, $contentType, $contentId, $action, array $extraData = null)
	{
		XenForo_Model::create(__CLASS__)->alertUser(
			$alertUserId,
			$userId, $username,
			$contentType, $contentId,
			$action, $extraData
		);
	}

	/**
	 * Send a user alert
	 *
	 * @param integer $alertUserId
	 * @param integer $userId
	 * @param string $username
	 * @param string $contentType
	 * @param integer $contentId
	 * @param string $action
	 * @param array $extraData
	 */
	public function alertUser($alertUserId, $userId, $username, $contentType, $contentId, $action, array $extraData = null)
	{
		$dw = XenForo_DataWriter::create('XenForo_DataWriter_Alert');

		$dw->set('alerted_user_id', $alertUserId);
		$dw->set('user_id', $userId);
		$dw->set('username', $username);
		$dw->set('content_type', $contentType);
		$dw->set('content_id', $contentId);
		$dw->set('action', $action);
		$dw->set('extra_data', $extraData);

		$dw->save();
	}

	/**
	 * Deletes the matching alerts.
	 *
	 * @param string $contentType
	 * @param integer|array $contentId
	 * @param integer|null $userId Ignored if null
	 * @param string|null $action Ignored if null
	 */
	public function deleteAlerts($contentType, $contentId, $userId = null, $action = null)
	{
		$db = $this->_getDb();


		$conditions = array();

		if (is_array($contentId))
		{
			if (!$contentId)
			{
				return;
			}

			$conditions[] = 'content_type = ' . $db->quote($contentType) . ' AND content_id IN (' . $db->quote($contentId) . ')';
		}
		else
		{
			$conditions[] = 'content_type = ' . $db->quote($contentType) . ' AND content_id = ' . $db->quote($contentId);
		}

		if ($userId !== null)
		{
			$conditions[] = 'user_id = ' . $db->quote($userId);
		}
		if ($action !== null)
		{
			$conditions[] = 'action = ' . $db->quote($action);
		}

		$alerts = $db->fetchAll('
			SELECT *
			FROM xf_user_alert
			WHERE (' . implode(') AND (', $conditions) . ')
		');

		XenForo_Db::beginTransaction($db);

		foreach ($alerts AS $alert)
		{
			$dw = XenForo_DataWriter::create('XenForo_DataWriter_Alert');
			$dw->setExistingData($alert, true);
			$dw->delete();
		}

		XenForo_Db::commit($db);
	}

	/**
	 * Returns false if the specified user has opted not to receive the specified alert type
	 *
	 * @param array $user
	 * @param string $contentType
	 * @param string $action
	 */
	public static function userReceivesAlert(array $user, $contentType, $action)
	{
		$optOuts = XenForo_Model::create(__CLASS__)->getAlertOptOuts($user);

		return (empty($optOuts["{$contentType}_{$action}"]));
	}

	/**
	 * Fetches an array containing the names of alert types the specified user
	 * has opted not to receive.
	 *
	 * @param array $user - defaults to visitor if null
	 * @param boolean If true, the $user array must contain the alert_optout key from the user_option table. If false, queries for the data.
	 *
	 * @return array [ a => true, b => true, c => true ]
	 */
	public function getAlertOptOuts(array $user = null, $useDenormalized = true)
	{
		if ($user === null)
		{
			$user = XenForo_Visitor::getInstance();
		}

		if (!$user['user_id'])
		{
			return array();
		}
		else if ($useDenormalized && isset($user['alert_optout']))
		{
			$optOuts = preg_split('/\s*,\s*/', $user['alert_optout'], -1, PREG_SPLIT_NO_EMPTY);
		}
		else
		{
			$optOuts = $this->_getDb()->fetchCol('
				SELECT alert
				FROM xf_user_alert_optout
				WHERE user_id = ?
			', $user['user_id']);
		}

		return array_fill_keys($optOuts, true);
	}

	/**
	 * Fetches an instance of the user model
	 *
	 * @return XenForo_Model_User
	 */
	protected function _getUserModel()
	{
		return $this->getModelFromCache('XenForo_Model_User');
	}

	protected function _getAlertHandlerForContent($contentType)
	{
		return $this->getContentTypeField($contentType, 'alert_handler_class');
	}

	/**
	 * Fetches an instance of the specified alert handler
	 *
	 * @param string $contentType
	 *
	 * @return XenForo_AlertHandler_Abstract|boolean
	 */
	protected function _getAlertHandlerFromCache($contentType)
	{
		$class = $this->_getAlertHandlerForContent($contentType);
		if (!$class || !class_exists($class))
		{
			return false;
		}

		if (!isset($this->_handlerCache[$contentType]))
		{
			$this->_handlerCache[$contentType] = XenForo_AlertHandler_Abstract::create($class);
		}

		return $this->_handlerCache[$contentType];
	}
}