<?php
/**
 * @brief		MetaData Trait for Content Models/Comments
 * @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
 * @since		12 Nov 2013
 */

namespace IPS\Content;

use IPS\Member;
use IPS\Db;
use IPS\Application;
use IPS\Request;
use IPS\Output;
use IPS\Theme;
use IPS\Member\Group;
use BadMethodCallException;
use OutOfRangeException;
use UnderflowException;
use InvalidArgumentException;

use function defined;
use function header;
use function get_class;
use function in_array;
use function time;
use function json_encode;
use function json_decode;
use function count;

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

/**
 * MetaData Trait for Content Models/Comments
 */
trait MetaData
{
	/**
	 * Metadata types supported by this content
	 *
	 * @return	array
	 */
	abstract static function supportedMetaDataTypes(): array;
	
	/**
	 * Check if this content has metadata
	 *
	 * @return	bool
	 * @throws    BadMethodCallException
	 */
	public function hasMetaData(): bool
	{
		if ( isset( static::$databaseColumnMap['meta_data'] ) )
		{
			$column = static::$databaseColumnMap['meta_data'];
			return (bool) $this->$column;
		}
		
		throw new BadMethodCallException( "Class using must define meta_data column in column map" );
	}
	
	/**
	 * @brief	Meta Data Cache
	 */
	protected ?array $_metaData = NULL;
	
	/**
	 * Fetch Meta Data
	 *
	 * @return	array
	 */
	public function getMeta(): array
	{
		/* If we don't have any, don't bother */
		if ( $this->hasMetaData() === FALSE )
		{
			return array();
		}
		
		$idColumn = static::$databaseColumnId;
		
		if ( $this->_metaData === NULL )
		{
			$this->_metaData = array();
			foreach( Db::i()->select( '*', 'core_content_meta', array( "meta_class=? AND meta_item_id=?", get_class( $this ), $this->$idColumn ) ) AS $row )
			{
				$this->_metaData[ $row['meta_type'] ][ $row['meta_id'] ] = json_decode( $row['meta_data'], TRUE );
			}
		}
		
		return $this->_metaData;
	}
	
	/**
	 * Add Meta Data
	 *
	 * @param	string	$type	The type of data
	 * @param	array	$data	The data
	 * @return	int		The ID of the inserted metadata record
	 * @throws    BadMethodCallException
	 */
	public function addMeta( string $type, array $data ): int
	{
		if ( !static::supportedMetaDataTypes() OR !in_array( $type, static::supportedMetaDataTypes() ) )
		{
			throw new BadMethodCallException;
		}
		
		$idColumn = static::$databaseColumnId;
		$data = json_encode( $data );
		$id = Db::i()->insert( 'core_content_meta', array(
			'meta_class'		=> get_class( $this ),
			'meta_item_id'		=> $this->$idColumn,
			'meta_type'			=> $type,
			'meta_data'			=> $data,
			'meta_item_author'  => (int) $this->author()->member_id,
			'meta_added' 		=> time()
		) );
		
		$column = static::$databaseColumnMap['meta_data'];
		$this->$column = 1;
		$this->save();
		
		$this->_metaData = NULL;
		
		return $id;
	}
	
	/**
	 * Edit Meta Data
	 *
	 * @param	int		$id		The ID
	 * @param	array	$data	The data
	 * @return	void
	 * @throws BadMethodCallException
	 */
	public function editMeta( int $id, array $data ): void
	{
		try
		{
			/* Get current data */
			$idColumn = static::$databaseColumnId;
			
			$current = json_decode( Db::i()->select( 'meta_data', 'core_content_meta', array( "meta_class=? AND meta_item_id=? AND meta_id=?", get_class( $this ), $this->$idColumn, $id ) )->first(), true );
			
			foreach ( $data as $key => $value )
			{
				$current[ $key ] = $value;
			}

			Db::i()->update( 'core_content_meta', array( 'meta_data' => json_encode( $current ) ), array( "meta_id=?", $id ) );
			
			/* Make sure our flag is set */
			$column = static::$databaseColumnMap['meta_data'];
			$this->$column = TRUE;
			$this->save();
			
			$this->_metaData = NULL;
		}
		catch( UnderflowException )
		{
			throw new OutOfRangeException;
		}
	}
	
	/**
	 * Delete Meta Data
	 *
	 * @param	int		$id		The ID
	 * @return	void
	 */
	public function deleteMeta( int $id ): void
	{
		$idColumn = static::$databaseColumnId;
		Db::i()->delete( 'core_content_meta', array( "meta_class=? AND meta_item_id=? AND meta_id=?", get_class( $this ), $this->$idColumn, $id ) );
		
		/* Any left? */
		$count = Db::i()->select( 'COUNT(*)', 'core_content_meta', array( "meta_class=? AND meta_item_id=?", get_class( $this ), $this->$idColumn ), NULL, NULL, NULL, NULL, Db::SELECT_FROM_WRITE_SERVER )->first();
		
		if ( !$count )
		{
			$column = static::$databaseColumnMap['meta_data'];
			$this->$column = FALSE;
			$this->save();
		}
		
		$this->_metaData = NULL;
	}
	
	/**
	 * Delete All Meta Data
	 *
	 * @return	void
	 */
	public function deleteAllMeta(): void
	{
		$idColumn = static::$databaseColumnId;
		Db::i()->delete( 'core_content_meta', array( "meta_class=? AND meta_item_id=?", get_class( $this ), $this->$idColumn ) );
	}
	
	/**
	 * Can perform an action on a message
	 *
	 * @param	string				$action	The action
	 * @param	Member|NULL	$member	The member, or NULL for currently logged in
	 * @return	bool
	 * @note This is a wrapper for the extension so content items can extend and apply their own logic
	 */
	public function canOnMessage( string $action, ?Member $member = NULL ): bool
	{
		/* Extensions go first */
		if( $permCheck = Permissions::can( 'onMessage', $this, $member ) )
		{
			return ( $permCheck === Permissions::PERM_ALLOW );
		}

		if( !$this->actionEnabled( 'onMessage', $member ) )
		{
			return false;
		}

		return Application::load('core')->extensions( 'core', 'MetaData' )['ContentMessages']->canOnMessage( $action, $this, $member );
	}
	
	/**
	 * Add Item Message
	 *
	 * @param	string				$message		The message
	 * @param	string|NULL			$color			The message color
	 * @param	Member|NULL	$member			User adding the message
	 * @param	bool				$isPublic		Who should see the message
	 * @return	int
	 * @note This is a wrapper for the extension so content items can extend and apply their own logic
	 */
	public function addMessage( string $message, ?string $color = NULL, ?Member $member = NULL, bool $isPublic = TRUE  ): int
	{
		return Application::load('core')->extensions( 'core', 'MetaData' )['ContentMessages']->addMessage( $message, $color, $this, $member, $isPublic );
	}
	
	/**
	 * Edit Item Message
	 *
	 * @param	int					$id			The ID
	 * @param	string				$message	The new message
	 * @param	string|NULL			$color		Color
	 * @param	Member|NULL	$member		The member editing the message, or NULL for currently logged in
	 * @param	bool				$onlyStaff		Who should see the message
	 * @return	void
	 * @note This is a wrapper for the extension so content items can extend and apply their own logic
	 */
	public function editMessage( int $id, string $message, ?string $color = NULL, ?Member $member = NULL, bool $onlyStaff = FALSE ): void
	{
		Application::load('core')->extensions( 'core', 'MetaData' )['ContentMessages']->editMessage( $id, $message, $color, $this, $member, $onlyStaff );
	}
	
	/**
	 * Delete Item Message
	 *
	 * @param	int					$id		The ID
	 * @param	Member|NULL	$member	The member deleting the message
	 * @return	void
	 * @note This is a wrapper for the extension so content items can extend and apply their own logic
	 */
	public function deleteMessage( int $id, ?Member $member = NULL ): void
	{
		Application::load('core')->extensions( 'core', 'MetaData' )['ContentMessages']->deleteMessage( $id, $this, $member );
	}
	
	/**
	 * Get Item Messages
	 *
	 * @return	array
	 * @note This is a wrapper for the extension so content items can extend and apply their own logic
	 */
	public function getMessages(): array
	{
		return Application::load('core')->extensions( 'core', 'MetaData' )['ContentMessages']->getMessages( $this );
	}

	/**
	 * Is a featured comment?
	 *
	 * @param Comment $comment
	 * @return    bool
	 * @note This is a wrapper for the extension so content items can extend and apply their own logic
	 */
	public function isFeaturedComment( Comment $comment ): bool
	{
		return Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->isFeaturedComment( $comment );
	}

	/**
	 * Can Feature a Comment
	 *
	 * @param	Member|NULL	$member	The member, or NULL for currently logged in
	 * @return	bool
	 * @note This is a wrapper for the extension so content items can extend and apply their own logic
	 */
	public function canFeatureComment( ?Member $member = NULL ): bool
	{
		/* Extensions go first */
		if( $permCheck = Permissions::can( 'featureComment', $this, $member ) )
		{
			return ( $permCheck === Permissions::PERM_ALLOW );
		}

		if( !$this->actionEnabled( 'featureComment', $member ) )
		{
			return false;
		}

		return Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->canFeatureComment( $this, $member );
	}
	
	/**
	 * Can Unfeature a Comment
	 *
	 * @param	Member|NULL	$member	The member, or NULL for currently logged in
	 * @return	bool
	 * @note This is a wrapper for the extension so content items can extend and apply their own logic
	 */
	public function canUnfeatureComment( ?Member $member = NULL ): bool
	{
		/* Extensions go first */
		if( $permCheck = Permissions::can( 'unfeatureComment', $this, $member ) )
		{
			return ( $permCheck === Permissions::PERM_ALLOW );
		}

		if( !$this->actionEnabled( 'unfeatureComment', $member ) )
		{
			return false;
		}

		return Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->canUnfeatureComment( $this, $member );
	}
	
	/**
	 * Feature A Comment
	 *
	 * @param	Comment	$comment	The Comment
	 * @param	string|NULL				$note		An optional note to include
	 * @param	Member|NULL		$member		The member featuring the comment
	 * @return	void
	 * @note This is a wrapper for the extension so content items can extend and apply their own logic
	 */
	public function featureComment( Comment $comment, ?string $note = NULL, ?Member $member = NULL ): void
	{
		Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->featureComment( $this, $comment, $note, $member );

		/* Give points */
		$comment->author()->achievementAction( 'core', 'ContentPromotion', [
			'content'	=> $comment,
			'promotype'	=> 'recommend' //Yeah it says feature, but it's really recommend
		] );
	}
	
	/**
	 * Unfeature a comment
	 *
	 * @param	Comment	$comment	The Comment
	 * @param	Member|NULL		$member		The member unfeaturing the comment
	 * @return	void
	 * @note This is a wrapper for the extension so content items can extend and apply their own logic
	 */
	public function unfeatureComment( Comment $comment, ?Member $member = NULL ): void
	{
		Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->unfeatureComment( $this, $comment, $member );
	}
	
	/**
	 * Get Featured Comments in the most efficient way possible
	 *
	 * @return	array
	 */
	public function featuredComments(): array
	{
		$featured = Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->featuredComments( $this );

		if ( Request::i()->isAjax() && Request::i()->recommended == 'comments' )
		{
			/* @todo this really shouldn't be here, and should be in the controller */
			Output::i()->json( array( 
				'html' => Theme::i()->getTemplate( 'global', 'core', 'front' )->featuredComments( $featured, $this->url()->setQueryString( 'recommended', 'comments' ) ),
				'count' => count( $featured )
			) );
		}

		return $featured;
	}
	
	/**
	 * Is item-level moderation enabled?
	 *
	 * @param	Member|Group|NULL	$memberOrGroup		A member of member group to check for bypassing moderation.
	 * @return	bool
	 */
	public function itemModerationEnabled( Member|Group|null $memberOrGroup = NULL ): bool
	{
		return (bool) Application::load('core')->extensions( 'core', 'MetaData' )['ItemModeration']->enabled( $this, $memberOrGroup );
	}
	
	/**
	 * Can toggle item-level moderation?
	 *
	 * @param	Member|NULL		$member
	 * @return	bool
	 */
	public function canToggleItemModeration( ?Member $member = NULL ): bool
	{
		/* Extensions go first */
		if( $permCheck = Permissions::can( 'toggleItemModeration', $this, $member ) )
		{
			return ( $permCheck === Permissions::PERM_ALLOW );
		}

		if( !$this->actionEnabled( 'toggleItemModeration', $member ) )
		{
			return false;
		}

		$member = $member ?: Member::loggedIn();
		
		return (bool) Application::load('core')->extensions( 'core', 'MetaData' )['ItemModeration']->canToggle( $this, $member );
	}
	
	/**
	 * Toggle item-level moderation
	 *
	 * @param	string				$action
	 * @param	Member|NULL		$member
	 * @return	void
	 * @throws
	 *	@li \InvalidArgumentException
	 *	@li \BadMethodCallException
	 */
	public function toggleItemModeration( string $action, ?Member $member = NULL ): void
	{
		if ( !in_array( $action, array( 'enable', 'disable' ) ) )
		{
			throw new InvalidArgumentException;
		}
		
		$member = $member ?: Member::loggedIn();
		
		if ( !$this->canToggleItemModeration( $member ) )
		{
			throw new BadMethodCallException;
		}
		
		Application::load('core')->extensions( 'core', 'MetaData' )['ItemModeration']->$action( $this, $member );
	}
}