/**
 * @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"

//
// 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();
    }

    clearChildren();
}

//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;
}

LLConversationItemSession::~LLConversationItemSession()
{
    clearAndDeparentModels();
}

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.

        for (auto 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