/**
 * @file llconversationlog.h
 *
 * $LicenseInfo:firstyear=2002&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2012, 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 "llconversationlog.h"
#include "lldiriterator.h"
#include "llnotificationsutil.h"
#include "lltrans.h"

#include "boost/lexical_cast.hpp"

const S32Days CONVERSATION_LIFETIME = (S32Days)30; // lifetime of LLConversation is 30 days by spec

struct ConversationParams : public LLInitParam::Block<ConversationParams>
{
    Mandatory<U64Seconds >  time;
    Mandatory<std::string>                      timestamp;
    Mandatory<SessionType>                      conversation_type;
    Mandatory<std::string>                      conversation_name,
                                                history_filename;
    Mandatory<LLUUID>                           session_id,
                                                participant_id;
    Mandatory<bool>                             has_offline_ims;
};

/************************************************************************/
/*             LLConversation implementation                            */
/************************************************************************/

LLConversation::LLConversation(const ConversationParams& params)
:   mTime(params.time),
    mTimestamp(params.timestamp.isProvided() ? params.timestamp : createTimestamp(params.time)),
    mConversationType(params.conversation_type),
    mConversationName(params.conversation_name),
    mHistoryFileName(params.history_filename),
    mSessionID(params.session_id),
    mParticipantID(params.participant_id),
    mHasOfflineIMs(params.has_offline_ims)
{
    setListenIMFloaterOpened();
}

LLConversation::LLConversation(const LLIMModel::LLIMSession& session)
:   mTime(time_corrected()),
    mTimestamp(createTimestamp(mTime)),
    mConversationType(session.mSessionType),
    mConversationName(session.mName),
    mHistoryFileName(session.mHistoryFileName),
    mSessionID(session.isOutgoingAdHoc() ? session.generateOutgoingAdHocHash() : session.mSessionID),
    mParticipantID(session.mOtherParticipantID),
    mHasOfflineIMs(session.mHasOfflineMessage)
{
    setListenIMFloaterOpened();
}

LLConversation::LLConversation(const LLConversation& conversation)
{
    mTime               = conversation.getTime();
    mTimestamp          = conversation.getTimestamp();
    mConversationType   = conversation.getConversationType();
    mConversationName   = conversation.getConversationName();
    mHistoryFileName    = conversation.getHistoryFileName();
    mSessionID          = conversation.getSessionID();
    mParticipantID      = conversation.getParticipantID();
    mHasOfflineIMs      = conversation.hasOfflineMessages();

    setListenIMFloaterOpened();
}

LLConversation::~LLConversation()
{
    mIMFloaterShowedConnection.disconnect();
}

void LLConversation::updateTimestamp()
{
    mTime = (U64Seconds)time_corrected();
    mTimestamp = createTimestamp(mTime);
}

void LLConversation::onIMFloaterShown(const LLUUID& session_id)
{
    if (mSessionID == session_id)
    {
        mHasOfflineIMs = false;
    }
}

// static
const std::string LLConversation::createTimestamp(const U64Seconds& utc_time)
{
    std::string timeStr;
    LLSD substitution;
    substitution["datetime"] = (S32)utc_time.value();

    timeStr = "["+LLTrans::getString ("TimeMonth")+"]/["
                 +LLTrans::getString ("TimeDay")+"]/["
                 +LLTrans::getString ("TimeYear")+"] ["
                 +LLTrans::getString ("TimeHour")+"]:["
                 +LLTrans::getString ("TimeMin")+"]";


    LLStringUtil::format (timeStr, substitution);
    return timeStr;
}

bool LLConversation::isOlderThan(U32Days days) const
{
    U64Seconds now(time_corrected());
    U32Days age = now - mTime;

    return age > days;
}

void LLConversation::setListenIMFloaterOpened()
{
    LLFloaterIMSession* floater = LLFloaterIMSession::findInstance(mSessionID);

    bool offline_ims_visible = LLFloaterIMSession::isVisible(floater) && floater->hasFocus();

    // we don't need to listen for im floater with this conversation is opened
    // if floater is already opened or this conversation doesn't have unread offline messages
    if (mHasOfflineIMs && !offline_ims_visible)
    {
        mIMFloaterShowedConnection = LLFloaterIMSession::setIMFloaterShowedCallback(boost::bind(&LLConversation::onIMFloaterShown, this, _1));
    }
    else
    {
        mHasOfflineIMs = false;
    }
}

/************************************************************************/
/*             LLConversationLogFriendObserver implementation           */
/************************************************************************/

// Note : An LLSingleton like LLConversationLog cannot be an LLFriendObserver
// at the same time.
// This is because avatar observers are deleted by the observed object which
// conflicts with the way LLSingleton are deleted.

class LLConversationLogFriendObserver : public LLFriendObserver
{
public:
    LLConversationLogFriendObserver() {}
    virtual ~LLConversationLogFriendObserver() {}
    virtual void changed(U32 mask);
};

void LLConversationLogFriendObserver::changed(U32 mask)
{
    if (mask & (LLFriendObserver::ADD | LLFriendObserver::REMOVE))
    {
        LLConversationLog::instance().notifyObservers();
    }
}

/************************************************************************/
/*             LLConversationLog implementation                         */
/************************************************************************/

LLConversationLog::LLConversationLog() :
    mAvatarNameCacheConnection(),
    mLoggingEnabled(false)
{
}

void LLConversationLog::enableLogging(S32 log_mode)
{
    mLoggingEnabled = log_mode > 0;
    if (log_mode > 0)
    {
        mConversations.clear();
        loadFromFile(getFileName());
        LLIMMgr::instance().addSessionObserver(this);
        mNewMessageSignalConnection = LLIMModel::instance().addNewMsgCallback(boost::bind(&LLConversationLog::onNewMessageReceived, this, _1));

        mFriendObserver = new LLConversationLogFriendObserver;
        LLAvatarTracker::instance().addObserver(mFriendObserver);
    }
    else
    {
        saveToFile(getFileName());

        LLIMMgr::instance().removeSessionObserver(this);
        mNewMessageSignalConnection.disconnect();
        LLAvatarTracker::instance().removeObserver(mFriendObserver);
    }

    notifyObservers();
}

void LLConversationLog::logConversation(const LLUUID& session_id, bool has_offline_msg)
{
    const LLIMModel::LLIMSession* session = LLIMModel::instance().findIMSession(session_id);
    LLConversation* conversation = findConversation(session);

    if (session && session->mOtherParticipantID != gAgentID)
    {
        if (conversation)
        {
            if(has_offline_msg)
            {
                updateOfflineIMs(session, has_offline_msg);
            }
            updateConversationTimestamp(conversation);
        }
        else
        {
            createConversation(session);
        }
    }
}

void LLConversationLog::createConversation(const LLIMModel::LLIMSession* session)
{
    if (session)
    {
        LLConversation conversation(*session);
        mConversations.push_back(conversation);

        if (LLIMModel::LLIMSession::P2P_SESSION == session->mSessionType)
        {
            if (mAvatarNameCacheConnection.connected())
            {
                mAvatarNameCacheConnection.disconnect();
            }
            mAvatarNameCacheConnection = LLAvatarNameCache::get(session->mOtherParticipantID, boost::bind(&LLConversationLog::onAvatarNameCache, this, _1, _2, session));
        }

        notifyObservers();
    }
}

void LLConversationLog::updateConversationName(const LLIMModel::LLIMSession* session, const std::string& name)
{
    if (!session)
    {
        return;
    }

    LLConversation* conversation = findConversation(session);
    if (conversation)
    {
        conversation->setConversationName(name);
        notifyParticularConversationObservers(conversation->getSessionID(), LLConversationLogObserver::CHANGED_NAME);
    }
}

void LLConversationLog::updateOfflineIMs(const LLIMModel::LLIMSession* session, bool new_messages)
{
    if (!session)
    {
        return;
    }

    LLConversation* conversation = findConversation(session);
    if (conversation)
    {
        conversation->setOfflineMessages(new_messages);
        notifyParticularConversationObservers(conversation->getSessionID(), LLConversationLogObserver::CHANGED_OfflineIMs);
    }
}

void LLConversationLog::updateConversationTimestamp(LLConversation* conversation)
{
    if (conversation)
    {
        conversation->updateTimestamp();
        notifyParticularConversationObservers(conversation->getSessionID(), LLConversationLogObserver::CHANGED_TIME);
    }
}

LLConversation* LLConversationLog::findConversation(const LLIMModel::LLIMSession* session)
{
    if (session)
    {
        const LLUUID session_id = session->isOutgoingAdHoc() ? session->generateOutgoingAdHocHash() : session->mSessionID;

        conversations_vec_t::iterator conv_it = mConversations.begin();
        for(; conv_it != mConversations.end(); ++conv_it)
        {
            if (conv_it->getSessionID() == session_id)
            {
                return &*conv_it;
            }
        }
    }

    return NULL;
}

void LLConversationLog::removeConversation(const LLConversation& conversation)
{
    conversations_vec_t::iterator conv_it = mConversations.begin();
    for(; conv_it != mConversations.end(); ++conv_it)
    {
        if (conv_it->getSessionID() == conversation.getSessionID() && conv_it->getTime() == conversation.getTime())
        {
            mConversations.erase(conv_it);
            notifyObservers();
            cache();
            return;
        }
    }
}

const LLConversation* LLConversationLog::getConversation(const LLUUID& session_id)
{
    conversations_vec_t::const_iterator conv_it = mConversations.begin();
    for(; conv_it != mConversations.end(); ++conv_it)
    {
        if (conv_it->getSessionID() == session_id)
        {
            return &*conv_it;
        }
    }

    return NULL;
}

void LLConversationLog::addObserver(LLConversationLogObserver* observer)
{
    mObservers.insert(observer);
}

void LLConversationLog::removeObserver(LLConversationLogObserver* observer)
{
    mObservers.erase(observer);
}

void LLConversationLog::sessionAdded(const LLUUID& session_id, const std::string& name, const LLUUID& other_participant_id, bool has_offline_msg)
{
    logConversation(session_id, has_offline_msg);
}

void LLConversationLog::cache()
{
    if (gSavedPerAccountSettings.getS32("KeepConversationLogTranscripts") > 0)
    {
        saveToFile(getFileName());
    }
}

void LLConversationLog::getListOfBackupLogs(std::vector<std::string>& list_of_backup_logs)
{
    // get Users log directory
    std::string dirname = gDirUtilp->getPerAccountChatLogsDir();

    // add final OS dependent delimiter
    dirname += gDirUtilp->getDirDelimiter();

    // create search pattern
    std::string pattern = "conversation.log.backup*";

    LLDirIterator iter(dirname, pattern);
    std::string filename;
    while (iter.next(filename))
    {
        list_of_backup_logs.push_back(gDirUtilp->add(dirname, filename));
    }
}

void LLConversationLog::deleteBackupLogs()
{
    std::vector<std::string> backup_logs;
    getListOfBackupLogs(backup_logs);

    for (const std::string& fullpath : backup_logs)
    {
        LLFile::remove(fullpath);
    }
}

void LLConversationLog::verifyFilename(const LLUUID& session_id, const std::string &expected_filename, const std::string &new_session_name)
{
    conversations_vec_t::iterator conv_it = mConversations.begin();
    for (; conv_it != mConversations.end(); ++conv_it)
    {
        if (conv_it->getSessionID() == session_id)
        {
            if (conv_it->getHistoryFileName() != expected_filename)
            {
                LLLogChat::renameLogFile(conv_it->getHistoryFileName(), expected_filename);
                conv_it->updateHistoryFileName(expected_filename);
                conv_it->setConversationName(new_session_name);
            }
            break;
        }
    }
}

bool LLConversationLog::moveLog(const std::string &originDirectory, const std::string &targetDirectory)
{

    std::string backupFileName;
    unsigned backupFileCount = 0;

    //Does the file exist in the current path, if it does lets move it
    if(LLFile::isfile(originDirectory))
    {
        //The target directory contains that file already, so lets store it
        if(LLFile::isfile(targetDirectory))
        {
            backupFileName = targetDirectory + ".backup";

            //If needed store backup file as .backup1 etc.
            while(LLFile::isfile(backupFileName))
            {
                ++backupFileCount;
                backupFileName = targetDirectory + ".backup" + std::to_string(backupFileCount);
            }

            //Rename the file to its backup name so it is not overwritten
            LLFile::rename(targetDirectory, backupFileName);
        }

        //Move the file from the current path to target path
        if(LLFile::rename(originDirectory, targetDirectory) != 0)
        {
            return false;
        }
    }

    return true;
}

void LLConversationLog::initLoggingState()
{
    if (gSavedPerAccountSettings.controlExists("KeepConversationLogTranscripts"))
    {
        LLControlVariable * keep_log_ctrlp = gSavedPerAccountSettings.getControl("KeepConversationLogTranscripts").get();
        S32 log_mode = keep_log_ctrlp->getValue();
        keep_log_ctrlp->getSignal()->connect(boost::bind(&LLConversationLog::enableLogging, this, _2));
        if (log_mode > 0)
        {
            enableLogging(log_mode);
        }
    }
}

std::string LLConversationLog::getFileName()
{
    std::string filename = "conversation";
    std::string log_address = gDirUtilp->getExpandedFilename(LL_PATH_PER_ACCOUNT_CHAT_LOGS, filename);
    if (!log_address.empty())
    {
        log_address += ".log";
    }
    return log_address;
}

bool LLConversationLog::saveToFile(const std::string& filename)
{
    if (!filename.size())
    {
        LL_WARNS() << "Call log list filename is empty!" << LL_ENDL;
        return false;
    }

    LLFILE* fp = LLFile::fopen(filename, "wb");
    if (!fp)
    {
        LL_WARNS() << "Couldn't open call log list" << filename << LL_ENDL;
        return false;
    }

    std::string participant_id;
    std::string conversation_id;

    conversations_vec_t::const_iterator conv_it = mConversations.begin();
    for (; conv_it != mConversations.end(); ++conv_it)
    {
        conv_it->getSessionID().toString(conversation_id);
        conv_it->getParticipantID().toString(participant_id);

        bool is_adhoc = (conv_it->getConversationType() == LLIMModel::LLIMSession::ADHOC_SESSION);
        std::string conv_name = is_adhoc ? conv_it->getConversationName() : LLURI::escape(conv_it->getConversationName());
        std::string file_name = is_adhoc ? conv_it->getHistoryFileName() : LLURI::escape(conv_it->getHistoryFileName());

        // examples of two file entries
        // [1343221177] 0 1 0 John Doe| 7e4ec5be-783f-49f5-71dz-16c58c64c145 4ec62a74-c246-0d25-2af6-846beac2aa55 john.doe|
        // [1343222639] 2 0 0 Ad-hoc Conference| c3g67c89-c479-4c97-b21d-32869bcfe8rc 68f1c33e-4135-3e3e-a897-8c9b23115c09 Ad-hoc Conference hash597394a0-9982-766d-27b8-c75560213b9a|
        fprintf(fp, "[%lld] %d %d %d %s| %s %s %s|\n",
                (S64)conv_it->getTime().value(),
                (S32)conv_it->getConversationType(),
                (S32)0,
                (S32)conv_it->hasOfflineMessages(),
                conv_name.c_str(),
                participant_id.c_str(),
                conversation_id.c_str(),
                file_name.c_str());
    }
    fclose(fp);
    return true;
}
bool LLConversationLog::loadFromFile(const std::string& filename)
{
    if(!filename.size())
    {
        LL_WARNS() << "Call log list filename is empty!" << LL_ENDL;
        return false;
    }

    LLFILE* fp = LLFile::fopen(filename, "rb");
    if (!fp)
    {
        LL_WARNS() << "Couldn't open call log list" << filename << LL_ENDL;
        return false;
    }
    bool purge_required = false;

    static constexpr int UTF_BUFFER{ 1024 }; // long enough to handle the most extreme Unicode nonsense and some to spare

    char buffer[UTF_BUFFER];
    char conv_name_buffer[MAX_STRING];
    char part_id_buffer[MAX_STRING];
    char conv_id_buffer[MAX_STRING];
    char history_file_name[MAX_STRING];
    S32 has_offline_ims;
    S32 stype;
    S64 time;
    // before CHUI-348 it was a flag of conversation voice state
    int prereserved_unused;

    memset(buffer, '\0', UTF_BUFFER);
    while (!feof(fp) && fgets(buffer, UTF_BUFFER, fp))
    {
        // force blank for added safety
        memset(conv_name_buffer, '\0', MAX_STRING);
        memset(part_id_buffer, '\0', MAX_STRING);
        memset(conv_id_buffer, '\0', MAX_STRING);
        memset(history_file_name, '\0', MAX_STRING);

        sscanf(buffer, "[%lld] %d %d %d %[^|]| %s %s %[^|]|",
                &time,
                &stype,
                &prereserved_unused,
                &has_offline_ims,
                conv_name_buffer,
                part_id_buffer,
                conv_id_buffer,
                history_file_name);

        bool is_adhoc = ((SessionType)stype == LLIMModel::LLIMSession::ADHOC_SESSION);
        std::string conv_name = is_adhoc ? conv_name_buffer : LLURI::unescape(conv_name_buffer);
        std::string file_name = is_adhoc ? history_file_name : LLURI::unescape(history_file_name);

        ConversationParams params;
        params.time(LLUnits::Seconds::fromValue(time))
            .conversation_type((SessionType)stype)
            .has_offline_ims(has_offline_ims)
            .conversation_name(conv_name)
            .participant_id(LLUUID(part_id_buffer))
            .session_id(LLUUID(conv_id_buffer))
            .history_filename(file_name);

        LLConversation conversation(params);

        // CHUI-325
        // The conversation log should be capped to the last 30 days. Conversations with the last utterance
        // being over 30 days old should be purged from the conversation log text file on login.
        if (conversation.isOlderThan(CONVERSATION_LIFETIME))
        {
            purge_required = true;
            continue;
        }

        mConversations.push_back(conversation);
        memset(buffer, '\0', UTF_BUFFER);
    }
    fclose(fp);

    if(purge_required)
    {
        LLFile::remove(filename);
        cache();
    }

    notifyObservers();
    return true;
}

void LLConversationLog::notifyObservers()
{
    std::set<LLConversationLogObserver*>::const_iterator iter = mObservers.begin();
    for (; iter != mObservers.end(); ++iter)
    {
        (*iter)->changed();
    }
}

void LLConversationLog::notifyParticularConversationObservers(const LLUUID& session_id, U32 mask)
{
    std::set<LLConversationLogObserver*>::const_iterator iter = mObservers.begin();
    for (; iter != mObservers.end(); ++iter)
    {
        (*iter)->changed(session_id, mask);
    }
}

void LLConversationLog::onNewMessageReceived(const LLSD& data)
{
    const LLUUID session_id = data["session_id"].asUUID();
    logConversation(session_id, false);
}

void LLConversationLog::onAvatarNameCache(const LLUUID& participant_id, const LLAvatarName& av_name, const LLIMModel::LLIMSession* session)
{
    mAvatarNameCacheConnection.disconnect();
    updateConversationName(session, av_name.getCompleteName());
}

void LLConversationLog::onClearLog()
{
    LLNotificationsUtil::add("PreferenceChatClearLog", LLSD(), LLSD(), boost::bind(&LLConversationLog::onClearLogResponse, this, _1, _2));
}

void LLConversationLog::onClearLogResponse(const LLSD& notification, const LLSD& response)
{
    if (0 == LLNotificationsUtil::getSelectedOption(notification, response))
    {
        mConversations.clear();
        notifyObservers();
        cache();
        deleteBackupLogs();
    }
}