<?php

class JaxChat {
	private $error;
	private $timeoutSeconds;
	private $messageTimeoutSeconds;
	private $maximumMessages;
	private $cssVariations;
	private $lockedFilePointer;
	private $lockedFilename;
	
	
	/**
	* The constructor
	*
	*/
	public function __construct() {
		$this->timeoutSeconds = CLIENT_TIMEOUT_SECONDS;
		$this->messageTimeoutSeconds = MESSAGE_TIMEOUT_SECONDS;
		$this->maximumMessages = MAXIMUM_MESSAGE_HISTORY_COUNT;
		
		for($i = 0; $i < USER_CLASS_VARIATION_COUNT; $i++) {
            $this->cssVariations[] = 'user' . str_pad($i, 3, '0', STR_PAD_LEFT);
        }
		
		$this->lockedFilePointer = NULL;
		$this->lockedFilename = '';
	}
	
	
	/**
	* request use of the specified name in the chat
	*
	* @param name    The name to request for use in chat.
	* @return array or boolean    Returns an array with the new name details or FALSE if the request failed.
	*/
	public function requestName($name, $ipAddress, $session_name = null, $session_id = null) {

		global $lang_chat_ban_ip, $lang_chat_ban_nome, $lang_chat_apelido_erro, $lang_chat_lotado, $lang_chat_apelido_uso, $lang_chat_salvar_erro;
		// clean up the name
		$name = trim($name);
		$name = preg_replace('/[\x00-\x1F\x80-\x9F]/u', '', $name);
		$name = htmlspecialchars($name);
		$name = substr($name, 0, MAXIMUM_NAME_LENGTH);
		$time = NULL;
		$id = NULL;
		
		// check for ban
		if ($this->isNameBanned($name)) {
			$this->error = $lang_chat_ban_nome;
			return FALSE;
		}
		
		if ($this->isIPAddressBanned($ipAddress)) {
			$this->error = $lang_chat_ban_ip;
			return FALSE;
		}
		
		// assume failure
		$success = FALSE;
			
		// get names list
		$namesList = $this->getNamesList();

		if(!empty($session_id)){

			if ($this->lockedWrite(JSON_FILE_PATH . 'jaxnames.json', json_encode($namesList))) {
			// delete the name from the chat list
					return $this->deleteName($session_id);
			}

		}
		
		if ($namesList === FALSE) {
			// failed to read names list
			$this->error = $lang_chat_apelido_erro;
		}
		elseif( count($namesList->names) >= MAXIMUM_USER_COUNT ) {
			// maximum user count reached
			$this->error = $lang_chat_lotado;
		}
		elseif (!in_array($name, $namesList->names) || in_array($session_name, $namesList->names)) {
			// name is available, reserve name
			// copy css variations into new array
			$classes = $this->cssVariations;
			
			// eliminate classes that are already in use
			foreach( $namesList->classes as $class ) {
                if( ($ci = array_search($class, $classes)) !== FALSE ) {
                    unset($classes[$ci]);
                }
            }
			
			if( count($classes) == 0 ) {
				// all classes in use, randomize
				$class = $this->cssVariations[rand(0, count($this->cssVariations) - 1)];
			}
			else {
				// use first unused class
				$classes = array_values($classes);
				$class = $classes[0];
			}
			
			// add this new name to the names object with the current time and a unique id
			$namesList->names[] = $name;
			$time = time();
			$namesList->times[] = $time;
			$id = uniqid();
			$namesList->ids[] = $id;
			$namesList->classes[] = $class;
			$namesList->ips[] = $ipAddress;
			
			// write the new names list
			$success = $this->lockedWrite(JSON_FILE_PATH . 'jaxnames.json', json_encode($namesList));
			
			if ($success === FALSE) {
                $this->error = $lang_chat_salvar_erro;
            }
		}
		else {
			$this->error = $lang_chat_apelido_uso;
		}	
		
		if ($success) {
            return array('name' => $name, 'time' => $time, 'id' => $id);
        }
		
        return FALSE;
	}
	
	
	/**
	* Delete a user name from chat list
	*
	* @param id    The name id to delete.
	* @return boolean    FALSE if the request failed.
	*/
	public function deleteName($id) {

		global $lang_chat_apelido_erro;

		// assume failure
		$success = FALSE;
		
		// get names list
		$namesList = $this->getNamesList();
		
		if ($namesList === FALSE) {
			// failed to read names list
			$this->error = $lang_chat_apelido_erro;
		}
		else {
			// remove specified name by id
			$namesList = $this->removeTimedOutNames($namesList, $id);
			
			if ($namesList !== FALSE) {
				// removal succeeded, save result
				$success = $this->lockedWrite(JSON_FILE_PATH . 'jaxnames.json', json_encode($namesList));
			}
		}
		
		return $success;
	}
	
	
	/**
	* Ban a user name
	*
	* @param id    The name id to ban.
	* @return boolean    FALSE if the request failed.
	*/
	public function banName($id) {

		global $lang_chat_apelido_erro, $lang_chat_ban_erro, $lang_chat_id_erro;
		// get names list
		$namesList = $this->getNamesList();
		
		// error if no names list
		if ($namesList === FALSE) {
			$this->error = $lang_chat_apelido_erro;
			return FALSE;
		}
		
		// get the name for the given id
		$index = array_search($id, $namesList->ids);
		if ($index === FALSE) {
			$this->error = $lang_chat_id_erro;
			return FALSE;
		}
		else {
			$name = $namesList->names[$index];
		}
		
		// get bans list
		$bansList = $this->getBansList();
		
		// error if no bans list
		if ($bansList === FALSE) {
			$this->error = $lang_chat_ban_erro;
			return FALSE;
		}
		
		// add name to bans
		$bansList->names[] = $name;
		
		// save bans
		if ($this->lockedWrite(JSON_FILE_PATH . 'jaxbans.json', json_encode($bansList))) {
			// delete the name from the chat list
			return $this->deleteName($id);
		}
		
		return FALSE;
	}
	
	
	/**
	* Remove name ban
	*
	* @param name    The name to remove from bans list.
	* @return boolean    FALSE if the request failed.
	*/
	public function removeNameBan($name) {

		global $lang_chat_ban_erro;
		// get bans list
		$bansList = $this->getBansList();
		
		// error if no bans list
		if ($bansList === FALSE) {
			$this->error = $lang_chat_ban_erro;
			return FALSE;
		}
		
		// remove name from bans
		$newBans = array();
		foreach($bansList->names as $banName) {
			if ($banName != $name) {
                $newBans[] = $banName;
            }
		}
		
		// change to new bans list
		$bansList->names = $newBans;
		
		// save bans
		return $this->lockedWrite(JSON_FILE_PATH . 'jaxbans.json', json_encode($bansList));
	}
	
	
	/**
	* Ban a IP address
	*
	* @param id    The name id to ban.
	* @return boolean    FALSE if the request failed.
	*/
	public function banIPAddress($id) {

		global $lang_chat_ban_erro, $lang_chat_id_erro, $lang_chat_apelido_erro;
		// get names list
		$namesList = $this->getNamesList();
		
		// error if no names list
		if ($namesList === FALSE) {
			$this->error = $lang_chat_apelido_erro;
			return FALSE;
		}
		
		// get the ip address for the given id
		$index = array_search($id, $namesList->ids);
		if ($index === FALSE) {
			$this->error = $lang_chat_id_erro;
			return FALSE;
		}
		else {
			$ip = $namesList->ips[$index];
		}
		
		// get bans list
		$bansList = $this->getBansList();
		
		// error if no bans list
		if ($bansList === FALSE) {
			$this->error = $lang_chat_ban_erro;
			return FALSE;
		}
		
		// add name to bans
		$bansList->ips[] = $ip;
		
		// save bans
		if ($this->lockedWrite(JSON_FILE_PATH . 'jaxbans.json', json_encode($bansList))) {
			// delete the name from the chat list
			return $this->deleteName($id);
		}
		
		return FALSE;
	}
	
	
	/**
	* Remove IP ban
	*
	* @param name    The name to remove from bans list.
	* @return boolean    FALSE if the request failed.
	*/
	public function removeIPAddressBan($ip) {
		global $lang_chat_ban_erro;
		// get bans list
		$bansList = $this->getBansList();
		
		// error if no bans list
		if ($bansList === FALSE) {
			$this->error = $lang_chat_ban_erro;
			return FALSE;
		}
		
		// remove name from bans
		$newBans = array();
		foreach($bansList->ips as $banIP) {
			if ($banIP != $ip) {
                $newBans[] = $banIP;
            }
		}
		
		// change to new bans list
		$bansList->ips = $newBans;
		
		// save bans
		return $this->lockedWrite(JSON_FILE_PATH . 'jaxbans.json', json_encode($bansList));
	}
	
	
	/**
	* Get current user name list
	*
	* @return object or boolean    Returns an object with the name details or FALSE if the request failed.
	*/
	public function getNamesList() {
		// get contents of names file
		$namesString = $this->lockedRead(JSON_FILE_PATH . 'jaxnames.json');
		
		// convert to object if read was successful
		if ($namesString !== FALSE) {
			$namesList = json_decode($namesString);
			
			if (empty($namesList)) {
				// decode failed, create an empty names list
				$namesList = new stdClass;
				$namesList->names = array();
				$namesList->times = array();
				$namesList->ids = array();
				$namesList->classes = array();
				$namesList->ips = array();
			}
			else {
				// clean up names list
				$namesList = $this->removeTimedOutNames($namesList);
			}
            
            // sort by name
            $names = $namesList->names;
            asort($names);
            $sortedList = new StdClass;
            $sortedList->names = array();
            $sortedList->times = array();
            $sortedList->ids = array();
            $sortedList->classes = array();
            $sortedList->ips = array();
            
            foreach ( $names as $ni => $nv ) {
                $sortedList->names[] = $nv;
                $sortedList->times[] = $namesList->times[$ni];
                $sortedList->ids[] = $namesList->ids[$ni];
                $sortedList->classes[] = $namesList->classes[$ni];
                $sortedList->ips[] = $namesList->ips[$ni];
            }
            
            $namesList = $sortedList;
		}
		else {
			// read failed
			$namesList = FALSE;
		}
		
		// return names list
		return $namesList;
	}
	
	
	/**
	* Get current ban list
	*
	* @return object or boolean    Returns an object with the ban details or FALSE if the request failed.
	*/
	public function getBansList() {
		// get contents of names file
		$bansString = $this->lockedRead(JSON_FILE_PATH . 'jaxbans.json');
		
		// convert to object if read was successful
		if ($bansString !== FALSE) {
			$bansList = json_decode($bansString);
			
			if (empty($bansList)) {
				// decode failed, create an empty list
				$bansList = new stdClass;
				$bansList->names = array();
				$bansList->ips = array();
			}
		}
		else {
			// read failed
			$bansList = FALSE;
		}
		
		// return names list
		return $bansList;
	}
	
	
	/**
	* Test if a name is banned
	*
	* @return boolean    Returns TRUE or FALSE.
	*/
	public function isNameBanned($name) {

		global $lang_chat_ban_erro;
		// get bans list
		$bansList = $this->getBansList();
		
		// error if no bans list
		if ($bansList === FALSE) {
			$this->error = $lang_chat_ban_erro;
			return FALSE;
		}
		
		if (in_array($name, $bansList->names)) {
            return TRUE;
        }
        
		return FALSE;
	}
	
	
	/**
	* Test if an IP address is banned
	*
	* @return boolean    Returns TRUE or FALSE.
	*/
	public function isIPAddressBanned($ip) {
		global $lang_chat_ban_erro;
		// get bans list
		$bansList = $this->getBansList();
		
		// error if no bans list
		if ($bansList === FALSE) {
			$this->error = $lang_chat_ban_erro;
			return FALSE;
		}
		
		if (in_array($ip, $bansList->ips)) {
            return TRUE;
        }
        
		return FALSE;
	}
	
	
	/**
	* Get messages from a template file
	*
	* @param template    The name of the template file without the .txt extension.
	* @return object or boolean    The messages list or FALSE on failure.
	*/
	public function getMessagesTemplate($template) {

		global $lang_chat_bem_vindo, $lang_chat_obg, $lang_chat_help, $lang_chat_cmd, $lang_chat_help_cmd, $lang_chat_me_cmd, $lang_chat_end_cmd;
		// make sure template file exists
		if (file_exists('templates/' . $template . '.txt')) {
			// create an empty messages list
			$messagesList = new stdClass;
			$messagesList->messages = array();

			// get template file
			$templateText = file('templates/' . $template . '.txt');

			//traduzir
			$search = array(
				'Bem-vindo ao chat', 
				'Obrigado por participar', 
				'Digite <strong>/help</strong> para saber como usar o chat.', 
				'Comandos:',
				'Exibe esta mensagem de ajuda.',
				'Destacar a mensagem.',
				'Termine sua sessão de bate-papo e liberar seu apelido.'
			);
			$replace = array(
				$lang_chat_bem_vindo, 
				$lang_chat_obg, 
				$lang_chat_help, 
				$lang_chat_cmd,
				$lang_chat_help_cmd,
				$lang_chat_me_cmd,
				$lang_chat_end_cmd
			);

			$templateText = str_replace($search, $replace, $templateText);
			
			// process lines into messages
			foreach ($templateText as $line) {
				if( !empty($line) ) {
					// append line to messages
					$messagesList->messages[] = array(
						'time' => null, 
						'id' => null, 
						'name' => '', 
						'message' => $line, 
						'cssclass' => 'user' . $template
					);
				}
				
			}
		}
		else {
			$this->error = 'O tempalte não existe!';
			return FALSE;
		}
		
		return $messagesList;
	}

	
	
	/**
	* Get current messages list
	*
	* @return object or boolean    Returns an object with the messages list details or FALSE if the request failed.
	*/
	public function getMessagesList() {
		// get contents of messages file
		$messagesString = $this->lockedRead(JSON_FILE_PATH . 'jaxmessages.json');
		
		// convert to object if read was successful
		if ($messagesString !== FALSE) {
			$messagesList = json_decode($messagesString);
			
			if (empty($messagesList)) {
				// decode failed, create an empty messages list
				$messagesList = new stdClass;
				$messagesList->messages = array();
			}
		}
		else {
			// read failed
			$messagesList = FALSE;
		}
		
		// return messages list
		return $messagesList;
	}
	
	
	/**
	* Save a chat message.
	*
	* @param name    The user's name.
	* @param id    The id for the user.
	* @param message    The user's chat message.
	* @param cssClass    The CSS class to use for this user's messages.
	* @return boolean    FALSE if save fails
	*/
	public function saveMessage($name, $id, $message, $cssClass, $nonce) {

		global $lang_chat_mensagem_info, $lang_chat_sair;
		// validate client
		if ($this->validateClient($name, $id) === FALSE) {
			return FALSE;
		}
		
        // check for repeat message
        $lastMessage = $this->getLastMessage($this->getMessagesList(), $name);
        if ( $lastMessage && $lastMessage->nonce == $nonce ) {
            // this message already received, must be a retry
            return TRUE;
        }
        
		// clean message
		$message = trim(htmlspecialchars($message));
		$message = substr($message, 0, MAXIMUM_MESSAGE_LENGTH);
		
		
		// check for emote command
		$userEmote = FALSE;
		if( preg_match('|^' . preg_quote('/me', '|') . '(.*)|i', $message, $match) ) {
			// Get emote
			$emote = trim($match[1]);
			if( !empty($emote) ) {
				// adjust message for user emote
				$message = '<img loading="lazy" class="chat" src="../inc/chat/images/membro.gif"/><b><i> ' . $name . ': <span style="color:#f75858;font-weight: bold;font-size: 18px;"> ' . $emote . ' </span></i></b>';
				$userEmote = TRUE;
			}
			else {
				// bad emote
				$message = '';
				$this->error = $lang_chat_mensagem_info;
			} 
		}
		
		
		// check for help command
		if( preg_match('|^' . preg_quote('/help', '|') . '|i', $message, $match) ) {
			// return message list from help template
			return $this->getMessagesTemplate('help');
		}
		
		
		// check for end command
		if( preg_match('|^' . preg_quote('/end', '|') . '|i', $message, $match) ) {
			// remove user
			$this->validateClient($name, $id, TRUE);
			unset($_SESSION['name_chat']);
			unset($_SESSION['id_chat']);
			$this->error = $lang_chat_sair;
			return FALSE;
		}
		
		
		// if message not empty then process the contents
		if( !empty($message) ) {
			// make links clickable
			$search = '!(((f|ht)tp(s)?://)[-a-zA-Zа-яА-Я()0-9@:%_+.~#?&;//=]+)!i';
			
			$message = preg_replace(
				$search, 
				'<a href="$1" target="blank">$1</a>', 
				$message
			);
			/*
			// insert any missing http:// to links
			$message = preg_replace('|href="(?!https?://)|i', 'href="http://', $message);
			*/
			// if not emote then add user name to message
			if( !$userEmote ) {
                $message = '<img loading="lazy" class="chat" src="../inc/chat/images/membro.gif"/> <b>' . $name . ':</b> ' . '<span style="color:#000;">' . $message . '</span>';
            }
			
			return $this->writeMessage($name, $message, $cssClass, TRUE, $nonce);
		}
		
		return FALSE;
	}
	
	
	/**
	* Write message to history.
	* WARNING: This function does not perform any validation or clean up, do not use directly for user submitted content!
	*
	* @param name    The user name for this message.
	* @param message    Message string.
	* @param cssClass    The CSS class to associate with this message.
	* @param throttle    Optional boolean to disable throttling.
    * @param nonce       The unique nonce value for the message.
	* @return boolean    FALSE if write fails
	*/
	public function writeMessage($name, $message, $cssClass, $throttle = TRUE, $nonce = 0) {

		global $lang_chat_post_erro;
		// get messages list
		$messagesList = $this->getMessagesList();
		
		// if we have the list then process the write
		if ($messagesList !== FALSE) {
			// remove old messages first
			$messagesList = $this->removeOldMessages($messagesList);
			
			// if enabled then check throttling
			if( !JAX_DEBUG && $throttle ) {
				// get the most recent message time for this user
				$lastTime = $this->getLastMessageTime($messagesList, $name);
				if( $lastTime !== FALSE && time() - $lastTime < MINIMUM_SECONDS_BETWEEN_MESSAGES ) {
					$this->error = $lang_chat_post_erro;
					return FALSE;
				}
			}
			
			// append message
			$messagesList->messages[] = array(
				'time' => time(), 
				'id' => uniqid(), 
				'name' => $name, 
				'message' => $message, 
				'cssclass' => $cssClass,
                'nonce' => $nonce
			);
			
			// write the new message list
			return $this->lockedWrite(JSON_FILE_PATH . 'jaxmessages.json', json_encode($messagesList));
		}
		
		return FALSE;
	}
	
	
	/**
	* Delete message from history
	*
	* @param id    Message id string
	* @return boolean    FALSE if delete fails
	*/
	public function deleteMessage($id) {
		// get messages list
		$messagesList = $this->getMessagesList();
		
		// if we have the list then process the write
		if ($messagesList !== FALSE) {
			// remove old messages and the specified message
			$messagesList = $this->removeOldMessages($messagesList, $id);
			
			// write the new message list
			return $this->lockedWrite(JSON_FILE_PATH . 'jaxmessages.json', json_encode($messagesList));
		}
		
		return FALSE;
	}
	
	
	/**
	* Get processed messages with optional last time and id for a partial list and
	* command for special messages from commands.
	*
	* @param lastTime    Optional time integer value for the last message already in list.
	* @param lastId    Optional id value of the last message already in the list.
	* @param command    Optional command modifier that will append messages based on command
	* @return object or boolean    The list of messages after the last already known message or FALSE on failure
	*/
	public function getMessages($lastTime = NULL, $lastId = NULL, $command = '') {
		// get messages list
		$messagesList = $this->getMessagesList();
		
		// if we have the list then process
		if ($messagesList !== FALSE) {
			// if last time and id provided then attempt to trim to last message
			if (!is_null($lastTime) && !is_null($lastId)) {
				// find index of last known message + 1
				$knownIndex = FALSE;
				foreach( $messagesList->messages as $index => $message ) {
					// check for match to last known message
					if( $message->time == $lastTime && $message->id == $lastId ) {
						$knownIndex = $index + 1;
					}
				}
				
				// if an known index was found based on user's last time and last id then trim messages
				if( $knownIndex ) {
					// trimming messages if more past known
					if( count($messagesList->messages) > $knownIndex ) {
						$newMessages = array_slice($messagesList->messages, $knownIndex);
						$messagesList->messages = $newMessages;
					}
					else {
						// no new messages, make blank
						$messagesList->messages = array();
					}
				}
			}
			
			// check to see if processing a command
			if( !empty($command) ) {
				$command = strtolower($command);
				
				switch($command) {
					case 'help':
					// get help messages
					$helpText = file('templates/help.txt');
					
					foreach( $helpText as $text ) {
						$text = trim($text);
						if( !empty($text) ) {
							// append help messages to messages
							$messagesList->messages[] = array(
								'time' => $lastTime, 
								'id' => $lastId, 
								'name' => '', 
								'message' => $text, 
								'cssclass' => 'userhelp'
							);
						}
					}
					break;
					
					
					case 'welcome':
					// get help messages
					$helpText = file('templates/welcome.txt');
					
					foreach( $helpText as $text ) {
						$text = trim($text);
						if( !empty($text) ) {
							// append help messages to messages
							$messagesList->messages[] = array(
								'time' => $lastTime, 
								'id' => $lastId, 
								'name' => '', 
								'message' => $text, 
								'cssclass' => 'userwelcome'
							);
						}
					}
					break;
					
				}
			}
		}
		
		return $messagesList;
	}
	
	
	/**
	* Validate the provided client information is good.
	*
	* @param name    The user's name.
	* @param id    The unique id string for the user.
	* @param remove    Option to force removal of the user.
	* @return boolean    Boolean whether valid or not.
	*/
	public function validateClient($name, $id, $remove = FALSE) {

		global $lang_chat_apelido_erro;
		// get names list
		$namesList = $this->getNamesList();
		
		if ($namesList === FALSE) {
			// failed to read names list
			$this->error = $lang_chat_apelido_erro;
			return FALSE;
		}
		else {
			// search for the name
			if( ($index = array_search($name, $namesList->names)) !== FALSE ) {
				// found name, check id
				if( JAX_DEBUG || $id == $namesList->ids[$index] ) {
					// user is valid, update user's time
					$namesList->times[$index] = time();
					
					// removed timed out users and this user if requested
					if( $remove ) {
                        $namesList = $this->removeTimedOutNames($namesList, $id);
                    }
					else {
                        $namesList = $this->removeTimedOutNames($namesList);
                    }
					
					// write new names list
					$success = $this->lockedWrite(JSON_FILE_PATH . 'jaxnames.json', json_encode($namesList));
					if ($success) {
						// return the user's details
                        if ( $remove ) {
                            return FALSE;
                        }
                        
						return array('name' => $name, 'time' => $namesList->times[$index], 'cssClass' => $namesList->classes[$index]);
					}
					else {
						return FALSE;
					}
				}
				else {
					$this->error = 'ID Inválido!';
					return FALSE;
				}
			}
			else {
					// if debugging then pick a random user
					if (JAX_DEBUG) {
						if (count($namesList->names) > 0) {
							$index = rand(0, count($namesList->names) - 1);
							return array(
								'name' => $namesList->names[$index], 
								'time' => $namesList->times[$index], 
								'cssClass' => $namesList->classes[$index]
							);
						}
						else {
                            return FALSE;
                        }
					}
					else {
				// name not found
				$this->error = 'Nome não encontrado!';
				return FALSE;
					}
			}
		}
		
		return FALSE;
	}
	
	
	/**
	* Removes timed out names from user name list
	*
	* @param names    Object containing arrays for name list
	* @param id    Optional id string for a user name that should be removed.
	* @return object    A new object with the timed out names removed
	*/
	public function removeTimedOutNames($names, $id = NULL) {
		// create a new object for the names
		$newNames = new stdClass;
		$newNames->names = array();
		$newNames->times = array();
		$newNames->ids = array();
		$newNames->classes = array();
		$newNames->ips = array();
		
		// get current time
		$currentTime = time();
		
		foreach( $names->times as $index => $time ) {
			if( $currentTime - $time < $this->timeoutSeconds && 
			    (is_null($id) || $names->ids[$index] != $id) ) {
				// not timed out and not the id of user to remove, add to new object
				$newNames->names[] = $names->names[$index];
				$newNames->times[] = $names->times[$index];
				$newNames->ids[] = $names->ids[$index];
				$newNames->classes[] = $names->classes[$index];
				$newNames->ips[] = $names->ips[$index];
			}
		}
		
		return $newNames;
	}
	
	
	/**
	* Remove old messages from messages object
	*
	* @param messages     The messages object to work with.
	* @param id    Optional message id string for a message that should be removed.
	* @return object    The new messages object with old messages removed.
	*/
	public function removeOldMessages($messages, $id = NULL) {
		// create a new object for the messages
		$newMessages = new stdClass;
		$newMessages->messages = array();
		
		// get current time
		$currentTime = time();
		$messageCount = count($messages->messages);
		$messageStart = $messageCount - $this->maximumMessages;
		
		foreach( $messages->messages as $index => $message ) {
			if( $index > $messageStart && $currentTime - $message->time < $this->messageTimeoutSeconds &&
			    (is_null($id) || $id != $message->id) ) {
				// not timed out and not requested id to delte, add to new object
				$newMessages->messages[] = $message;
			}
		}
		
		return $newMessages;
	}
	
	
	/**
	* Get the time for the last message posted by the given user name.
	*
	* @param messages    The messages object to work with.
	* @return integer or boolean    The time of the last message or FALSE if none found
	*/
	public function getLastMessageTime($messages, $name) {
		//$time = FALSE;
        $message = $this->getLastMessage($messages, $name);
        if ( $message ) {
            return $message->time;
        }

        return FALSE;
	}
    
    
    public function getLastMessage($messages, $name) {
		for( $mi = count($messages->messages) - 1; $mi >= 0; $mi-- ) {
			if( $messages->messages[$mi]->name == $name ) {
				return $messages->messages[$mi];
			}
		}
        
        return FALSE;
    }
	
	
	/**
	* Open and lock a file for both read and write operations.
	*
	* @param filename    Filename of file to open and lock.
	* @param mode    The file open mode.
	* @param createIfNotExist    Optional boolean to specify if the file should be created if it does not exist.
	* @return boolean    Open status.
	*/
	public function lockFile($filename, $createIfNotExist = TRUE) {
		// check to see if opening a new file
		if ($filename != $this->lockedFilename) {
			// create file if it does not exist
			if ($createIfNotExist && !file_exists($filename)) {
                touch($filename);
            }
		
			// if file pointer is already open then close
			if ($this->lockedFilePointer) {
				flock($this->lockedFilePointer, LOCK_UN);
				fclose($this->lockedFilePointer);
				$this->lockedFilePointer = NULL;
				$this->lockedFilename = '';
			}
			
			// open the file
			$this->lockedFilePointer = fopen($filename, 'r+');
			
			// try to get exclusive lock on the file
			if ($this->lockedFilePointer && flock($this->lockedFilePointer, LOCK_EX)) {
				$this->lockedFilename = $filename;
			}
			else {
				// lock failed
				$this->error = "Falha ao obter o bloqueio no arquivo!";
				$this->lockedFilePointer = NULL;
				$this->lockedFilename = '';
				return FALSE;
			}
		}
	}
	
	
	/**
	* Read file with lock.
	*
	* @param filename    Filename of file to read.
	* @param createIfNotExist    Optional boolean to specify if the file should be created if it does not exist.
	* @return string or boolean    Returns file contents in a string or FALSE on failure.
	*/
	public function lockedRead($filename, $createIfNotExist = TRUE) {
		// make sure file is opened and locked
		$this->lockFile($filename, $createIfNotExist);
		
		// if we have the file locked then read
		if( $this->lockedFilePointer ) {
			// make sure we are at beginning of file
			fseek($this->lockedFilePointer, 0);
			
			// read lines from the file
			$buffer = '';
			while ($line = fgets($this->lockedFilePointer)) {
				$buffer .= $line;
			}
			
			// return buffer
			return $buffer;
		}
		else {
			$this->error = "Falha ao obter o bloqueio no arquivo!";
			return FALSE;
		}
	}
	
	
	/**
	* Write file with lock
	*
	* @param message    Message string
	* @return boolean    FALSE if write fails
	*/
	public function lockedWrite($filename, $buffer, $createIfNotExist = TRUE) {
		// make sure file is opened and locked
		$this->lockFile($filename, $createIfNotExist);
		
		// if we have the file locked then write
		if( $this->lockedFilePointer ) {
			// make sure we are at beginning of file
			fseek($this->lockedFilePointer, 0);
			
			// erase file using truncate
			ftruncate($this->lockedFilePointer, 0);
			
			// write buffer
			fwrite($this->lockedFilePointer, $buffer);
			fflush($this->lockedFilePointer);
			
			// return success
			return TRUE;
		}
		else {
			$this->error = "Falha ao obter o bloqueio no arquivo!";
			return FALSE;
		}
	}
	
	
	/**
	* Get the current object error message.
	*
	* @return string    The current error message.
	*/
	public function getError() {
		return $this->error;
	}
	
} // endof JaxChat class definition

