<?php
class ModEss_UserNotes_Model_UserNote extends XenForo_Model
{
    /**
     * Constants to allow joins to extra tables in certain queries
     *
     * @var integer Join user table (poster)
     * @var integer Join user table (receiver)
     * @var integer Join user privacy table (receiver)
     * @var integer Join deletion log
     */
    const FETCH_USER_POSTER = 0x01;
    const FETCH_USER_RECEIVER = 0x02;
    const FETCH_USER_RECEIVER_PRIVACY = 0x04;
    const FETCH_DELETION_LOG = 0x08;

    /**
     * Gets the specified profile post.
     *
     * @param integer $id
     * @param array $fetchOptions
     *
     * @return array|false
     */
    public function getUserNoteById($id, array $fetchOptions = array())
    {
        $sqlClauses = $this->prepareUserNoteFetchOptions($fetchOptions);

        return $this->_getDb()->fetchRow('
			SELECT user_note.*
				' . $sqlClauses['selectFields'] . '
			FROM xm_user_note AS user_note
			' . $sqlClauses['joinTables'] . '
			WHERE user_note.user_note_id = ?
		', $id);
    }

    /**
     * Gets profile posts for the specified user that meet the given conditions.
     *
     * @param integer $userId
     * @param array $conditions
     * @param array $fetchOptions
     *
     * @return array [profile post id] => info
     */
    public function getUserNotesForUserId($userId, array $conditions = array(), array $fetchOptions = array())
    {
        $conditions['user_id'] = $userId;
        $whereClause = $this->prepareUserNoteConditions($conditions, $fetchOptions);

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

        return $this->fetchAllKeyed($this->limitQueryResults(
            '
                SELECT user_note.*
                    ' . $sqlClauses['selectFields'] . '
				FROM xm_user_note AS user_note
				' . $sqlClauses['joinTables'] . '
				WHERE ' . $whereClause . '
				ORDER BY user_note.post_date DESC
			', $limitOptions['limit'], $limitOptions['offset']
        ), 'user_note_id');
    }

    /**
     * Counts the profile posts that were left for the specified user (with the given conditions).
     *
     * @param integer $userId
     * @param array $conditions
     *
     * @return integer
     */
    public function countUserNotesForUserId($userId, array $conditions = array())
    {
        $fetchOptions = array();
        $conditions['user_id'] = $userId;
        $whereClause = $this->prepareUserNoteConditions($conditions, $fetchOptions);

        $sqlClauses = $this->prepareUserNoteFetchOptions($fetchOptions);

        return $this->_getDb()->fetchOne('
			SELECT COUNT(*)
			FROM xm_user_note AS user_note
			' . $sqlClauses['joinTables'] . '
			WHERE ' . $whereClause
        );
    }

    /**
     * Gets the specified profile posts.
     *
     * @param array $messageIds
     * @param array $fetchOptions
     *
     * @return array [profile post id] => info
     */
    public function getUserNotesByIds(array $messageIds, array $fetchOptions = array())
    {
        if (!$messageIds)
        {
            return array();
        }

        $sqlClauses = $this->prepareUserNoteFetchOptions($fetchOptions);

        return $this->fetchAllKeyed('
			SELECT user_note.*
				' . $sqlClauses['selectFields'] . '
			FROM xm_user_note AS user_note
			' . $sqlClauses['joinTables'] . '
			WHERE user_note.user_note_id IN(' . $this->_getDb()->quote($messageIds) . ')
		', 'user_note_id');
    }

    /**
     * Fetches all profile posts posted by the specified user, on their own and others' profile pages.
     *
     * @param integer $userId
     * @param array $fetchOptions
     *
     * @return array
     */
    public function getUserNotesByUserId($userId, array $fetchOptions = array())
    {
        $sqlClauses = $this->prepareUserNoteFetchOptions($fetchOptions);

        return $this->fetchAllKeyed('
			SELECT user_note.*
				' . $sqlClauses['selectFields'] . '
			FROM xm_user_note AS user_note
			' . $sqlClauses['joinTables'] . '
			WHERE user_note.user_id = ?
		', 'user_note_id', $userId);
    }

    /**
     * Gets profile post IDs in the specified range. The IDs returned will be those immediately
     * after the "start" value (not including the start), up to the specified limit.
     *
     * @param integer $start IDs greater than this will be returned
     * @param integer $limit Number of posts to return
     *
     * @return array List of IDs
     */
    public function getUserNoteIdsInRange($start, $limit)
    {
        $db = $this->_getDb();

        return $db->fetchCol($db->limit('
			SELECT user_note_id
			FROM xm_user_note
			WHERE user_note_id > ?
			ORDER BY user_note_id
		', $limit), $start);
    }

    /**
     * Gets the latest profile posts.
     *
     * @param array $conditions
     * @param array $fetchOptions
     *
     * @return array [profile post id] => info
     */
    public function getLatestUserNotes(array $conditions = array(), array $fetchOptions = array())
    {
        $whereClause = $this->prepareUserNoteConditions($conditions, $fetchOptions);

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

        return $this->fetchAllKeyed($this->limitQueryResults(
            '
                SELECT user_note.*
                    ' . $sqlClauses['selectFields'] . '
				FROM xm_user_note AS user_note
				' . $sqlClauses['joinTables'] . '
				WHERE ' . $whereClause . '
				ORDER BY user_note.post_date DESC
			', $limitOptions['limit'], $limitOptions['offset']
        ), 'user_note_id');
    }

    /**
     * Prepares a collection of profile post fetching related conditions into an SQL clause
     *
     * @param array $conditions List of conditions
     * @param array $fetchOptions Modifiable set of fetch options (may have joins pushed on to it)
     *
     * @return string SQL clause (at least 1=1)
     */
    public function prepareUserNoteConditions(array $conditions, array &$fetchOptions)
    {
        $sqlConditions = array();
        $db = $this->_getDb();

        if (!empty($conditions['user_id']))
        {
            $sqlConditions[] = "user_note.profile_user_id = " . $db->quote($conditions['user_id']);
        }

        if (!empty($conditions['post_date']) && is_array($conditions['post_date']))
        {
            list($operator, $cutOff) = $conditions['post_date'];

            $this->assertValidCutOffOperator($operator);
            $sqlConditions[] = "user_note.post_date $operator " . $db->quote($cutOff);
        }

        if (isset($conditions['deleted']) || isset($conditions['moderated']))
        {
            $sqlConditions[] = $this->prepareStateLimitFromConditions($conditions, 'user_note', 'message_state');
        }

        return $this->getConditionsForClause($sqlConditions);
    }

    /**
     * Checks the 'join' key of the incoming array for the presence of the FETCH_x bitfields in this class
     * and returns SQL snippets to join the specified tables if required
     *
     * @param array $fetchOptions containing a 'join' integer key build from this class's FETCH_x bitfields
     *
     * @return array Containing 'selectFields' and 'joinTables' keys. Example: selectFields = ', user.*, foo.title'; joinTables = ' INNER JOIN foo ON (foo.id = other.id) '
     */
    public function prepareUserNoteFetchOptions(array $fetchOptions)
    {
        $selectFields = '';
        $joinTables = '';

        $db = $this->_getDb();

        if (!empty($fetchOptions['join']))
        {
            if ($fetchOptions['join'] & self::FETCH_USER_POSTER)
            {
                $selectFields .= ',
					posting_user.*,
					IF(posting_user.username IS NULL, user_note.username, posting_user.username) AS username';
                $joinTables .= '
					LEFT JOIN xf_user AS posting_user ON
						(posting_user.user_id = user_note.user_id)';
            }

            if ($fetchOptions['join'] & self::FETCH_USER_RECEIVER)
            {
                $selectFields .= ',
					receiving_user.username
						AS profile_username,
					receiving_user.gender
						AS profile_gender,
					receiving_user.user_state
						AS profile_user_state,
					receiving_user.user_group_id
						AS profile_user_group_id,
					receiving_user.secondary_group_ids
						AS profile_secondary_group_ids,
					receiving_user.display_style_group_id
						AS profile_display_style_group_id,
					receiving_user.is_banned
						AS profile_is_banned,
					receiving_user.is_admin
						AS profile_is_admin,
					receiving_user.is_moderator
						AS profile_is_moderator,
					receiving_user.avatar_date
						AS profile_avatar_date,
					receiving_user.gravatar
						AS profile_gravatar,
					receiving_user.warning_points
						AS profile_warning_points,
					receiving_user.permission_combination_id
						AS profile_permission_combination_id';
                $joinTables .= '
					LEFT JOIN xf_user AS receiving_user ON
						(receiving_user.user_id = user_note.profile_user_id)';
            }

            if ($fetchOptions['join'] & self::FETCH_USER_RECEIVER_PRIVACY)
            {
                $visitingUser = (isset($fetchOptions['visitingUser']) ? $fetchOptions['visitingUser'] : XenForo_Visitor::getInstance());

                $selectFields .= ',
					receiving_user_privacy.allow_view_profile
						AS profile_allow_view_profile,
					receiving_user_privacy.allow_post_profile
						AS profile_allow_post_profile,
					receiving_user_privacy.allow_send_personal_conversation
						AS profile_allow_send_personal_conversation,
					receiving_user_privacy.allow_view_identities
						AS profile_allow_view_identities,
					receiving_user_privacy.allow_receive_news_feed
						AS profile_allow_allow_receive_news_feed,
					IF (receiving_user_follow.follow_user_id, 1, 0)
						AS following_' . intval($visitingUser->getUserId());
                $joinTables .= '
					LEFT JOIN xm_user_privacy AS receiving_user_privacy ON
						(receiving_user_privacy.user_id = user_note.profile_user_id)
					LEFT JOIN xm_user_follow AS receiving_user_follow ON
						(receiving_user_follow.user_id = user_note.profile_user_id AND follow_user_id = ' . $db->quote($visitingUser->getUserId()) . ')';
            }

            if ($fetchOptions['join'] & self::FETCH_DELETION_LOG)
            {
                $selectFields .= ',
					deletion_log.delete_date, deletion_log.delete_reason,
					deletion_log.delete_user_id, deletion_log.delete_username';
                $joinTables .= '
					LEFT JOIN xm_deletion_log AS deletion_log ON
						(deletion_log.content_type = \'user_note\' AND deletion_log.content_id = user_note.user_note_id)';
            }
        }

        if (isset($fetchOptions['likeUserId']))
        {
            if (empty($fetchOptions['likeUserId']))
            {
                $selectFields .= ',
					0 AS like_date';
            }
            else
            {
                $selectFields .= ',
					liked_content.like_date';
                $joinTables .= '
					LEFT JOIN xm_liked_content AS liked_content
						ON (liked_content.content_type = \'user_note\'
							AND liked_content.content_id = user_note.user_note_id
							AND liked_content.like_user_id = ' .$db->quote($fetchOptions['likeUserId']) . ')';
            }
        }

        return array(
            'selectFields' => $selectFields,
            'joinTables'   => $joinTables
        );
    }

    /**
     * Prepares a profile post.
     *
     * @param array $UserNote
     * @param array $user User whose profile the post is on
     * @param array|null $viewingUser Viewing user reference
     *
     * @return array
     */
    public function prepareUserNote(array $UserNote, array $user, array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        $UserNote['canDelete'] = $this->canDeleteUserNote($UserNote, $user, 'soft', $null, $viewingUser);
        $UserNote['canEdit'] = $this->canEditUserNote($UserNote, $user, $null, $viewingUser);
        $UserNote['canLike'] = $this->canLikeUserNote($UserNote, $user, $null, $viewingUser);
        $UserNote['canComment'] = $this->canCommentOnUserNote($UserNote, $user, $null, $viewingUser);
        $UserNote['canReport'] = $this->canReportUserNote($UserNote, $user, $null, $viewingUser);
        $UserNote['canWarn'] = $this->canWarnUserNote($UserNote, $user, $null, $viewingUser);

        $UserNote['isDeleted'] = ($UserNote['message_state'] == 'deleted');
        $UserNote['isModerated'] = ($UserNote['message_state'] == 'moderated');

        $UserNote['canCleanSpam'] = (
            $UserNote['canDelete']
            && $user['user_id']
            && XenForo_Permission::hasPermission($viewingUser['permissions'], 'general', 'cleanSpam')
            && $this->_getUserModel()->couldBeSpammer($UserNote)
        );

        if (!empty($UserNote['delete_date']))
        {
            $UserNote['deleteInfo'] = array(
                'user_id' => $UserNote['delete_user_id'],
                'username' => $UserNote['delete_username'],
                'date' => $UserNote['delete_date'],
                'reason' => $UserNote['delete_reason'],
            );
        }

        if (!empty($UserNote['profile_username']))
        {
            $UserNote['profileUser'] = $this->getProfileUserFromUserNote($UserNote);
        }

        if ($UserNote['likes'])
        {
            $UserNote['likeUsers'] = unserialize($UserNote['like_users']);
        }

        if (array_key_exists('user_group_id', $UserNote))
        {
            $UserNote = $this->getModelFromCache('XenForo_Model_User')->prepareUser($UserNote);
        }

        return $UserNote;
    }

    /**
     * Prepares a batch of profile posts.
     *
     * @param array $UserNotes
     * @param array $user User whose profile the post is on
     * @param array|null $viewingUser Viewing user reference
     *
     * @return array
     */
    public function prepareUserNotes(array $UserNotes, array $user, array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        foreach ($UserNotes AS &$UserNote)
        {
            $UserNote = $this->prepareUserNote($UserNote, $user, $viewingUser);
        }

        return $UserNotes;
    }

    /**
     * Extracts the data relating to a profile owner user from a profile post,
     * that has been fetched with XenForo_Model_UserNote::FETCH_USER_RECEIVER
     *
     * @param array $UserNote
     *
     * @return array
     */
    public function getProfileUserFromUserNote(array $UserNote, array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        $user = array();

        $followingKey = 'following_' . $viewingUser['user_id'];
        if (isset($UserNote[$followingKey]))
        {
            $user[$followingKey] = $UserNote[$followingKey];
        }

        foreach ($UserNote AS $key => $val)
        {
            if (preg_match('/^profile_(.+)$/', $key, $match))
            {
                $user[$match[1]] = $val;
            }
        }

        return $user;
    }

    /**
     * Determines if the given profile post can be viewed. This does not
     * check user profile viewing permissions.
     *
     * @param array $UserNote
     * @param array $user
     * @param string $errorPhraseKey By ref. More specific error, if available.
     * @param array|null $viewingUser Viewing user reference
     *
     * @return boolean
     */
    public function canViewUserNote(array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        if (!XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'view'))
        {
            return false;
        }

        if ($UserNote['message_state'] == 'moderated'
            && !XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'viewModerated')
        )
        {
            if (!$viewingUser['user_id'] || $viewingUser['user_id'] != $UserNote['user_id'])
            {
                return false;
            }
        }
        else if ($UserNote['message_state'] == 'deleted'
            && !XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'viewDeleted')
        )
        {
            return false;
        }

        return $this->_getUserProfileModel()->canViewUserNotes($user, $errorPhraseKey, $viewingUser);
    }

    /**
     * Determines if the given profile post can be viewed. This checks parent permissions.
     *
     * @param array $UserNote
     * @param array $user
     * @param string $errorPhraseKey By ref. More specific error, if available.
     * @param array|null $viewingUser Viewing user reference
     *
     * @return boolean
     */
    public function canViewUserNoteAndContainer(array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        if (!$this->_getUserProfileModel()->canViewFullUserProfile($user, $errorPhraseKey, $viewingUser))
        {
            return false;
        }

        return $this->canViewUserNote($UserNote, $user, $errorPhraseKey, $viewingUser);
    }

    /**
     * Determines if the given profile post can be edited. This does not
     * check parent (viewing) permissions.
     *
     * @param array $UserNote
     * @param array $user
     * @param string $errorPhraseKey By ref. More specific error, if available.
     * @param array|null $viewingUser Viewing user reference
     *
     * @return boolean
     */
    public function canEditUserNote(array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        if (!$viewingUser['user_id'])
        {
            return false;
        }

        if ($viewingUser['user_id'] == $UserNote['user_id'])
        {
            return XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'editOwn');
        }
        else
        {
            return XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'editAny');
        }
    }

    /**
     * Determines if the given profile post can be deleted. This does not
     * check parent (viewing) permissions.
     *
     * @param array $UserNote
     * @param array $user
     * @param string $deleteType Type of deletion (soft or hard)
     * @param string $errorPhraseKey By ref. More specific error, if available.
     * @param array|null $viewingUser Viewing user reference
     *
     * @return boolean
     */
    public function canDeleteUserNote(array $UserNote, array $user, $deleteType = 'soft', &$errorPhraseKey = '', array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        if (!$viewingUser['user_id'])
        {
            return false;
        }

        if ($deleteType != 'soft' && !XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'hardDeleteAny'))
        {
            return false;
        }

        if (XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'deleteAny'))
        {
            return true;
        }

        if ($viewingUser['user_id'] == $UserNote['user_id'])
        {
            return XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'deleteOwn');
        }
        else if ($viewingUser['user_id'] == $UserNote['profile_user_id'])
        {
            return XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'manageOwn');
        }
        else
        {
            return false;
        }
    }

    /**
     * Determines if the given profile post can be undeleted. This does not
     * check parent (viewing) permissions.
     *
     * @param array $UserNote
     * @param array $user
     * @param string $errorPhraseKey By ref. More specific error, if available.
     * @param array|null $viewingUser Viewing user reference
     *
     * @return boolean
     */
    public function canUndeleteUserNote(array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);
        return ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'undelete'));
    }

    /**
     * Determines if the given profile post can be approved/unapproved. This does not
     * check parent (viewing) permissions.
     *
     * @param array $UserNote
     * @param array $user
     * @param string $errorPhraseKey By ref. More specific error, if available.
     * @param array|null $viewingUser Viewing user reference
     *
     * @return boolean
     */
    public function canApproveUnapproveUserNote(array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);
        return ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'approveUnapprove'));
    }

    /**
     * Determines if the given profile post can be warned. This does not
     * check parent (viewing) permissions.
     *
     * @param array $UserNote
     * @param array $user
     * @param string $errorPhraseKey By ref. More specific error, if available.
     * @param array|null $viewingUser Viewing user reference
     *
     * @return boolean
     */
    public function canWarnUserNote(array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        if ($UserNote['warning_id'] || empty($UserNote['user_id']))
        {
            return false;
        }

        if (!empty($UserNote['is_admin']) || !empty($UserNote['is_moderator']))
        {
            return false;
        }

        $this->standardizeViewingUserReference($viewingUser);

        return ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'warn'));
    }

    /**
     * Determines if the profile post can be liked with the given permissions.
     * This does not check profile post viewing permissions.
     *
     * @param array $UserNote Profile post info
     * @param array $user User info for where profile post is posted
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    public function canLikeUserNote(array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        if (!$viewingUser['user_id'])
        {
            return false;
        }

        if ($UserNote['message_state'] != 'visible')
        {
            return false;
        }

        if ($UserNote['user_id'] == $viewingUser['user_id'])
        {
            $errorPhraseKey = 'liking_own_content_cheating';
            return false;
        }

        return XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'like');
    }

    /**
     * Determines if the given profile post can be commented on. This does not
     * check parent (viewing) permissions.
     *
     * @param array $UserNote
     * @param array $user
     * @param string $errorPhraseKey By ref. More specific error, if available.
     * @param array|null $viewingUser Viewing user reference
     *
     * @return boolean
     */
    public function canCommentOnUserNote(array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        if (!$viewingUser['user_id'])
        {
            return false;
        }

        if ($UserNote['message_state'] != 'visible')
        {
            return false;
        }

        return (
            XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'view')
            && XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'comment')
            && $this->_getUserModel()->passesPrivacyCheck($user['allow_post_profile'], $user, $viewingUser)
        );
    }

    /**
     * Determines if the viewing user can view the IP of the stated profile post
     *
     * @param array $UserNote
     * @param array $user
     * @param string $errorPhraseKey By ref. More specific error, if available.
     * @param array|null $viewingUser Viewing user reference
     *
     * @return boolean
     */
    public function canViewIps(array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        return $this->getModelFromCache('XenForo_Model_User')->canViewIps($errorPhraseKey, $viewingUser);
    }

    /**
     * Checks that the viewing user may report the specified profile post
     *
     * @param array $UserNote
     * @param array $user
     * @param string
     * @param boolean $errorPhraseKey
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    public function canReportUserNote(array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        return $this->getModelFromCache('XenForo_Model_User')->canReportContent($errorPhraseKey, $viewingUser);
    }

    /**
     * Adds the inline mod option to a profile post.
     *
     * @param array $UserNote Profile post. By reference; canInlineMod key added
     * @param array $user
     * @param array|null $viewingUser
     *
     * @return array List of inline mod options the user can do
     */
    public function addInlineModOptionToUserNote(array &$UserNote, array $user, array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        $options = array();
        $canInlineMod = ($viewingUser['user_id'] && (
                XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'deleteAny')
                || XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'undelete')
                || XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'approveUnapprove')
            ));

        if ($canInlineMod)
        {
            if ($this->canDeleteUserNote($UserNote, $user, 'soft', $null, $viewingUser))
            {
                $options['delete'] = true;
            }
            if ($this->canUndeleteUserNote($UserNote, $user, $null, $viewingUser))
            {
                $options['undelete'] = true;
            }
            if ($this->canApproveUnapproveUserNote($UserNote, $user, $null, $viewingUser))
            {
                $options['approve'] = true;
                $options['unapprove'] = true;
            }
        }

        $UserNote['canInlineMod'] = (count($options) > 0);

        return $options;
    }

    /**
     * Adds the inline mod option to a batch of profile posts.
     *
     * @param array $UserNotes Batch of profile posts. By reference; all modified to add canInlineMod key
     * @param array $user
     * @param array|null $viewingUser
     *
     * @return array List of inline mod options the user can do on at least one post
     */
    public function addInlineModOptionToUserNotes(array &$UserNotes, array $user, array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        $options = array();
        foreach ($UserNotes AS &$UserNote)
        {
            $options += $this->addInlineModOptionToUserNote($UserNote, $user, $viewingUser);
        }

        return $options;
    }

    /**
     * Gets permission-based conditions that apply to profile post fetching functions.
     *
     * @param array $user User the profile posts will belong to
     * @param array|null $viewingUser Viewing user ref; defaults to visitor
     *
     * @return array Keys: deleted (boolean), moderated (boolean, integer if can only view specific user ID)
     */
    public function getPermissionBasedUserNoteConditions(array $user, array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        if (XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'viewModerated'))
        {
            $viewModerated = true;
        }
        else if ($viewingUser['user_id'])
        {
            $viewModerated = $viewingUser['user_id'];
        }
        else
        {
            $viewModerated = false;
        }

        return array(
            'deleted' => XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'viewDeleted'),
            'moderated' => $viewModerated
        );
    }

    /**
     * Gets the message state for a newly inserted profile post by the viewing user.
     *
     * @param array $user User whose profile is being posted on
     * @param array|null $viewingUser
     *
     * @return string Message state (visible, moderated, deleted)
     */
    public function getUserNoteInsertMessageState(array $user, array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        if ($viewingUser['user_id'] && XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'approveUnapprove'))
        {
            return 'visible';
        }
        else if (XenForo_Permission::hasPermission($viewingUser['permissions'], 'general', 'followModerationRules'))
        {
            return 'visible'; // TODO: follow profile-level settings when they exist
        }
        else
        {
            return 'moderated';
        }
    }

    /**
     * Constant for fetching the info about the comment user.
     *
     * @var integer
     */
    const FETCH_COMMENT_USER = 0x01;

    /**
     * Prepares the fetching options for profile post comments.
     *
     * @param array $fetchOptions
     *
     * @return array
     */
    public function prepareUserNoteCommentFetchOptions(array $fetchOptions)
    {
        $selectFields = '';
        $joinTables = '';

        if (!empty($fetchOptions['join']))
        {
            if ($fetchOptions['join'] & self::FETCH_COMMENT_USER)
            {
                $selectFields .= ',
					user.*,
					IF(user.username IS NULL, user_note_comment.username, user.username) AS username';
                $joinTables .= '
					LEFT JOIN xf_user AS user ON
						(user.user_id = user_note_comment.user_id)';
            }
        }

        return array(
            'selectFields' => $selectFields,
            'joinTables'   => $joinTables
        );
    }

    /**
     * Gets the specified profile post comment.
     *
     * @param integer $UserNoteCommentId
     * @param array $fetchOptions
     *
     * @return array|false
     */
    public function getUserNoteCommentById($UserNoteCommentId, array $fetchOptions = array())
    {
        $sqlClauses = $this->prepareUserNoteCommentFetchOptions($fetchOptions);

        return $this->_getDb()->fetchRow('
			SELECT user_note_comment.*
			' . $sqlClauses['selectFields'] . '
			FROM xm_user_note_comment AS user_note_comment
			' . $sqlClauses['joinTables'] . '
			WHERE user_note_comment.user_note_comment_id = ?
		', $UserNoteCommentId);
    }

    /**
     * Gets the profile post comments with the specified IDs.
     *
     * @param array $ids
     * @param array $fetchOptions
     *
     * @return array [id] => info
     */
    public function getUserNoteCommentsByIds(array $ids, array $fetchOptions = array())
    {
        if (!$ids)
        {
            return array();
        }

        $sqlClauses = $this->prepareUserNoteCommentFetchOptions($fetchOptions);

        return $this->fetchAllKeyed('
			SELECT user_note_comment.*
			' . $sqlClauses['selectFields'] . '
			FROM xm_user_note_comment AS user_note_comment
			' . $sqlClauses['joinTables'] . '
			WHERE user_note_comment.user_note_comment_id IN (' . $this->_getDb()->quote($ids) . ')
		', 'user_note_comment_id');
    }

    /**
     * Gets the profile post comments make by a particular user.
     *
     * @param array $ids
     * @param array $fetchOptions
     *
     * @return array [id] => info
     */
    public function getUserNoteCommentsByUserId($userId, array $fetchOptions = array())
    {
        $sqlClauses = $this->prepareUserNoteCommentFetchOptions($fetchOptions);

        return $this->fetchAllKeyed('
			SELECT user_note_comment.*
			' . $sqlClauses['selectFields'] . '
			FROM xm_user_note_comment AS user_note_comment
			' . $sqlClauses['joinTables'] . '
			WHERE user_note_comment.user_id = ?
		', 'user_note_comment_id', $userId);
    }

    /**
     * Gets all comments that belong to the specified post. If a limit is specified,
     * more recent comments are returned.
     *
     * @param integer $UserNoteId
     * @param integer $beforeDate If specified, gets posts before specified date only
     * @param array $fetchOptions
     *
     * @return array [id] => info
     */
    public function getUserNoteCommentsByUserNote($UserNoteId, $beforeDate = 0, array $fetchOptions = array())
    {
        $sqlClauses = $this->prepareUserNoteCommentFetchOptions($fetchOptions);
        $limitOptions = $this->prepareLimitFetchOptions($fetchOptions);

        if ($beforeDate)
        {
            $beforeCondition = 'AND user_note_comment.comment_date < ' . $this->_getDb()->quote($beforeDate);
        }
        else
        {
            $beforeCondition = '';
        }

        $results = $this->fetchAllKeyed($this->limitQueryResults(
            '
                SELECT user_note_comment.*
                ' . $sqlClauses['selectFields'] . '
				FROM xm_user_note_comment AS user_note_comment
				' . $sqlClauses['joinTables'] . '
				WHERE user_note_comment.user_note_id = ?
					' . $beforeCondition . '
				ORDER BY user_note_comment.comment_date DESC
			', $limitOptions['limit'], $limitOptions['offset']
        ), 'user_note_comment_id', $UserNoteId);

        return array_reverse($results, true);
    }

    /**
     * Merges comments into existing profile post data.
     *
     * @param array $UserNotes
     * @param array $fetchOptions
     *
     * @return array Posts with comments merged
     */
    public function addUserNoteCommentsToUserNotes(array $UserNotes, array $fetchOptions = array())
    {
        $commentIdMap = array();

        foreach ($UserNotes AS &$UserNote)
        {
            if ($UserNote['latest_comment_ids'])
            {
                foreach (explode(',', $UserNote['latest_comment_ids']) AS $commentId)
                {
                    $commentIdMap[intval($commentId)] = $UserNote['user_note_id'];
                }
            }

            $UserNote['comments'] = array();
        }

        if ($commentIdMap)
        {
            $comments = $this->getUserNoteCommentsByIds(array_keys($commentIdMap), $fetchOptions);
            foreach ($commentIdMap AS $commentId => $UserNoteId)
            {
                if (isset($comments[$commentId]))
                {
                    if (!isset($UserNotes[$UserNoteId]['first_shown_comment_date']))
                    {
                        $UserNotes[$UserNoteId]['first_shown_comment_date'] = $comments[$commentId]['comment_date'];
                    }
                    $UserNotes[$UserNoteId]['comments'][$commentId] = $comments[$commentId];
                }
            }
        }

        return $UserNotes;
    }

    /**
     * Gets the user IDs that have commented on a profile post.
     *
     * @param integer $UserNoteId
     *
     * @return array
     */
    public function getUserNoteCommentUserIds($UserNoteId)
    {
        return $this->_getDb()->fetchCol('
			SELECT DISTINCT user_id
			FROM xm_user_note_comment
			WHERE user_note_id = ?
		', $UserNoteId);
    }

    /**
     * Determines if the given profile post comment can be deleted. This does not
     * check parent (viewing) permissions.
     *
     * @param array $comment
     * @param array $UserNote
     * @param array $user
     * @param string $errorPhraseKey By ref. More specific error, if available.
     * @param array|null $viewingUser Viewing user reference
     *
     * @return boolean
     */
    public function canDeleteUserNoteComment(array $comment, array $UserNote, array $user, &$errorPhraseKey = '', array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        if (!$viewingUser['user_id'])
        {
            return false;
        }

        if (XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'deleteAny'))
        {
            return true;
        }

        if ($viewingUser['user_id'] == $comment['user_id'])
        {
            return XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'deleteOwn');
        }
        else if ($viewingUser['user_id'] == $UserNote['profile_user_id'])
        {
            return XenForo_Permission::hasPermission($viewingUser['permissions'], 'UserNote', 'manageOwn');
        }
        else
        {
            return false;
        }
    }

    /**
     * Prepares a profile post comment for display.
     *
     * @param array $comment
     * @param array $UserNote
     * @param array $user
     * @param array|null $viewingUser
     *
     * @return array
     */
    public function prepareUserNoteComment(array $comment, array $UserNote, array $user, array $viewingUser = null)
    {
        $comment['canDelete'] = $this->canDeleteUserNoteComment($comment, $UserNote, $user, $null, $viewingUser);

        if (array_key_exists('user_group_id', $comment))
        {
            $comment = $this->getModelFromCache('XenForo_Model_User')->prepareUser($comment);
        }

        return $comment;
    }

    /**
     * Fetch view parameters for profile posts, primarily for meta controls like warn, view IPs etc.
     *
     * @param array $UserNotes
     * @param array $user (owner of the profile on which the profile posts are shown)
     * @param array $viewingUser
     *
     * @return array
     */
    public function getUserNoteViewParams(array $UserNotes, array $user, array $viewingUser = null)
    {
        $this->standardizeViewingUserReference($viewingUser);

        $userModel = $this->_getUserModel();

        return array(
            'UserNotes' => $UserNotes,
            'canViewIps'   => $userModel->canViewIps(),
            'canViewWarnings' => $userModel->canViewWarnings(),
            'canWarn' => $userModel->canWarnUser($user),
        );
    }

    /**
     * Attempts to update any instances of an old username in like_users with a new username
     *
     * @param integer $oldUserId
     * @param integer $newUserId
     * @param string $oldUsername
     * @param string $newUsername
     */
    public function batchUpdateLikeUser($oldUserId, $newUserId, $oldUsername, $newUsername)
    {
        $db = $this->_getDb();

        // note that xm_liked_content should have already been updated with $newUserId
        $db->query('
			UPDATE (
				SELECT content_id FROM xm_liked_content
				WHERE content_type = \'user_note\'
				AND like_user_id = ?
			) AS temp
			INNER JOIN xm_user_note AS user_note ON (user_note.user_note_id = temp.content_id)
			SET like_users = REPLACE(like_users, ' .
            $db->quote('i:' . $oldUserId . ';s:8:"username";s:' . strlen($oldUsername) . ':"' . $oldUsername . '";') . ', ' .
            $db->quote('i:' . $newUserId . ';s:8:"username";s:' . strlen($newUsername) . ':"' . $newUsername . '";') . ')
		', $newUserId);
    }

    public function alertTaggedMembers(
        array $UserNote, array $profileUser, array $tagged, array $alreadyAlerted = array(),
        $isComment = false, array $taggingUser = null
    )
    {
        $userIds = XenForo_Application::arrayColumn($tagged, 'user_id');
        $userIds = array_diff($userIds, $alreadyAlerted);
        $alertedUserIds = array();

        if (!$taggingUser)
        {
            $taggingUser = $UserNote;
        }

        if ($userIds)
        {
            $userModel = $this->_getUserModel();
            $users = $userModel->getUsersByIds($userIds, array(
                'join' => XenForo_Model_User::FETCH_USER_OPTION
                    | XenForo_Model_User::FETCH_USER_PROFILE
                    | XenForo_Model_User::FETCH_USER_PERMISSIONS
            ));
            foreach ($users AS $user)
            {
                if (isset($alertedUserIds[$user['user_id']]))
                {
                    continue;
                }

                $user['permissions'] = XenForo_Permission::unserializePermissions($user['global_permission_cache']);

                if ($user['user_id'] != $taggingUser['user_id']
                    && !$userModel->isUserIgnored($user, $UserNote['user_id'])
                    && !$userModel->isUserIgnored($user, $profileUser['user_id'])
                    && XenForo_Model_Alert::userReceivesAlert($user, 'user_note', 'tag')
                    && $this->canViewUserNoteAndContainer($UserNote, $profileUser, $null, $user)
                )
                {
                    $alertedUserIds[$user['user_id']] = true;

                    XenForo_Model_Alert::alert($user['user_id'],
                        $taggingUser['user_id'], $taggingUser['username'],
                        'user_note', $UserNote['user_note_id'],
                        $isComment ? 'tag_comment' : 'tag'
                    );
                }
            }
        }

        return array_keys($alertedUserIds);
    }

    /**
     * @return XenForo_Model_User
     */
    protected function _getUserModel()
    {
        return $this->getModelFromCache('XenForo_Model_User');
    }

    /**
     * @return XenForo_Model_UserProfile
     */
    protected function _getUserProfileModel()
    {
        return $this->getModelFromCache('XenForo_Model_UserProfile');
    }
}