<?php

/**
* Data writer for discussion.
*
* @package XenForo_Discussion
*/
abstract class XenForo_DataWriter_DiscussionMessage extends XenForo_DataWriter
{
	/**
	 * Gets the object that represents the definition of this type of message.
	 *
	 * @return XenForo_DiscussionMessage_Definition_Abstract
	 */
	abstract public function getDiscussionMessageDefinition();

	/**
	 * Meta-option for discussion message submissions that are via automated means.
	 * Some dynamic checks are skipped (eg, message length and flood check), and
	 * other data is ignored (eg, IP address).
	 */
	const OPTION_IS_AUTOMATED = 'isAutomated';

	/**
	 * Option that controls whether an IP address should be recorded for this message.
	 * Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_SET_IP_ADDRESS = 'setIpAddress';

	/**
	 * Option that controls whether data in the parent discussion should be updated,
	 * including reply counts and last post info. Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_UPDATE_PARENT_DISCUSSION = 'updateParentDiscussion';

	/**
	 * Option that controls whether the data in this message should be indexed for
	 * search. If this value is set inconsistently for the same message, data might
	 * be orphaned in the search index.
	 *
	 * @var string
	 */
	const OPTION_INDEX_FOR_SEARCH = 'indexForSearch';

	/**
	 * Option that controls the maximum number of characters that are allowed in
	 * a message.
	 *
	 * @var string
	 */
	const OPTION_MAX_MESSAGE_LENGTH = 'maxMessageLength';

	/**
	 * Maximum number of images allowed in a message.
	 *
	 * @var string
	 */
	const OPTION_MAX_IMAGES = 'maxImages';

	/**
	 * Maximum pieces of media allowed in a message.
	 *
	 * @var string
	 */
	const OPTION_MAX_MEDIA = 'maxMedia';

	/**
	 * Option that controls whether the posting user's message count will be
	 * changed by posting this message.
	 *
	 * @var string
	 */
	const OPTION_CHANGE_USER_MESSAGE_COUNT = 'changeUserMessageCount';

	/**
	 * Option that controls whether a guest user name is checked to confirm that it
	 * doesn't conflict with a valid user. Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_VERIFY_GUEST_USERNAME = 'verifyGuestUsername';

	/**
	 * Option that controls whether this should be published in the news feed. Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_PUBLISH_FEED = 'publishFeed';

	/**
	 * Option that controls whether the discussion is removed if deleting the first message.
	 * Note that if the discussion only has the first message and it's deleted, it will always be removed.
	 *
	 * @var string
	 */
	const OPTION_DELETE_DISCUSSION_FIRST_MESSAGE = 'deleteDiscussionFirstMessage';

	/**
	 * Option that controls whether edits are logged via the edit date, if supported. Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_UPDATE_EDIT_DATE = 'logEditDate';


	/**
	 * Option that controls how long (in seconds) the delay is before an edit log will be added (if supported/enabled).
	 *
	 * @var string
	 */
	const OPTION_EDIT_DATE_DELAY = 'editDateDelay';

	const OPTION_LOG_EDIT = 'logEdit';

	/**
	 * Holds the temporary hash used to pull attachments and associate them with this message.
	 *
	 * @var string
	 */
	const DATA_ATTACHMENT_HASH = 'attachmentHash';

	/**
	 * Holds the reason for soft deletion.
	 *
	 * @var string
	 */
	const DATA_DELETE_REASON = 'deleteReason';

	/**
	 * Default value for the change user message count option.
	 *
	 * @var boolean
	 */
	protected $_defaultChangeUserMessageCount = true;

	/**
	* Identifies if a message has a parent discussion item.
	* Must overload {@see getDiscussionDataWriter()} if set to true.
	*
	* @var boolean
	*/
	protected $_hasParentDiscussion = true;

	/**
	 * Data about the message's definition.
	 *
	 * @var XenForo_DiscussionMessage_Definition_Abstract
	 */
	protected $_messageDefinition = null;

	/**
	 * Reflects important changes to the parent discussion. Current values:
	 * 	* [empty] - no change of value
	 *  * delete - discussion has been deleted
	 *  * firstDelete - first message has been deleted, restructure has been carried out
	 *
	 * @var string
	 */
	protected $_discussionChange = '';

	/**
	 * The discussion data writer.
	 *
	 * @var XenForo_DataWriter_Discussion
	 */
	protected $_discussionDw = null;

	/**
	 * The insert/update mode of the discussion data writer
	 *
	 * @var string insert|update
	 */
	protected $_discussionMode = null;

	/**
	 * Controls whether edit history/logging is supported by this content type.
	 *
	 * @var boolean
	 */
	protected $_supportEditLog = false;

	/**
	* Constructor.
	*
	* @param mixed  $errorHandler  Error handler. See {@link ERROR_EXCEPTION} and related.
	* @param array|null $inject Dependency injector. Array keys available: db, cache.
	*/
	public function __construct($errorHandler = self::ERROR_EXCEPTION, array $inject = null)
	{
		$this->_messageDefinition = $this->getDiscussionMessageDefinition();

		$config = $this->_messageDefinition->getMessageConfiguration();
		$this->_hasParentDiscussion = $config['hasParentDiscussion'];
		$this->_defaultChangeUserMessageCount = $config['changeUserMessageCount'];

		parent::__construct($errorHandler, $inject);
	}

	/**
	* Gets the fields that are defined for the table. See parent for explanation.
	*
	* @return array
	*/
	protected function _getCommonFields()
	{
		$structure = $this->_messageDefinition->getMessageStructure();

		$fields = array(
			$structure['table'] => array(
				$structure['key']        => array('type' => self::TYPE_UINT, 'autoIncrement' => true),
				$structure['container']  => array('type' => self::TYPE_UINT, 'required' => true),
				'user_id'                => array('type' => self::TYPE_UINT,   'required' => true),
				'username'               => array('type' => self::TYPE_STRING, 'required' => true, 'maxLength' => 50,
					'requiredError' => 'please_enter_valid_name'
				),
				'post_date'              => array('type' => self::TYPE_UINT,   'required' => true, 'default' => XenForo_Application::$time),
				'message'                => array('type' => self::TYPE_STRING, 'required' => true,
					'requiredError' => 'please_enter_valid_message'
				),
				'ip_id'                  => array('type' => self::TYPE_UINT,   'default' => 0),
				'message_state'          => array('type' => self::TYPE_STRING, 'default' => 'visible',
					'allowedValues' => array('visible', 'moderated', 'deleted')
				),
				'attach_count'           => array('type' => self::TYPE_UINT_FORCED, 'default' => 0, 'max' => 65535),
				'likes'                  => array('type' => self::TYPE_UINT_FORCED, 'default' => 0),
				'like_users'             => array('type' => self::TYPE_SERIALIZED, 'default' => 'a:0:{}'),
				'warning_id'             => array('type' => self::TYPE_UINT,   'default' => 0),
				'warning_message'        => array('type' => self::TYPE_STRING, 'default' => ''),
			)
		);

		if ($this->_hasParentDiscussion)
		{
			$fields[$structure['table']]['position'] = array('type' => self::TYPE_UINT_FORCED);
		}
		if ($this->_supportEditLog)
		{
			$fields[$structure['table']]['last_edit_date'] = array('type' => self::TYPE_UINT, 'default' => 0);
			$fields[$structure['table']]['last_edit_user_id'] = array('type' => self::TYPE_UINT, 'default' => 0);
			$fields[$structure['table']]['edit_count'] = array('type' => self::TYPE_UINT_FORCED, 'default' => 0);
		}

		return $fields;
	}

	/**
	* Gets SQL condition to update the existing record.
	*
	* @return string
	*/
	protected function _getUpdateCondition($tableName)
	{
		$keyName = $this->getDiscussionMessageKeyName();

		return $keyName . ' = ' . $this->_db->quote($this->getExisting($keyName));
	}

	/**
	* Gets the default set of options for this data writer.
	*
	* @return array
	*/
	protected function _getDefaultOptions()
	{
		$options = XenForo_Application::get('options');

		return array(
			self::OPTION_SET_IP_ADDRESS => true,
			self::OPTION_UPDATE_PARENT_DISCUSSION => true,
			self::OPTION_INDEX_FOR_SEARCH => true,
			self::OPTION_MAX_MESSAGE_LENGTH => $options->messageMaxLength,
			self::OPTION_MAX_IMAGES => $options->messageMaxImages,
			self::OPTION_MAX_MEDIA => $options->messageMaxMedia,
			self::OPTION_CHANGE_USER_MESSAGE_COUNT => $this->_defaultChangeUserMessageCount,
			self::OPTION_VERIFY_GUEST_USERNAME => true,
			self::OPTION_PUBLISH_FEED => true,
			self::OPTION_DELETE_DISCUSSION_FIRST_MESSAGE => true,
			self::OPTION_UPDATE_EDIT_DATE => true,
			self::OPTION_LOG_EDIT => $options->editHistory['enabled'],
			self::OPTION_EDIT_DATE_DELAY => $options->editLogDisplay['enabled'] ? $options->editLogDisplay['delay'] * 60 : -1
		);

		// note: OPTION_IS_AUTOMATED is not here, see setOption()
	}

	/**
	 * Sets an option. If the IS_AUTOMATED option is specified, other options are
	 * set instead.
	 *
	 * @param string $name
	 * @param mixed $value
	 */
	public function setOption($name, $value)
	{
		if ($name === self::OPTION_IS_AUTOMATED)
		{
			if ($value)
			{
				parent::setOption(self::OPTION_SET_IP_ADDRESS, false);
				parent::setOption(self::OPTION_MAX_MESSAGE_LENGTH, 0);
				parent::setOption(self::OPTION_MAX_IMAGES, 0);
				parent::setOption(self::OPTION_MAX_MEDIA, 0);
			}
		}
		else
		{
			parent::setOption($name, $value);
		}
	}

	/**
	* Generic discussion message pre-save handler.
	*/
	protected final function _preSave()
	{
		if ($this->isInsert()
			&& !$this->get('user_id') && $this->isChanged('username')
			&& $this->getOption(self::OPTION_VERIFY_GUEST_USERNAME)
		)
		{
			$userDw = XenForo_DataWriter::create('XenForo_DataWriter_User', XenForo_DataWriter::ERROR_ARRAY);
			$userDw->set('username', $this->get('username'));
			$userErrors = $userDw->getErrors();
			if ($userErrors)
			{
				$this->error(reset($userErrors), 'username');
			}
		}

		if ($this->isInsert() && !$this->isChanged('message_state'))
		{
			$this->set('message_state', 'visible');
		}
		if ($this->isInsert() && !$this->isChanged('post_date'))
		{
			$this->set('post_date', XenForo_Application::$time);
		}

		if ($this->isChanged('message'))
		{
			$this->_checkMessageValidity();
		}

		if ($this->_hasParentDiscussion)
		{
			$this->_checkFirstMessageState();

			if (!$this->isChanged('position'))
			{
				$this->_setPosition();
			}
		}

		if ($this->_supportEditLog)
		{
			if ($this->isUpdate() && $this->isChanged('message'))
			{
				if ($this->getOption(self::OPTION_UPDATE_EDIT_DATE)
					&& !$this->isChanged('last_edit_date')
					&& $this->getOption(self::OPTION_EDIT_DATE_DELAY) >= 0
				)
				{
					if ($this->get('post_date') + $this->getOption(self::OPTION_EDIT_DATE_DELAY) <= XenForo_Application::$time)
					{
						$this->set('last_edit_date', XenForo_Application::$time);
						if (!$this->isChanged('last_edit_user_id'))
						{
							$this->set('last_edit_user_id', XenForo_Visitor::getUserId());
						}
					}
				}

				if (!$this->isChanged('edit_count'))
				{
					$this->set('edit_count', $this->get('edit_count') + 1);
				}
			}
			if ($this->isChanged('edit_count') && $this->get('edit_count') == 0)
			{
				$this->set('last_edit_date', 0);
			}
			if (!$this->get('last_edit_date'))
			{
				$this->set('last_edit_user_id', 0);
			}
		}

		$this->_messagePreSave();
	}

	/**
	 * Check that the contents of the message are valid, based on length, images, etc.
	 */
	protected function _checkMessageValidity()
	{
		$message = $this->get('message');

		$maxLength = $this->getOption(self::OPTION_MAX_MESSAGE_LENGTH);
		if ($maxLength && utf8_strlen($message) > $maxLength)
		{
			$this->error(new XenForo_Phrase('please_enter_message_with_no_more_than_x_characters', array('count' => $maxLength)), 'message');
		}
		else
		{
			$maxImages = $this->getOption(self::OPTION_MAX_IMAGES);
			$maxMedia = $this->getOption(self::OPTION_MAX_MEDIA);
			if ($maxImages || $maxMedia)
			{
				$formatter = XenForo_BbCode_Formatter_Base::create('ImageCount', false);
				$parser = XenForo_BbCode_Parser::create($formatter);
				$parser->render($message);

				if ($maxImages && $formatter->getImageCount() > $maxImages)
				{
					$this->error(new XenForo_Phrase('please_enter_message_with_no_more_than_x_images', array('count' => $maxImages)), 'message');
				}
				if ($maxMedia && $formatter->getMediaCount() > $maxMedia)
				{
					$this->error(new XenForo_Phrase('please_enter_message_with_no_more_than_x_media', array('count' => $maxMedia)), 'message');
				}
			}
		}
	}

	/**
	 * Sets the message's position within the discussion.
	 */
	protected function _setPosition()
	{
		if ($this->isInsert() || $this->isChanged('message_state'))
		{
			$discussionDw = $this->getDiscussionDataWriter();
			if ($this->isInsert())
			{
				if (!$discussionDw || $discussionDw->isInsert())
				{
					// likely the first message
					$this->set('position', 0);
				}
				else
				{
					if ($this->get('post_date') < $discussionDw->get('last_post_date'))
					{
						// TODO: this doesn't deal with inserting a message in the middle of a discussion
						throw new XenForo_Exception('Cannot insert a message in the middle of a discussion.');
					}

					$discussion = $discussionDw->getDiscussionForUpdate();
					$replyCount = ($discussion && isset($discussion['reply_count'])
						? $discussion['reply_count']
						: $discussionDw->get('reply_count')
					);

					if ($this->get('message_state') == 'visible')
					{
						$this->set('position', $replyCount + 1);
					}
					else
					{
						$this->set('position', $replyCount);
					}
				}
			}
			else
			{
				// updated the state on an existing message -- need to slot in
				if ($this->get('message_state') == 'visible' && $this->getExisting('message_state') != 'visible')
				{
					$this->set('position', $this->get('position') + 1);
				}
				else if ($this->get('message_state') != 'visible' && $this->getExisting('message_state') == 'visible')
				{
					$this->set('position', $this->get('position') - 1);
				}
			}
		}
	}

	/**
	 * Checks that the first message has a valid state. Normally,
	 * the first message must always be visible. If a non-visible
	 * first message is required, change the discussion.
	 */
	protected function _checkFirstMessageState()
	{
		if ($this->get('message_state') != 'visible')
		{
			if ($this->isUpdate() && $this->get('position') > 0)
			{
				// position > 0 means this isn't the first post
				return;
			}

			$discussionDw = $this->getDiscussionDataWriter();
			if (!$discussionDw)
			{
				// debugging message, no need for phrasing
				$this->error('The first message of a discussion must be visible.');
			}
			else if ($discussionDw->isInsert() || $discussionDw->get('first_post_id') == $this->getDiscussionMessageId())
			{
				// treat first message state as the discussion state
				$discussionDw->set('discussion_state', $this->get('message_state'));
				$this->set('message_state', 'visible');

				// in case we're deleting the discussion, push the reason up
				$discussionDw->setExtraData(XenForo_DataWriter_Discussion::DATA_DELETE_REASON,
					$this->getExtraData(self::DATA_DELETE_REASON)
				);
			}
		}
	}

	/**
	* Designed to be overridden by child classes
	*/
	protected function _messagePreSave()
	{
	}

	/**
	* Generic discussion message post-save handler.
	*/
	protected final function _postSave()
	{
		if ($this->isInsert() && $this->getOption(self::OPTION_SET_IP_ADDRESS) && !$this->get('ip_id'))
		{
			$this->_updateIpData();
		}

		$attachmentHash = $this->getExtraData(self::DATA_ATTACHMENT_HASH);
		if ($attachmentHash)
		{
			$this->_associateAttachments($attachmentHash);
		}

		if ($this->_hasParentDiscussion && $this->getOption(self::OPTION_UPDATE_PARENT_DISCUSSION))
		{
			$this->_updateDiscussionPostSave();
		}

		if ($this->_hasParentDiscussion && $this->isUpdate() && $this->isChanged('message_state'))
		{
			$this->_updateMessagePositionList();
		}

		$this->_updateDeletionLog();
		$this->_updateModerationQueue();
		$this->_submitSpamLog();

		if ($this->get('user_id') && $this->isChanged('message_state')
		)
		{
			if ($this->getOption(self::OPTION_CHANGE_USER_MESSAGE_COUNT))
			{
				$this->_updateUserMessageCount();
			}

			if ($this->get('likes'))
			{
				$this->_updateUserLikeCount();
			}
		}

		if ($this->_supportEditLog
			&& $this->getOption(self::OPTION_LOG_EDIT)
			&& $this->isUpdate() && $this->isChanged('edit_count') && $this->get('edit_count'))
		{
			$this->_insertEditHistory();
		}

		if ($this->isUpdate() && $this->isChanged('message'))
		{
			$this->getModelFromCache('XenForo_Model_BbCode')->deleteBbCodeParseCacheForContent(
				$this->getContentType(), $this->getDiscussionMessageId()
			);
		}

		if ($this->getOption(self::OPTION_INDEX_FOR_SEARCH))
		{
			$this->_indexForSearch();
		}

		$this->_publishAndNotify();

		$this->_messagePostSave();

		$this->_saveDiscussionDataWriter();
	}

	/**
	 * Updates the discussion container info.
	 */
	protected function _updateDiscussionPostSave()
	{
		$discussionDw = $this->getDiscussionDataWriter();
		if ($discussionDw && $discussionDw->isUpdate())
		{
			$discussionDw->updateCountersAfterMessageSave($this);
		}
	}

	/**
	 * Updates the position list based on state changes.
	 */
	protected function _updateMessagePositionList()
	{
		if ($this->get('message_state') == 'visible' && $this->getExisting('message_state') != 'visible')
		{
			$this->_adjustPositionListForInsert();
		}
		else if ($this->get('message_state') != 'visible' && $this->getExisting('message_state') == 'visible')
		{
			$this->_adjustPositionListForRemoval();
		}
	}

	/**
	* Upates the IP data.
	*/
	protected function _updateIpData()
	{
		if (!empty($this->_extraData['ipAddress']))
		{
			$ipAddress = $this->_extraData['ipAddress'];
		}
		else
		{
			$ipAddress = null;
		}

		$ipId = XenForo_Model_Ip::log(
			$this->get('user_id'), $this->getContentType(), $this->getDiscussionMessageId(), 'insert', $ipAddress
		);
		$this->set('ip_id', $ipId, '', array('setAfterPreSave' => true));

		// TODO: ideally, this can be consolidated with other post-save message updates (see associateAttachments)
		$this->_db->update($this->getDiscussionMessageTableName(), array(
			'ip_id' => $ipId
		), $this->getDiscussionMessageKeyName() . ' = ' .  $this->_db->quote($this->getDiscussionMessageId()));
	}

	/**
	 * Associates attachments with this message.
	 *
	 * @param string $attachmentHash
	 */
	protected function _associateAttachments($attachmentHash)
	{
		$rows = $this->_db->update('xf_attachment', array(
			'content_type' => $this->getContentType(),
			'content_id' => $this->getDiscussionMessageId(),
			'temp_hash' => '',
			'unassociated' => 0
		), 'temp_hash = ' . $this->_db->quote($attachmentHash));
		if ($rows)
		{
			// TODO: ideally, this can be consolidated with other post-save message updates (see updateIpData)
			$this->set('attach_count', $this->get('attach_count') + $rows, '', array('setAfterPreSave' => true));

			$this->_db->update($this->getDiscussionMessageTableName(), array(
				'attach_count' => $this->get('attach_count')
			), $this->getDiscussionMessageKeyName() . ' = ' .  $this->_db->quote($this->getDiscussionMessageId()));
		}
	}

	/**
	 * Updates the deletion log if necessary.
	 */
	protected function _updateDeletionLog()
	{
		if (!$this->isChanged('message_state'))
		{
			return;
		}

		if ($this->get('message_state') == 'deleted')
		{
			$reason = $this->getExtraData(self::DATA_DELETE_REASON);
			$this->getModelFromCache('XenForo_Model_DeletionLog')->logDeletion(
				$this->getContentType(), $this->getDiscussionMessageId(), $reason
			);
		}
		else if ($this->getExisting('message_state') == 'deleted')
		{
			$this->getModelFromCache('XenForo_Model_DeletionLog')->removeDeletionLog(
				$this->getContentType(), $this->getDiscussionMessageId()
			);
		}
	}

	/**
	 * Updates the moderation queue if necessary.
	 */
	protected function _updateModerationQueue()
	{
		if (!$this->isChanged('message_state'))
		{
			return;
		}

		if ($this->get('message_state') == 'moderated' )
		{
			$this->getModelFromCache('XenForo_Model_ModerationQueue')->insertIntoModerationQueue(
				$this->getContentType(), $this->getDiscussionMessageId(), $this->get('post_date')
			);
		}
		else if ($this->getExisting('message_state') == 'moderated')
		{
			$this->getModelFromCache('XenForo_Model_ModerationQueue')->deleteFromModerationQueue(
				$this->getContentType(), $this->getDiscussionMessageId()
			);
		}
	}

	protected function _submitSpamLog()
	{
		if ($this->getExisting('message_state') == 'moderated' && $this->get('message_state') == 'visible')
		{
			/** @var $spamModel XenForo_Model_SpamPrevention */
			$spamModel = $this->getModelFromCache('XenForo_Model_SpamPrevention');
			$spamModel->submitHamCommentData($this->getContentType(), $this->getDiscussionMessageId());
		}
	}

	/**
	 * Updates the search index for this message.
	 */
	protected function _indexForSearch()
	{
		if ($this->get('message_state') == 'visible')
		{
			if ($this->getExisting('message_state') != 'visible' || $this->isChanged('message'))
			{
				$this->_insertOrUpdateSearchIndex();
			}
		}
		else if ($this->isUpdate() && $this->get('message_state') != 'visible' && $this->getExisting('message_state') == 'visible')
		{
			$this->_deleteFromSearchIndex();
		}
	}

	/**
	 * Inserts or updates a record in the search index for this message.
	 */
	protected function _insertOrUpdateSearchIndex()
	{
		$dataHandler = $this->_messageDefinition->getSearchDataHandler();
		if (!$dataHandler)
		{
			return;
		}

		$indexer = new XenForo_Search_Indexer();
		$dataHandler->insertIntoIndex($indexer, $this->getMergedData(), $this->getDiscussionData());
	}

	/**
	 * Inserts a record for the edit history.
	 */
	protected function _insertEditHistory()
	{
		$historyDw = XenForo_DataWriter::create('XenForo_DataWriter_EditHistory', XenForo_DataWriter::ERROR_SILENT);
		$historyDw->bulkSet(array(
			'content_type' => $this->getContentType(),
			'content_id' => $this->getDiscussionMessageId(),
			'edit_user_id' => XenForo_Visitor::getUserId(),
			'old_text' => $this->getExisting('message')
		));
		$historyDw->save();
	}

	/**
	* Designed to be overridden by child classes
	*/
	protected function _messagePostSave()
	{
	}

	/**
	 * Generic discussion message pre-delete handler.
	 */
	protected final function _preDelete()
	{
		$this->_messagePreDelete();
	}

	/**
	* Designed to be overridden by child classes
	*/
	protected function _messagePreDelete()
	{
	}

	/**
	 * Generic discussion message post-delete handler.
	 */
	protected final function _postDelete()
	{
		if ($this->_hasParentDiscussion && $this->getOption(self::OPTION_UPDATE_PARENT_DISCUSSION))
		{
			$this->_updateDiscussionPostDelete();
		}

		// firstDelete would trigger this
		if (!$this->discussionDeleted())
		{
			if ($this->_hasParentDiscussion)
			{
				$this->_adjustPositionListForRemoval();
			}
		}

		$this->getModelFromCache('XenForo_Model_DeletionLog')->removeDeletionLog(
			$this->getContentType(), $this->getDiscussionMessageId()
		);
		$this->getModelFromCache('XenForo_Model_ModerationQueue')->deleteFromModerationQueue(
			$this->getContentType(), $this->getDiscussionMessageId()
		);
		$this->getModelFromCache('XenForo_Model_EditHistory')->deleteEditHistoryForContent(
			$this->getContentType(), $this->getDiscussionMessageId()
		);
		$this->getModelFromCache('XenForo_Model_BbCode')->deleteBbCodeParseCacheForContent(
			$this->getContentType(), $this->getDiscussionMessageId()
		);

		if ($this->get('attach_count'))
		{
			$this->_deleteAttachments();
		}

		if ($this->get('user_id'))
		{
			if ($this->getOption(self::OPTION_CHANGE_USER_MESSAGE_COUNT))
			{
				$this->_updateUserMessageCount(true);
			}
		}

		if ($this->getOption(self::OPTION_INDEX_FOR_SEARCH))
		{
			$this->_deleteFromSearchIndex();
		}

		if ($this->get('likes'))
		{
			$this->_deleteLikes();
		}

		$this->_messagePostDelete();

		$this->_deleteFromNewsFeed();

		if (!$this->discussionDeleted())
		{
			$this->_saveDiscussionDataWriter();
		}
	}

	/**
	 * Updates data in the discussion after the message is deleted.
	 * This may cause the entire discussion to be deleted.
	 */
	protected function _updateDiscussionPostDelete()
	{
		$discussionDw = $this->getDiscussionDataWriter();
		if ($discussionDw)
		{
			$deleteIfFirst = $this->getOption(self::OPTION_DELETE_DISCUSSION_FIRST_MESSAGE);
			$discussionChange = $discussionDw->updateCountersAfterMessageDelete($this, $deleteIfFirst);
			$this->_discussionChange = $discussionChange;

			if ($discussionChange == 'delete')
			{
				$discussionDw->delete();
			}
		}
	}

	/**
	 * Deletes the attachments associated with this message.
	 */
	protected function _deleteAttachments()
	{
		$this->getModelFromCache('XenForo_Model_Attachment')->deleteAttachmentsFromContentIds(
			$this->getContentType(),
			array($this->getDiscussionMessageId())
		);
	}

	/**
	 * Updates the user message count.
	 *
	 * @param boolean $isDelete True if hard deleting the message
	 */
	protected function _updateUserMessageCount($isDelete = false)
	{
		if ($this->_hasParentDiscussion)
		{
			$discussionDw = $this->getDiscussionDataWriter();
			if ($discussionDw && $discussionDw->get('discussion_state') != 'visible')
			{
				return;
			}
		}

		if ($this->getExisting('message_state') == 'visible'
			&& ($this->get('message_state') != 'visible' || $isDelete)
		)
		{
			$this->_db->query('
				UPDATE xf_user
				SET message_count = IF(message_count > 0, message_count - 1, 0)
				WHERE user_id = ?
			', $this->get('user_id'));
		}
		else if ($this->get('message_state') == 'visible' && $this->getExisting('message_state') != 'visible')
		{
			$this->_db->query('
				UPDATE xf_user
				SET message_count = message_count + 1
				WHERE user_id = ?
			', $this->get('user_id'));
		}
	}

	/**
	 * Updates the user like count.
	 *
	 * @param boolean $isDelete True if hard deleting the message
	 */
	protected function _updateUserLikeCount($isDelete = false)
	{
		$likes = $this->get('likes');
		if (!$likes)
		{
			return;
		}

		if ($this->_hasParentDiscussion)
		{
			$discussionDw = $this->getDiscussionDataWriter();
			if ($discussionDw && $discussionDw->get('discussion_state') != 'visible')
			{
				return;
			}
		}

		if ($this->getExisting('message_state') == 'visible'
			&& ($this->get('message_state') != 'visible' || $isDelete)
		)
		{
			$this->_db->query('
				UPDATE xf_user
				SET like_count = IF(like_count > ?, like_count - ?, 0)
				WHERE user_id = ?
			', array($likes, $likes, $this->get('user_id')));
		}
		else if ($this->get('message_state') == 'visible' && $this->getExisting('message_state') != 'visible')
		{
			$this->_db->query('
				UPDATE xf_user
				SET like_count = like_count + ?
				WHERE user_id = ?
			', array($likes, $this->get('user_id')));
		}
	}

	/**
	 * Deletes this message from the search index.
	 */
	protected function _deleteFromSearchIndex()
	{
		$dataHandler = $this->_messageDefinition->getSearchDataHandler();
		if (!$dataHandler)
		{
			return;
		}

		$indexer = new XenForo_Search_Indexer();
		$dataHandler->deleteFromIndex($indexer, $this->getMergedData());
	}

	/**
	 * Delete all like entries for content.
	 */
	protected function _deleteLikes()
	{
		$updateUserLikeCounter = ($this->get('message_state') == 'visible');

		if ($updateUserLikeCounter && $this->_hasParentDiscussion)
		{
			$discussionDw = $this->getDiscussionDataWriter();
			if ($discussionDw && $discussionDw->get('discussion_state') != 'visible')
			{
				$updateUserLikeCounter = false;
			}
		}

		$this->getModelFromCache('XenForo_Model_Like')->deleteContentLikes(
			$this->getContentType(), $this->getDiscussionMessageId(), $updateUserLikeCounter
		);
	}

	/**
	* Designed to be overridden by child classes
	*/
	protected function _messagePostDelete()
	{
	}

	/**
	 * Adjust the position list surrounding this message, when this message
	 * has been put from a position that "counts" (removed or hidden).
	 */
	protected function _adjustPositionListForInsert()
	{
		if ($this->get('message_state') != 'visible')
		{
			// only renumber if becoming visible
			return;
		}

		$containerKey = $this->getContainerKeyName();
		$containerCondition = "$containerKey = " . $this->get($containerKey);

		$positionQuoted = $this->_db->quote($this->getExisting('position'));
		$postDateQuoted = $this->_db->quote($this->get('post_date'));
		$messageKeyCondition = $this->getDiscussionMessageKeyName() . ' <> ' . $this->_db->quote($this->getDiscussionMessageId());

		$this->_db->query("
			UPDATE " . $this->getDiscussionMessageTableName() . "
			SET position = position + 1
			WHERE $containerCondition
				AND (position > $positionQuoted
					OR (position = $positionQuoted AND post_date > $postDateQuoted)
				)
				AND $messageKeyCondition
		");
	}

	/**
	 * Adjust the position list surrounding this message, when this message
	 * has been removed from a position that "counts" (removed or hidden).
	 */
	protected function _adjustPositionListForRemoval()
	{
		if ($this->getExisting('message_state') != 'visible')
		{
			// no need to renumber after removal something that didn't count
			return;
		}

		$containerKey = $this->getContainerKeyName();
		$containerCondition = "$containerKey = " . $this->get($containerKey);

		$messageKeyCondition = $this->getDiscussionMessageKeyName() . ' <> ' . $this->_db->quote($this->getDiscussionMessageId());

		$this->_db->query('
			UPDATE ' . $this->getDiscussionMessageTableName() . '
			SET position = IF(position > 0, position - 1, 0)
			WHERE ' . $containerCondition . '
				AND position >= ?
				AND ' . $messageKeyCondition . '
		', $this->getExisting('position'));
	}

	/**
	 * Gets the discussion data writer. Note that if the container value changes,
	 * this cache will not be removed.
	 *
	 * @return XenForo_DataWriter_Discussion|false
	 */
	public function getDiscussionDataWriter()
	{
		if (!$this->_hasParentDiscussion)
		{
			return false;
		}

		if ($this->_discussionDw === null)
		{
			$containerId = $this->get($this->getContainerKeyName());
			if (!$containerId)
			{
				$this->_discussionDw = false;
			}
			else
			{
				$this->_discussionDw = $this->_messageDefinition->getDiscussionDataWriter($containerId, $this->_errorHandler);
				if ($this->_discussionDw && $this->_discussionMode === null)
				{
					$this->_discussionMode = 'update';
				}
			}
		}

		return $this->_discussionDw;
	}

	/**
	 * Sets the data writer for the discussion this message is in--or will be in.
	 *
	 * @param XenForo_DataWriter_Discussion|null $discussionDw
	 * @param boolean True if $discussionDataWriter->isInsert()
	 */
	public function setDiscussionDataWriter(XenForo_DataWriter_Discussion $discussionDw = null, $isInsert = null)
	{
		$this->_discussionDw = $discussionDw;

		if ($isInsert !== null)
		{
			$this->_discussionMode = ($isInsert ? 'insert' : 'update');
		}
	}

	/**
	 * Saves the discussion data writer if it exists and has changed.
	 */
	protected function _saveDiscussionDataWriter()
	{
		if ($this->_discussionDw && $this->_discussionDw->hasChanges())
		{
			$this->_discussionDw->save();
		}
	}

	/**
	 * Gets the data about the discussion this message is in. This may use the
	 * discussion data writer, or some other source if needed.
	 *
	 * @return array|null
	 */
	public function getDiscussionData()
	{
		if ($this->_hasParentDiscussion)
		{
			$discussionDw = $this->getDiscussionDataWriter();
			if ($discussionDw)
			{
				return $discussionDw->getMergedData();
			}
		}

		return null;
	}

	/**
	 * Returns true if the parent discussion has been deleted (instead of this).
	 *
	 * @return boolean
	 */
	public function discussionDeleted()
	{
		return ($this->_discussionChange == 'delete');
	}

	/**
	 * Gets the current value of the discussion message ID for this message.
	 *
	 * @return integer
	 */
	public function getDiscussionMessageId()
	{
		return $this->get($this->getDiscussionMessageKeyName());
	}

	/**
	 * Returns true if this is the first message in a newly inserted discussion
	 *
	 * @return boolean
	 */
	public function isDiscussionFirstMessage()
	{
		return ($this->_discussionMode == 'insert');
	}

	/**
	 * Called during post-save, handles publishing to news feed
	 * and sending notifications
	 */
	protected function _publishAndNotify()
	{
		if ($this->isInsert() && !$this->isDiscussionFirstMessage() && $this->getOption(self::OPTION_PUBLISH_FEED))
		{
			$this->_publishToNewsFeed();
		}
	}

	/**
	 * Publishes an insert or update event to the news feed
	 */
	protected function _publishToNewsFeed()
	{
		$this->_getNewsFeedModel()->publish(
			$this->get('user_id'),
			$this->get('username'),
			$this->getContentType(),
			$this->getDiscussionMessageId(),
			($this->isUpdate() ? 'update' : 'insert')
		);
	}

	/**
	 * Removes an already published news feed item
	 */
	protected function _deleteFromNewsFeed()
	{
		$this->_getNewsFeedModel()->delete(
			$this->getContentType(),
			$this->getDiscussionMessageId()
		);
	}

	/**
	 * The name of the table that holds this type of discussion message.
	 *
	 * @return string
	 */
	public function getDiscussionMessageTableName()
	{
		return $this->_messageDefinition->getMessageTableName();
	}

	/**
	 * The name of the discussion message primary key. Must be an auto increment column.
	 *
	 * @return string
	 */
	public function getDiscussionMessageKeyName()
	{
		return $this->_messageDefinition->getMessageKeyName();
	}

	/**
	 * Gets the field name of the container this message belongs to. This may
	 * be a discussion (eg, thread) or something more general (a user for profile posts).
	 *
	 * @return string
	 */
	public function getContainerKeyName()
	{
		return $this->_messageDefinition->getContainerKeyName();
	}

	/**
	 * Gets the content type for tables that contain multiple data types together.
	 *
	 * @return string
	 */
	public function getContentType()
	{
		return $this->_messageDefinition->getContentType();
	}
}