<?php
/**
 * @brief		Records Model
 * @author		<a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) 2001 - SVN_YYYY Invision Power Services, Inc.
 * @license		http://www.invisionpower.com/legal/standards/
 * @package		IPS Social Suite
 * @since		8 April 2014
 * @version		SVN_VERSION_NUMBER
 */

namespace IPS\cms;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * @brief Records Model
 */
class _Records extends \IPS\Content\Item implements
	\IPS\Content\Permissions,
	\IPS\Content\Pinnable, \IPS\Content\Lockable, \IPS\Content\Hideable, \IPS\Content\Featurable,
	\IPS\Content\Tags,
	\IPS\Content\Followable,
	\IPS\Content\Shareable,
	\IPS\Content\Reputation,
	\IPS\Content\ReportCenter,
	\IPS\Content\ReadMarkers,
	\IPS\Content\Views,
	\IPS\Content\Ratings,
	\IPS\Content\Searchable,
	\IPS\Content\FuturePublishing,
	\IPS\Content\Embeddable
{
	/**
	 * @brief	Multiton Store
	 */
	protected static $multitons = array();
	
	/**
	 * @brief	[ActiveRecord] Database Table
	 */
	public static $databaseTable = NULL;
	
	/**
	 * @brief	[ActiveRecord] ID Database Column
	 */
	public static $databaseColumnId = 'primary_id_field';

    /**
     * @brief	[ActiveRecord] Database ID Fields
     */
    protected static $databaseIdFields = array('record_static_furl', 'record_topicid');

	/**
	 * @brief	Database Prefix
	 */
	public static $databasePrefix = '';

	/**
	 * @brief	Application
	 */
	public static $application = 'cms';
	
	/**
	 * @brief	Module
	 */
	public static $module = 'records';
	
	/**
	 * @brief	Database Column Map
	 */
	public static $databaseColumnMap = NULL;
	
	/**
	 * @brief	Node Class
	 */
	public static $containerNodeClass = NULL;
	
	/**
	 * @brief	[Content\Item]	Comment Class
	 */
	public static $commentClass = NULL;
	
	/**
	 * @brief	[Content\Item]	First "comment" is part of the item?
	 */
	public static $firstCommentRequired = FALSE;
	
	/**
	 * @brief	[Content\Item]	Form field label prefix
	 */
	public static $formLangPrefix = 'content_record_form_';
	
	/**
	 * @brief	[Records] Custom Database Id
	 */
	public static $customDatabaseId = NULL;
	
	/**
	 * @brief 	[Records] Database object
	 */
	protected static $database = array();
	
	/**
	 * @brief 	[Records] Database object
	 */
	public static $title = 'content_record_title';
		
	/**
	 * @brief	[Recordss] Standard fields
	 */
	protected static $standardFields = array( 'record_publish_date', 'record_expiry_date', 'record_allow_comments', 'record_comment_cutoff' );
	
	/**
	 * @brief	[Content]	Key for hide reasons
	 */
	public static $hideLogKey = 'ccs-records';

	/**
	 * @brief	Icon
	 */
	public static $icon = 'file-text-o';

	/**
	 * @brief	Reputation Type
	 */
	public static $reputationType = NULL;
	
	/**
	 * @brief	Include In Sitemap (We do not want to include in Content sitemap, as we have a custom extension
	 */
	public static $includeInSitemap = FALSE;
	
	/**
	 * @brief	Prevent custom fields being fetched twice when loading/saving a form
	 */
	public static $customFields = NULL;
	
	/**
	 * Construct ActiveRecord from database row
	 *
	 * @param	array	$data							Row from database table
	 * @param	bool	$updateMultitonStoreIfExists	Replace current object in multiton store if it already exists there?
	 * @return	static
	 */
	public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
	{
		$obj = parent::constructFromData( $data, $updateMultitonStoreIfExists );

		/* Prevent infinite redirects */
		if ( ! $obj->record_dynamic_furl and ! $obj->record_static_furl )
		{
			if ( $obj->_title )
			{
				$obj->record_dynamic_furl = \IPS\Http\Url::seoTitle( $obj->_title );
				$obj->save();
			}
		}

		if ( $obj->useForumComments() )
		{
			$obj::$commentClass = 'IPS\cms\Records\CommentTopicSync' . static::$customDatabaseId;
		}

		return $obj;
	}

	/**
	 * Set custom posts per page setting
	 *
	 * @return int
	 */
	public static function getCommentsPerPage()
	{
		/* Normally we check if we are using the forums at the record level, however this is a static method called so we can only try to do our best to guess */
		if( static::database()->forum_record and static::database()->forum_comments and \IPS\Application::appIsEnabled('forums') )
		{
			return \IPS\forums\Topic::getCommentsPerPage();
		}

		return static::database()->field_perpage;
	}

	/**
	 * Returns the database parent
	 * 
	 * @return \IPS\cms\Databases
	 */
	public static function database()
	{
		if ( ! isset( static::$database[ static::$customDatabaseId ] ) )
		{
			static::$database[ static::$customDatabaseId ] = \IPS\cms\Databases::load( static::$customDatabaseId );
		}
		
		return static::$database[ static::$customDatabaseId ];
	}
	
	/**
	 * Load record based on a URL
	 *
	 * @param	\IPS\Http\Url	$url	URL to load from
	 * @return	static
	 * @throws	\InvalidArgumentException
	 * @throws	\OutOfRangeException
	 */
	public static function loadFromUrl( \IPS\Http\Url $url )
	{
		$qs = array_merge( $url->queryString, $url->getFriendlyUrlData() );
		
		if ( isset( $qs['path'] ) )
		{
			try
			{
				return static::loadFromSlug( $qs['path'], FALSE );
			}
			catch ( \Exception $e ) { }
		}
		
		return parent::loadFromUrl( $url );
	}

	/**
	 * Load from slug
	 * 
	 * @param	string		$slug							Thing that lives in the garden and eats your plants
	 * @param	bool		$redirectIfSeoTitleIsIncorrect	If the SEO title is incorrect, this method may redirect... this stops that
	 * @return	\IPS\cms\Record
	 */
	public static function loadFromSlug( $slug, $redirectIfSeoTitleIsIncorrect=TRUE )
	{
		$slug = trim( $slug, '/' );

		/* Try the easiest option */
		preg_match( '#-r(\d+?)$#', $slug, $matches );

		if ( isset( $matches[1] ) AND is_numeric( $matches[1] ) )
		{
			try
			{
				$record = static::load( $matches[1] );

				/* Check to make sure the SEO title is correct */
				if ( $redirectIfSeoTitleIsIncorrect and urldecode( str_replace( $matches[0], '', $slug ) ) !== $record->record_dynamic_furl and !\IPS\Request::i()->isAjax() and mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' and !\IPS\ENFORCE_ACCESS )
				{
					\IPS\Output::i()->redirect( $record->url() );
				}

				static::$multitons[ $record->primary_id_field ] = $record;

				return static::$multitons[ $record->primary_id_field ];
			}
			catch( \OutOfRangeException $ex ) { }
		}

		foreach( \IPS\Db::i()->select( '*', static::$databaseTable, array( '? LIKE CONCAT( record_dynamic_furl, \'%\') OR record_static_furl=?', $slug, $slug ) ) as $record )
		{
			$pass = FALSE;
			
			if ( $slug === $record['record_static_furl'] )
			{
				$pass = TRUE;
			}
			else
			{
				if ( isset( $matches[1] ) AND is_numeric( $matches[1] ) AND $matches[1] == $record['primary_id_field'] )
				{
					$pass = TRUE;
				}
			}
				
			if ( $pass === TRUE )
			{
				static::$multitons[ $record['primary_id_field'] ] = static::constructFromData( $record );
			
				return static::$multitons[ $record['primary_id_field'] ];
			}	
		}
		
		/* Still here? Consistent with AR pattern */
		throw new \OutOfRangeException();	
	}

	/**
	 * Load from slug history so we can 301 to the correct record.
	 *
	 * @param	string		$slug	Thing that lives in the garden and eats your plants
	 * @return	\IPS\cms\Record
	 */
	public static function loadFromSlugHistory( $slug )
	{
		$slug = trim( $slug, '/' );

		try
		{
			$row = \IPS\Db::i()->select( '*', 'cms_url_store', array( 'store_type=? and store_path=?', 'record', $slug ) )->first();

			return static::load( $row['store_current_id'] );
		}
		catch( \UnderflowException $ex ) { }

		/* Still here? Consistent with AR pattern */
		throw new \OutOfRangeException();
	}

	/**
	 * Indefinite Article
	 *
	 * @param	\IPS\Lang|NULL	$language	The language to use, or NULL for the language of the currently logged in member
	 * @return	string
	 */
	public function indefiniteArticle( \IPS\Lang $lang = NULL )
	{
		$lang = $lang ?: \IPS\Member::loggedIn()->language();
		return $lang->addToStack( 'content_db_lang_ia_' . static::$customDatabaseId, FALSE );
	}
	
	/**
	 * Get elements for add/edit form
	 *
	 * @param	\IPS\Content\Item|NULL	$item		The current item if editing or NULL if creating
	 * @param	\IPS\Node\Model|NULL	$container	Container (e.g. forum), if appropriate
	 * @return	array
	 */
	public static function formElements( $item=NULL, \IPS\Node\Model $container=NULL )
	{
		$customValues = ( $item ) ? $item->fieldValues() : array();
		$database     = \IPS\cms\Databases::load( static::$customDatabaseId );
		$fieldsClass  = 'IPS\cms\Fields' .  static::$customDatabaseId;
		$formElements = array();
		$elements     = parent::formElements( $item, $container );
		static::$customFields = $fieldsClass::fields( $customValues, ( $item ? 'edit' : 'add' ), $container, 0, ( ! $item ? NULL : $item ) );

		/* Build the topic state toggles */
		$options = array();
		$toggles = array();
		$values  = array();
		
		/* Title */
		if ( isset( static::$customFields[ $database->field_title ] ) )
		{
			$formElements['title'] = static::$customFields[ $database->field_title ];
		}

		if ( \IPS\Member::loggedIn()->modPermission('can_content_edit_record_slugs') )
		{
			$formElements['record_static_furl_set'] = new \IPS\Helpers\Form\YesNo( 'record_static_furl_set', ( ( $item AND $item->record_static_furl ) ? TRUE : FALSE ), FALSE, array(
					'togglesOn' => array( 'record_static_furl' )
			)  );
			$formElements['record_static_furl'] = new \IPS\Helpers\Form\Text( 'record_static_furl', ( ( $item AND $item->record_static_furl ) ? $item->record_static_furl : NULL ), FALSE, array(), function( $val ) use ( $database )
            {
                /* Make sure key is unique */
                if ( empty( $val ) )
                {
                    return true;
                }

                try
                {
                    $cat = intval( ( isset( \IPS\Request::i()->content_record_form_container ) ) ? \IPS\Request::i()->content_record_form_container : 0 );
                    $recordsClass = '\IPS\cms\Records' . $database->id;

                    /* Fetch record by static slug */
                    $record = $recordsClass::load( $val, 'record_static_furl' );

                    /* In the same category though? */
                    if ( isset( \IPS\Request::i()->id ) and $record->_id == \IPS\Request::i()->id )
                    {
                        /* It's ok, it's us! */
                        return true;
                    }

                    if ( $cat === $record->category_id )
                    {
                        throw new \InvalidArgumentException('content_record_slug_not_unique');
                    }
                }
                catch ( \OutOfRangeException $e )
                {
                    /* Slug is OK as load failed */
                    return true;
                }

                return true;
            }, \IPS\Member::loggedIn()->language()->addToStack('record_static_url_prefix', FALSE, array( 'sprintf' => array( \IPS\Settings::i()->base_url ) ) ), NULL, 'record_static_furl' );
		}
		
		if ( isset( $elements['tags'] ) )
		{ 
			$formElements['tags'] = $elements['tags'];
		}

		/* Now custom fields */
		foreach( static::$customFields as $id => $obj )
		{
			if ( $database->field_title === $id )
			{
				continue;
			}

			$formElements['field_' . $id ] = $obj;

			if ( $database->field_content == $id )
			{
				if ( isset( $elements['auto_follow'] ) )
				{
					$formElements['auto_follow'] = $elements['auto_follow'];
				}

				if ( \IPS\Settings::i()->edit_log and $item )
				{
					if ( \IPS\Settings::i()->edit_log == 2 )
					{
						$formElements['record_edit_reason'] = new \IPS\Helpers\Form\Text( 'record_edit_reason', ( $item ) ? $item->record_edit_reason : NULL, FALSE, array( 'maxLength' => 255 ) );
					}
					if ( \IPS\Member::loggedIn()->group['g_append_edit'] )
					{
						$formElements['record_edit_show'] = new \IPS\Helpers\Form\Checkbox( 'record_edit_show', \IPS\Member::loggedIn()->member_id == $item->author()->member_id );
					}
				}
			}
		}

		$postKey = ( $item ) ? $item->_post_key : md5( uniqid() );

		if ( $fieldsClass::fixedFieldFormShow( 'record_publish_date' ) AND \IPS\Member::loggedIn()->modPermission( "can_future_publish_content" ) )
		{
			$formElements['record_publish_date'] = $elements['date'];
		}

		if ( $fieldsClass::fixedFieldFormShow( 'record_image' ) )
		{
			$fixedFieldSettings = static::database()->fixed_field_settings;
			$dims = TRUE;

			if ( isset( $fixedFieldSettings['record_image']['image_dims'] ) )
			{
				$dims = array( 'maxWidth' => $fixedFieldSettings['record_image']['image_dims'][0], 'maxHeight' => $fixedFieldSettings['record_image']['image_dims'][1] );
			}

			$formElements['record_image'] = new \IPS\Helpers\Form\Upload( 'record_image', ( ( $item and $item->record_image ) ? \IPS\File::get( 'cms_Records', $item->record_image ) : NULL ), FALSE, array( 'image' => $dims, 'storageExtension' => 'cms_Records', 'postKey' => $postKey, 'multiple' => false ), NULL, NULL, NULL, 'record_image' );
		}

		if ( $fieldsClass::fixedFieldFormShow( 'record_expiry_date' ) )
		{
			$formElements['record_expiry_date'] = new \IPS\Helpers\Form\Date( 'record_expiry_date', ( ( $item AND $item->record_expiry_date ) ? \IPS\DateTime::ts( $item->record_expiry_date ) : -1 ), FALSE, array(
					'time'          => true,
					'unlimited'     => -1,
					'unlimitedLang' => 'record_datetime_noval'
			) );
		}

		if ( $fieldsClass::fixedFieldFormShow( 'record_allow_comments' ) )
		{
			$formElements['record_allow_comments'] = new \IPS\Helpers\Form\YesNo( 'record_allow_comments', ( ( $item ) ? $item->record_allow_comments : TRUE ), FALSE, array(
					'togglesOn' => array( 'record_comment_cutoff' )
			)  );
		}
		
		if ( $fieldsClass::fixedFieldFormShow( 'record_comment_cutoff' ) )
		{
			$formElements['record_comment_cutoff'] = new \IPS\Helpers\Form\Date( 'record_comment_cutoff', ( ( $item AND $item->record_comment_cutoff ) ? \IPS\DateTime::ts( $item->record_comment_cutoff ) : -1 ), FALSE, array(
					'time'          => true,
					'unlimited'     => -1,
					'unlimitedLang' => 'record_datetime_noval'
			), NULL, NULL, NULL, 'record_comment_cutoff' );
		}

		if ( static::modPermission( 'lock', NULL, $container ) )
		{
			$options['lock'] = 'create_record_locked';
			$toggles['lock'] = array( 'create_record_locked' );
			
			if ( $item AND $item->record_locked )
			{
				$values[] = 'lock';
			}
		}
			
		if ( static::modPermission( 'pin', NULL, $container ) )
		{
			$options['pin'] = 'create_record_pinned';
			$toggles['pin'] = array( 'create_record_pinned' );
			
			if ( $item AND $item->record_pinned )
			{
				$values[] = 'pin';
			}
		}
			
		if ( static::modPermission( 'hide', NULL, $container ) )
		{
			$options['hide'] = 'create_record_hidden';
			$toggles['hide'] = array( 'create_record_hidden' );
			
			if ( $item AND $item->record_approved === -1 )
			{
				$values[] = 'hide';
			}
		}
			
		if ( static::modPermission( 'feature', NULL, $container ) )
		{
			$options['feature'] = 'create_record_featured';
			$toggles['feature'] = array( 'create_record_featured' );

			if ( $item AND $item->record_featured === 1 )
			{
				$values[] = 'feature';
			}
		}
		
		if ( \IPS\Member::loggedIn()->modPermission('can_content_edit_meta_tags') )
		{
			$formElements['record_meta_keywords'] = new \IPS\Helpers\Form\TextArea( 'record_meta_keywords', $item ? $item->record_meta_keywords : '', FALSE );
			$formElements['record_meta_description'] = new \IPS\Helpers\Form\TextArea( 'record_meta_description', $item ? $item->record_meta_description : '', FALSE );
		}
		
		if ( count( $options ) or count ( $toggles ) )
		{
			$formElements['create_record_state'] = new \IPS\Helpers\Form\CheckboxSet( 'create_record_state', $values, FALSE, array(
					'options' 	=> $options,
					'toggles'	=> $toggles,
					'multiple'	=> TRUE
			) );
		}

		return $formElements;
	}

	/**
	 * Total item count (including children)
	 *
	 * @param	\IPS\Node\Model	$container			The container
	 * @param	bool			$includeItems		If TRUE, items will be included (this should usually be true)
	 * @param	bool			$includeComments	If TRUE, comments will be included
	 * @param	bool			$includeReviews		If TRUE, reviews will be included
	 * @param	int				$depth				Used to keep track of current depth to avoid going too deep
	 * @return	int|NULL|string	When depth exceeds 10, will return "NULL" and initial call will return something like "100+"
	 * @note	This method may return something like "100+" if it has lots of children to avoid exahusting memory. It is intended only for display use
	 * @note	This method includes counts of hidden and unapproved content items as well
	 */
	public static function contentCount( \IPS\Node\Model $container, $includeItems=TRUE, $includeComments=FALSE, $includeReviews=FALSE, $depth=0 )
	{
		/* Are we in too deep? */
		if ( $depth > 10 )
		{
			return '+';
		}

		$count = $container->_items;

		if ( static::canViewHiddenItems( NULL, $container ) )
		{
			$count += $container->_unapprovedItems;
		}

		if ( static::canViewFutureItems( NULL, $container ) )
		{
			$count += $container->_futureItems;
		}

		if ( $includeComments )
		{
			$count += $container->record_comments;
		}

		/* Add Children */
		$childDepth	= $depth++;
		foreach ( $container->children() as $child )
		{
			$toAdd = static::contentCount( $child, $includeItems, $includeComments, $includeReviews, $childDepth );
			if ( is_string( $toAdd ) )
			{
				return $count . '+';
			}
			else
			{
				$count += $toAdd;
			}

		}
		return $count;
	}

	/**
	 * [brief] Display title
	 */
	protected $displayTitle = NULL;

	/**
	 * [brief] Display content
	 */
	protected $displayContent = NULL;

	/**
	 * [brief] Record page
	 */
	protected $recordPage = NULL;

	/**
	 * [brief] Custom Display Fields
	 */
	protected $customDisplayFields = array();

	/**
	 * Process create/edit form
	 *
	 * @param	array				$values	Values from form
	 * @return	void
	 */
	public function processForm( $values )
	{
		$fieldsClass  = 'IPS\cms\Fields' . static::$customDatabaseId;
		$database     = \IPS\cms\Databases::load( static::$customDatabaseId );

		/* Store a revision */
		if ( $database->revisions AND ! $this->_new )
		{
			$revision = new \IPS\cms\Records\Revisions;
			$revision->database_id = static::$customDatabaseId;
			$revision->record_id   = $this->_id;
			$revision->data        = $this->fieldValues( TRUE );

			$revision->save();
		}

		if ( isset( \IPS\Request::i()->postKey ) )
		{
			$this->post_key = \IPS\Request::i()->postKey;
		}

		if ( $this->_new )
		{
			$this->record_approved = ( static::moderateNewItems( \IPS\Member::loggedIn() ) ) ? 0 : 1;
		}

		/* Moderator actions */
		if ( isset( $values['create_record_state'] ) )
		{
			if ( in_array( 'lock', $values['create_record_state'] ) )
			{
				$this->record_locked = 1;
			}
			else
			{
				$this->record_locked = 0;
			}
	
			if ( in_array( 'hide', $values['create_record_state'] ) )
			{
				$this->record_approved = -1;
			}
			else
			{
				$this->record_approved = 1;
			}
	
			if ( in_array( 'pin', $values['create_record_state'] ) )
			{
				$this->record_pinned = 1;
			}
			else
			{
				$this->record_pinned = 0;
			}
	
			if ( in_array( 'feature', $values['create_record_state'] ) )
			{
				$this->record_featured = 1;
			}
			else
			{
				$this->record_featured = 0;
			}
		}
	
		/* Dates */
		if ( isset( $values['record_expiry_date'] ) and $values['record_expiry_date'] )
		{
			if ( $values['record_expiry_date'] === -1 )
			{
				$this->record_expiry_date = 0;
			}
			else
			{
				$this->record_expiry_date = $values['record_expiry_date']->getTimestamp();
			}
		}
		if ( isset( $values['record_comment_cutoff'] ) and $values['record_comment_cutoff'] )
		{
			if ( $values['record_comment_cutoff'] === -1 )
			{
				$this->record_comment_cutoff = 0;
			}
			else
			{
				$this->record_comment_cutoff = $values['record_comment_cutoff']->getTimestamp();
			}
		}

		/* Edit stuff */
		if ( ! $this->_new )
		{
			if ( isset( $values['record_edit_reason'] ) )
			{
				$this->record_edit_reason = $values['record_edit_reason'];
			}

			$this->record_edit_time        = time();
			$this->record_edit_member_id   = \IPS\Member::loggedIn()->member_id;
			$this->record_edit_member_name = \IPS\Member::loggedIn()->name;

			if ( isset( $values['record_edit_show'] ) )
			{
				$this->record_edit_show = \IPS\Member::loggedIn()->group['g_append_edit'] ? $values['record_edit_show'] : TRUE;
			}
		}

		/* Record image */
		if ( array_key_exists( 'record_image', $values ) )
		{			
			if ( $values['record_image'] === NULL )
			{			
				if ( $this->record_image )
				{
					try
					{
						\IPS\File::get( 'cms_Records', $this->record_image )->delete();
					}
					catch ( \Exception $e ) { }
				}
				if ( $this->record_image_thumb )
				{
					try
					{
						\IPS\File::get( 'cms_Records', $this->record_image_thumb )->delete();
					}
					catch ( \Exception $e ) { }
				}
					
				$this->record_image = NULL;
				$this->record_image_thumb = NULL;
			}
			else
			{
				$fixedFieldSettings = static::database()->fixed_field_settings;

				if ( isset( $fixedFieldSettings['record_image']['thumb_dims'] ) )
				{
					if ( $this->record_image_thumb )
					{
						try
						{
							\IPS\File::get( 'cms_Records', $this->record_image_thumb )->delete();
						}
						catch ( \Exception $e ) { }
					}
					
					$thumb = $values['record_image']->thumbnail( 'cms_Records', $fixedFieldSettings['record_image']['thumb_dims'][0], $fixedFieldSettings['record_image']['thumb_dims'][1] );
				}

				$this->record_image       = (string)$values['record_image'];
				$this->record_image_thumb = (string)$thumb;
			}
		}
		
		/* Should we just lock this? */
		if ( ( isset( $values['record_allow_comments'] ) AND ! $values['record_allow_comments'] ) OR ( $this->record_comment_cutoff > $this->record_publish_date ) )
		{
			$this->record_locked = 1;
		}
		
		if ( \IPS\Member::loggedIn()->modPermission('can_content_edit_meta_tags') )
		{
			foreach( array( 'record_meta_keywords', 'record_meta_description' ) as $k )
			{
				if ( isset( $values[ $k ] ) )
				{
					$this->$k = $values[ $k ];
				}
			}
		}

		/* Custom fields */
		$customValues = array();
		$afterEditNotificationsExclude = array();
	
		foreach( $values as $k => $v )
		{
			if ( mb_substr( $k, 0, 14 ) === 'content_field_' )
			{
				$customValues[$k ] = $v;
			}
		}

		$categoryClass = 'IPS\cms\Categories' . static::$customDatabaseId;
		$container    = ( ! isset( $values['content_record_form_container'] ) ? $categoryClass::load( $this->category_id ) : $values['content_record_form_container'] );
		$fieldObjects = $fieldsClass::data( NULL, $container );
		
		if ( static::$customFields === NULL )
		{
			static::$customFields = $fieldsClass::fields( $customValues, ( $this->_new ? 'add' : 'edit' ), $container, 0, ( $this->_new ? NULL : $this ) );
		}
		
		foreach( static::$customFields as $key => $field )
		{
			$seen[] = $key;
			$key = 'field_' . $key;
			
			if ( !$this->_new )
			{
				$afterEditNotificationsExclude = array_merge_recursive( static::_getQuoteAndMentionIdsFromContent( $this->$key ) );
			}
			
			$this->$key = $field::stringValue( isset( $customValues[ $field->name ] ) ? $customValues[ $field->name ] : NULL );
		}

		/* Now set up defaults */
		if ( $this->_new )
		{
			foreach ( $fieldObjects as $obj )
			{
				if ( !in_array( $obj->id, $seen ) )
				{
					/* We've not got a value for this as the field is hidden from us, so let us add the default value here */
					$key        = 'field_' . $obj->id;
					$this->$key = $obj->default_value;
				}
			}
		}

		/* Other data */
		if ( $this->_new OR $database->_comment_bump & \IPS\cms\Databases::BUMP_ON_EDIT )
		{
			$this->record_saved   = time();
			$this->record_updated = time();
		}

		$this->record_allow_comments   = isset( $values['record_allow_comments'] ) ? $values['record_allow_comments'] : ( ! $this->record_locked );
		
		if ( isset( $values[ 'content_field_' . $database->field_title ] ) )
		{
			$this->record_dynamic_furl     = \IPS\Http\Url::seoTitle( $values[ 'content_field_' . $database->field_title ] );
		}

		if ( isset( $values['record_static_furl_set'] ) and $values['record_static_furl_set'] and isset( $values['record_static_furl'] ) and $values['record_static_furl'] )
		{
			$newFurl = \IPS\Http\Url::seoTitle( $values['record_static_furl'] );

			if ( $newFurl != $this->record_static_furl )
			{
				$this->storeUrl();
			}
			
			$this->record_static_furl = $newFurl;
		}
		else
		{
			$this->record_static_furl = NULL;
		}
		
		if ( $this->_new )
		{
			/* Set the author ID on 'new' only */
			$this->member_id = (int) \IPS\Member::loggedIn()->member_id;
		}
		else
		{
			$this->sendQuoteAndMentionNotifications( array_unique( array_merge( $afterEditNotificationsExclude['quotes'], $afterEditNotificationsExclude['mentions'] ) ) );
		}
		
		if ( isset( $values['content_record_form_container'] ) )
		{
			$this->category_id = ( $values['content_record_form_container'] === 0 ) ? 0 : $values['content_record_form_container']->id;
		}

		$isNew    = $this->_new;
		$idColumn = static::$databaseColumnId;
		if ( ! $this->$idColumn )
		{
			$this->save();
		}

		/* Claim attachments once we have an ID */
		foreach( $fieldObjects as $id => $row )
		{
			if ( $row->can( ( $isNew ? 'add' : 'edit' ) ) and $row->type == 'Editor' )
			{
				\IPS\File::claimAttachments( 'RecordField_' . ( $isNew ? 'new' : $this->_id ) . '_' . $row->id, $this->primary_id_field, $id, static::$customDatabaseId );
			}
		}

		parent::processForm( $values );
	}

	/**
	 * Stores the URL so when its changed, the old can 301 to the new location
	 *
	 * @return void
	 */
	public function storeUrl()
	{
		if ( $this->record_static_furl )
		{
			\IPS\Db::i()->insert( 'cms_url_store', array(
				'store_path'       => $this->record_static_furl,
			    'store_current_id' => $this->_id,
			    'store_type'       => 'record'
			) );
		}
	}

	/**
	 * Stats for table view
	 *
	 * @param	bool	$includeFirstCommentInCommentCount	Determines whether the first comment should be inlcluded in the comment count (e.g. For "posts", use TRUE. For "replies", use FALSE)
	 * @return	array
	 */
	public function stats( $includeFirstCommentInCommentCount=TRUE )
	{
		$return = array();

		if ( static::$commentClass and static::database()->options['comments'] )
		{
			$return['comments'] = (int) $this->mapped('num_comments');
		}

		if ( $this instanceof \IPS\Content\Views )
		{
			$return['num_views'] = (int) $this->mapped('views');
		}

		return $return;
	}

	/**
	 * Get URL
	 *
	 * @param	string|NULL		$action		Action
	 * @return	\IPS\Http\Url
	 */
	public function url( $action=NULL )
	{
		if ( ! $this->recordPage )
		{
			/* If we're coming through the database controller embedded in a page, $currentPage will be set. If we're coming in via elsewhere, we need to fetch the page */
			try
			{
				$this->recordPage = \IPS\cms\Pages\Page::loadByDatabaseId( static::$customDatabaseId );
			}
			catch( \OutOfRangeException $ex )
			{
				if ( \IPS\cms\Pages\Page::$currentPage )
				{
					$this->recordPage = \IPS\cms\Pages\Page::$currentPage;
				}
				else
				{
					throw new \LogicException;
				}
			}
		}

		if ( $this->recordPage )
		{
			$pagePath   = $this->recordPage->full_path;
			$class		= '\IPS\cms\Categories' . static::$customDatabaseId;
			$catPath    = $class::load( $this->category_id )->full_path;
			$recordSlug = ! $this->record_static_furl ? $this->record_dynamic_furl . '-r' . $this->primary_id_field : $this->record_static_furl;

			if ( static::database()->use_categories )
			{
				$url = \IPS\Http\Url::internal( "app=cms&module=pages&controller=page&path=" . $pagePath . '/' . $catPath . '/' . $recordSlug, 'front', 'content_page_path', $recordSlug );
			}
			else
			{
				$url = \IPS\Http\Url::internal( "app=cms&module=pages&controller=page&path=" . $pagePath . '/' . $recordSlug, 'front', 'content_page_path', $recordSlug );
			}
		}

		if ( $action )
		{
			$url = $url->setQueryString( 'do', $action );
			$url = $url->setQueryString( 'd' , static::database()->id );
			$url = $url->setQueryString( 'id', $this->primary_id_field );
		}

		return $url;
	}

	/**
	 * Template helper method to fetch custom fields to display
	 *
	 * @param   string  $type       Type of display
	 * @return  array
	 */
	public function customFieldsForDisplay( $type='display' )
	{
		if ( ! isset( $this->customDisplayFields['all'][ $type ] ) )
		{
			$fieldsClass = '\IPS\cms\Fields' . static::$customDatabaseId;
			$this->customDisplayFields['all'][ $type ] = $fieldsClass::display( $this->fieldValues(), $type, $this->container(), 'key', $this );
		}

		return $this->customDisplayFields['all'][ $type ];
	}

	/**
	 * @param mixed      $key       Key to fetch
	 * @param string     $type      Type of display to fetch
	 *
	 * @return mixed
	 */
	public function customFieldDisplayByKey( $key, $type='display' )
	{
		$fieldsClass = '\IPS\cms\Fields' . static::$customDatabaseId;

		if ( ! isset( $this->customDisplayFields[ $key ][ $type ] ) )
		{
			foreach ( $fieldsClass::roots( 'view' ) as $row )
			{
				if ( $row->key === $key )
				{
					$field = 'field_' . $row->id;
					$value = ( ! $this->$field ) ? $row->default_value : $this->$field;

					$this->customDisplayFields[ $key ][ $type ] = $row->formatForDisplay( $row->displayValue( $value ), $value, $type, $this );
				}
			}
		}

		/* Still nothing? */
		if ( ! isset( $this->customDisplayFields[ $key ][ $type ] ) )
		{
			$this->customDisplayFields[ $key ][ $type ] = NULL;
		}

		return $this->customDisplayFields[ $key ][ $type ];
	}

	/**
	 * Get custom field_x keys and values
	 *
	 * @param	boolean	$allData	All data (true) or just custom field data (false)
	 * @return	array
	 */
	public function fieldValues( $allData=FALSE )
	{
		$fields = array();
		
		foreach( $this->_data as $k => $v )
		{
			if ( $allData === TRUE OR mb_substr( $k, 0, 6 ) === 'field_')
			{
				$fields[ $k ] = $v;
			}
		}

		return $fields;
	}

	/**
	 * Get the post key or create one if one doesn't exist
	 *
	 * @return  string
	 */
	public function get__post_key()
	{
		return ! empty( $this->post_key ) ? $this->post_key : md5( uniqid() );
	}

	/**
	 * Get the publish date
	 *
	 * @return	string
	 */
	public function get__publishDate()
	{
		return $this->record_publish_date ? $this->record_publish_date : $this->record_saved;
	}

	/**
	 * Get the record id
	 *
	 * @return	int
	 */
	public function get__id()
	{
		return $this->primary_id_field;
	}

	/**
	 * Get the record title for display
	 *
	 * @return	string
	 */
	public function get__title()
	{
		$field = 'field_' . static::database()->field_title;

		try
		{
			if ( ! $this->displayTitle )
			{
				$class = '\IPS\cms\Fields' .  static::database()->id;
				$this->displayTitle = $class::load( static::database()->field_title )->displayValue( $this->$field );
			}

			return $this->displayTitle;
		}
		catch( \Exception $e )
		{
			return $this->$field;
		}
	}

	/**
	 * Get the record content for display
	 *
	 * @return	string
	 */
	public function get__content()
	{
		$field = 'field_' . static::database()->field_content;

		try
		{
			if ( ! $this->displayContent )
			{
				$class = '\IPS\cms\Fields' .  static::database()->id;

				$this->displayContent = $class::load( static::database()->field_content )->displayValue( $this->$field );
			}

			return $this->displayContent;
		}
		catch( \Exception $e )
		{
			return $this->$field;
		}
	}
	
	/**
	 * Return forum sync on or off
	 *
	 * @return	int
	 */
	public function get__forum_record()
	{
		if ( $this->container()->forum_override )
		{
			return $this->container()->forum_record;
		}
		
		return static::database()->forum_record;
	}
	
	/**
	 * Return forum post on or off
	 *
	 * @return	int
	 */
	public function get__forum_comments()
	{
		if ( $this->container()->forum_override )
		{
			return $this->container()->forum_comments;
		}
		
		return static::database()->forum_comments;
	}
	
	/**
	 * Return forum sync delete
	 *
	 * @return	int
	 */
	public function get__forum_delete()
	{
		if ( $this->container()->forum_override )
		{
			return $this->container()->forum_delete;
		}
		
		return static::database()->forum_delete;
	}
	
	/**
	 * Return forum sync forum
	 *
	 * @return	int
	 */
	public function get__forum_forum()
	{
		if ( $this->container()->forum_override )
		{
			return $this->container()->forum_forum;
		}
		
		return static::database()->forum_forum;
	}
	
	/**
	 * Return forum sync prefix
	 *
	 * @return	int
	 */
	public function get__forum_prefix()
	{
		if ( $this->container()->forum_override )
		{
			return $this->container()->forum_prefix;
		}
	
		return static::database()->forum_prefix;
	}
	
	/**
	 * Return forum sync suffix
	 *
	 * @return	int
	 */
	public function get__forum_suffix()
	{
		if ( $this->container()->forum_override )
		{
			return $this->container()->forum_suffix;
		}
	
		return static::database()->forum_suffix;
	}

	/**
	 * Return record image thumb
	 *
	 * @return	int
	 */
	public function get__record_image_thumb()
	{
		return $this->record_image_thumb ? $this->record_image_thumb : $this->record_image;
	}

	/**
	 * Get edit line
	 *
	 * @return	string|NULL
	 */
	public function editLine()
	{
		if ( $this->record_edit_time and ( $this->record_edit_show or \IPS\Member::loggedIn()->modPermission('can_view_editlog') ) )
		{
			return \IPS\cms\Theme::i()->getTemplate( static::database()->template_display, 'cms', 'database' )->recordEditLine( $this );
		}
		return NULL;
	}

	/**
	 * Get mapped value
	 *
	 * @param	string	$key	date,content,ip_address,first
	 * @return	mixed
	 */
	public function mapped( $key )
	{
		if ( $key === 'title' )
		{
			return $this->_title;
		}
		else if ( $key === 'content' )
		{
			return $this->_content;
		}
		
		if ( isset( static::$databaseColumnMap[ $key ] ) )
		{
			$field = static::$databaseColumnMap[ $key ];
				
			if ( is_array( $field ) )
			{
				$field = array_pop( $field );
			}
				
			return $this->$field;
		}
		return NULL;
	}
	
	/**
	 * Save
	 *
	 * @return void
	 */
	public function save()
	{
		$new = $this->_new;
			
		if ( $new OR static::database()->_comment_bump & \IPS\cms\Databases::BUMP_ON_EDIT )
		{
			$member = \IPS\Member::load( $this->member_id );
	
			/* Set last comment as record so that category listing is correct */
			if ( $this->record_saved > $this->record_last_comment )
			{
				$this->record_last_comment = $this->record_saved;
			}

			if ( $new )
			{
				$this->record_last_comment_by   = $this->member_id;
				$this->record_last_comment_name = $member->name;
				$this->record_last_comment_seo  = \IPS\Http\Url::seoTitle( $member->name );
			}
		}
	
		parent::save();

        if ( $this->category_id )
        {
            unset( static::$multitons[ $this->primary_id_field ] );

            $class = '\IPS\cms\Categories' . static::$customDatabaseId;
            $category = $class::load( $this->category_id );
            $category->setLastComment();
            $category->save();
        }
	}
	
	/**
	 * Resync last comment
	 *
	 * @return	void
	 */
	public function resyncLastComment()
	{
		if ( $this->useForumComments() )
		{
			$topic = $this->topic( FALSE );
			$topic->resyncLastComment();
		}
		
		parent::resyncLastComment();
	}
	
	/**
	 * Utility method to reset the last commenter of a record
	 *
	 * @param   boolean     $setCategory    Check and set the last commenter for a category
	 * @return void
	 */
	public function resetLastComment( $setCategory=false )
	{
		$comment = $this->comments( 1, 0, 'date', 'desc', NULL, FALSE );

		if ( $comment )
		{
			$this->record_last_comment      = $comment->mapped('date');
			$this->record_last_comment_by   = $comment->author()->member_id;
			$this->record_last_comment_name = $comment->author()->name;
			$this->record_last_comment_seo  = \IPS\Http\Url::seoTitle( $comment->author()->name );
			$this->save();

			if ( $setCategory and $this->category_id )
			{
				$class = '\IPS\cms\Categories' . static::$customDatabaseId;
				$class::load( $this->category_id )->setLastComment( NULL );
				$class::load( $this->category_id )->save();
			}
		}
	}

	/**
	 * Resync the comments/unapproved comment counts
	 *
	 * @param	string	$commentClass
	 * @return void
	 */
	public function resyncCommentCounts( $commentClass=NULL )
	{
		if ( $this->useForumComments() )
		{
			$topic = $this->topic( FALSE );

			if ( $topic )
			{
				$this->record_comments = $topic->posts - 1;
				$this->record_comments_queued = $topic->topic_queuedposts;
				$this->record_comments_hidden = $topic->topic_hiddenposts;
				$this->save();
			}
		}
		else
		{
			parent::resyncCommentCounts( $commentClass );
		}
	}

	/* !IP.Board Integration */
	
	/**
	 * Use forum for comments
	 *
	 * @return boolean
	 */
	public function useForumComments()
	{
		return $this->_forum_record and $this->_forum_comments and $this->record_topicid and \IPS\Application::appIsEnabled('forums');
	}

	/**
	 * Get comments
	 *
	 * @param	int|NULL			$limit					The number to get (NULL to use static::getCommentsPerPage())
	 * @param	int|NULL			$offset					The number to start at (NULL to examine \IPS\Request::i()->page)
	 * @param	string				$order					The column to order by
	 * @param	string				$orderDirection			"asc" or "desc"
	 * @param	\IPS\Member|NULL	$member					If specified, will only get comments by that member
	 * @param	bool|NULL			$includeHiddenComments	Include hidden comments or not? NULL to base of currently logged in member's permissions
	 * @param	\IPS\DateTime|NULL	$cutoff					If an \IPS\DateTime object is provided, only comments posted AFTER that date will be included
	 * @param	mixed				$extraWhereClause	Additional where clause(s) (see \IPS\Db::build for details)
	 * @param	bool|NULL			$bypassCache			Used in cases where comments may have already been loaded i.e. splitting comments on an item.
	 * @return	array|NULL|\IPS\Content\Comment	If $limit is 1, will return \IPS\Content\Comment or NULL for no results. For any other number, will return an array.
	 */
	public function comments( $limit=NULL, $offset=NULL, $order='date', $orderDirection='asc', $member=NULL, $includeHiddenComments=NULL, $cutoff=NULL, $extraWhereClause=NULL, $bypassCache=FALSE )
	{
		/* Lets prevent infinite recursion with \IPS\cms\Records calling \IPS\cms\Records\RecordsTopicSync calling \IPS\cms\Records */
		if ( $this->useForumComments() and get_called_class() === 'IPS\cms\Records' . static::$customDatabaseId )
		{
			$recordClass = 'IPS\cms\Records\RecordsTopicSync' . static::$customDatabaseId;
			
			/* If we are pulling in ASC order we want to jump up by 1 to account for the first post, which is not a comment */
			if( mb_strtolower( $orderDirection ) == 'asc' )
			{
				$_pageValue = ( \IPS\Request::i()->page ? intval( \IPS\Request::i()->page ) : 1 );

				if( $_pageValue < 1 )
				{
					$_pageValue = 1;
				}
				
				$offset = ( ( $_pageValue - 1 ) * static::getCommentsPerPage() ) + 1;
			}

			return $recordClass::load( $this->record_topicid )->comments( $limit, $offset, $order, $orderDirection, $member, $includeHiddenComments, $cutoff );
		}

		$where = NULL;
		if( static::$commentClass != 'IPS\cms\Records\CommentTopicSync' . static::$customDatabaseId )
		{
			$where = array( array( 'comment_database_id=?', static::$customDatabaseId ) );
		}

		return parent::comments( $limit, $offset, $order, $orderDirection, $member, $includeHiddenComments, $cutoff, $where );
	}

	/**
	 * Get review page count
	 *
	 * @return	int
	 */
	public function reviewPageCount()
	{
		if ( $this->reviewPageCount === NULL )
		{
			$reviewClass = static::$reviewClass;
			$idColumn = static::$databaseColumnId;
			$where = array( array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $this->$idColumn ) );
			$where[] = array( 'review_database_id=?', static::$customDatabaseId );
			$count = $reviewClass::getItemsWithPermission( $where )->count();
			$this->reviewPageCount = ceil( $count / static::$reviewsPerPage );

			if( $this->reviewPageCount < 1 )
			{
				$this->reviewPageCount	= 1;
			}
		}
		return $this->reviewPageCount;
	}

	/**
	 * Get reviews
	 *
	 * @param	int|NULL			$limit					The number to get (NULL to use static::getCommentsPerPage())
	 * @param	int|NULL			$offset					The number to start at (NULL to examine \IPS\Request::i()->page)
	 * @param	string				$order					The column to order by (NULL to examine \IPS\Request::i()->sort)
	 * @param	string				$orderDirection			"asc" or "desc" (NULL to examine \IPS\Request::i()->sort)
	 * @param	\IPS\Member|NULL	$member					If specified, will only get comments by that member
	 * @param	bool|NULL			$includeHiddenReviews	Include hidden comments or not? NULL to base of currently logged in member's permissions
	 * @param	\IPS\DateTime|NULL	$cutoff					If an \IPS\DateTime object is provided, only comments posted AFTER that date will be included
	 * @param	mixed				$extraWhereClause		Additional where clause(s) (see \IPS\Db::build for details)
	 * @return	array|NULL|\IPS\Content\Comment	If $limit is 1, will return \IPS\Content\Comment or NULL for no results. For any other number, will return an array.
	 */
	public function reviews( $limit=NULL, $offset=NULL, $order=NULL, $orderDirection='desc', $member=NULL, $includeHiddenReviews=NULL, $cutoff=NULL, $extraWhereClause=NULL )
	{
		$where = array( array( 'review_database_id=?', static::$customDatabaseId ) );

		return parent::reviews( $limit, $offset, $order, $orderDirection, $member, $includeHiddenReviews, $cutoff, $where );
	}

	/**
	 * Get available comment/review tabs
	 *
	 * @return	array
	 */
	public function commentReviewTabs()
	{
		$tabs = array();
		if ( static::database()->options['reviews'] )
		{
			$tabs['reviews'] = \IPS\Member::loggedIn()->language()->pluralize( \IPS\Member::loggedIn()->language()->get( 'cms_review_count' ), array( $this->mapped('num_reviews') ) );
		}
		if ( static::database()->options['comments'] )
		{
			$tabs['comments'] = \IPS\Member::loggedIn()->language()->pluralize( \IPS\Member::loggedIn()->language()->get( 'cms_comment_count' ), array( $this->mapped( 'num_comments' ) ) );
		}

		return $tabs;
	}

	/**
	 * Get comment/review output
	 *
	 * @param	string	$tab	Active tab
	 * @return	string
	 */
	public function commentReviews( $tab )
	{
		if ( $tab === 'reviews' )
		{
			return \IPS\cms\Theme::i()->getTemplate( static::database()->template_display, 'cms', 'database' )->reviews( $this );
		}
		elseif( $tab === 'comments' )
		{
			return \IPS\cms\Theme::i()->getTemplate( static::database()->template_display, 'cms', 'database' )->comments( $this );
		}

		return '';
	}

	/**
	 * Should new items be moderated?
	 *
	 * @param	\IPS\Member		$member		The member posting
	 * @param	\IPS\Node\Model	$container	The container
	 * @return	bool
	 */
	public static function moderateNewItems( \IPS\Member $member, \IPS\Node\Model $container = NULL )
	{
		return ( static::database()->record_approve and !$member->group['g_avoid_q'] ) or parent::moderateNewItems( $member, $container );
	}

	/**
	 * Should new comments be moderated?
	 *
	 * @param	\IPS\Member	$member	The member posting
	 * @return	bool
	 */
	public function moderateNewComments( \IPS\Member $member )
	{
		return ( static::database()->options['comments_mod'] and !$member->group['g_avoid_q'] ) or parent::moderateNewComments( $member );
	}

	/**
	 * Should new reviews be moderated?
	 *
	 * @param	\IPS\Member	$member	The member posting
	 * @return	bool
	 */
	public function moderateNewReviews( \IPS\Member $member )
	{
		return static::database()->options['reviews_mod'] and !$member->group['g_avoid_q'];

	}

	/**
	 * Create from form
	 *
	 * @param	array					$values				Values from form
	 * @param	\IPS\Node\Model|NULL	$container			Container (e.g. forum), if appropriate
	 * @param	bool					$sendNotification	Send Notification
	 * @return	\IPS\cms\Records
	 */
	public static function createFromForm( $values, \IPS\Node\Model $container = NULL, $sendNotification = TRUE )
	{
		$record = parent::createFromForm( $values, $container, $sendNotification );

		if ( \IPS\Application::appIsEnabled('forums') and $record->_forum_record and $record->_forum_forum and ! $record->hidden() and ! $record->record_future_date )
		{
			try
			{
				$record->syncTopic();
			}
			catch( \Exception $ex ) { }
		}
		return $record;
	}
	
	/**
	 * Process after the object has been edited on the front-end
	 *
	 * @param	array	$values		Values from form
	 * @return	void
	 */
	public function processAfterEdit( $values )
	{
		if ( \IPS\Application::appIsEnabled('forums') and $this->topic() )
		{
			try
			{
				$this->syncTopic();
			}
			catch( \Exception $ex ) { }
		}

		parent::processAfterEdit( $values );
	}
	
	/**
	 * Process the comment form
	 *
	 * @param	array	$values		Array of $form values
	 * @return  \IPS\Content\Comment
	 */
	public function processCommentForm( $values )
	{
		if ( $this->useForumComments() )
		{
			$topic   = $this->topic();
			$comment = $values[ static::$formLangPrefix . 'comment' . '_' . $this->_id ];
			$post    = \IPS\forums\Topic\Post::create( $topic, $comment, FALSE );
			
			$commentClass = 'IPS\cms\Records\CommentTopicSync' . static::$customDatabaseId;
			
			return $commentClass::load( $post->pid );
		}
		else
		{
			return parent::processCommentForm( $values );
		}
	}
	
	/**
	 * Syncing to run when hiding
	 *
	 * @return	void
	 */
	public function onHide()
	{
		parent::onHide();
		if ( \IPS\Application::appIsEnabled('forums') and $topic = $this->topic() )
		{
			$topic->hide();
		}
	}
	
	/**
	 * Future Publish
	 *
	 * @return	void
	 */
	public function onPublish()
	{
		parent::onPublish();
		if ( \IPS\Application::appIsEnabled('forums') )
		{
			if ( $topic = $this->topic() )
			{
				if ( $topic->hidden() )
				{
					$topic->unhide();
				}
			}
			else if ( $this->_forum_forum )
			{
				$this->syncTopic();
			}
		}
	}
	
	/**
	 * Unpublish
	 * @return	void
	 */
	public function onUnpublish()
	{
		parent::onUnpublish();
		if ( \IPS\Application::appIsEnabled('forums') AND $topic = $this->topic() )
		{
			$topic->hide();
		}
	}
	
	/**
	 * Syncing to run when unhiding
	 *
	 * @param	bool	$approving	If true, is being approved for the first time
	 * @return	void
	 */
	public function onUnhide( $approving )
	{
		parent::onUnhide( $approving );
	
		if ( \IPS\Application::appIsEnabled('forums') )
		{
			if ( $topic = $this->topic() )
			{ 
				$topic->unhide();
			}
			elseif ( $this->_forum_forum )
			{
				$this->syncTopic();
			}
		}
	}

	/**
	 * Change Author
	 *
	 * @param	\IPS\Member	$newAuthor	The new author
	 * @return	void
	 */
	public function changeAuthor( \IPS\Member $newAuthor )
	{
		parent::changeAuthor( $newAuthor );

		$topic = $this->topic();

		if ( $topic )
		{
			$topic->changeAuthor( $newAuthor );
		}
	}

	/**
	 * Get Topic (checks member's permissions)
	 *
	 * @param	bool	$checkPerms		Should check if the member can read the topic?
	 * @return	\IPS\forums\Topic|NULL
	 */
	public function topic( $checkPerms=TRUE )
	{
		if ( \IPS\Application::appIsEnabled('forums') and $this->record_topicid )
		{
			try
			{
				return $checkPerms ? \IPS\forums\Topic::loadAndCheckPerms( $this->record_topicid ) : \IPS\forums\Topic::load( $this->record_topicid );
			}
			catch ( \OutOfRangeException $e )
			{
				return NULL;
			}
		}
	
		return NULL;
	}
	
	/**
	 * Post this record as a forum topic
	 * 
	 * @return void
	 */
	public function syncTopic()
	{
		if ( ! \IPS\Application::appIsEnabled( 'forums' ) )
		{
			throw new \UnexpectedValueException('content_record_no_forum_app_for_topic');
		}

		/* Fetch the forum */
		try
		{
			$forum = \IPS\forums\Forum::load( $this->_forum_forum );
		}
		catch( \OutOfRangeException $ex )
		{
			throw new \UnexpectedValueException('content_record_bad_forum_for_topic');
		}

		/* Existing topic */
		if ( $this->record_topicid )
		{
			/* Get */
			try
			{
				$topic = \IPS\forums\Topic::load( $this->record_topicid );
				if ( !$topic )
				{
					return;
				}
				$topic->title = $this->_forum_prefix . $this->_title . $this->_forum_suffix;
				if ( \IPS\Settings::i()->tags_enabled )
				{
					$topic->setTags( $this->prefix() ? array_merge( $this->tags(), array( 'prefix' => $this->prefix() ) ) : $this->tags() );
				}
				
				if ( $this->hidden() )
				{
					$topic->hide();
				}
				else if ( $topic->hidden() )
				{
					$topic->unhide();
				}
				
				$topic->save();
				$firstPost = $topic->comments( 1 );

				$content = \IPS\Theme::i()->getTemplate( 'submit', 'cms', 'front' )->topic( $this );
				\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $content );

				$firstPost->post = $content;
				$firstPost->save();
			}
			catch ( \OutOfRangeException $e )
			{
				return;
			}
		}
		/* New topic */
		else
		{
			/* Create topic */
			$topic = \IPS\forums\Topic::createItem( $this->author(), \IPS\Request::i()->ipAddress(), \IPS\DateTime::ts( $this->record_publish_date ? $this->record_publish_date : $this->record_saved ), \IPS\forums\Forum::load( $this->_forum_forum ), $this->hidden() );
			$topic->title = $this->_forum_prefix . $this->_title . $this->_forum_suffix;
			$topic->topic_archive_status = \IPS\forums\Topic::ARCHIVE_EXCLUDE;
			$topic->save();

			if ( \IPS\Settings::i()->tags_enabled )
			{
				$topic->setTags( $this->prefix() ? array_merge( $this->tags(), array( 'prefix' => $this->prefix() ) ) : $this->tags() );
			}

			/* Create post */
			$content = \IPS\Theme::i()->getTemplate( 'submit', 'cms', 'front' )->topic( $this );
			\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $content );

			$post = \IPS\forums\Topic\Post::create( $topic, $content, TRUE, NULL, NULL, NULL, \IPS\DateTime::ts( $this->record_publish_date ? $this->record_publish_date : $this->record_saved ) );
			$post->save();

			$topic->topic_firstpost = $post->pid;
			$topic->save();
				
			/* Update file */
			$this->record_topicid = $topic->tid;
			$this->save();
		}
	}

	/**
	 * Sync topic details to the record
	 *
	 * @param   \IPS\forums\Topic   $topic  Forums topic
	 * @return  void
	 */
	public function syncRecordFromTopic( $topic )
	{
		if ( $this->_forum_record and $this->_forum_forum and $this->_forum_comments )
		{
			$this->record_last_comment_by   = $topic->last_poster_id;
			$this->record_last_comment_name = $topic->last_poster_name;
			$this->record_last_comment      = $topic->last_post;
			$this->record_comments_queued   = $topic->topic_queuedposts;
			$this->record_comments_hidden 	= $topic->topic_hiddenposts;
			$this->record_comments          = $topic->posts - 1;
			$this->save();
		}
	}

	/**
	 * Get fields for the topic
	 * 
	 * @return array
	 */
	public function topicFields()
	{
		$fieldsClass = 'IPS\cms\Fields' . static::$customDatabaseId;
		$fieldData   = $fieldsClass::data( 'view', $this->container() );
		$fieldValues = $fieldsClass::display( $this->fieldValues(), 'record', $this->container(), 'id' );

		$fields = array();
		foreach( $fieldData as $id => $data )
		{
			if ( $data->topic_format )
			{
				if ( isset( $fieldValues[ $data->id ] ) )
				{
					$html = str_replace( '{title}'  , $data->_title, $data->topic_format );
					$html = str_replace( '{content}', $fieldValues[ $data->id ], $html );
					$html = str_replace( '{value}'  , $fieldValues[ $data->id ], $html );
				
					$fields[ $data->id ] = $html;
				}
			}
		}

		if ( ! count( $fields ) )
		{
			$fields[ static::database()->field_content ] = $fieldValues['content'];
		}

		return $fields;
	}

	/**
	 * Get comment page count
	 *
	 * @return	int
	 */
	public function commentPageCount( $recache=FALSE )
	{
		if ( $this->useForumComments() )
		{
			try
			{
				$topic = $this->topic();

				if( $topic !== NULL )
				{
					$topic->posts = $topic->posts - 1;

					return $topic->commentPageCount();
				}

				return 0;
			}
			catch( \Exception $e ) { }
		}

		return parent::commentPageCount();
	}
	
	/**
	 * Delete Record
	 *
	 * @return	void
	 */
	public function delete()
	{
		$topic        = $this->topic();
		$commentClass = static::$commentClass;
		
		if ( $this->topic() and $this->_forum_delete )
		{
			$topic->delete();
		}
		else if ( $this->topic() )
		{
			/* We have an attached topic, but we don't want to delete the topic so remove commentClass otherwise we'll delete posts */
			static::$commentClass = NULL;
		}
		
		parent::delete();
		
		if ( $this->topic() )
		{
			static::$commentClass = $commentClass;
		}
	}

	/**
	 * Can view?
	 *
	 * @param	\IPS\Member|NULL	$member	The member to check for or NULL for the currently logged in member
	 * @return	bool
	 */
	public function canView( $member=NULL )
	{
		try
		{
			\IPS\cms\Pages\Page::loadByDatabaseId( static::database()->id );
		}
		catch( \OutOfRangeException $e )
		{
			/* This prevents auto share and notifications being sent out */
			return FALSE;
		}

		return parent::canView( $member );
	}

	/* ! Moderation */
	
	/**
	 * Can edit?
	 *
	 * @param	\IPS\Member|NULL	$member	The member to check for (NULL for currently logged in member)
	 * @return	bool
	 */
	public function canEdit( $member=NULL )
	{
		if ( ( ( static::database()->options['indefinite_own_edit'] AND \IPS\Member::loggedIn()->member_id === $this->member_id ) OR ( \IPS\Member::loggedIn()->member_id and static::database()->all_editable ) ) AND ! $this->locked() AND ! $this->hidden() )
		{
			return TRUE;
		}
		
		if ( parent::canEdit( $member ) )
		{
			/* Test against specific perms for this category */
			return $this->container()->can( 'edit', $member );
		}
		
		return FALSE;
	}

	/**
	 * Can move?
	 *
	 * @param	\IPS\Member|NULL	$member	The member to check for (NULL for currently logged in member)
	 * @return	bool
	 */
	public function canMove( $member=NULL )
	{
		if ( ! static::database()->use_categories )
		{
			return FALSE;
		}
		
		return parent::canMove( $member );
	}

	/**
	 * Can manage revisions?
	 *
	 * @param	\IPS\Member|NULL		$member		The member to check for (NULL for currently logged in member)
	 * @return	bool
	 * @throws	\BadMethodCallException
	 */
	public function canManageRevisions( \IPS\Member $member = NULL )
	{
		return static::database()->revisions and static::modPermission( 'content_revisions', $member );
	}

	/**
	 * Can comment?
	 *
	 * @param	\IPS\Member\NULL	$member	The member (NULL for currently logged in member)
	 * @return	bool
	 */
	public function canComment( $member=NULL )
	{
		return ( static::database()->options['comments'] and parent::canComment( $member ) );
	}

	/**
	 * Can review?
	 *
	 * @param	\IPS\Member\NULL	$member	The member (NULL for currently logged in member)
	 * @return	bool
	 */
	public function canReview( $member=NULL )
	{
		return ( static::database()->options['reviews'] and parent::canReview( $member ) );
	}

	/**
	 * During canCreate() check, verify member can access the module too
	 *
	 * @param	\IPS\Member	$member		The member
	 * @note	The only reason this is abstracted at this time is because Pages creates dynamic 'modules' with its dynamic records class which do not exist
	 * @return	bool
	 */
	protected static function _canAccessModule( \IPS\Member $member )
	{
		/* Can we access the module */
		return $member->canAccessModule( \IPS\Application\Module::get( static::$application, 'database', 'front' ) );
	}

	/**
	 * Already reviewed?
	 *
	 * @param	\IPS\Member\NULL	$member	The member (NULL for currently logged in member)
	 * @return	bool
	 */
	public function hasReviewed( $member=NULL )
	{
		/* Check cache */
		if ( $this->_hasReviewed !== NULL )
		{
			return $this->_hasReviewed;
		}

		$member = $member ?: \IPS\Member::loggedIn();

		$reviewClass = static::$reviewClass;
		$idColumn    = static::$databaseColumnId;

		$this->_hasReviewed = \IPS\Db::i()->select(
			'COUNT(*)', $reviewClass::$databaseTable, array(
				array(
					$reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['author'] . '=?',
					$member->member_id
				),
				array(
					$reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?',
					$this->$idColumn
				),
				array( $reviewClass::$databasePrefix . 'database_id=?', static::$customDatabaseId )
			)
		)->first();

		return $this->_hasReviewed;
	}

	/* ! Rating */
	
	/**
	 * Can Rate?
	 *
	 * @param	\IPS\Member|NULL		$member		The member to check for (NULL for currently logged in member)
	 * @return	bool
	 * @throws	\BadMethodCallException
	 */
	public function canRate( \IPS\Member $member = NULL )
	{
		return parent::canRate( $member ) and ( $this->container()->allow_rating );
	}
	
	/* ! Comments */
	/**
	 * Add the comment form elements
	 *
	 * @return	array
	 */
	public function commentFormElements()
	{
		return parent::commentFormElements();
	}

	/**
	 * Add a comment when the filtes changed. If they changed.
	 *
	 * @param   array   $values   Array of new form values
	 * @return  boolean|\IPS\cms\Records\Comment
	 */
	public function addCommentWhenFiltersChanged( $values )
	{
		if ( ! $this->canComment() )
		{
			return FALSE;
		}

		$currentValues = $this->fieldValues();
		$commentClass  = 'IPS\cms\Records\Comment' . static::$customDatabaseId;
		$categoryClass = 'IPS\cms\Categories' . static::$customDatabaseId;
		$fieldsClass   = 'IPS\cms\Fields' . static::$customDatabaseId;
		$newValues     = array();
		$fieldsFields  = $fieldsClass::fields( $values, 'edit', $this->category_id ?  $categoryClass::load( $this->category_id ) : NULL, $fieldsClass::FIELD_DISPLAY_COMMENTFORM );

		foreach( $currentValues as $name => $data )
		{
			$id = mb_substr( $name, 6 );
			if ( $id == static::database()->field_title or $id == static::database()->field_content )
			{
				unset( $currentValues[ $name ] );
			}

			/* Not filterable? */
			if ( ! isset( $fieldsFields[ $id ] ) )
			{
				unset( $currentValues[ $name ] );
			}
		}

		foreach( $fieldsFields as $key => $field )
		{
			$newValues[ 'field_' . $key ] = $field::stringValue( isset( $values[ $field->name ] ) ? $values[  $field->name ] : NULL );
		}

		$diff = array_diff_assoc( $currentValues, $newValues );

		if ( count( $diff ) )
		{
			$show    = array();
			$display = $fieldsClass::display( $newValues, NULL, NULL, 'id' );

			foreach( $diff as $name => $value )
			{
				$id = mb_substr( $name, 6 );

				if ( $display[ $id ] )
				{
					$show[ $name ] = sprintf( \IPS\Member::loggedIn()->language()->get( 'cms_record_field_changed' ), \IPS\Member::loggedIn()->language()->get( 'content_field_' . $id ), $display[ $id ] );
				}
			}

			if ( count( $show ) )
			{
				$post = \IPS\cms\Theme::i()->getTemplate( static::database()->template_display, 'cms', 'database' )->filtersAddComment( $show );
				\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $post );

				return $commentClass::create( $this, $post, FALSE );
			}
		}

		return TRUE;
	}

	/* ! Tags */
	
	/**
	 * Can tag?
	 *
	 * @param	\IPS\Member|NULL		$member		The member to check for (NULL for currently logged in member)
	 * @param	\IPS\Node\Model|NULL	$container	The container to check if tags can be used in, if applicable
	 * @return	bool
	 */
	public static function canTag( \IPS\Member $member = NULL, \IPS\Node\Model $container = NULL )
	{
		return parent::canTag( $member, $container ) and ( static::database()->tags_enabled );
	}
	
	/**
	 * Can use prefixes?
	 *
	 * @param	\IPS\Member|NULL		$member		The member to check for (NULL for currently logged in member)
	 * @param	\IPS\Node\Model|NULL	$container	The container to check if tags can be used in, if applicable
	 * @return	bool
	 */
	public static function canPrefix( \IPS\Member $member = NULL, \IPS\Node\Model $container = NULL )
	{
		return parent::canPrefix( $member, $container ) and ( ! static::database()->tags_noprefixes );
	}
	
	/**
	 * Defined Tags
	 *
	 * @param	\IPS\Node\Model|NULL	$container	The container to check if tags can be used in, if applicable
	 * @return	array
	 */
	public static function definedTags( \IPS\Node\Model $container = NULL )
	{
		if ( static::database()->tags_predefined )
		{
			return explode( ',', static::database()->tags_predefined );
		}
	
		return parent::definedTags( $container );
	}

	/**
	 * Use a custom table helper when building content item tables
	 *
	 * @param	\IPS\Helpers\Table	$table	Table object to modify
	 * @return	\IPS\Helpers\Table
	 */
	public function reputationTableCallback( $table, $currentClass )
	{
		return $table;
	}
	
	/* !Notifications */
	
	/**
	 * Send quote and mention notifications
	 *
	 * @param	array	$exclude		An array of member IDs *not* to send notifications to
	 * @return	array	Member IDs sent to
	 */
	protected function sendQuoteAndMentionNotifications( $exclude=array() )
	{
		$data = array( 'quotes' => array(), 'mentions' => array() );
		
		foreach ( call_user_func( array( 'IPS\cms\Fields' .  static::$customDatabaseId, 'data' ) ) as $field )
		{
			if ( $field->type == 'Editor' )
			{
				$key = "field_{$field->id}";
				
				$_data = static::_getQuoteAndMentionIdsFromContent( $this->$key );
				foreach ( $_data as $type => $memberIds )
				{
					$_data[ $type ] = array_filter( $memberIds, function( $memberId ) use ( $field )
					{
						return $field->can( 'view', \IPS\Member::load( $memberId ) );
					} );
				}
				
				$data = array_map( 'array_unique', array_merge_recursive( $data, $_data ) );
			}
		}
		
		return $this->_sendQuoteAndMentionNotifications( $data, $exclude );
	}

    /**
     * Get average review rating
     *
     * @return	int
     */
    public function averageReviewRating()
    {
        if( $this->_averageReviewRating !== NULL )
        {
            return $this->_averageReviewRating;
        }

        $reviewClass = static::$reviewClass;
        $idColumn = static::$databaseColumnId;

        $where = array();
        $where[] = array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=? AND review_database_id=?', $this->$idColumn, static::$customDatabaseId );
        if ( in_array( 'IPS\Content\Hideable', class_implements( $reviewClass ) ) )
        {
            if ( isset( $reviewClass::$databaseColumnMap['approved'] ) )
            {
                $where[] = array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['approved'] . '=?', 1 );
            }
            elseif ( isset( $reviewClass::$databaseColumnMap['hidden'] ) )
            {
                $where[] = array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['hidden'] . '=?', 0 );
            }
        }

        $this->_averageReviewRating = round( \IPS\Db::i()->select( 'AVG(' . $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['rating'] . ')', $reviewClass::$databaseTable, $where )->first() );

        return $this->_averageReviewRating;
    }

	/**
	 * If, when making a post, we should merge with an existing comment, this method returns the comment to merge with
	 *
	 * @return	\IPS\Content\Comment|NULL
	 */
	public function mergeConcurrentComment()
	{
		$lastComment = parent::mergeConcurrentComment();

		/* If we sync to the forums, make sure that the "last comment" is not actually the first post */
		if( $this->record_topicid AND $lastComment !== NULL )
		{
			$firstComment = \IPS\forums\Topic::load( $this->record_topicid )->comments( 1, 0, 'date', 'asc' );

			if( $firstComment->pid == $lastComment->pid )
			{
				return NULL;
			}
		}

		return $lastComment;
	}
}