/** 
 * @file llconversationmodel.cpp
 * @brief Implementation of conversations list
 *
 * $LicenseInfo:firstyear=2009&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */


#include "llviewerprecompiledheaders.h"

#include "llagent.h"
#include "llavatarnamecache.h"
#include "llavataractions.h"
#include "llevents.h"
#include "llfloaterimsession.h"
#include "llsdutil.h"
#include "llconversationmodel.h"
#include "llimview.h" //For LLIMModel
#include "lltrans.h"

#include <boost/foreach.hpp>

//
// Conversation items : common behaviors
//

LLConversationItem::LLConversationItem(std::string display_name, const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) :
	LLFolderViewModelItemCommon(root_view_model),
	mName(display_name),
	mUUID(uuid),
	mNeedsRefresh(true),
	mConvType(CONV_UNKNOWN),
	mLastActiveTime(0.0),
	mDisplayModeratorOptions(false),
	mDisplayGroupBanOptions(false),
	mAvatarNameCacheConnection()
{
}

LLConversationItem::LLConversationItem(const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) :
	LLFolderViewModelItemCommon(root_view_model),
	mName(""),
	mUUID(uuid),
	mNeedsRefresh(true),
	mConvType(CONV_UNKNOWN),
	mLastActiveTime(0.0),
	mDisplayModeratorOptions(false),
	mDisplayGroupBanOptions(false),
	mAvatarNameCacheConnection()
{
}

LLConversationItem::LLConversationItem(LLFolderViewModelInterface& root_view_model) :
	LLFolderViewModelItemCommon(root_view_model),
	mName(""),
	mUUID(),
	mNeedsRefresh(true),
	mConvType(CONV_UNKNOWN),
	mLastActiveTime(0.0),
	mDisplayModeratorOptions(false),
	mDisplayGroupBanOptions(false),
	mAvatarNameCacheConnection()
{
}

LLConversationItem::~LLConversationItem()
{
	// Disconnect any previous avatar name cache connection to ensure
	// that the callback method is not called after destruction
	if (mAvatarNameCacheConnection.connected())
	{
		mAvatarNameCacheConnection.disconnect();
	}
}

//virtual
void LLConversationItem::addChild(LLFolderViewModelItem* child)
{
    // Avoid duplicates: bail out if that child is already present in the list
    // Note: this happens when models are created and 'parented' before views
    // This is performance unfriendly, but conversation can addToFolder multiple times
    child_list_t::const_iterator iter;
    for (iter = mChildren.begin(); iter != mChildren.end(); iter++)
    {
        if (child == *iter)
        {
            return;
        }
    }
    LLFolderViewModelItemCommon::addChild(child);
}

void LLConversationItem::postEvent(const std::string& event_type, LLConversationItemSession* session, LLConversationItemParticipant* participant)
{
	LLUUID session_id = (session ? session->getUUID() : LLUUID());
	LLUUID participant_id = (participant ? participant->getUUID() : LLUUID());
	LLSD event(LLSDMap("type", event_type)("session_uuid", session_id)("participant_uuid", participant_id));
	LLEventPumps::instance().obtain("ConversationsEvents").post(event);
}

// Virtual action callbacks
void LLConversationItem::performAction(LLInventoryModel* model, std::string action)
{
}

void LLConversationItem::openItem( void )
{
}

void LLConversationItem::closeItem( void )
{
}

void LLConversationItem::previewItem( void )
{
}

void LLConversationItem::showProperties(void)
{
}

void LLConversationItem::buildParticipantMenuOptions(menuentry_vec_t& items, U32 flags)
{
	if (flags & ITEM_IN_MULTI_SELECTION)
	{
		items.push_back(std::string("im"));
		items.push_back(std::string("offer_teleport"));
		items.push_back(std::string("voice_call"));
		items.push_back(std::string("remove_friends"));
	}
	else 
	{
		items.push_back(std::string("view_profile"));
		items.push_back(std::string("im"));
		items.push_back(std::string("offer_teleport"));
		items.push_back(std::string("request_teleport"));

		if (getType() != CONV_SESSION_1_ON_1)
		{
			items.push_back(std::string("voice_call"));
		}
		else
		{
			LLVoiceChannel* voice_channel = LLIMModel::getInstance() ? LLIMModel::getInstance()->getVoiceChannel(this->getUUID()) : NULL;
			if(voice_channel != LLVoiceChannel::getCurrentVoiceChannel())
			{
				items.push_back(std::string("voice_call"));
			}
			else
			{
				items.push_back(std::string("disconnect_from_voice"));
			}
		}

		items.push_back(std::string("chat_history"));
		items.push_back(std::string("separator_chat_history"));
		items.push_back(std::string("add_friend"));
		items.push_back(std::string("remove_friend"));
		items.push_back(std::string("invite_to_group"));
		items.push_back(std::string("separator_invite_to_group"));
		if (static_cast<LLConversationItem*>(mParent)->getType() == CONV_SESSION_NEARBY)
			items.push_back(std::string("zoom_in"));
		items.push_back(std::string("map"));
		items.push_back(std::string("share"));
		items.push_back(std::string("pay"));
        items.push_back(std::string("report_abuse"));
		items.push_back(std::string("block_unblock"));
		items.push_back(std::string("MuteText"));

		if ((getType() != CONV_SESSION_1_ON_1) && mDisplayModeratorOptions)
		{
			items.push_back(std::string("Moderator Options Separator"));
			items.push_back(std::string("Moderator Options"));
			items.push_back(std::string("AllowTextChat"));
			items.push_back(std::string("moderate_voice_separator"));
			items.push_back(std::string("ModerateVoiceMuteSelected"));
			items.push_back(std::string("ModerateVoiceUnMuteSelected"));
			items.push_back(std::string("ModerateVoiceMute"));
			items.push_back(std::string("ModerateVoiceUnmute"));
		}

		if ((getType() != CONV_SESSION_1_ON_1) && mDisplayGroupBanOptions)
		{
			items.push_back(std::string("Group Ban Separator"));
			items.push_back(std::string("BanMember"));
		}
	}
}

// method does subscription to changes in avatar name cache for current session/participant conversation item.
void LLConversationItem::fetchAvatarName(bool isParticipant /*= true*/)
{
	LLUUID item_id = getUUID();

	// item should not be null for participants
	if (isParticipant)
	{
		llassert(item_id.notNull());
	}

	// disconnect any previous avatar name cache connection
	if (mAvatarNameCacheConnection.connected())
	{
		mAvatarNameCacheConnection.disconnect();
	}

	// exclude nearby chat item
	if (item_id.notNull())
	{
		// for P2P session item, override it as item of called agent
		if (CONV_SESSION_1_ON_1 == getType())
		{
			item_id = LLIMModel::getInstance()->getOtherParticipantID(item_id);
		}

		// subscribe on avatar name cache changes for participant and session items
		mAvatarNameCacheConnection = LLAvatarNameCache::get(item_id, boost::bind(&LLConversationItem::onAvatarNameCache, this, _2));
	}
}

//
// LLConversationItemSession
// 

LLConversationItemSession::LLConversationItemSession(std::string display_name, const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) :
	LLConversationItem(display_name,uuid,root_view_model),
	mIsLoaded(false)
{
	mConvType = CONV_SESSION_UNKNOWN;
}

LLConversationItemSession::LLConversationItemSession(const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) :
	LLConversationItem(uuid,root_view_model)
{
	mConvType = CONV_SESSION_UNKNOWN;
}

bool LLConversationItemSession::hasChildren() const
{
	return getChildrenCount() > 0;
}

void LLConversationItemSession::addParticipant(LLConversationItemParticipant* participant)
{
	addChild(participant);
	mIsLoaded = true;
	mNeedsRefresh = true;
	updateName(participant);
	postEvent("add_participant", this, participant);
}

void LLConversationItemSession::updateName(LLConversationItemParticipant* participant)
{
	EConversationType conversation_type = getType();
	// We modify the session name only in the case of an ad-hoc session or P2P session, exit otherwise (nothing to do)
	if ((conversation_type != CONV_SESSION_AD_HOC) && (conversation_type != CONV_SESSION_1_ON_1))
	{
		return;
	}

	// Avoid changing the default name if no participant present yet
	if (mChildren.size() == 0)
	{
		return;
	}

	uuid_vec_t temp_uuids; // uuids vector for building the added participants' names string
	if (conversation_type == CONV_SESSION_AD_HOC || conversation_type == CONV_SESSION_1_ON_1)
	{
		// Build a string containing the participants UUIDs (minus own agent) and check if ready for display (we don't want "(waiting)" in there)
		// Note: we don't bind ourselves to the LLAvatarNameCache event as updateParticipantName() is called by
		// onAvatarNameCache() which is itself attached to the same event.

		// In the case of a P2P conversation, we need to grab the name of the other participant in the session instance itself
		// as we do not create participants for such a session.

		LLFolderViewModelItem * itemp;
		BOOST_FOREACH(itemp, mChildren)
		{
			LLConversationItem* current_participant = dynamic_cast<LLConversationItem*>(itemp);
			// Add the avatar uuid to the list (except if it's the own agent uuid)
			if (current_participant->getUUID() != gAgentID)
			{
				LLAvatarName av_name;
				if (LLAvatarNameCache::get(current_participant->getUUID(), &av_name))
				{
					temp_uuids.push_back(current_participant->getUUID());

					if (conversation_type == CONV_SESSION_1_ON_1)
					{
						break;
					}
				}
			}
		}
	}

	if (temp_uuids.size() != 0)
	{
		std::string new_session_name;
		LLAvatarActions::buildResidentsString(temp_uuids, new_session_name);
		renameItem(new_session_name);
		postEvent("update_session", this, NULL);
	}
}

void LLConversationItemSession::removeParticipant(LLConversationItemParticipant* participant)
{
	removeChild(participant);
	mNeedsRefresh = true;
	updateName(participant);
	postEvent("remove_participant", this, participant);
}

void LLConversationItemSession::removeParticipant(const LLUUID& participant_id)
{
	LLConversationItemParticipant* participant = findParticipant(participant_id);
	if (participant)
	{
		removeParticipant(participant);
	}
}

void LLConversationItemSession::clearParticipants()
{
    // clearParticipants function potentially is malfunctioning since it only cleans children of models,
    // it does nothing to views that own those models (listeners)
    // probably needs to post some kind of 'remove all participants' event
	clearChildren();
	mIsLoaded = false;
	mNeedsRefresh = true;
}


void LLConversationItemSession::clearAndDeparentModels()
{
    std::for_each(mChildren.begin(), mChildren.end(),
        [](LLFolderViewModelItem* c)
        {
            if (c->getNumRefs() == 0)
            {
                // LLConversationItemParticipant can be created but not assigned to any view,
                // it was waiting for an "add_participant" event to be processed
                delete c;
            }
            else
            {
                // Model is still assigned to some view/widget
                c->setParent(NULL);
            }
        }
    );
    mChildren.clear();
}

LLConversationItemParticipant* LLConversationItemSession::findParticipant(const LLUUID& participant_id)
{
	// This is *not* a general tree parsing algorithm. It assumes that a session contains only 
	// items (LLConversationItemParticipant) that have themselve no children.
	LLConversationItemParticipant* participant = NULL;
	child_list_t::iterator iter;
	for (iter = mChildren.begin(); iter != mChildren.end(); iter++)
	{
		participant = dynamic_cast<LLConversationItemParticipant*>(*iter);
		if (participant && participant->hasSameValue(participant_id))
		{
			break;
		}
	}
	return (iter == mChildren.end() ? NULL : participant);
}

void LLConversationItemSession::setParticipantIsMuted(const LLUUID& participant_id, bool is_muted)
{
	LLConversationItemParticipant* participant = findParticipant(participant_id);
	if (participant)
	{
		participant->moderateVoice(is_muted);
	}
}

void LLConversationItemSession::setParticipantIsModerator(const LLUUID& participant_id, bool is_moderator)
{
	LLConversationItemParticipant* participant = findParticipant(participant_id);
	if (participant)
	{
		participant->setIsModerator(is_moderator);
	}
}

void LLConversationItemSession::setTimeNow(const LLUUID& participant_id)
{
	mLastActiveTime = LLFrameTimer::getElapsedSeconds();
	mNeedsRefresh = true;
	LLConversationItemParticipant* participant = findParticipant(participant_id);
	if (participant)
	{
		participant->setTimeNow();
	}
}

void LLConversationItemSession::setDistance(const LLUUID& participant_id, F64 dist)
{
	LLConversationItemParticipant* participant = findParticipant(participant_id);
	if (participant)
	{
		participant->setDistance(dist);
		mNeedsRefresh = true;
	}
}

void LLConversationItemSession::buildContextMenu(LLMenuGL& menu, U32 flags)
{
    LL_DEBUGS() << "LLConversationItemParticipant::buildContextMenu()" << LL_ENDL;
    menuentry_vec_t items;
    menuentry_vec_t disabled_items;
    if((flags & ITEM_IN_MULTI_SELECTION) && (this->getType() != CONV_SESSION_NEARBY))
    {
    	items.push_back(std::string("close_selected_conversations"));
    }
    if(this->getType() == CONV_SESSION_1_ON_1)
    {
        items.push_back(std::string("close_conversation"));
        items.push_back(std::string("separator_disconnect_from_voice"));
        buildParticipantMenuOptions(items, flags);
    }
    else if(this->getType() == CONV_SESSION_GROUP)
    {
        items.push_back(std::string("close_conversation"));
        addVoiceOptions(items);
        items.push_back(std::string("chat_history"));
        items.push_back(std::string("separator_chat_history"));
        items.push_back(std::string("group_profile"));
        items.push_back(std::string("activate_group"));
        items.push_back(std::string("leave_group"));
    }
    else if(this->getType() == CONV_SESSION_AD_HOC)
    {
        items.push_back(std::string("close_conversation"));
        addVoiceOptions(items);
        items.push_back(std::string("chat_history"));
    }
    else if(this->getType() == CONV_SESSION_NEARBY)
    {
        items.push_back(std::string("chat_history"));
    }

    hide_context_entries(menu, items, disabled_items);
}

void LLConversationItemSession::addVoiceOptions(menuentry_vec_t& items)
{
    LLVoiceChannel* voice_channel = LLIMModel::getInstance() ? LLIMModel::getInstance()->getVoiceChannel(this->getUUID()) : NULL;

    if(voice_channel != LLVoiceChannel::getCurrentVoiceChannel())
    {
        items.push_back(std::string("open_voice_conversation"));
    }
    else
    {
        items.push_back(std::string("disconnect_from_voice"));
    }
}

// The time of activity of a session is the time of the most recent activity, session and participants included
const bool LLConversationItemSession::getTime(F64& time) const
{
	F64 most_recent_time = mLastActiveTime;
	bool has_time = (most_recent_time > 0.1);
	LLConversationItemParticipant* participant = NULL;
	child_list_t::const_iterator iter;
	for (iter = mChildren.begin(); iter != mChildren.end(); iter++)
	{
		participant = dynamic_cast<LLConversationItemParticipant*>(*iter);
		F64 participant_time;
		if (participant && participant->getTime(participant_time))
		{
			has_time = true;
			most_recent_time = llmax(most_recent_time,participant_time);
		}
	}
	if (has_time)
	{
		time = most_recent_time;
	}
	return has_time;
}

void LLConversationItemSession::dumpDebugData(bool dump_children)
{
	// Session info
	LL_INFOS() << "Merov debug : session " << this << ", uuid = " << mUUID << ", name = " << mName << ", is loaded = " << mIsLoaded << LL_ENDL;
	// Children info
	if (dump_children)
	{
		for (child_list_t::iterator iter = mChildren.begin(); iter != mChildren.end(); iter++)
		{
			LLConversationItemParticipant* participant = dynamic_cast<LLConversationItemParticipant*>(*iter);
			if (participant)
			{
				participant->dumpDebugData();
			}
		}
	}
}

// should be invoked only for P2P sessions
void LLConversationItemSession::onAvatarNameCache(const LLAvatarName& av_name)
{
	if (mAvatarNameCacheConnection.connected())
	{
		mAvatarNameCacheConnection.disconnect();
	}

	renameItem(av_name.getDisplayName());
	postEvent("update_session", this, NULL);
}

//
// LLConversationItemParticipant
// 

LLConversationItemParticipant::LLConversationItemParticipant(std::string display_name, const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) :
	LLConversationItem(display_name,uuid,root_view_model),
	mIsModeratorMuted(false),
	mIsModerator(false),
	mDisplayModeratorLabel(false),
	mDistToAgent(-1.0)
{
	mDisplayName = display_name;
	mConvType = CONV_PARTICIPANT;
}

LLConversationItemParticipant::LLConversationItemParticipant(const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) :
	LLConversationItem(uuid,root_view_model),
	mIsModeratorMuted(false),
	mIsModerator(false),
	mDisplayModeratorLabel(false),
	mDistToAgent(-1.0)
{
	mConvType = CONV_PARTICIPANT;
}

void LLConversationItemParticipant::updateName()
{
	llassert(getUUID().notNull());
	if (getUUID().notNull())
	{
		LLAvatarName av_name;
		if (LLAvatarNameCache::get(getUUID(),&av_name))
		{
			updateName(av_name);
		}
	}
}

void LLConversationItemParticipant::onAvatarNameCache(const LLAvatarName& av_name)
{
	if (mAvatarNameCacheConnection.connected())
	{
		mAvatarNameCacheConnection.disconnect();
	}

	updateName(av_name);
}

void LLConversationItemParticipant::updateName(const LLAvatarName& av_name)
{
	mName = av_name.getUserName();
	mDisplayName = av_name.getDisplayName();
	
	if (mDisplayModeratorLabel)
	{
		mDisplayName += " " + LLTrans::getString("IM_moderator_label");
	}
	
	renameItem(mDisplayName);
	if (mParent != NULL)
	{
		LLConversationItemSession* parent_session = dynamic_cast<LLConversationItemSession*>(mParent);
		if (parent_session != NULL)
		{
			parent_session->requestSort();
			parent_session->updateName(this);
			postEvent("update_participant", parent_session, this);
		}
	}
}

void LLConversationItemParticipant::buildContextMenu(LLMenuGL& menu, U32 flags)
{
    menuentry_vec_t items;
    menuentry_vec_t disabled_items;
	
	buildParticipantMenuOptions(items, flags);
	
    hide_context_entries(menu, items, disabled_items);
}

LLConversationItemSession* LLConversationItemParticipant::getParentSession()
{
	LLConversationItemSession* parent_session = NULL;
	if (hasParent())
	{
		parent_session = dynamic_cast<LLConversationItemSession*>(mParent);
	}
	return parent_session;
}

void LLConversationItemParticipant::dumpDebugData()
{
	LL_INFOS() << "Merov debug : participant, uuid = " << mUUID << ", name = " << mName << ", display name = " << mDisplayName << ", muted = " << isVoiceMuted() << ", moderator = " << mIsModerator << LL_ENDL;
}

void LLConversationItemParticipant::setDisplayModeratorRole(bool displayRole)
{ 
	if (displayRole != mDisplayModeratorLabel)
	{
		mDisplayModeratorLabel = displayRole;
		updateName();
	}
}

bool LLConversationItemParticipant::isVoiceMuted()
{
	return mIsModeratorMuted || LLMuteList::getInstance()->isMuted(mUUID, LLMute::flagVoiceChat);
}

//
// LLConversationSort
// 

// Comparison operator: returns "true" is a comes before b, "false" otherwise
bool LLConversationSort::operator()(const LLConversationItem* const& a, const LLConversationItem* const& b) const
{
	LLConversationItem::EConversationType type_a = a->getType();
	LLConversationItem::EConversationType type_b = b->getType();

	if ((type_a == LLConversationItem::CONV_PARTICIPANT) && (type_b == LLConversationItem::CONV_PARTICIPANT))
	{
		// If both items are participants
		U32 sort_order = getSortOrderParticipants();
		if (sort_order == LLConversationFilter::SO_DATE)
		{
			F64 time_a = 0.0;
			F64 time_b = 0.0;
			bool has_time_a = a->getTime(time_a);
			bool has_time_b = b->getTime(time_b);
			if (has_time_a && has_time_b)
			{
				// Most recent comes first
				return (time_a > time_b);
			}
			else if (has_time_a || has_time_b)
			{
				// If we have only one time available, the element with time must come first
				return has_time_a;
			}
			// If no time available, we'll default to sort by name at the end of this method
		}
		else if (sort_order == LLConversationFilter::SO_DISTANCE)
		{
			F64 dist_a = 0.0;
			F64 dist_b = 0.0;
			bool has_dist_a = a->getDistanceToAgent(dist_a);
			bool has_dist_b = b->getDistanceToAgent(dist_b);
			if (has_dist_a && has_dist_b)
			{
				// Closest comes first
				return (dist_a < dist_b);
			}
			else if (has_dist_a || has_dist_b)
			{
				// If we have only one distance available, the element with it must come first
				return has_dist_a;
			}
			// If no distance available, we'll default to sort by name at the end of this method
		}
	}
	else if ((type_a > LLConversationItem::CONV_PARTICIPANT) && (type_b > LLConversationItem::CONV_PARTICIPANT))
	{
		// If both are sessions
		U32 sort_order = getSortOrderSessions();

		if (sort_order == LLConversationFilter::SO_DATE)
		{
			// Sort by time
			F64 time_a = 0.0;
			F64 time_b = 0.0;
			bool has_time_a = a->getTime(time_a);
			bool has_time_b = b->getTime(time_b);
			if (has_time_a && has_time_b)
			{
				// Most recent comes first
				return (time_a > time_b);
			}
			else if (has_time_a || has_time_b)
			{
				// If we have only one time available, the element with time must come first
				return has_time_a;
			}
			// If no time available, we'll default to sort by name at the end of this method
		}
		else
		{
			if ((type_a == LLConversationItem::CONV_SESSION_NEARBY) || (type_b == LLConversationItem::CONV_SESSION_NEARBY))
			{
				// If one is the nearby session, put nearby session *always* last
				return (type_b == LLConversationItem::CONV_SESSION_NEARBY);
			}
			else if (sort_order == LLConversationFilter::SO_SESSION_TYPE)
			{
				if (type_a != type_b)
				{
					// Lowest types come first. See LLConversationItem definition of types
					return (type_a < type_b);
				}
			// If types are identical, we'll default to sort by name at the end of this method
			}
		}
	}
	else
	{
		// If one item is a participant and the other a session, the session comes before the participant
		// so we simply compare the type
		// Notes: as a consequence, CONV_UNKNOWN (which should never get created...) always come first
		return (type_a > type_b);
	}
	// By default, in all other possible cases (including sort order type LLConversationFilter::SO_NAME of course), 
	// we sort by name
	S32 compare = LLStringUtil::compareDict(a->getName(), b->getName());
	return (compare < 0);
}

//
// LLConversationViewModel
//

void LLConversationViewModel::sort(LLFolderViewFolder* folder) 
{
	base_t::sort(folder);
}

// EOF