<?php
/**
 * Conversations component
 *
 * @author Pierre HUBERT
 */

class conversations {

	/**
	 * @var String $conversationsListTable Name of the conversation list table 
	 */
	private $conversationsListTable;

	/**
	 * @var String $conversationsUsersTable Name of the conversation users table 
	 */
	private $conversationsUsersTable;

	/**
	 * @var String $conversationMessagesTabel Name of the conversation messages table
	 */
	private $conversationsMessagesTable;


	/**
	 * Public constructor
	 */
	public function __construct(){
		$this->conversationsListTable = CS::get()->config->get("dbprefix")."conversations_list";
		$this->conversationsUsersTable = CS::get()->config->get("dbprefix")."conversations_users";
		$this->conversationsMessagesTable = CS::get()->config->get("dbprefix")."conversations_messages";
	}

	/**
	 * Get the conversations list of a specified user
	 * or get informations of a specific conversation
	 *
	 * @param int $userID The ID of the user to get the list
	 * @param int $conversationID Optionnal, the ID of conversation to get informatios from
	 * @return Mixed Array in case of result / False else
	 */
	public function getList(int $userID, int $conversationID = 0){

		//Prepare database request
		$tablesName = $this->conversationsListTable.", ".$this->conversationsUsersTable;
		
		//Prepare conditions
		$tableJoinCondition = $this->conversationsListTable.".ID = ".$this->conversationsUsersTable.".ID_".$this->conversationsListTable."";
		$userCondition = $this->conversationsUsersTable.".ID_utilisateurs = ?";
		$orderResults = "ORDER BY ".$this->conversationsListTable.".last_active DESC";

		//Specify conditions values
		$conditionsValues = array($userID);

		//Check if we have to get informations about just one conversation
		if($conversationID != 0){
			$specificConditions = "AND ".$this->conversationsListTable.".ID  = ?";
			$conditionsValues[] = $conversationID;
		}
		else
			$specificConditions = ""; //Nothing now

		//Compile conditions
		$conditions = "WHERE ".$tableJoinCondition." AND (".$userCondition.") ".$specificConditions." ".$orderResults;
		
		//Fields list
		$requiredFields = array(
			$this->conversationsListTable.".ID",
			$this->conversationsListTable.".last_active",
			$this->conversationsListTable.".name",
			$this->conversationsListTable.".ID_utilisateurs AS ID_owner",
			$this->conversationsUsersTable.".following",
			$this->conversationsUsersTable.".saw_last_message",
		);

		//Perform database request
		$results = CS::get()->db->select($tablesName, $conditions, $conditionsValues, $requiredFields);

		//Check for errors
		if($results === false)
			return false; //An error occurred

		//Process results
		$conversationsList = array();
		foreach($results as $processConversation){
			$conversationsList[] = array(
				"ID" => $processConversation["ID"],
				"ID_owner" => $processConversation["ID_owner"],
				"last_active" => $processConversation["last_active"],
				"name" => ($processConversation["name"] == "" ? false : $processConversation["name"]),
				"following" => $processConversation["following"],
				"saw_last_message" => $processConversation["saw_last_message"],

				//Get and add conversation members
				"members" => $this->getConversationMembers($processConversation["ID"]),
			);
		}

		//Return results
		return $conversationsList;
	}


	/**
	 * Get a conversation members
	 *
	 * @param int $conversationID The ID of the conversation
	 * @return array A list of the conversation members (empty arary may means that an error occured)
	 */
	public function getConversationMembers(int $conversationID) : array {

		//Perform a request on the database
		$tableName = $this->conversationsUsersTable;
		$conditions = "WHERE ID_".$this->conversationsListTable." = ?";
		$conditionsValues = array($conversationID*1);
		$getFields = array("ID_utilisateurs as userID");

		//Perform the request
		$results = CS::get()->db->select($tableName, $conditions, $conditionsValues, $getFields);

		if($results === false)
			return array(); //An error occured

		//Process results
		$membersList = array();

		foreach($results as $processUser)
			$membersList[] = $processUser["userID"];

		//Return result
		return $membersList;
	}

	/**
	 * Create a new conversation
	 *
	 * @param int $userID The ID of the user creating the conversation
	 * @param bool $follow Defines if the user creating the conversation will follow it 
	 * @param array $usersList The list of users following the conversation
	 * @param Mixed $name Optionnal, the name of the conversation
	 * @return int 0 for a fail else the ID of the newly created conversation
	 */
	public function create(int $userID, bool $follow, array $usersList, $name = "") : int{
		
		$mainInformations = array(
			"ID_utilisateurs" => $userID*1,
			"name" => ($name ? $name : ""),
			"last_active" => time(),
			"creation_time" => time()
		);

		//First, insert the conversation in the main table
		if(!CS::get()->db->addLine($this->conversationsListTable, $mainInformations))
			return 0; //An error occured
		
		//Get the last inserted ID
		$conversationID = CS::get()->db->getLastInsertedID();

		//Check for errors
		if($conversationID == 0)
			return 0;
		
		//Insert users registrattions
		foreach($usersList as $processUser){

			//Make user follow the conversation if required
			$processUserFollowing = false;
			if($userID == $processUser)
				$processUserFollowing = $follow;

			//Try to insert user in conversation
			if(!$this->addMember($conversationID, $processUser, $processUserFollowing))
				return 0; //Error
		}

		//Conversation creation is a success
		return $conversationID;
	}

	/**
	 * Check if a user is a member of a conversation or not
	 *
	 * @param int $userID The ID of the user to check
	 * @param int $conversationID The ID of the conversation to check
	 * @return bool True if the user belongs to the conversation
	 */
	public function userBelongsTo(int $userID, int $conversationID) : bool {
		
		//Prepare a request on the database
		$tableName = $this->conversationsUsersTable;
		$conditions = "WHERE ID_".$this->conversationsListTable." = ? AND ID_utilisateurs = ?";
		$values = array(
			$conversationID,
			$userID
		);

		//Peform a request on the database
		$result = CS::get()->db->count($tableName, $conditions, $values);

		//Check if request failed
		if($result === false)
			return false; // An error occured

		//Analyse result and return it
		return $result != 0;
	}

	/**
	 * Change the follow state of a user on conversation
	 *
	 * @param int $userID The ID to update
	 * @param int $conversationID The ID of the conversation
	 * @param bool $follow Specify if the conversation is followed or not
	 * @return bool True for a success
	 */
	public function changeFollowState(int $userID, int $conversationID, bool $follow) : bool{
		
		//Prepare the request on the database
		$tableName = $this->conversationsUsersTable;
		$conditions = "ID_".$this->conversationsListTable." = ? AND ID_utilisateurs = ?";
		$condVals = array(
			$conversationID,
			$userID
		);

		//Defines modifications
		$modifs = array(
			"following" => $follow ? 1 : 0,
		);
		
		//Update the table
		if(!CS::get()->db->updateDB($tableName, $conditions, $modifs, $condVals))
			return false; //An error occured

		//Success
		return true;
	}

	/**
	 * Change conversation name
	 *
	 * @param int $conversationID The ID of the conversation
	 * @param string $conversationName The name of the conversation
	 * @return bool True for a success
	 */
	public function changeName(int $conversationID, string $conversationName) : bool{
		//Prepare database request
		$tableName = $this->conversationsListTable;
		$conditions = "ID = ?";
		$condVals = array($conversationID);

		//Changes
		$changes = array(
			"name" => $conversationName,
		);

		//Try to perform request
		if(!CS::get()->db->updateDB($tableName, $conditions, $changes, $condVals))
			return false; //An error occured

		//Success
		return true;
	}

	/**
	 * Update conversation members list
	 *
	 * @param int $conversationID The ID of the conversation to update
	 * @param array $conversationMembers The new list of conversation members
	 * @return bool True for a success
	 */
	public function updateMembers(int $conversationID, array $conversationMembers) : bool{

		//Get the current conversation list
		$currentMembers = $this->getConversationMembers($conversationID);

		//Determinate entries to add
		$toAdd = array_diff($conversationMembers, $currentMembers);

		//Determinate entries to remove
		$toRemove = array_diff($currentMembers, $conversationMembers);

		//Add new member
		foreach($toAdd as $processInsert){
			if(!$this->addMember($conversationID, $processInsert))
				return false; //An error occured
		}

		//Remove old members
		foreach($toRemove as $processDelete){
			if(!$this->removeMember($conversationID, $processDelete))
				return false; //An error occured
		}

		//Success
		return true;
	}

	/**
	 * Add a member to the list
	 *
	 * @param int $conversationID The ID of the target conversation
	 * @param int $userID The ID of the user to add to the conversation
	 * @param bool $follow Optionnal, specify if the user will follow or not the conversation
	 * @return bool True for a success
	 */
	private function addMember(int $conversationID, int $userID, bool $follow = false) : bool {

		//Prepare database request
		$tableName = $this->conversationsUsersTable;
		$values = array(
			"ID_".$this->conversationsListTable => $conversationID,
			"ID_utilisateurs" => $userID,
			"time_add" => time(),
			"following" => $follow ? 1 : 0,
			"saw_last_message" => 1
		);

		//Try to perform request
		return CS::get()->db->addLine($tableName, $values);
	}

	/**
	 * Remove a member from the list
	 *
	 * @param int $conversationID The ID of the target conversation
	 * @param int $userID The ID of the user to remove from the conversation
	 * @return bool True for a success
	 */
	private function removeMember(int $conversationID, int $userID) : bool {
		//Prepare database request
		$tableName = $this->conversationsUsersTable;
		$conditions = "ID_".$this->conversationsListTable." = ? AND ID_utilisateurs = ?";
		$values = array(
			$conversationID,
			$userID
		);

		//Try to perform request
		return CS::get()->db->deleteEntry($tableName, $conditions, $values);
	}

	/**
	 * Check if a user is a conversation moderator or not
	 *
	 * @param int $userID The ID of the user to check
	 * @param int $conversationID The ID of the conversation to check
	 * @return bool True if the user is a conversation moderator / false else
	 */
	public function userIsModerator(int $userID, int $conversationID) : bool {
		//Prepare database request
		$tableName = $this->conversationsListTable;
		$conditions = "WHERE ID = ?";
		$values = array($conversationID);
		$requiredFields = array(
			"ID_utilisateurs"
		);

		//Peform a request on the database
		$results = CS::get()->db->select($tableName, $conditions, $values, $requiredFields);

		//Check for errors
		if($results === false)
			return false; // An error occured
		
		//Check if there was results
		if(count($results) === 0)
			return false;

		//Check the first result only
		return $results[0]["ID_utilisateurs"] == $userID;
	}

	/**
	 * Search for a private conversation between two users
	 *
	 * @param int $user1 The first user
	 * @param int $user2 The second user
	 * @return array The list of private conversations
	 */
	public function findPrivate(int $user1, int $user2) : array{
		
		//Prepare database request
		$tableName = $this->conversationsUsersTable." AS table1 JOIN ".
			$this->conversationsUsersTable." AS table2";
		
		//Prepare conditions
		$joinCondition = "table1.ID_".$this->conversationsListTable." = table2.ID_".$this->conversationsListTable;
		$whereConditions = "table1.ID_utilisateurs = ? OR table1.ID_utilisateurs = ?";
		$groupCondition = "table1.ID_".$this->conversationsListTable." having count(*) = 4";

		//Conditions values
		$condValues = array($user1, $user2);

		//Required fields
		$requiredFields = array(
			"table1.ID_".$this->conversationsListTable." as conversationID",
		);

		//Build conditions
		$conditions = "ON ".$joinCondition." WHERE ".$whereConditions." GROUP BY ".$groupCondition;

		//Try to perform request
		$results = CS::get()->db->select($tableName, $conditions, $condValues, $requiredFields);

		//Check for errors
		if($results === false)
			return false;
		
		//Prepare return
		$conversationsID = array();
		foreach($results as $processConversation)
			$conversationsID[] = $processConversation["conversationID"];
		
		//Return result
		return $conversationsID;
	}

	/**
	 * Insert a new message in the database
	 *
	 * @param int $userID The ID of the user inserting the message
	 * @param int $conversationID The ID of the target conversation
	 * @param string $message The message to insert
	 * @param Mixed $image_path Optionnal, the path to an image associated with the message
	 * @return bool True for a success
	 */
	private function insertMessage(int $userID, int $conversationID, string $message, $image_path = false) : bool{

		//Prepare values
		$tableName = $this->conversationsMessagesTable;
		$values = array(
			"ID_".$this->conversationsListTable => $conversationID,
			"ID_utilisateurs" => $userID,
			"time_insert" => time(),
			"message" => $message
		);

		//Add image path (if required)
		if($image_path)
			$values['image_path'] = $image_path;

		//Try to insert new value in database
		if(!CS::get()->db->addLine($tableName, $values))
			return false; //An error occured

		//Success
		return true;
	}

	/**
	 * Update the last time a conversation was active
	 *
	 * @param int $conversationID The ID of the conversation to update
	 * @param int $time The new time of last activity to set
	 * @return bool True for a success
	 */
	private function updateLastActive(int $conversationID, int $time) : bool{

		//Perform a request on the database
		$tableName = $this->conversationsListTable;
		$conditions = "ID = ?";
		$condVals = array($conversationID);

		//Set new values
		$newValues = array(
			"last_active" => $time,
		);

		//Try to perform a request on the database
		if(!CS::get()->db->updateDB($tableName, $conditions, $newValues, $condVals))
			return false; //An error occured

		//Success
		return true;
	}

	/**
	 * Mark all the users of a conversation as "unread"
	 *
	 * @param int $conversationID The ID of the conversation to update
	 * @param array $exceptions Users that should not be marked as read
	 * @return bool True for a success
	 */
	private function allUsersAsUnread(int $conversationID, array $exceptions) : bool{

		//Prepare request
		$tableName = $this->conversationsUsersTable;
		$conditions = "ID_".$this->conversationsListTable." = ?";
		$condVals = array($conversationID);

		//Remove users exceptions
		foreach($exceptions as $userID){
			$conditions.= " AND ID_utilisateurs != ?";
			$condVals[] = $userID;
		}

		//Define new values
		$newValues = array(
			"saw_last_message" => 0
		);

		//Try to perform a request on the database
		if(!CS::get()->db->updateDB($tableName, $conditions, $newValues, $condVals))
			return false; //An error occured

		//Success
		return true;
	}

	/**
	 * Mark the user of a conversation as "read" for a conversation
	 *
	 * @param int $userID The ID of the user to update
	 * @param int $conversationID The ID of a conversation to update
	 * @return bool True for a success
	 */
	public function markUserAsRead(int $userID, int $conversationID) : bool {

		//Prepare database request
		$tableName = $this->conversationsUsersTable;
		$conditions = "ID_".$this->conversationsListTable." = ? AND ID_utilisateurs = ?";
		$condVals = array(
			$conversationID,
			$userID
		);

		//Define new values
		$newValues = array(
			"saw_last_message" => 1
		);

		//Try to perform a request on the database
		if(!CS::get()->db->updateDB($tableName, $conditions, $newValues, $condVals))
			return false; //An error occured

		//Success
		return true;
	}

	/**
	 * Send a new message
	 *
	 * @param int $userID The ID of the user sending the message
	 * @param int $conversationID The ID of the target conversation
	 * @param string $message The message
	 * @param Mixed $image_path Optionnal, define the path to an image associated with the message
	 * @return bool True for a success
	 */
	public function sendMessage(int $userID, int $conversationID, string $message, $image_path = false) : bool{

		//GUIDE LINE : this method act like a "controller" : it doesn't perform any database operation
		//But it manage all operations (insert message; save image; inform other users; ...)

		//First, try to insert the message
		if(!$this->insertMessage($userID, $conversationID, $message, $image_path))
			return false; //An error occured
		
		//Then, update the last activity of the conversation
		if(!$this->updateLastActive($conversationID, time()))
			return false; //An error occured (2)
		
		//Then, set all the users of the conversation as unread
		if(!$this->allUsersAsUnread($conversationID, array($userID)))
			return false; //An error occured (3)

		 //Success
		 return true;
	}

	/**
	 * Get the last messages of a conversation
	 *
	 * @param int $conversationID The ID of the target conversation
	 * @param int $numberOfMessages The number of messages to return
	 * @return array The messages of the conversation
	 */
	public function getLastMessages(int $conversationID, int $numberOfMessages) : array {

		//Define conditions
		$conditions = "WHERE ID_".$this->conversationsListTable." = ? ORDER BY ID DESC LIMIT ".($numberOfMessages*1);
		$condVals = array(
			$conversationID
		);

		//Perform request
		$messages = $this->getMessages($conditions, $condVals);

		//Revert messages order
		$messages = array_reverse($messages);

		//Return messages
		return $messages;
	}

	/**
	 * Get the new messages of a conversation
	 *
	 * @param int $conversationID The ID of the target conversation
	 * @param int $lastMessageID The ID of the last know message
	 * @return array A list of messages
	 */
	public function getNewMessages(int $conversationID, int $lastMessageID) : array {

		//Define conditions
		$conditions = "WHERE ID_".$this->conversationsListTable." = ? AND ID > ? ORDER BY ID";
		$condVals = array(
			$conversationID,
			$lastMessageID
		);

		//Perform request
		$messages = $this->getMessages($conditions, $condVals);

		//Return messages
		return $messages;
	}

	/**
	 * Check whether a conversation exists or not
	 * 
	 * @param int $convID The ID of the conversation to check
	 * @return bool TRUE if it exists / false else
	 */
	public function exist(int $convID) : bool {

		//Perform a request on the database
		$tableName = $this->conversationsListTable;

		return CS::get()->db->count($tableName, "WHERE ID = ?", array($convID)) > 0;

	}

	/**
	 * Delete a conversation
	 * 
	 * Delete all the messages, membership and informations about a conversation
	 * 
	 * @param int $conID The conversation to delete
	 * @return bool True in case of success / False else
	 */
	public function delete_conversation(int $convID) : bool {

		//Get all the messages of the conversation
		$messages = $this->getMessages("WHERE ID_".$this->conversationsListTable." = ?", array($convID));

		//Delete each message
		foreach($messages as $message){
			if(!$this->delete_message($message['ID'], $convID))
				return false;
		}

		//Delete all the members of the conversation
		if(!$this->delete_all_members($convID))
			return false;

		//Delete the conversation entry itself
		if(!$this->delete_conversation_entry($convID))
			return false;

		//Success
		return true;
	}

	/**
	 * Delete a conversation membership
	 * 
	 * This function is just removing a user from a conversation by 
	 * deleting all the messages the user has posted and removing its 
	 * membership
	 * 
	 * @param int $convID The target conversation
	 * @param int $memberID The ID of the member to delete from the conversation
	 * @return bool True in case of success / false else
	 */
	public function delete_member(int $convID, int $memberID) : bool {

		//Get all the messages of member the conversation
		$messages = $this->getMessages(
			"WHERE ID_".$this->conversationsListTable." = ? AND ID_utilisateurs = ?", 
			array($convID, $memberID));

		//Delete each message
		foreach($messages as $message){
			if(!$this->delete_message($message['ID'], $convID))
				return false;
		}

		//Delete user membership
		if(!$this->removeMember($convID, $memberID))
			return false;

		//Success
		return true;
	}


	/**
	 * Delete a single message of a conversation
	 * 
	 * @param int $messageID The ID of the message to delete
	 * @param int $convID The target conversation
	 * @param array $informations Optionnal, informations about the message
	 * @return bool True in case of success / false else
	 */
	private function delete_message(int $messageID, int $convID, array $informations = null) : bool {

		//Check if we have to fetch informations about the message
		if(is_null($informations)){
			$messages = $this->getMessages("WHERE ID_".$this->conversationsListTable." = ? AND ID = ?", array($convID, $messageID), false);

			if(count($messages) == 0)
				return false;
			
			$informations = $messages[0];
		}

		//Check if we have to delete an image
		if(!is_null($informations["image_path"]) AND $informations["image_path"] !== ""){

			//Get system path of the image
			$img_sys_path = path_user_data($informations["image_path"], true);

			if(file_exists($img_sys_path)){
				unlink($img_sys_path);
			}

		}

		//Delete message from the database
		$conditions = "ID = ?";
		$condValues = array($messageID);
		return CS::get()->db->deleteEntry($this->conversationsMessagesTable, $conditions, $condValues);

	}

	/**
	 * Delete all the members of a conversation
	 * 
	 * @param int $convID The target conversation
	 * @return bool TRUE in case of success / false else
	 */
	private function delete_all_members(int $convID) : bool {

		//Prepare request on the database
		$conditions = "ID_".$this->conversationsListTable." = ?";
		$values = array(
			$convID
		);

		//Try to perform request
		return CS::get()->db->deleteEntry($this->conversationsUsersTable, $conditions, $values);

	}

	/**
	 * Delete a conversation entry in the database
	 * 
	 * @param int $convID The target conversation
	 * @return bool True in case of success / false else
	 */
	private function delete_conversation_entry(int $convID) : bool {
		return CS::get()->db->deleteEntry($this->conversationsListTable, "ID = ?", array($convID));
	}

	/**
	 * Get a list of conversation messages based on specified conditions
	 *
	 * @param string $conditions The conditions of the request
	 * @param Array $conditionsValues The values of the conditions (Optionnal)
	 * @param bool $transformPath Transform the path of the files into URLS (true by default)
	 * @return Array The list of messages
	 */
	private function getMessages(string $conditions, array $conditionsValues = array(), bool $transformPath = true) : array{

		//Prepare database request
		$tableName = $this->conversationsMessagesTable;

		//Define required fields
		$requiredFields = array(
			"ID",
			"ID_utilisateurs AS ID_user",
			"image_path",
			"message",
			"time_insert"
		);

		//Try to perform request on the database
		$messages = CS::get()->db->select($tableName, $conditions, $conditionsValues, $requiredFields);

		//Process each message
		if($transformPath){
			array_walk($messages, function(&$item){

				//Check if the image of the message is not null
				if($item["image_path"] !== null && $item["image_path"] != ""){
					//Replace image name with full URL
					$item["image_path"] = path_user_data($item["image_path"]);
				}
			});
		}

		//Return result
		return $messages;
	}

}

//Register component
Components::register("conversations", new conversations());