<?php

/**
 * Model to handle inline mod-style actions on posts. Generally, these are simply
 * bulk actions. They can be applied to other circumstances if desired.
 *
 * @package XenForo_Post
 */
class XenForo_Model_InlineMod_Post extends XenForo_Model
{
	public $enableLogging = true;

	/**
	 * Determines if the selected post IDs can be deleted.
	 *
	 * @param array $postIds List of post IDs check
	 * @param string $deleteType The type of deletion being requested (soft or hard)
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canDeletePosts(array $postIds, $deleteType = 'soft', &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);
		return $this->canDeletePostsData($posts, $deleteType, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Determines if the selected posts can be deleted. This is a slightly more
	 * "internal" version of the canDeletePosts() function, as the required data
	 * must already be retrieved.
	 *
	 * @param array $posts List of information about posts to be deleted
	 * @param string $deleteType Type of deletion (soft or hard)
	 * @param array $threads List of information about threads the posts are in
	 * @param array $forums List of information about forums the threads/posts are in; must include unserialized permissions in 'nodePermissions' key
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canDeletePostsData(array $posts, $deleteType, array $threads, array $forums, &$errorKey = '', array $viewingUser = null)
	{
		// note: this cannot use _checkPermissionOnPosts because of extra param

		if (!$posts)
		{
			return true;
		}

		$this->standardizeViewingUserReference($viewingUser);

		$postModel = $this->_getPostModel();

		foreach ($posts AS $post)
		{
			list($thread, $forum) = $this->_getThreadAndForumFromPost($post, $threads, $forums);

			if (!$postModel->canDeletePost($post, $thread, $forum, $deleteType, $errorKey, $forum['nodePermissions'], $viewingUser))
			{
				return false;
			}
		}

		return true;
	}

	/**
	 * Deletes the specified posts if permissions are sufficient.
	 *
	 * @param array $postIds List of post IDs to delete
	 * @param array $options Options that control the delete. Supports deleteType (soft or hard).
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean True if permissions were ok
	 */
	public function deletePosts(array $postIds, array $options = array(), &$errorKey = '', array $viewingUser = null)
	{
		$options = array_merge(
			array(
				'deleteType' => '',
				'reason' => '',
				'authorAlert' => false,
				'authorAlertReason' => ''
			), $options
		);

		if (!$options['deleteType'])
		{
			throw new XenForo_Exception('No deletion type specified.');
		}

		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);

		if (empty($options['skipPermissions']) && !$this->canDeletePostsData($posts, $options['deleteType'], $threads, $forums, $errorKey, $viewingUser))
		{
			return false;
		}

		$skipThreads = array();

		foreach ($posts AS $post)
		{
			if (!empty($skipThreads[$post['thread_id']]))
			{
				continue;
			}

			list($thread, $forum) = $this->_getThreadAndForumFromPost($post, $threads, $forums);

			$dw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post', XenForo_DataWriter::ERROR_SILENT);
			$dw->setExistingData($post);
			if (!$dw->get('post_id'))
			{
				// this may happen if the post was already removed
				continue;
			}

			$dw->setExtraData(XenForo_DataWriter_DiscussionMessage_Post::DATA_FORUM, $forum);

			if ($options['deleteType'] == 'hard')
			{
				$dw->delete();

				if ($dw->discussionDeleted())
				{
					if ($this->enableLogging)
					{
						XenForo_Model_Log::logModeratorAction('thread', $thread, 'delete_hard');
					}

					// all posts in this thread have been removed, so skip them
					$skipThreads[$post['thread_id']] = true;
				}
				else
				{
					if ($this->enableLogging)
					{
						XenForo_Model_Log::logModeratorAction('post', $post, 'delete_hard', array(), $thread);
					}
				}
			}
			else
			{
				$dw->setExtraData(XenForo_DataWriter_DiscussionMessage::DATA_DELETE_REASON, $options['reason']);
				$dw->set('message_state', 'deleted');
				$dw->save();

				if ($this->enableLogging)
				{
					XenForo_Model_Log::logModeratorAction(
						'post', $post, 'delete_soft', array('reason' => $options['reason']), $thread
					);
				}
			}

			if ($post['message_state'] == 'visible' && $options['authorAlert'])
			{
				$this->_getPostModel()->sendModeratorActionAlert(
					'delete', $post, $thread, $options['authorAlertReason']
				);
			}
		}

		return true;
	}

	/**
	 * Determines if the selected post IDs can be undeleted.
	 *
	 * @param array $postIds List of post IDs check
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canUndeletePosts(array $postIds, &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);
		return $this->canUndeletePostsData($posts, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Determines if the selected posts can be undeleted. This is a slightly more
	 * "internal" version of the canUndeletePosts() function, as the required data
	 * must already be retrieved.
	 *
	 * @param array $posts List of information about posts to be checked
	 * @param array $threads List of information about threads the posts are in
	 * @param array $forums List of information about forums the threads/posts are in; must include unserialized permissions in 'nodePermissions' key
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canUndeletePostsData(array $posts, array $threads, array $forums, &$errorKey = '', array $viewingUser = null)
	{
		return $this->_checkPermissionOnPosts('canUndeletePost', $posts, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Undeletes the specified posts if permissions are sufficient.
	 *
	 * @param array $postIds List of post IDs to undelete
	 * @param array $options Options that control the action. Nothing supported at this time.
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean True if permissions were ok
	 */
	public function undeletePosts(array $postIds, array $options = array(), &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);

		if (empty($options['skipPermissions']) && !$this->canUndeletePostsData($posts, $threads, $forums, $errorKey, $viewingUser))
		{
			return false;
		}

		$this->_updatePostsMessageState($posts, $threads, $forums, 'visible', 'deleted');

		return true;
	}

	/**
	 * Determines if the selected post IDs can be approved/unapproved.
	 *
	 * @param array $postIds List of post IDs check
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canApproveUnapprovePosts(array $postIds, &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);
		return $this->canApproveUnapprovePostsData($posts, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Determines if the selected posts can be approved/unapproved. This is a slightly more
	 * "internal" version of the canApproveUnapprovePosts() function, as the required data
	 * must already be retrieved.
	 *
	 * @param array $posts List of information about posts to be checked
	 * @param array $threads List of information about threads the posts are in
	 * @param array $forums List of information about forums the threads/posts are in; must include unserialized permissions in 'nodePermissions' key
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canApproveUnapprovePostsData(array $posts, array $threads, array $forums, &$errorKey = '', array $viewingUser = null)
	{
		return $this->_checkPermissionOnPosts('canApproveUnapprovePost', $posts, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Approves the specified posts if permissions are sufficient.
	 *
	 * @param array $postIds List of post IDs to approve
	 * @param array $options Options that control the action. Nothing supported at this time.
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean True if permissions were ok
	 */
	public function approvePosts(array $postIds, array $options = array(), &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);

		if (empty($options['skipPermissions']) && !$this->canApproveUnapprovePostsData($posts, $threads, $forums, $errorKey, $viewingUser))
		{
			return false;
		}

		$this->_updatePostsMessageState($posts, $threads, $forums, 'visible', 'moderated');

		return true;
	}

	/**
	 * Unapproves the specified posts if permissions are sufficient.
	 *
	 * @param array $postIds List of post IDs to unapprove
	 * @param array $options Options that control the action. Nothing supported at this time.
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean True if permissions were ok
	 */
	public function unapprovePosts(array $postIds, array $options = array(), &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);

		if (empty($options['skipPermissions']) && !$this->canApproveUnapprovePostsData($posts, $threads, $forums, $errorKey, $viewingUser))
		{
			return false;
		}

		$this->_updatePostsMessageState($posts, $threads, $forums, 'moderated', 'visible');

		return true;
	}

	/**
	 * Determines if the selected post IDs can be moved.
	 *
	 * @param array $postIds List of post IDs check
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canMovePosts(array $postIds, &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);
		return $this->canMovePostsData($posts, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Determines if the selected posts can be moved. This is a slightly more
	 * "internal" version of the canMovePosts() function, as the required data
	 * must already be retrieved.
	 *
	 * @param array $posts List of information about posts to be checked
	 * @param array $threads List of information about threads the posts are in
	 * @param array $forums List of information about forums the threads/posts are in; must include unserialized permissions in 'nodePermissions' key
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canMovePostsData(array $posts, array $threads, array $forums, &$errorKey = '', array $viewingUser = null)
	{
		return $this->_checkPermissionOnPosts('canMovePost', $posts, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Moves the specified posts if permissions are sufficient.
	 *
	 * @param array $postIds List of post IDs to move
	 * @param array $options Options that control the action. Nothing supported at this time.
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return array|false False if error; array of info about new thread otherwise
	 */
	public function movePosts(array $postIds, array $options = array(), &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);

		if (empty($options['skipPermissions']) && !$this->canMovePostsData($posts, $threads, $forums, $errorKey, $viewingUser))
		{
			return false;
		}

		return $this->_moveOrCopyPosts(
			'move', $posts, $threads, $forums, $options, $errorKey
		);
	}

	/**
	 * Determines if the selected post IDs can be copied.
	 *
	 * @param array $postIds List of post IDs check
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canCopyPosts(array $postIds, &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);
		return $this->canMovePostsData($posts, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Determines if the selected posts can be copied. This is a slightly more
	 * "internal" version of the canCopyPosts() function, as the required data
	 * must already be retrieved.
	 *
	 * @param array $posts List of information about posts to be checked
	 * @param array $threads List of information about threads the posts are in
	 * @param array $forums List of information about forums the threads/posts are in; must include unserialized permissions in 'nodePermissions' key
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canCopyPostsData(array $posts, array $threads, array $forums, &$errorKey = '', array $viewingUser = null)
	{
		return $this->_checkPermissionOnPosts('canCopyPost', $posts, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Copies the specified posts if permissions are sufficient.
	 *
	 * @param array $postIds List of post IDs to copy
	 * @param array $options Options that control the action. Nothing supported at this time.
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return array|false False if error; array of info about new thread otherwise
	 */
	public function copyPosts(array $postIds, array $options = array(), &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);

		if (empty($options['skipPermissions']) && !$this->canCopyPostsData($posts, $threads, $forums, $errorKey, $viewingUser))
		{
			return false;
		}

		return $this->_moveOrCopyPosts(
			'copy', $posts, $threads, $forums, $options, $errorKey
		);
	}

	protected function _moveOrCopyPosts($action, array $posts, array $threads, array $forums, array $options, &$errorKey = '')
	{
		$options = array_merge(
			array(
				'threadType' => 'new',
				'threadNodeId' => 0,
				'threadTitle' => '',
				'threadPrefixId' => 0,

				'threadId' => 0,

				'authorAlert' => false,
				'authorAlertReason' => ''
			),
			$options
		);

		if ($options['threadType'] == 'existing')
		{
			$newThread = $this->_getThreadModel()->getThreadById($options['threadId']);
			if (!$newThread)
			{
				return false;
			}
		}
		else
		{
			$forum = $this->_getForumModel()->getForumById($options['threadNodeId']);
			if (!$forum)
			{
				$errorKey = 'please_select_valid_forum';
				return false;
			}

			$newThread = array(
				'node_id' => $options['threadNodeId'],
				'title' => $options['threadTitle'],
				'prefix_id' => $options['threadPrefixId'],
			);
		}

		$actionOptions = array(
			'log' => $this->enableLogging,
			'authorAlert' => $options['authorAlert'],
			'authorAlertReason' => $options['authorAlertReason']
		);

		if ($action == 'move')
		{
			return $this->_getPostModel()->movePosts($posts, $threads, $newThread, $actionOptions);
		}
		else if ($action == 'copy')
		{
			return $this->_getPostModel()->copyPosts($posts, $threads, $newThread, $actionOptions);
		}
		else
		{
			return false;
		}
	}

	/**
	 * Determines if the selected post IDs can be merged.
	 *
	 * @param array $postIds List of post IDs check
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canMergePosts(array $postIds, &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);
		return $this->canMergePostsData($posts, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Determines if the selected posts can be merged. This is a slightly more
	 * "internal" version of the canMergePosts() function, as the required data
	 * must already be retrieved.
	 *
	 * @param array $posts List of information about posts to be checked
	 * @param array $threads List of information about threads the posts are in
	 * @param array $forums List of information about forums the threads/posts are in; must include unserialized permissions in 'nodePermissions' key
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	public function canMergePostsData(array $posts, array $threads, array $forums, &$errorKey = '', array $viewingUser = null)
	{
		if (count($posts) <= 1)
		{
			$errorKey = 'please_select_more_one_post_merge';
			return false;
		}

		return $this->_checkPermissionOnPosts('canMergePost', $posts, $threads, $forums, $errorKey, $viewingUser);
	}

	/**
	 * Merges the specified posts if permissions are sufficient.
	 *
	 * @param array $postIds List of post IDs to merge
	 * @param array $options Options that control the action. Nothing supported at this time.
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean True if permissions were ok
	 */
	public function mergePosts(array $postIds, array $options = array(), &$errorKey = '', array $viewingUser = null)
	{
		list($posts, $threads, $forums) = $this->getPostsAndParentData($postIds, $viewingUser);

		if (empty($options['skipPermissions']) && !$this->canMergePostsData($posts, $threads, $forums, $errorKey, $viewingUser))
		{
			return false;
		}

		$options = array_merge(
			array(
				'targetPostId' => 0,
				'newMessage' => '',
				'log' => $this->enableLogging
			),
			$options
		);

		return $this->_getPostModel()->mergePosts($posts, $threads, $options['targetPostId'], $options['newMessage'], $options);
	}

	/**
	 * Checks a standard post permission against a collection of posts.
	 * True is returned only if the action is possible on all posts.
	 *
	 * @param string $permissionMethod Name of the permission method to call in the post model
	 * @param array $posts List of posts to check
	 * @param array $threads List of threads the posts are in
	 * @param array $forums List of forums the threads are in
	 * @param string $errorKey Modified by reference. If no permission, may include a key of a phrase that gives more info
	 * @param array|null $viewingUser
	 *
	 * @return boolean
	 */
	protected function _checkPermissionOnPosts($permissionMethod, array $posts, array $threads, array $forums, &$errorKey = '', array $viewingUser = null)
	{
		if (!$posts)
		{
			return true;
		}

		$this->standardizeViewingUserReference($viewingUser);

		$postModel = $this->_getPostModel();

		foreach ($posts AS $post)
		{
			list($thread, $forum) = $this->_getThreadAndForumFromPost($post, $threads, $forums);

			if (!$postModel->$permissionMethod($post, $thread, $forum, $errorKey, $forum['nodePermissions'], $viewingUser))
			{
				return false;
			}
		}

		return true;
	}

	/**
	 * Internal helper to update the message_state of a collection of posts.
	 *
	 * @param array $posts Information about the posts to update
	 * @param array $threads Information about the threads that the posts are in
	 * @param array $forums Information about the forums that the threads/posts are in
	 * @param string $newState New message state (visible, moderated, deleted)
	 * @param string|false $expectedOldState If specified, only updates if the old state matches
	 */
	protected function _updatePostsMessageState(array $posts, array $threads, array $forums, $newState, $expectedOldState = false)
	{
		switch ($newState)
		{
			case 'visible':
				switch (strval($expectedOldState))
				{
					case 'visible': return;
					case 'moderated': $logAction = 'approve'; break;
					case 'deleted': $logAction = 'undelete'; break;
					default: $logAction = 'undelete'; break;
				}
				break;

			case 'moderated':
				switch (strval($expectedOldState))
				{
					case 'visible': $logAction = 'unapprove'; break;
					case 'moderated': return;
					case 'deleted': $logAction = 'unapprove'; break;
					default: $logAction = 'unapprove'; break;
				}
				break;

			case 'deleted':
				switch (strval($expectedOldState))
				{
					case 'visible': $logAction = 'delete_soft'; break;
					case 'moderated': $logAction = 'delete_soft'; break;
					case 'deleted': return;
					default: $logAction = 'delete_soft'; break;
				}
				break;

			default: return;
		}

		foreach ($posts AS $post)
		{
			list($thread, $forum) = $this->_getThreadAndForumFromPost($post, $threads, $forums);

			if ($post['post_id'] == $thread['first_post_id'])
			{
				$dw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread', XenForo_DataWriter::ERROR_SILENT);
				$dw->setExistingData($thread);

				if ($expectedOldState && $dw->get('discussion_state') != $expectedOldState)
				{
					continue;
				}

				$dw->set('discussion_state', $newState);
				$dw->setExtraData(XenForo_DataWriter_Discussion_Thread::DATA_FORUM, $forum);
				$dw->save();

				if ($this->enableLogging)
				{
					XenForo_Model_Log::logModeratorAction('thread', $thread, $logAction);
				}
			}
			else
			{
				if ($expectedOldState && $post['message_state'] != $expectedOldState)
				{
					continue;
				}

				$dw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post', XenForo_DataWriter::ERROR_SILENT);
				$dw->setExistingData($post);
				$dw->set('message_state', $newState);
				$dw->setExtraData(XenForo_DataWriter_DiscussionMessage_Post::DATA_FORUM, $forum);
				$dw->save();

				if ($this->enableLogging)
				{
					XenForo_Model_Log::logModeratorAction('post', $post, $logAction, array(), $thread);
				}
			}
		}
	}

	/**
	 * Gets information about the thread and forum a post belongs to,
	 * from the post's info.
	 *
	 * @param array $post Info about the post
	 * @param array $threads List of threads that the post could belong to
	 * @param array $forums List of forums that the post's thread could be long to
	 *
	 * @return array Format: [0] => thread, [1] => forum
	 */
	protected function _getThreadAndForumFromPost(array $post, array $threads, array $forums)
	{
		$thread = $threads[$post['thread_id']];
		$forum = $forums[$thread['node_id']];

		return array($thread, $forum);
	}

	/**
	 * From a list of post IDs, gets info about the posts, their threads, and
	 * the forums the threads are in.
	 *
	 * @param array $postIds List of post IDs
	 * @param array|null $viewingUser
	 *
	 * @return array Format: [0] => list of posts, [1] => list of threads, [2] => list of forums
	 */
	public function getPostsAndParentData(array $postIds, array $viewingUser = null)
	{
		$this->standardizeViewingUserReference($viewingUser);
		return $this->_getPostModel()->getPostsAndParentData($postIds, $viewingUser['permission_combination_id']);
	}

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

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

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