<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/

namespace phpbb\notification\type;

use phpbb\auth\auth;
use phpbb\avatar\helper as avatar_helper;
use phpbb\controller\helper;
use phpbb\db\driver\driver_interface;
use phpbb\language\language;
use phpbb\user;

/**
* Base notifications class
*/
abstract class base implements \phpbb\notification\type\type_interface
{
	/** @var \phpbb\notification\manager */
	protected $notification_manager;

	/** @var avatar_helper */
	protected $avatar_helper;

	/** @var helper|null */
	protected $controller_helper;

	/** @var driver_interface */
	protected $db;

	/** @var language */
	protected $language;

	/** @var user */
	protected $user;

	/** @var auth */
	protected $auth;

	/** @var string */
	protected $phpbb_root_path;

	/** @var string */
	protected $php_ext;

	/** @var string */
	protected $user_notifications_table;

	/**
	* Notification option data (for outputting to the user)
	*
	* @var bool|array False if the service should use its default data
	* 					Array of data (including keys 'id', 'lang', and 'group')
	*/
	public static $notification_option = false;

	/**
	* The notification_type_id, set upon creation of the class
	* This is the notification_type_id from the notification_types table
	*
	* @var int
	*/
	protected $notification_type_id;

	/**
	* Identification data
	* notification_type_id	- ID of the item type (auto generated, from notification types table)
	* item_id				- ID of the item (e.g. post_id, msg_id)
	* item_parent_id		- Parent item id (ex: for topic => forum_id, for post => topic_id, etc)
	* user_id
	* notification_read
	* notification_time
	* notification_data (special serialized field that each notification type can use to store stuff)
	*
	* @var array $data Notification row from the database
	* 		This must be private, all interaction should use __get(), __set(), get_data(), set_data()
	*/
	private $data = array();

	/**
	 * Notification Type Base Constructor
	 *
	 * @param avatar_helper		$avatar_helper
	 * @param helper			$controller_helper
	 * @param driver_interface	$db
	 * @param language			$language
	 * @param user				$user
	 * @param auth				$auth
	 * @param string			$phpbb_root_path
	 * @param string			$php_ext
	 * @param string			$user_notifications_table
	 */
	public function __construct(avatar_helper $avatar_helper, helper $controller_helper, driver_interface $db, language $language,
								user $user, auth $auth, string $phpbb_root_path, string $php_ext, string $user_notifications_table)
	{
		$this->avatar_helper = $avatar_helper;
		$this->controller_helper = $controller_helper;
		$this->db = $db;
		$this->language = $language;
		$this->user = $user;
		$this->auth = $auth;

		$this->phpbb_root_path = $phpbb_root_path;
		$this->php_ext = $php_ext;

		$this->user_notifications_table = $user_notifications_table;
	}

	/**
	* Set notification manager (required)
	*
	* @param \phpbb\notification\manager $notification_manager
	*/
	public function set_notification_manager(\phpbb\notification\manager $notification_manager)
	{
		$this->notification_manager = $notification_manager;

		$this->notification_type_id = $this->notification_manager->get_notification_type_id($this->get_type());
	}

	/**
	* Set initial data from the database
	*
	* @param array $data Row directly from the database
	*/
	public function set_initial_data($data = array())
	{
		// The row from the database (unless this is a new notification we're going to add)
		$this->data = $data;
		$this->data['notification_data'] = !empty($this->data['notification_data']) ? unserialize($this->data['notification_data']) : [];
	}

	/**
	* Magic method to get data from this notification
	*
	* @param mixed $name
	* @return mixed
	*/
	public function __get($name)
	{
		return $this->data[$name] ?? null;
	}

	/**
	* Magic method to set data on this notification
	*
	* @param mixed $name
	* @param mixed $value
	*
	* @return void
	*/
	public function __set($name, $value)
	{
		$this->data[$name] = $value;
	}

	/**
	 * Magic method check if a variable is defined and is not null
	 *
	 * @param mixed $name
	 *
	 * @return bool
	 */
	public function __isset($name)
	{
		return isset($this->data[$name]);
	}

	/**
	* Magic method to get a string of this notification
	*
	* Primarily for testing
	*
	* @return mixed
	*/
	public function __toString()
	{
		return (!empty($this->data)) ? var_export($this->data, true) : $this->get_type();
	}

	/**
	* Get special data (only important for the classes that extend this)
	*
	* @param string|false $name Name of the variable to get, false if all data should be returned
	* @return mixed
	*/
	protected function get_data($name)
	{
		return ($name === false) ? $this->data['notification_data'] : ($this->data['notification_data'][$name] ?? null);
	}

	/**
	* Set special data (only important for the classes that extend this)
	*
	* @param string $name Name of the variable to set
	* @param mixed $value Value to set to the variable
	*/
	protected function set_data($name, $value)
	{
		$this->data['notification_data'][$name] = $value;
	}

	/**
	* {@inheritdoc}
	*/
	public function create_insert_array($type_data, $pre_create_data = array())
	{
		// Defaults
		$this->data = array_merge(array(
			'item_id'				=> static::get_item_id($type_data),
			'notification_type_id'	=> $this->notification_type_id,
			'item_parent_id'		=> static::get_item_parent_id($type_data),

			'notification_time'		=> time(),
			'notification_read'		=> false,

			'notification_data'		=> array(),
		), $this->data);
	}

	/**
	* {@inheritdoc}
	*/
	public function get_insert_array()
	{
		$data = $this->data;

		$data['notification_data'] = serialize($data['notification_data']);

		return $data;
	}

	/**
	* Function for preparing the data for update in an SQL query
	* (The service handles insertion)
	*
	* @param array $type_data Data unique to this notification type
	* @return array Array of data ready to be updated in the database
	*/
	public function create_update_array($type_data)
	{
		$this->create_insert_array($type_data);
		$data = $this->get_insert_array();

		// Unset data unique to each row
		unset(
			$data['notification_time'], // Also unsetting time, since it always tries to change the time to current (if you actually need to change the time, over-ride this function)
			$data['notification_id'],
			$data['notification_read'],
			$data['user_id']
		);

		return $data;
	}

	/**
	* Mark this item read
	*
	* @param bool $return True to return a string containing the SQL code to update this item, False to execute it (Default: False)
	* @return string|null If $return is False, nothing will be returned, else the sql code to update this item
	*/
	public function mark_read($return = false)
	{
		return $this->mark(false, $return);
	}

	/**
	* Mark this item unread
	*
	* @param bool $return True to return a string containing the SQL code to update this item, False to execute it (Default: False)
	* @return string|null If $return is False, nothing will be returned, else the sql code to update this item
	*/
	public function mark_unread($return = false)
	{
		return $this->mark(true, $return);
	}

	/**
	* {inheritDoc}
	*/
	public function get_redirect_url()
	{
		return $this->get_url();
	}

	/**
	* Prepare to output the notification to the template
	*
	* @return array Template variables
	*/
	public function prepare_for_display()
	{
		$mark_hash = generate_link_hash('mark_notification_read');

		if ($this->get_url())
		{
			$u_mark_read = $this->controller_helper->route('phpbb_notifications_mark_read', ['id' => $this->notification_id, 'hash' => $mark_hash]);
		}
		else
		{
			$redirect = (($this->user->page['page_dir']) ? $this->user->page['page_dir'] . '/' : '') . $this->user->page['page_name'] . (($this->user->page['query_string']) ? '?' . $this->user->page['query_string'] : '');
			$u_mark_read = $this->controller_helper->route('phpbb_notifications_mark_read', ['id' => $this->notification_id, 'hash' => $mark_hash, 'redirect' => $redirect]);
		}

		$avatar = $this->get_avatar();
		$avatar_data = count($avatar) ? $this->avatar_helper->get_template_vars($avatar) : [];

		return array_merge($avatar_data, [
			'NOTIFICATION_ID'	=> $this->notification_id,
			'STYLING'			=> $this->get_style_class(),
			'FORMATTED_TITLE'	=> $this->get_title(),
			'REFERENCE'			=> $this->get_reference(),
			'FORUM'				=> $this->get_forum(),
			'REASON'			=> $this->get_reason(),
			'URL'				=> $this->get_url(),
			'TIME'	   			=> $this->user->format_date($this->notification_time),
			'UNREAD'			=> !$this->notification_read,

			'U_MARK_READ'		=> (!$this->notification_read) ? $u_mark_read : '',
		]);
	}

	/**
	* -------------- Fall back functions -------------------
	*/

	/**
	* {@inheritDoc}
	*/
	public function get_unsubscribe_url($method = false)
	{
		return false;
	}

	/**
	* Get the CSS style class of the notification (fall back)
	*
	* @return string
	*/
	public function get_style_class()
	{
		return '';
	}

	/**
	* Get the user's avatar (fall back)
	*
	* @return array
	*/
	public function get_avatar()
	{
		return [];
	}

	/**
	* Get the reference of the notification (fall back)
	*
	* @return string
	*/
	public function get_reference()
	{
		return '';
	}

	/**
	* Get the forum of the notification reference (fall back)
	*
	* @return string
	*/
	public function get_forum()
	{
		return '';
	}

	/**
	* Get the reason for the notification (fall back)
	*
	* @return string
	*/
	public function get_reason()
	{
		return '';
	}

	/**
	* Get the special items to load (fall back)
	*
	* @return array
	*/
	public function get_load_special()
	{
		return array();
	}

	/**
	 * Load the special items (fall back)
	 *
	 * @param array $data
	 * @param array $notifications
	 */
	public function load_special($data, $notifications)
	{
	}

	/**
	* Is available (fall back)
	*
	* @return bool
	*/
	public function is_available()
	{
		return true;
	}

	/**
	 * {@inheritdoc}
	 */
	public function get_email_template()
	{
		return false;
	}

	/**
	 * Pre create insert array function (fall back)
	 *
	 * @param array $type_data
	 * @param array $notify_users
	 * @return array
	 */
	public function pre_create_insert_array($type_data, $notify_users)
	{
		return array();
	}

	/**
	* -------------- Helper functions -------------------
	*/

	/**
	 * Find the users who want to receive notifications (helper)
	 *
	 * @param array|bool $user_ids User IDs to check if they want to receive notifications
	 *                             (Bool False to check all users besides anonymous and bots (USER_IGNORE))
	 * @param array      $options
	 * @return array
	 */
	protected function check_user_notification_options($user_ids = false, $options = array())
	{
		$options = array_merge(array(
			'ignore_users'		=> array(),
			'item_type'			=> $this->get_type(),
			'item_id'			=> 0, // Global by default
		), $options);

		if ($user_ids === false)
		{
			$user_ids = array();

			$sql = 'SELECT user_id
				FROM ' . USERS_TABLE . '
				WHERE user_id <> ' . ANONYMOUS . '
					AND user_type <> ' . USER_IGNORE;
			$result = $this->db->sql_query($sql);
			while ($row = $this->db->sql_fetchrow($result))
			{
				$user_ids[] = $row['user_id'];
			}
			$this->db->sql_freeresult($result);
		}

		if (empty($user_ids))
		{
			return array();
		}

		$rowset = $output = array();

		$sql = 'SELECT user_id, method, notify
			FROM ' . $this->user_notifications_table . '
			WHERE ' . $this->db->sql_in_set('user_id', $user_ids) . "
				AND item_type = '" . $this->db->sql_escape($options['item_type']) . "'
				AND item_id = " . (int) $options['item_id'];
		$result = $this->db->sql_query($sql);

		while ($row = $this->db->sql_fetchrow($result))
		{
			if (isset($options['ignore_users'][$row['user_id']]) && in_array($row['method'], $options['ignore_users'][$row['user_id']]))
			{
				continue;
			}

			if (!isset($rowset[$row['user_id']]))
			{
				$rowset[$row['user_id']] = array();
			}
			$rowset[$row['user_id']][$row['method']] = $row['notify'];

			if (!isset($output[$row['user_id']]))
			{
				$output[$row['user_id']] = array();
			}
			if ($row['notify'])
			{
				$output[$row['user_id']][] = $row['method'];
			}
		}

		$this->db->sql_freeresult($result);

		$default_methods = $this->notification_manager->get_default_methods();

		foreach ($user_ids as $user_id)
		{
			if (isset($options['ignore_users'][$user_id]))
			{
				continue;
			}
			if (!array_key_exists($user_id, $rowset))
			{
				// No rows at all for this user, use the default methods
				$output[$user_id] = $default_methods;
			}
			else
			{
				foreach ($default_methods as $default_method)
				{
					if (!array_key_exists($default_method, $rowset[$user_id]))
					{
						// No user preference for this type recorded, but it should be enabled by default.
						$output[$user_id][] = $default_method;
					}
				}
			}
		}

		return $output;
	}

	/**
	* Mark this item read/unread helper
	*
	* @param bool $unread Unread (True/False) (Default: False)
	* @param bool $return True to return a string containing the SQL code to update this item, False to execute it (Default: False)
	* @return string|null If $return is False, nothing will be returned, else the sql code to update this item
	*/
	protected function mark($unread = true, $return = false)
	{
		$this->notification_read = (bool) !$unread;

		if ($return)
		{
			$where = array(
				'notification_type_id = ' . (int) $this->notification_type_id,
				'item_id = ' . (int) $this->item_id,
				'user_id = ' . (int) $this->user_id,
			);

			$where = implode(' AND ', $where);
			return $where;
		}
		else
		{
			$this->notification_manager->mark_notifications($this->get_type(), (int) $this->item_id, (int) $this->user_id, false, $this->notification_read);
		}

		return null;
	}

	/**
	 * Get a list of users that are authorised to receive notifications
	 *
	 * @param array $users Array of users that have subscribed to a notification
	 * @param int $forum_id Forum ID of the forum
	 * @param array $options Array of notification options
	 * @param bool $sort Whether the users array should be sorted. Default: false
	 * @return array Array of users that are authorised recipients
	 */
	protected function get_authorised_recipients($users, $forum_id, $options, $sort = false)
	{
		if (empty($users))
		{
			return array();
		}

		$users = array_unique($users);

		if ($sort)
		{
			sort($users);
		}

		$auth_read = $this->auth->acl_get_list($users, 'f_read', $forum_id);

		if (empty($auth_read))
		{
			return array();
		}

		return $this->check_user_notification_options($auth_read[$forum_id]['f_read'], $options);
	}
}
