<?php
/**
 * @brief		Album Node
 * @author		<a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) Invision Power Services, Inc.
 * @license		https://www.invisioncommunity.com/legal/standards/
 * @package		Invision Community
 * @subpackage	Gallery
 * @since		04 Mar 2014
 */

namespace IPS\gallery;

/* To prevent PHP errors (extending class does not exist) revealing path */

use BadMethodCallException;
use DomainException;
use Exception;
use InvalidArgumentException;
use IPS\Application;
use IPS\calendar\Calendar;
use IPS\calendar\Event;
use IPS\Content\Comment;
use IPS\Content\Item as ContentItem;
use IPS\Content\Review;
use IPS\Content\Search\Index;
use IPS\Db;
use IPS\File;
use IPS\gallery\Album\Item;
use IPS\Helpers\Form;
use IPS\Helpers\Form\CheckboxSet;
use IPS\Helpers\Form\Editor;
use IPS\Helpers\Form\Node;
use IPS\Helpers\Form\Radio;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\SocialGroup;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\YesNo;
use IPS\Http\Url;
use IPS\Http\Url\Friendly;
use IPS\Lang;
use IPS\Member;
use IPS\Member\Group;
use IPS\Node\DelayedCount;
use IPS\Node\Model;
use IPS\Node\Ratings;
use IPS\Output;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Request;
use IPS\Text\Parser;
use IPS\Theme;
use OutOfBoundsException;
use OutOfRangeException;
use RuntimeException;
use UnderflowException;
use function count;
use function defined;
use function in_array;
use function is_array;
use function ucwords;

if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Album Node
 */
class Album extends Model
{
	use DelayedCount,
	Ratings
	{
		Ratings::canRate as public _canRate;
	}
	
	/**
	 * @brief	Define view access levels
	 */
	const AUTH_TYPE_PUBLIC		= 1;
	const AUTH_TYPE_PRIVATE		= 2;
	const AUTH_TYPE_RESTRICTED	= 3;
	const AUTH_TYPE_DELETED		= 4;

	/**
	 * @brief	Define submit access levels
	 */
	const AUTH_SUBMIT_OWNER			= 0;
	const AUTH_SUBMIT_PUBLIC		= 1;
	const AUTH_SUBMIT_GROUPS		= 2;
	const AUTH_SUBMIT_MEMBERS		= 3;
	const AUTH_SUBMIT_CLUB			= 4;
	
	/**
	 * @brief	[ActiveRecord] Multiton Store
	 */
	protected static array $multitons;
	
	/**
	 * @brief	[ActiveRecord] Database Table
	 */
	public static ?string $databaseTable = 'gallery_albums';
	
	/**
	 * @brief	[ActiveRecord] Database Prefix
	 */
	public static string $databasePrefix = 'album_';

	/**
	 * @brief	[Node] Parent Node ID Database Column
	 */
	public static string $parentNodeColumnId = 'category_id';
	
	/**
	 * @brief	[Node] Node Title
	 */
	public static string $nodeTitle = 'albums';

	/**
	 * @brief	[Node] Node Database Order Column
	 */
	public static ?string $databaseColumnOrder = 'name';

	/**
	 * @brief	[Node] Sortable?
	 */
	public static bool $nodeSortable = FALSE;

	/**
	 * @brief	Content Item Class
	 */
	public static ?string $contentItemClass = 'IPS\gallery\Image';

	/* These are required to be declared here as well as the asItem class for embeds.

	/**
	 * @brief	Review Class
	 */
	public static string $reviewClass = 'IPS\gallery\Album\Review';

	/**
	 * @brief	Comment Class
	 */
	public static ?string $commentClass = 'IPS\gallery\Album\Comment';

	/**
	 * @brief	[Node] If the node can be "owned", the owner "type" (typically "member" or "group") and the associated database column
	 */
	public static ?array $ownerTypes = array( 'member' => 'owner_id' );

	/**
	 * @brief	[Node] By mapping appropriate columns (rating_average and/or rating_total + rating_hits) allows to cache rating values
	 */
	public static array $ratingColumnMap	= array(
		'rating_average'	=> 'rating_aggregate',
		'rating_total'		=> 'rating_total',
		'rating_hits'		=> 'rating_count',
	);

	/**
	 * Mapping of node columns to specific actions (e.g. comment, review)
	 * Note: Mappings can also reference bitoptions keys.
	 *
	 * @var array
	 */
	public static array $actionColumnMap = array(
		'comments' 			=> 'allow_comments',
		'reviews'			=> 'allow_reviews'
	);

	/**
	 * @brief	[Node] Maximum results to display at a time in any node helper form elements. Useful for user-submitted node types when there may be a lot. NULL for no limit.
	 */
	public static ?int $maxFormHelperResults = 2000;

	/**
	 * Determines if this class can be extended via UI Extension
	 *
	 * @var bool
	 */
	public static bool $canBeExtended = true;

	/**
	 * Return this album as a content item instead of a node
	 *
	 * @return Item
	 */
	public function asItem(): Item
	{
		$data = $this->_data;

		foreach( $this->_data as $k => $v )
		{
			$data['album_' . $k ] = $v;
		}

		return Item::constructFromData( $data );
	}

	/**
	 * [Node] Return the owner if this node can be owned
	 *
	 * @throws	RuntimeException
	 * @return	Member|null
	 */
	public function owner(): ?Member
	{
		$owner = parent::owner();

		/* Gallery albums have to be owned by a user, so return a guest user if the owner is invalid */
		if( $owner === NULL OR $owner->member_id === null )
		{
			return new Member;
		}
		
		return $owner;
	}

	/**
	 * [Node] Load and check permissions
	 *
	 * @param	mixed				$id		ID
	 * @param string $perm	Permission Key
	 * @param	Member|NULL	$member	Member, or NULL for logged in member
	 * @return	static
	 * @throws	OutOfRangeException
	 * @note	Album 'add' permissions are properly checked via the can() method
	 */
	public static function loadAndCheckPerms( mixed $id, string $perm='view', Member $member = NULL ): static
	{
		$obj = parent::loadAndCheckPerms( $id );
		$member = $member ?: Member::loggedIn();
		
		if ( $obj->type == static::AUTH_TYPE_DELETED )
		{
			throw new OutOfRangeException;
		}
		
		if ( !$obj->can( $perm ) )
		{
			/* If we are adding and the member has mod permissions to edit in the category, they can create an album for another user. In that case
				we need to allow the add permissions or they won't be able to add images to the new album */
			if( $perm != 'add' OR !Item::modPermission( 'edit', $member, $obj->category() ) )
			{
				throw new OutOfRangeException;
			}
		}

		return $obj;
	}

	/**
	 * Fetch all albums we can submit to
	 *
	 * @param Category $category		Category we are submitting in
	 * @return	array
	 * @throws	RuntimeException
	 */
	public static function loadForSubmit( Category $category ): array
	{
		$ownedAlbums		= static::loadByOwner( NULL, array( array( 'album_category_id=?', $category->id ) ) );
		$permittedAlbums	= Item::getItemsWithPermission( array( array( 'album_category_id=?', $category->id ) ), NULL, NULL, 'add' );
		$finalAlbums		= $ownedAlbums;

		foreach( $permittedAlbums as $album )
		{
			$finalAlbums[ $album->id ] = $album->asNode();
		}

		return $finalAlbums;
	}
	
	/**
	 * Fetch all nodes owned by a given user
	 *
	 * @param Member|int|null $member		The member whose nodes to load
	 * @param array $where		Initial where clause
	 * @return	array
	 * @throws	RuntimeException
	 */
	public static function loadByOwner( Member|int $member=NULL, array $where=array() ): array
	{
		$where[] = array( 'album_type !=?', static::AUTH_TYPE_DELETED );
		
		return parent::loadByOwner( $member, $where );
	}

	/**
	 * @brief Cached approved members
	 */
	protected mixed $_approvedMembers	= FALSE;

	/**
	 * Get members with access to view restricted album
	 *
	 * @return	array|null
	 * @note	This list will NOT include the album owner, who also inherently has access
	 */
	protected function get_approvedMembers() : array|null
	{
		if( $this->_approvedMembers !== FALSE )
		{
			return $this->_approvedMembers;
		}

		if( $this->type === static::AUTH_TYPE_RESTRICTED )
		{
			$members	= array();

			foreach( Db::i()->select( '*', 'core_sys_social_group_members', array( 'group_id=?', $this->allowed_access ) ) as $member )
			{
				$members[]	= Member::load( $member['member_id'] );
			}

			$this->_approvedMembers	= $members;

			return $this->_approvedMembers;
		}

		$this->_approvedMembers = NULL;

		return $this->_approvedMembers;
	}

	/**
	 * [Node] Get title
	 *
	 * @return	string
	 */
	protected function get__title(): string
	{
		return $this->name;
	}

	/**
	 * Get SEO name
	 *
	 * @return	string
	 */
	public function get_name_seo(): string
	{
		if( !$this->_data['name_seo'] )
		{
			$this->name_seo	= Friendly::seoTitle( $this->name );

			if( $this->_data['name_seo'] )
			{
				$this->save();
			}
		}

		return $this->_data['name_seo'] ?: Friendly::seoTitle( $this->name );
	}

	/**
	 * [Node] Get whether or not this node is enabled
	 *
	 * @note	Return value NULL indicates the node cannot be enabled/disabled
	 * @return	bool|null
	 */
	protected function get__enabled(): ?bool
	{
		return NULL;
	}

	/**
	 * Get sort order
	 *
	 * @return	string|null
	 */
	public function get__sortBy(): ?string
	{
		return $this->sort_options;
	}

	/**
	 * [Node] Get number of content items
	 *
	 * @return	int|null
	 */
	protected function get__items(): ?int
	{
		return $this->count_imgs;
	}

	/**
	 * Get items count language string
	 *
	 * @return string
	 * @throws	BadMethodCallException
	 */
	public function get__countLanguageString(): string
	{
		return 'num_images';
	}

	/**
	 * [Node] Get number of content comments
	 *
	 * @return int|null
	 */
	protected function get__comments(): ?int
	{
		return $this->count_comments;
	}

	/**
	 * [Node] Get number of content comments (for display)
	 *
	 * @return	int
	 */
	protected function get__commentsForDisplay(): int
	{
		return $this->_comments;
	}

	/**
	 * [Node] Get number of content reviews
	 *
	 * @return int|null
	 */
	protected function get__reviews(): ?int
	{
		return $this->count_reviews;
	}

	/**
	 * [Node] Get number of unapproved content items
	 *
	 * @return int|null
	 */
	protected function get__unnapprovedItems(): ?int
	{
		return $this->count_imgs_hidden;
	}

	/**
	 * [Node] Get number of unapproved content reviews
	 *
	 * @return int|null
	 */
	protected function get__unapprovedReviews(): ?int
	{
		return $this->count_reviews_hidden;
	}
	
	/**
	 * [Node] Get number of unapproved content comments
	 *
	 * @return	int|null
	 */
	protected function get__unapprovedComments(): ?int
	{
		return $this->count_comments_hidden;
	}

	/**
	 * [Node] Get content table description
	 *
	 * @return string|null
	 */
	protected function get_description(): ?string
	{
		return $this->_data['description'];
	}

	/**
	 * Set number of items
	 *
	 * @param int $val	Items
	 * @return	void
	 */
	protected function set__items( int $val ) : void
	{
		$this->count_imgs = $val;
	}


	/**
	 * Set number of items
	 *
	 * @param	int	$val		Comments
	 * @return	void
	 */
	protected function set__comments( int $val ) : void
	{
		$this->count_comments = $val;
	}

	/**
	 * Set number of items
	 *
	 * @param	int	$val		Reviews
	 * @return	void
	 */
	protected function set__reviews( int $val ) : void
	{
		$this->count_reviews = $val;
	}

	/**
	 * [Node] Get number of unapproved content items
	 *
	 * @param	int	$val	Unapproved Items
	 * @return	void
	 */
	protected function set__unapprovedItems( int $val ) : void
	{
		$this->count_imgs_hidden = $val;
	}
	
	/**
	 * [Node] Get number of unapproved content comments
	 *
	 * @param	int	$val	Unapproved Comments
	 * @return	void
	 */
	protected function set__unapprovedComments( int $val ) : void
	{
		$this->count_comments_hidden = $val;
	}

	/**
	 * [Node] Get number of unapproved content reviews
	 *
	 * @param	int	$val		Unapproved Reviews
	 * @return	void
	 */
	protected function set__unapprovedReviews( int $val ) : void
	{
		$this->count_reviews_hidden = $val;
	}
	
	/**
	 * [Node] Add/Edit Form
	 *
	 * @param	Form	$form	The form
	 * @return	void
	 */
	public function form( Form &$form ) : void
	{
		foreach (static::formFields($this->_id ? $this : NULL) as $field )
		{
			$form->add( $field );
		}

        parent::form( $form );
	}
	
	/**
	 * Get fields
	 *
	 * @param Album|NULL	$album		The album
	 * @param bool $forOther	Is this specifically not for the current member (e.g. on a move form)?
	 * @param	bool					$required	If TRUE, required elements (like name are actually required) otherwise they just appear so (e.g. on move form)
	 * @return	array
	 */
	public static function formFields( Album $album = NULL, bool $forOther=FALSE, bool $required = TRUE ): array
	{
		/* Save my sanity here.... load the category if we have one */
		$category = NULL;
		if( $album AND $album->category_id )
		{
			$category = $album->category();
		}
		elseif( !$album AND ( isset( Request::i()->chosenCategory ) or isset( Request::i()->category ) ) )
		{
			try
			{
				$category = Category::load( (int)Request::i()->chosenCategory ?: Request::i()->category );
			}
			catch( OutOfRangeException ){}
		}

		$return = array();
		
		$return[] = new Text( 'album_name', $album ? $album->name : '', $required ?: NULL );

		$return[] = new Editor( 'album_description', $album ? $album->description : '', FALSE, array(
			'app' => 'gallery',
			'key' => 'Albums',
			'autoSaveKey' => ( $album ? "gallery_album_{$album->id}_desc" : "gallery-new-album" ),
			'attachIds' => $album ? array( $album->id, NULL, 'description' ) : NULL,
			'allowAttachments' => false
		) );
		
		$return[] = new Node( 'album_category', $category?->id, $required ?: NULL, array(
			'class'		      => 'IPS\gallery\Category',
			'disabled'	      => false,
			'permissionCheck' => function( $node )
			{
				if ( ! $node->allow_albums )
				{
					return false;
				}
				
				if ( ! $node->can( 'add' ) )
				{
					return false;
				}
				
				return true;
			}
		) );

		if( $forOther or Item::modPermission( 'edit', NULL, ($album?->category()) ) )
		{
			if ( !$forOther )
			{
				$return[] = new Radio( 'set_album_owner', ( $album AND $album->owner_id !== Member::loggedIn()->member_id ) ? 'other' : 'me', $required ?: NULL, array( 'options' => array( 'me' => 'set_album_owner_me', 'other' => 'set_album_owner_other' ), 'toggles' => array( 'other' => array( 'album_owner' ) ) ), NULL, NULL, NULL, 'set_album_owner' );
			}
			$return[] = new Form\Member( 'album_owner', $album ? Member::load( $album->owner_id )->name : NULL, NULL, array(), function($val ) use ( $required, $forOther )
			{
				if ( !$val and $required and ( $forOther or Request::i()->set_album_owner == 'other' ) )
				{
					throw new DomainException('form_required');
				}
			}, NULL, NULL, 'album_owner' );
		}

		$types		= array( static::AUTH_TYPE_PUBLIC => 'album_public' );
		$toggles	= array();

		if( Member::loggedIn()->group['g_create_albums_private'] )
		{
			$types[ static::AUTH_TYPE_PRIVATE ]	= ( Image::modPermission( 'edit', NULL, ($album?->category()) ) ) ? 'album_private_mod' : 'album_private';
		}

		if( Member::loggedIn()->group['g_create_albums_fo'] )
		{
			$types[ static::AUTH_TYPE_RESTRICTED ]	= 'album_friend_only';
			$toggles[ static::AUTH_TYPE_RESTRICTED ]	= array( 'album_allowed_access' );
		}
		$return[] = new Radio( 'album_type', ( $album and $album->type ) ? $album->type : static::AUTH_TYPE_PUBLIC, FALSE, array( 'options' => $types, 'toggles' => $toggles ), NULL, NULL, NULL, 'album_type' );

		if( Member::loggedIn()->group['g_create_albums_fo'] )
		{
			$return[] = new SocialGroup( 'album_allowed_access', $album ? (int) $album->allowed_access : NULL, FALSE, array( 'owner' => $album ? Member::load( $album->owner_id ) : ( Request::i()->album_owner ? Member::load( Request::i()->album_owner, 'name' ) : Member::loggedIn() ) ), NULL, NULL, NULL, 'album_allowed_access' );
		}

		$submitTypes = array(
			static::AUTH_SUBMIT_OWNER		=> 'album_submittype_owner',
			static::AUTH_SUBMIT_PUBLIC		=> 'album_submittype_public',
			static::AUTH_SUBMIT_GROUPS		=> 'album_submittype_groups',
			static::AUTH_SUBMIT_MEMBERS		=> 'album_submittype_members'
		);

		/* Is the category in a club */
		if( $category !== NULL AND $club = $category->club() )
		{
			$submitTypes[ static::AUTH_SUBMIT_CLUB ]	= 'album_submittype_club';
		}

		$return[] = new Radio( 'album_submit_type', ( $album and $album->submit_type ) ? $album->submit_type : static::AUTH_SUBMIT_OWNER, FALSE, array( 'options' => $submitTypes, 'toggles' => array( static::AUTH_SUBMIT_MEMBERS => array( 'album_submit_access_members' ), static::AUTH_SUBMIT_GROUPS => array( 'album_submit_access_groups' ) ) ), NULL, NULL, NULL, 'album_submit_type' );

		$return[] = new SocialGroup( 'album_submit_access_members', ( $album AND $album->submit_type == static::AUTH_SUBMIT_MEMBERS ) ? (int) $album->submit_access : NULL, FALSE, array( 'owner' => $album ? Member::load( $album->owner_id ) : ( Request::i()->album_owner ? Member::load( Request::i()->album_owner, 'name' ) : Member::loggedIn() ) ), NULL, NULL, NULL, 'album_submit_access_members' );
		$groups		= array_combine( array_keys( Group::groups( TRUE, FALSE ) ), array_map( function( $_group ) { return (string) $_group; }, Group::groups( TRUE, FALSE ) ) );
		$return[] = new CheckboxSet( 'album_submit_access_groups', ( $album and $album->submit_type == static::AUTH_SUBMIT_GROUPS ) ? explode( ',', $album->submit_access ) : NULL, FALSE, array( 'options' => $groups, 'multiple' => true ), NULL, NULL, NULL, 'album_submit_access_groups' );

		$return[] = new Select( 'album_sort_options', ( ( $album and $album->sort_options ) ? $album->sort_options : ( ( $category AND $category->sort_options_img ) ? $category->sort_options_img : 'updated' ) ), FALSE, array( 'options' => array( 'updated' => 'sort_updated', 'last_comment' => 'sort_last_comment', 'title' => 'album_sort_caption', 'rating' => 'sort_rating', 'date' => 'sort_date', 'num_comments' => 'sort_num_comments', 'num_reviews' => 'sort_num_reviews', 'views' => 'sort_views' ) ), NULL, NULL, NULL, 'album_sort_options' );

		$return[] = new YesNo( 'album_allow_rating', $album ? $album->allow_rating : TRUE, FALSE );

		if( $category AND $category->allow_comments )
		{
			$return[] = new YesNo('album_allow_comments', $album ? $album->allow_comments : TRUE, FALSE);
		}
		if( $category AND $category->allow_reviews )
		{
			$return[] = new YesNo('album_allow_reviews', $album ? $album->allow_reviews : TRUE, FALSE);
		}

		if( $category AND $category->allow_comments )
		{
			$return[] = new YesNo('album_use_comments', $album ? $album->use_comments : TRUE, FALSE );
		}
		if( $category AND $category->allow_reviews )
		{
			$return[] = new YesNo('album_use_reviews', $album ? $album->use_reviews : TRUE, FALSE );
		}
		
		return $return;
	}
	
	/**
	 * [Node] Format form values from add/edit form for save
	 *
	 * @param	array	$values	Values from the form
	 * @return	array
	 */
	public function formatFormValues( array $values ): array
	{
		$this->postSaveIsEdit	= FALSE;

		/* Claim attachments */
		if ( !$this->id )
		{
			$this->save();
			File::claimAttachments( 'gallery-new-album', $this->id, NULL, 'description' );

			/* Update public/non-public album count */
			if( isset( $values['album_type'] ) AND isset( $values['album_category'] ) )
			{
				if( $values['album_type'] == static::AUTH_TYPE_PUBLIC )
				{
					$values['album_category']->public_albums	= $values['album_category']->public_albums + 1;
				}
				else
				{
					$values['album_category']->nonpublic_albums	= $values['album_category']->nonpublic_albums + 1;
				}

				$values['album_category']->save();
			}
		}
		else
		{
			$this->postSaveIsEdit = TRUE;
			
			/* Are we changing from a private / friends only album to a public one? */
			if ( $values['album_type'] != $this->type )
			{
				/* Remember the current type */
				$this->postSaveType	= $this->type;

				/* Private to Public */
				if ( $values['album_type'] == static::AUTH_TYPE_PUBLIC )
				{
					$values['album_category']->public_albums	= $values['album_category']->public_albums + 1;
					$values['album_category']->nonpublic_albums	= $values['album_category']->nonpublic_albums - 1;
					$values['album_category']->save();
				}
				else
				{
					/* Public to Private... but only if was really public previously (we don't want to do this for private to friends-only) */
					if ( $this->type == static::AUTH_TYPE_PUBLIC )
					{
						$values['album_category']->public_albums	= $values['album_category']->public_albums - 1;
						$values['album_category']->nonpublic_albums	= $values['album_category']->nonpublic_albums + 1;
						$values['album_category']->save();
					}
				}
			}
		}

		/* Custom language fields */
		if( isset( $values['album_name'] ) )
		{
			Lang::saveCustom( 'gallery', "gallery_album_{$this->id}", $values['album_name'] );
			$values['name_seo']	= Friendly::seoTitle( is_array( $values['album_name'] ) ? $values['album_name'][ Lang::defaultLanguage() ] : $values['album_name'] );
		}

		if( isset( $values['album_description'] ) )
		{
			Lang::saveCustom( 'gallery', "gallery_album_{$this->id}_desc", $values['album_description'] );
		}

		/* Related ID */
		if( isset( $values['album_category'] ) )
		{
			$this->postSaveCategory	= $this->category_id;
			$values['category_id']	= $values['album_category']->id;
			unset( $values['album_category'] );
		}
		
		if( isset( $values['set_album_owner'] ) )
		{
			$values['owner_id']		= ( $values['set_album_owner'] == 'me' ) ? Member::loggedIn()->member_id : ( ( $values['album_owner'] instanceof Member ) ? $values['album_owner']->member_id : $values['album_owner'] );

			if( !$values['owner_id'] )
			{
				$values['owner_id']	= Member::loggedIn()->member_id;
			}
			unset( $values['set_album_owner'] );

			if( array_key_exists( 'album_owner', $values ) )
			{
				unset( $values['album_owner'] );
			}
		}
		else if( array_key_exists( 'album_owner', $values ) )
		{
			$values['owner_id']	= ( $values['album_owner'] instanceof Member ) ? $values['album_owner']->member_id : $values['album_owner'];
			unset( $values['album_owner'] );
		}
		else if ( !$this->postSaveIsEdit )
		{
			$values['owner_id']	= Member::loggedIn()->member_id;
		}

		switch( $values['album_submit_type'] )
		{
			case static::AUTH_SUBMIT_GROUPS:
				$values['submit_access']	= implode( ',', $values['album_submit_access_groups'] );
			break;

			case static::AUTH_SUBMIT_MEMBERS:
				$values['submit_access']	= $values['album_submit_access_members'];
			break;
		}

		unset( $values['album_submit_access_members'] );
		unset( $values['album_submit_access_groups'] );

		/* Send to parent */
		return $values;
	}

	/**
	 * @brief	Remember if we are editing or adding
	 */
	protected bool $postSaveIsEdit	= FALSE;

	/**
	 * @brief	Remember previous category when editing
	 */
	protected ?int $postSaveCategory	= 0;

	/**
	 * @brief	Remember previous album status when editing
	 */
	protected mixed $postSaveType		= NULL;
	
	/**
	 * [Node] Perform actions after saving the form
	 *
	 * @param	array	$values	Values from the form
	 * @return	void
	 */
	public function postSaveForm( array $values ) : void
	{
		File::claimAttachments( 'gallery-new-album', $this->id );
		
		$sendFilterNotifications = $this->asItem()->checkProfanityFilters( FALSE, $this->postSaveIsEdit );
		if ( $sendFilterNotifications )
		{
			/* Save it so it hides */
			$this->asItem()->save();
		}

		/* Update counts in categories if we move the album */
		if( $this->postSaveIsEdit and $this->postSaveCategory != $values['category_id'] )
		{
			$this->moveTo( Category::load( $values['category_id'] ), Category::load( $this->postSaveCategory ) );
		}
		
		/* Update search index */
		if( $this->postSaveIsEdit and ( $this->postSaveType != $values['album_type'] OR $this->postSaveCategory != $values['category_id'] ) )
		{
			Index::i()->massUpdate( 'IPS\gallery\Image', $this->id, NULL, $this->searchIndexPermissions() );
		}

        parent::postSaveForm( $values );
	}

	/**
	 * Get category album belongs to
	 *
	 * @return    Category
	 */
	public function category(): Category
	{
		return Category::load( $this->category_id );
	}
	
	/**
	 * Move to a different category
	 *
	 * @param Category $newCategory		New category
	 * @param Category|NULL	$existingCategory	Old category
	 * @return	void
	 */
	public function moveTo( Category $newCategory, Category $existingCategory = NULL ) : void
	{
		if ( $existingCategory === NULL )
		{
			$existingCategory = $this->category();
		}
		$this->category_id	= $newCategory->id;
		$this->save();
		
		/* Update images */
		Db::i()->update( 'gallery_images', array( 'image_category_id' => $newCategory->id ), array( 'image_album_id=?', $this->id ) );

		/* Update categories */
		foreach ( array( $newCategory, $existingCategory ) as $category )
		{
			$category->setLastComment();
			$category->public_albums			= (int) Db::i()->select( 'COUNT(*)', 'gallery_albums', array( 'album_category_id=? and album_type=1', $category->_id ) )->first();
			$category->nonpublic_albums			= (int) Db::i()->select( 'COUNT(*)', 'gallery_albums', array( 'album_category_id=? and album_type>1', $category->_id ) )->first();
			$category->save();
		}

		/* Tags */
		Db::i()->update( 'core_tags', array(
			'tag_aap_lookup'		=> md5( 'gallery;category;' . $newCategory->_id ),
			'tag_meta_parent_id'	=> $newCategory->_id
		), array( 'tag_meta_app=? and tag_meta_area=? and tag_meta_parent_id=?', 'gallery', 'images', $existingCategory->_id ) );
		
		Db::i()->update( 'core_tags_perms', array(
			'tag_perm_aap_lookup'	=> md5( 'gallery;category;' . $newCategory->_id ),
			'tag_perm_text'			=> Db::i()->select( 'perm_2', 'core_permission_index', array( 'app=? AND perm_type=? AND perm_type_id=?', 'gallery', 'category', $newCategory->_id ) )->first()
		), array( 'tag_perm_aap_lookup=?', md5( 'gallery;category;' . $existingCategory->_id ) ) );

		/* Load the item */
		$asItem = $this->asItem();

		/* Add to search index */
		Index::i()->index( $asItem );

		foreach ( array( 'commentClass', 'reviewClass' ) as $class )
		{
			$className = Item::$$class;
			Index::i()->massUpdate( $className, NULL, $this->id, $this->searchIndexPermissions(), NULL, $newCategory->_id );
		}

		/* Update caches */
		$asItem->expireWidgetCaches();
		$asItem->adjustSessions();
	}

	/**
	 * @brief	Cached URL
	 */
	protected mixed $_url = NULL;
	
	/**
	 * @brief	URL Base
	 */
	public static string $urlBase = 'app=gallery&module=gallery&controller=browse&album=';
	
	/**
	 * @brief	URL Base
	 */
	public static string $urlTemplate = 'gallery_album';
	
	/**
	 * @brief	SEO Title Column
	 */
	public static string $seoTitleColumn = 'name_seo';

	/**
	 * Get latest image information
	 *
	 * @return    Image|NULL
	 */
	public function lastImage(): ?Image
	{
		if( !$this->last_img_id )
		{
			return NULL;
		}

		try
		{
			return Image::load( $this->last_img_id );
		}
		catch ( Exception ) /* Catch both Underflow and OutOfRange */
		{
			return NULL;
		}
	}

	/**
	 * Set last comment
	 *
	 * @param Comment|null $comment The latest comment or NULL to work it out
	 * @param Item|null $updatedItem We sometimes run setLastComment() when an item has been edited, if so, that item will be here
	 * @return    void
	 */
	protected function _setLastComment( Comment $comment=NULL, ContentItem $updatedItem=NULL ) : void
	{
		/* If this updated comment is older than the last shown on the node, do nothing */
		if( $updatedItem !== NULL AND $updatedItem->date < $this->last_img_date )
		{
			$updatedItem = null;
		}

		$this->setLastImage( $updatedItem );
	}

	/**
	 * Set last review
	 *
	 * @param	Review|NULL	$review	The latest review or NULL to work it out
	 * @return	void
	 * @note	We actually want to set the last image info, not the last review, so we ignore $review
	 */
	public function setLastReview( Review $review=NULL ) : void
	{
		$this->setLastImage();
	}

	/**
	 * Set last image data
	 *
	 * @param Image|null	$image
	 * @param string $sortBy		The column to sort by for last X images (defaults to image_date DESC; third parties can override)
	 * @return	void
	 * @note	This is called from the category, so we don't need to update our parent (the category)
	 */
	public function setLastImage( ?Image $image=null, string $sortBy='image_date DESC' ) : void
	{
		/* Make sure the image is actually the latest */
		if( $image !== null and $image->date < $this->last_img_date )
		{
			$image = null;
		}

		/* If we have one, just use it */
		if( $image !== null )
		{
			$this->last_img_date = $image->date;
			$this->last_img_id = $image->id;

			$_latestImages = $this->last_x_images ? json_decode( $this->last_x_images, true ) : [];
			if( !is_array($_latestImages ) )
			{
				$_latestImages = [];
			}
			if( !in_array( $image->id, $_latestImages ) )
			{
				array_unshift( $_latestImages, $image->id );
				if( count( $_latestImages ) > 20 )
				{
					array_pop( $_latestImages );
				}
				$this->last_x_images = json_encode( $_latestImages );
			}
		}
		else
		{
			/* Figure out our latest images in this album */
			$_latestImages	= array();

			$this->last_img_date	= 0;
			$this->last_img_id		= 0;
			foreach( Db::i()->select( '*', 'gallery_images', array( 'image_album_id=? AND image_approved=1', $this->id ), $sortBy, array( 0, 20 ) ) as $image )
			{
				if( $image['image_date'] > $this->last_img_date )
				{
					$this->last_img_date	= $image['image_date'];
					$this->last_img_id		= $image['image_id'];
				}

				$_latestImages[]	= $image['image_id'];
			}

			$this->last_x_images	= json_encode( $_latestImages );
		}

		$this->save();

		/* Save and then make sure search index is updated */
		Index::i()->index( $this->asItem() );
	}
	
	/**
	 * Returns the content images
	 *
	 * @param	int|null	$limit				Number of attachments to fetch, or NULL for all
	 * @param	bool		$ignorePermissions	If set to TRUE, permission to view the images will not be checked
	 * @return	array|NULL
	 * @throws	BadMethodCallException
	 */
	public function contentImages( int $limit = NULL, bool $ignorePermissions = FALSE ): array|null
	{
		return $this->asItem()->contentImages( $limit, $ignorePermissions );
	}
	
	/**
	 * Retrieve the latest images
	 *
	 * @return	iterable
	 */
	public function get__latestImages(): iterable
	{
		if ( ! $this->last_x_images )
		{
			return [];
		}

		$_latestImages	= json_decode( $this->last_x_images, TRUE ) ?? array();

		if( ! count( $_latestImages ) )
		{
			return [];
		}

		return Image::getItemsWithPermission( array( array( 'image_id IN(' . implode( ',', $_latestImages ) . ')' ) ), NULL, 20 );
	}

	/**
	 * @brief	Cached calendar events
	 */
	protected ?array $_events	= NULL;

	/**
	 * Get any associated calendar events
	 *
	 * @return	array|null
	 */
	public function get__event(): ?array
	{
		if( $this->_events !== NULL )
		{
			return $this->_events;
		}

		if( Application::appIsEnabled( 'calendar' ) )
		{
			try
			{
				$events	= iterator_to_array( Event::getItemsWithPermission( array( array( 'event_album=?', $this->id ) ) ) );

				if( !count( $events ) )
				{
					$this->_events	= array();
					return $this->_events;
				}

				Calendar::addCss();
				Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'calendar.css', 'calendar', 'front' ) );

				$this->_events	= $events;
				return $this->_events;
			}
			catch( OutOfRangeException ){}
		}

		$this->_events	= array();
		return $this->_events;
	}
	
	/**
	 * Delete Record
	 *
	 * @return    void
	 */
	public function delete(): void
	{
		File::unclaimAttachments( 'gallery_Albums', $this->id );
		parent::delete();

		Lang::deleteCustom( 'gallery', "gallery_album_{$this->id}" );
		Lang::deleteCustom( 'gallery', "gallery_album_{$this->id}_desc" );
		
		/* If there was a social group saved, delete it */
		if( $this->allowed_access )
		{
			Db::i()->delete( 'core_sys_social_groups', array( 'group_id=?', $this->allowed_access ) );
			Db::i()->delete( 'core_sys_social_group_members', array( 'group_id=?', $this->allowed_access ) );
		}

		/* If any calendar events are associated, unassociate */
		if( Application::appIsEnabled( 'calendar' ) )
		{
			Db::i()->update( 'calendar_events', array( 'event_album' => 0 ), array( 'event_album=?', $this->id ) );
		}

		/* Delete as an item to remove comments, meta data, search index, reviews, etc. */
		$this->asItem()->delete();

		/* Update category information */
		if( $this->type == static::AUTH_TYPE_PUBLIC )
		{
			$this->category()->public_albums	= $this->category()->public_albums - 1;
		}
		else if( $this->type == static::AUTH_TYPE_PRIVATE or $this->type == static::AUTH_TYPE_RESTRICTED )
		{
			$this->category()->nonpublic_albums	= $this->category()->nonpublic_albums - 1;
		}

		$this->category()->save();
	}

	/**
	 * Retrieve the content item count
	 *
	 * @param array|null $data	Data array for mass move/delete
	 * @return	null|int
	 */
	public function getContentItemCount( array $data=NULL ): ?int
	{
		/* @var ContentItem $contentItemClass */
		$contentItemClass = static::$contentItemClass;

		$where = array( array( $contentItemClass::$databasePrefix . 'album_id=?', $this->id ) );

		if( $data )
		{
			$where = array_merge_recursive( $where, $this->massMoveorDeleteWhere( $data ) );
		}

		return (int) Db::i()->select( 'COUNT(*)', $contentItemClass::$databaseTable, $where )->first();
	}

	/**
	 * Retrieve content items (if applicable) for a node.
	 *
	 * @param int|null $limit The limit
	 * @param int|null $offset The offset
	 * @param array $additionalWhere
	 * @param bool|int $countOnly If TRUE, will get the number of results
	 * @return    ActiveRecordIterator|int
	 */
	public function getContentItems( ?int $limit, ?int $offset, array $additionalWhere = array(), bool|int $countOnly=FALSE ): ActiveRecordIterator|int
	{
		if ( !isset( static::$contentItemClass ) )
		{
			throw new BadMethodCallException;
		}

		/* @var ContentItem $contentItemClass */
		$contentItemClass = static::$contentItemClass;

		$where		= array();
		$where[]	= array( $contentItemClass::$databasePrefix . 'album_id=?', $this->_id );

		if ( count( $additionalWhere ) )
		{
			foreach( $additionalWhere AS $clause )
			{
				$where[] = $clause;
			}
		}

		if ( $countOnly )
		{
			return Db::i()->select( 'COUNT(*)', $contentItemClass::$databaseTable, $where )->first();
		}
		else
		{
			$limit	= ( $offset !== NULL ) ? array( $offset, $limit ) : NULL;
			return new ActiveRecordIterator( Db::i()->select( '*', $contentItemClass::$databaseTable, $where, $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId, $limit ), $contentItemClass );
		}
	}

	/**
	 * Text for use with data-ipsTruncate
	 * Returns the post with paragraphs turned into line breaks
	 *
	 * @return	string
	 */
	public function truncated(): string
	{
		$text = Parser::removeElements( $this->description, array( '//blockquote' ) );
		$text = str_replace( array( '</p>', '</h1>', '</h2>', '</h3>', '</h4>', '</h5>', '</h6>' ), '<br>', $text );
		$text = strip_tags( str_replace( ">", "> ", $text ), '<br>' );

		return $text;
	}

	/**
	 * @brief	Cached cover photo
	 */
	protected mixed $coverPhoto	= NULL;

	/**
	 * Retrieve a cover photo
	 *
	 * @param string $size	Masked or small
	 * @return	mixed
	 */
	public function coverPhoto( string $size='small' ): mixed
	{
		if ( !$this->can( 'read' ) )
		{
			return NULL;
		}

		/* Make sure it's a valid size */
		if( !in_array( $size, array( 'masked', 'small' ) ) )
		{
			throw new InvalidArgumentException;
		}
		
		$property = $size . "_file_name";

		/* If we have an explicit cover photo set, make sure it's valid and load/cache it */
		if( $this->cover_img_id )
		{
			if( $this->coverPhoto === NULL )
			{
				$this->coverPhoto = $this->coverPhotoObject();
			}

			if( $this->coverPhoto !== NULL )
			{
				return (string) File::get( 'gallery_Images', $this->coverPhoto->$property )->url;
			}
		}

		if( $lastImage = $this->lastImage() AND !$lastImage->media )
		{
			if( $lastImage->$property )
			{
				return (string) File::get( 'gallery_Images', $lastImage->$property )->url;
			}
		}

		return NULL;
	}

	/**
	 * Retrieve a cover photo object
	 *
	 * @return    Image|null
	 */
	public function coverPhotoObject(): ?Image
	{
		/* If we have an explicit cover photo set, make sure it's valid and load/cache it */
		if( $this->cover_img_id )
		{
			if( $this->coverPhoto === NULL )
			{
				try
				{
					$this->coverPhoto	= Image::load( $this->cover_img_id );
				}
				catch( OutOfRangeException )
				{
					/* Cover photo isn't valid, reset album automatically */
					$this->cover_img_id	= 0;
					$this->save();
				}
			}
		}

		if( $this->coverPhoto !== NULL )
		{
			return $this->coverPhoto;
		}

		if( $lastImage = $this->lastImage() AND !$lastImage->media )
		{
			return $lastImage;
		}

		return NULL;
	}

	/**
	 * Load record based on a URL
	 *
	 * @param	Url	$url	URL to load from
	 * @return	mixed
	 * @throws	InvalidArgumentException
	 * @throws	OutOfRangeException
	 */
	public static function loadFromUrl( Url $url ): mixed
	{
		$qs = array_merge( $url->queryString, $url->hiddenQueryString );
		
		if ( isset( $qs['album'] ) )
		{
			return static::load( $qs['album'] );
		}
		
		throw new InvalidArgumentException;
	}

	/**
	 * [Node] Does the currently logged in user have permission to delete this node?
	 *
	 * @return    bool
	 */
	public function canDelete(): bool
	{
		if( static::restrictionCheck( 'delete' ) )
		{
			return TRUE;
		}

		return $this->asItem()->canDelete();
	}

	/**
	 * Check permissions
	 *
	 * @param	mixed								$permission		A key which has a value in static::$permissionMap['view'] matching a column ID in core_permission_index
	 * @param Group|Member|null $member							The member or group to check (NULL for currently logged in member)
	 * @param bool $considerPostBeforeRegistering	If TRUE, and $member is a guest, will return TRUE if "Post Before Registering" feature is enabled
	 * @return	bool
	 * @throws	OutOfBoundsException	If $permission does not exist in static::$permissionMap
	 * @note	Albums don't have permissions, but instead check against the category they are in
	 */
	public function can( mixed $permission, Group|Member $member=NULL, bool $considerPostBeforeRegistering = TRUE ): bool
	{
		/* Load the item */
		$asItem = $this->asItem();

		/* Figure out member */
		$member = $member ?: Member::loggedIn();

		/* Are we looking at a member? */
		if( $member instanceof Member )
		{
			/* If we don't have edit permission in the category, check album restrictions */
			if( !Image::modPermission( 'edit', $member, $this->category() ) )
			{
				/* Deny access if the album is private and we aren't the owner */
				if( $this->type == static::AUTH_TYPE_PRIVATE AND $this->owner() !== $member )
				{
					return FALSE;
				}

				/* If this is a restricted album, check that we're "on the list" */
				if( $this->type == static::AUTH_TYPE_RESTRICTED AND $this->owner() !== $member )
				{
					try
					{
						if( !$member->member_id )
						{
							return FALSE;
						}

						Db::i()->select( '*', 'core_sys_social_group_members', array( 'group_id=? AND member_id=?', $this->allowed_access, $member->member_id ) )->first();
					}
					catch( UnderflowException )
					{
						return FALSE;
					}
				}
			}

			/* If we are checking 'add' permission, verify if we can add */
			if( $permission == 'add' )
			{
				/* First check we can submit to this album based on the "Submissions" setting */
				switch ( $this->submit_type )
				{
					/* Owner */
					case static::AUTH_SUBMIT_OWNER:
						if ( $this->owner_id != $member->member_id )
						{
							return FALSE;
						}
						break;
											
					/* Chosen groups */
					case static::AUTH_SUBMIT_GROUPS:
						if ( !$member->inGroup( explode( ',', $this->submit_access ) ) )
						{
							return FALSE;
						}
						break;
						
					/* Chosen members */
					case static::AUTH_SUBMIT_MEMBERS:
						if ( !in_array( $this->submit_access, $member->socialGroups() ) AND $this->owner_id != $member->member_id )
						{
							return FALSE;
						}
						break;
					
					/* Club */
					case static::AUTH_SUBMIT_CLUB:
						if ( $club = $this->category()->club() and !in_array( $club->id, $member->clubs() ) )
						{
							return FALSE;
						}
						break;
				}
				
				/* Verify if we can add any more files to this album */
				if( $member->group['g_img_album_limit'] AND $member->group['g_img_album_limit'] - ( $this->count_imgs + $this->count_imgs_hidden ) < 1 )
				{
					return FALSE;
				}
			}

			/* Albums can be hidden, so make sure to test permissions */
			if( $asItem->hidden() )
			{
				if( !$asItem->canView( $member ) )
				{
					return FALSE;
				}

				$methodToCheck = "can" . ucwords( $permission );

				if( method_exists( $asItem, $methodToCheck ) )
				{
					return $asItem->$methodToCheck( $member );
				}
				else
				{
					return $asItem->can( $permission, $member, $considerPostBeforeRegistering );
				}
			}
		}
		/* Or are we looking at a group? */
		else
		{
			if( $permission == 'add' )
			{
				/* Verify if the group is in the submit list */
				if( $this->submit_type == static::AUTH_SUBMIT_GROUPS AND !in_array( $member->g_id, explode( ',', $this->submit_access ) ) )
				{
					return FALSE;
				}

				/* Verify if we can add any more files to this album */
				if( $member->g_img_album_limit AND $member->g_img_album_limit - ( $this->count_imgs + $this->count_imgs_hidden ) < 1 )
				{
					return FALSE;
				}
			}

			/* Albums can be hidden, so make sure to test permissions */
			if( $asItem->hidden() )
			{
				if( !$asItem->can( 'view', $member, $considerPostBeforeRegistering ) )
				{
					return FALSE;
				}

				return $asItem->can( $permission, $member, $considerPostBeforeRegistering );
			}
		}

		/* We'll just rely on category permissions if the album isn't hidden */
		try
		{
			return $this->category()->can( $permission, $member, $considerPostBeforeRegistering );
		}
		catch( OutOfRangeException )
		{
			return FALSE;
		}
	}
	
	/**
	 * Search Index Permissions
	 *
	 * @return	string	Comma-delimited values or '*'
	 * 	@li			Number indicates a group
	 *	@li			Number prepended by "m" indicates a member
	 *	@li			Number prepended by "s" indicates a social group
	 */
	public function searchIndexPermissions(): string
	{
		$return = $this->category()->searchIndexPermissions();
		
		if ( $this->type != static::AUTH_TYPE_PUBLIC )
		{
			$return = [];
			
			if ( $this->owner_id )
			{
				$return[] = "m{$this->owner_id}";
			}
			
			if ( $this->type == static::AUTH_TYPE_RESTRICTED )
			{
				$return[] = "s{$this->allowed_access}";
			}
			
			$return = implode( ',', array_unique( $return ) );
		}
		return $return;
	}

	/**
	 * Can Rate?
	 *
	 * @param	Member|NULL		$member		The member to check for (NULL for currently logged in member)
	 * @return	bool
	 * @throws	BadMethodCallException
	 */
	public function canRate( Member $member = NULL ): bool
	{
		if( $this->_canRate( $member ) )
		{
			if( $this->category()->allow_rating )
			{
				return $this->category()->can( 'rate', $member );
			}
			else
			{
				return FALSE;
			}
		}

		return FALSE;
	}

	/**
	 * Get template for node tables
	 *
	 * @return callable|array
	 */
	public static function nodeTableTemplate(): callable|array
	{
		\IPS\gallery\Application::outputCss();
		
		return array( Theme::i()->getTemplate( 'browse', 'gallery' ), 'albums' );
	}
	
	/**
	 * Get output for API
	 *
	 * @param	Member|NULL	$authorizedMember	The member making the API request or NULL for API Key / client_credentials
	 * @return	array
	 * @apiresponse	int						id					ID number
	 * @apiresponse	string					name				Name
	 * @apiresponse	string					description			Description
	 * @apiresponse	\IPS\gallery\Category	category			The category
	 * @apiresponse	\IPS\Member				owner				The owner
	 * @apiresponse	string					privacy				'public', 'private' (can only be viewed by owner) or 'restricted' (can only be viewed by owner or approved members)
	 * @apiresponse	[\IPS\Member]			approvedMembers		If the album is restricted, the members who can view it, in addition to the owner and moderators with appropriate permission
	 * @apiresponse	int						images				Number of images
	 * @apiresponse	string					url					URL
	 */
	public function apiOutput( Member $authorizedMember = NULL ): array
	{
		return array(
			'id'				=> $this->id,
			'name'				=> $this->name,
			'description'		=> Member::loggedIn()->language()->addToStack("gallery_album_{$this->id}_desc"),
			'category'			=> $this->category()->apiOutput( $authorizedMember ),
			'owner'				=> $this->owner()->apiOutput( $authorizedMember ),
			'privacy'			=> ( $this->type == static::AUTH_TYPE_PUBLIC ) ? 'public' : ( $this->type == static::AUTH_TYPE_PRIVATE ? 'private' : ( $this->type == static::AUTH_TYPE_RESTRICTED ? 'restricted' : null ) ),
			'approvedMembers'	=> $this->approvedMembers ? array_map( function( $val ) use ( $authorizedMember ) {
				return $val->apiOutput( $authorizedMember );
			}, $this->approvedMembers ) : null,
			'images'			=> $this->count_imgs,
			'url'				=> (string) $this->url()
		);
	}
	
	/**
	 * Webhook filters
	 *
	 * @return	array
	 */
	public function webhookFilters(): array
	{
		$filters = array();
		$filters['privacy'] = ( $this->type == static::AUTH_TYPE_PUBLIC ) ? 'public' : ( $this->type == static::AUTH_TYPE_PRIVATE ? 'private' : ( $this->type == static::AUTH_TYPE_RESTRICTED ? 'restricted' : null ) );
		return $filters;
	}

	/**
	 * Get template for managing this nodes follows
	 *
	 * @return callable|array
	 */
	public static function manageFollowNodeRow(): callable|array
	{
		\IPS\gallery\Application::outputCss();
		
		return array( Theme::i()->getTemplate( 'global', 'gallery' ), 'manageFollowNodeRow' );
	}
	
	/**
	 * Get the title for a node using the specified language object
	 * This is commonly used where we cannot use the logged in member's language, such as sending emails
	 *
	 * @param Lang $language	Language object to fetch the title with
	 * @param	array 		$options	What options to use for language parsing
	 * @return	string
	 */
	public function getTitleForLanguage( Lang $language, array $options=array() ): string
	{
		return $this->name;
	}

	/**
	 * Allow for individual classes to override and
	 * specify a primary image. Used for grid views, etc.
	 *
	 * @return File|null
	 */
	public function primaryImage() : ?File
	{
		if( $image = $this->coverPhotoObject() )
		{
			return $image->primaryImage();
		}

		return parent::primaryImage();
	}

	/**
	 * Count all comments, items, etc
	 *
	 * @return void
	 */
	protected function recount() : void
	{
		/* Now get the counts and set them for our images */
		$this->_items				= Db::i()->select( 'COUNT(*) as total', 'gallery_images', array( 'image_album_id=? AND image_approved=1', $this->id ) )->first();
		$this->_unapprovedItems		= Db::i()->select( 'COUNT(*) as total', 'gallery_images', array( 'image_album_id=? AND image_approved=0', $this->id ) )->first();

		if( $this->allow_comments )
		{
			$this->_comments			= Db::i()->select( 'COUNT(*) as total', 'gallery_comments', array( 'gallery_images.image_album_id=? AND comment_approved=1 AND gallery_images.image_approved=1', $this->id ) )->join( 'gallery_images', 'image_id=comment_img_id' )->first();
			$this->_unapprovedComments	= Db::i()->select( 'COUNT(*) as total', 'gallery_comments', array( 'gallery_images.image_album_id=? AND comment_approved=0', $this->id ) )->join( 'gallery_images', 'image_id=comment_img_id' )->first();
		}

		if( $this->use_comments )
		{
			/* And then get counts/latest data for the direct comments/reviews */
			$this->comments				= (int) Db::i()->select( 'COUNT(*) as total', 'gallery_album_comments', array( 'comment_album_id=? AND comment_approved=1', $this->id ) )->first();
			$this->comments_unapproved	= (int) Db::i()->select( 'COUNT(*) as total', 'gallery_album_comments', array( 'comment_album_id=? AND comment_approved=0', $this->id ) )->first();
			$this->comments_hidden		= (int) Db::i()->select( 'COUNT(*) as total', 'gallery_album_comments', array( 'comment_album_id=? AND comment_approved=-1', $this->id ) )->first();
		}

		if( $this->allow_reviews )
		{
			$this->_reviews	= Db::i()->select( 'COUNT(*) as total', 'gallery_reviews', array( 'gallery_images.image_album_id=? AND review_approved=1', $this->id ) )->join( 'gallery_images', 'image_id=review_image_id' )->first();
			$this->_unapprovedReviews	= Db::i()->select( 'COUNT(*) as total', 'gallery_reviews', array( 'gallery_images.image_album_id=? AND review_approved=0', $this->id ) )->join( 'gallery_images', 'image_id=review_image_id' )->first();
		}

		if( $this->use_reviews )
		{
			$this->reviews				= (int) Db::i()->select( 'COUNT(*) as total', 'gallery_album_reviews', array( 'review_album_id=? AND review_approved=1', $this->id ) )->first();
			$this->reviews_unapproved	= (int) Db::i()->select( 'COUNT(*) as total', 'gallery_album_reviews', array( 'review_album_id=? AND review_approved=0', $this->id ) )->first();
			$this->reviews_hidden		= (int) Db::i()->select( 'COUNT(*) as total', 'gallery_album_reviews', array( 'review_album_id=? AND review_approved=-1', $this->id ) )->first();
		}
	}
}