// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
/**
 * @file   llavatarrendernotifier.cpp
 * @author andreykproductengine
 * @date   2015-08-05
 * @brief  
 * 
 * $LicenseInfo:firstyear=2013&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2013, 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 "llavatarrendernotifier.h"
#include "llagentcamera.h"
#include "llagentwearables.h"
#include "llappearancemgr.h"
#include "llattachmentsmgr.h"
#include "llnotifications.h"
#include "llnotificationsutil.h"
#include "llnotificationtemplate.h"
#include "llslurl.h"
#include "llstatusbar.h" // gStatusBar
#include "lltimer.h"
#include "llvoavatarself.h"
#include "llviewercontrol.h"
#include "lltrans.h"

// when change exceeds this ration, notification is shown
static const F32 RENDER_ALLOWED_CHANGE_PCT = 0.1f;
// wait seconds before processing over limit updates after last complexity change
static const U32 OVER_LIMIT_UPDATE_DELAY = 70;

static const U32 WARN_HUD_OBJECTS_LIMIT = 1000;
static const U32 WARN_HUD_TEXTURES_LIMIT = 200;
static const U32 WARN_HUD_OVERSIZED_TEXTURES_LIMIT = 6;
static const U32 WARN_HUD_TEXTURE_MEMORY_LIMIT = 32000000; // in bytes


LLAvatarRenderNotifier::LLAvatarRenderNotifier() :
mAgentsCount(0),
mOverLimitAgents(0),
mAgentComplexity(0),
mOverLimitPct(0.0f),
mLatestAgentsCount(0),
mLatestOverLimitAgents(0),
mLatestAgentComplexity(0),
mLatestOverLimitPct(0.0f),
mShowOverLimitAgents(false),
mNotifyOutfitLoading(false),
mLastCofVersion(LLViewerInventoryCategory::VERSION_UNKNOWN),
mLastSkeletonSerialNum(-1),
mLastOutfitRezStatus(-1)
{
    mPopUpDelayTimer.resetWithExpiry(OVER_LIMIT_UPDATE_DELAY);
}

std::string LLAvatarRenderNotifier::overLimitMessage()
{
    static const char* everyone_now = "av_render_everyone_now";
    static const char* not_everyone = "av_render_not_everyone";
    static const char* over_half = "av_render_over_half";
    static const char* most = "av_render_most_of";
    static const char* anyone = "av_render_anyone";

    std::string message;
    if ( mLatestOverLimitPct >= 99.f )
    {
        message = anyone;
    }
    else if ( mLatestOverLimitPct >= 75.f )
    {
        message = most;
    }
    else if ( mLatestOverLimitPct >= 50.f )
    {
        message = over_half;
    }
    else if ( mLatestOverLimitPct > 10.f )
    {
        message = not_everyone;
    }
    else
    {
        // Will be shown only after overlimit was > 0
        message = everyone_now;
    }
    return LLTrans::getString(message);
}

void LLAvatarRenderNotifier::triggerNotification(bool show_over_limit)
{
	gStatusBar->setAvComplexity(mLatestAgentComplexity, mLatestOverLimitPct);
    mAgentComplexity = mLatestAgentComplexity;
    mShowOverLimitAgents = show_over_limit;
	static LLCachedControl<U32> expire_delay(gSavedSettings, "ShowMyComplexityChanges", 20);

	LLDate expire_date(LLDate::now().secondsSinceEpoch() + expire_delay);
	LLSD args;
	args["AGENT_COMPLEXITY"] = LLSD::Integer(mLatestAgentComplexity);
	std::string notification_name;
    if (mShowOverLimitAgents)
    {
        notification_name = "AgentComplexityWithVisibility";
        args["OVERLIMIT_MSG"] = overLimitMessage();

        // remember what the situation was so that we only notify when it has changed
        mAgentsCount = mLatestAgentsCount;
        mOverLimitAgents = mLatestOverLimitAgents;
        mOverLimitPct = mLatestOverLimitPct;
	}
	else
	{
        // no change in visibility, just update complexity
        notification_name = "AgentComplexity";
	}

	if (mNotificationPtr != nullptr && mNotificationPtr->getName() != notification_name)
	{
		// since unique tag works only for same notification,
		// old notification needs to be canceled manually
		LLNotifications::instance().cancel(mNotificationPtr);
	}

    // log unconditionally
    LL_WARNS("AvatarRenderInfo") << notification_name << " " << args << LL_ENDL;

    if (   expire_delay // expiration of zero means do not show the notices
        && gAgentCamera.getLastCameraMode() != CAMERA_MODE_MOUSELOOK // don't display notices in Mouselook
        )
    {
        mNotificationPtr = LLNotifications::instance().add(LLNotification::Params()
                                                           .name(notification_name)
                                                           .expiry(expire_date)
                                                           .substitutions(args));
    }
}

bool LLAvatarRenderNotifier::isNotificationVisible()
{
	return mNotificationPtr != nullptr && mNotificationPtr->isActive();
}

void LLAvatarRenderNotifier::updateNotificationRegion(U32 agent_count, U32 over_limit)
{
	if (agent_count == 0)
	{
		// Data not ready
		return;
	}

	// save current values for later use
	mLatestAgentsCount = agent_count > over_limit ? agent_count - 1 : agent_count; // subtract self
	mLatestOverLimitAgents = over_limit;
	mLatestOverLimitPct = (mLatestAgentsCount == 0 ? 0 : (100.f * over_limit) / mLatestAgentsCount);

    if (mAgentsCount == mLatestAgentsCount
        && mOverLimitAgents == mLatestOverLimitAgents)
    {
        // no changes since last notification
        return;
    }

    if ((mPopUpDelayTimer.hasExpired() || (isNotificationVisible() && mShowOverLimitAgents))
        && (mOverLimitPct > 0 || mLatestOverLimitPct > 0)
        && std::abs(mOverLimitPct - mLatestOverLimitPct) > mLatestOverLimitPct * RENDER_ALLOWED_CHANGE_PCT
        )
    {
        // display in case of drop to/from zero and in case of significant (RENDER_ALLOWED_CHANGE_PCT) changes
        triggerNotification(true);

        // default timeout before next notification
        static LLCachedControl<U32> pop_up_delay(gSavedSettings, "ComplexityChangesPopUpDelay", 300);
        mPopUpDelayTimer.resetWithExpiry(pop_up_delay);
    }
}

void LLAvatarRenderNotifier::updateNotificationState()
{
    if (!isAgentAvatarValid())
    {
        // data not ready, nothing to show.
        return;
    }

    // Don't use first provided COF and Sceleton versions - let them load anf 'form' first
    if (mLastCofVersion < 0
        && gAgentWearables.areWearablesLoaded()
        && LLAttachmentsMgr::getInstance()->isAttachmentStateComplete())
    {
        // cof formed
        mLastCofVersion = LLAppearanceMgr::instance().getCOFVersion();
        mLastSkeletonSerialNum = gAgentAvatarp->mLastSkeletonSerialNum;
    }
    else if (mLastCofVersion >= 0
        && (mLastCofVersion != LLAppearanceMgr::instance().getCOFVersion()
        || mLastSkeletonSerialNum != gAgentAvatarp->mLastSkeletonSerialNum))
    {
        // version mismatch in comparison to previous outfit - outfit changed
        mNotifyOutfitLoading = true;
        mLastCofVersion = LLAppearanceMgr::instance().getCOFVersion();
        mLastSkeletonSerialNum = gAgentAvatarp->mLastSkeletonSerialNum;
    }

    if (gAgentAvatarp->mLastRezzedStatus < mLastOutfitRezStatus)
    {
        // rez status decreased - outfit related action was initiated
        mNotifyOutfitLoading = true;
    }

    mLastOutfitRezStatus = gAgentAvatarp->mLastRezzedStatus;
}
void LLAvatarRenderNotifier::updateNotificationAgent(U32 agentComplexity)
{
    // save the value for use in following messages
    mLatestAgentComplexity = agentComplexity;

    if (!isAgentAvatarValid() || !gAgentWearables.areWearablesLoaded())
    {
        // data not ready, nothing to show.
        return;
    }

    if (!mNotifyOutfitLoading)
    {
        // We should not notify about initial outfit and it's load process without reason
        updateNotificationState();

        if (mLatestOverLimitAgents > 0)
        {
            // Some users can't see agent already, notify user about complexity growth
            mNotifyOutfitLoading = true;
        }

        if (!mNotifyOutfitLoading)
        {
            // avatar or outfit not ready
            mAgentComplexity = mLatestAgentComplexity;
			gStatusBar->setAvComplexity(mLatestAgentComplexity, mLatestOverLimitPct);
            return;
        }
    }

    if (mAgentComplexity != mLatestAgentComplexity)
    {
        // if we have an agent complexity change, we always display it and hide 'over limit'
        triggerNotification(false);

        // next 'over limit' update should be displayed after delay to make sure information got updated at server side
        mPopUpDelayTimer.resetWithExpiry(OVER_LIMIT_UPDATE_DELAY);
    }
}

// LLHUDRenderNotifier

static const char* e_hud_messages[] =
{
    "hud_render_textures_warning",
    "hud_render_cramped_warning",
    "hud_render_heavy_textures_warning",
    "hud_render_cost_warning",
    "hud_render_memory_warning",
};

LLHUDRenderNotifier::LLHUDRenderNotifier() :
mReportedHUDWarning(WARN_NONE)
{
}

LLHUDRenderNotifier::~LLHUDRenderNotifier()
{
}

void LLHUDRenderNotifier::updateNotificationHUD(hud_complexity_list_t complexity)
{
    if (!isAgentAvatarValid() || !gAgentWearables.areWearablesLoaded())
    {
        // data not ready.
        return;
    }

    // TODO:
    // Find a way to show message with list of issues, but without making it too large
    // and intrusive.

    LLHUDComplexity new_total_complexity;
    LLHUDComplexity report_complexity;

    EWarnLevel warning_level = WARN_NONE;
	for (auto object_complexity : complexity)
    {
        EWarnLevel object_level = getWarningType(object_complexity, report_complexity);
        if (object_level >= 0)
        {
            warning_level = object_level;
            report_complexity = object_complexity;
        }
        new_total_complexity.objectsCost += object_complexity.objectsCost;
        new_total_complexity.objectsCount += object_complexity.objectsCount;
        new_total_complexity.texturesCost += object_complexity.texturesCost;
        new_total_complexity.texturesCount += object_complexity.texturesCount;
        new_total_complexity.largeTexturesCount += object_complexity.largeTexturesCount;
        new_total_complexity.texturesMemoryTotal += object_complexity.texturesMemoryTotal;
    }

    if (mHUDPopUpDelayTimer.hasExpired() || isNotificationVisible())
    {
        if (warning_level >= 0)
        {
            // Display info about most complex HUD object
            // make sure it shown only once unless object's complexity or object itself changed
            if (mReportedHUDComplexity.objectId != report_complexity.objectId
                || mReportedHUDWarning != warning_level)
            {
                displayHUDNotification(warning_level, report_complexity.objectId, report_complexity.objectName, report_complexity.jointName);
                mReportedHUDComplexity = report_complexity;
                mReportedHUDWarning = warning_level;
            }
        }
        else
        {
            // Check if total complexity is above threshold and above previous warning
            // Show warning with highest importance (5m delay between warnings by default)
            if (!mReportedHUDComplexity.objectId.isNull())
            {
                mReportedHUDComplexity.reset();
                mReportedHUDWarning = WARN_NONE;
            }

            warning_level = getWarningType(new_total_complexity, mReportedHUDComplexity);
            if (warning_level >= 0 && mReportedHUDWarning != warning_level)
            {
                displayHUDNotification(warning_level);
            }
            mReportedHUDComplexity = new_total_complexity;
            mReportedHUDWarning = warning_level;
        }
    }
    else if (warning_level >= 0)
    {
        LL_DEBUGS("HUDdetail") << "HUD individual warning postponed" << LL_ENDL;
    }

    if (mLatestHUDComplexity.objectsCost != new_total_complexity.objectsCost
        || mLatestHUDComplexity.objectsCount != new_total_complexity.objectsCount
        || mLatestHUDComplexity.texturesCost != new_total_complexity.texturesCost
        || mLatestHUDComplexity.texturesCount != new_total_complexity.texturesCount
        || mLatestHUDComplexity.largeTexturesCount != new_total_complexity.largeTexturesCount
        || mLatestHUDComplexity.texturesMemoryTotal != new_total_complexity.texturesMemoryTotal)
    {
        LL_INFOS("HUDdetail") << "HUD textures count: " << new_total_complexity.texturesCount
            << " HUD textures cost: " << new_total_complexity.texturesCost
            << " Large textures: " << new_total_complexity.largeTexturesCount
            << " HUD objects cost: " << new_total_complexity.objectsCost
            << " HUD objects count: " << new_total_complexity.objectsCount << LL_ENDL;

        mLatestHUDComplexity = new_total_complexity;
    }
}

bool LLHUDRenderNotifier::isNotificationVisible()
{
    return mHUDNotificationPtr != nullptr && mHUDNotificationPtr->isActive();
}

// private static
LLHUDRenderNotifier::EWarnLevel LLHUDRenderNotifier::getWarningType(LLHUDComplexity object_complexity, LLHUDComplexity cmp_complexity)
{
    static LLCachedControl<U32> max_render_cost(gSavedSettings, "RenderAvatarMaxComplexity", 0U); // ties max HUD cost to avatar cost
    static LLCachedControl<U32> max_objects_count(gSavedSettings, "RenderHUDObjectsWarning", WARN_HUD_OBJECTS_LIMIT);
    static LLCachedControl<U32> max_textures_count(gSavedSettings, "RenderHUDTexturesWarning", WARN_HUD_TEXTURES_LIMIT);
    static LLCachedControl<U32> max_oversized_count(gSavedSettings, "RenderHUDOversizedTexturesWarning", WARN_HUD_OVERSIZED_TEXTURES_LIMIT);
    static LLCachedControl<U32> max_texture_memory(gSavedSettings, "RenderHUDTexturesMemoryWarning", WARN_HUD_TEXTURE_MEMORY_LIMIT);

    if (cmp_complexity.texturesMemoryTotal < object_complexity.texturesMemoryTotal
        && object_complexity.texturesMemoryTotal > (F64Bytes)max_texture_memory)
    {
        // Note: Memory might not be accurate since texture is still loading or discard level changes

        LL_DEBUGS("HUDdetail") << "HUD " << object_complexity.objectName << " memory usage over limit, "
            << " was " << cmp_complexity.texturesMemoryTotal
            << " is " << object_complexity.texturesMemoryTotal << LL_ENDL;

        return WARN_MEMORY;
    }
    else if ((cmp_complexity.objectsCost < object_complexity.objectsCost
             || cmp_complexity.texturesCost < object_complexity.texturesCost)
            && max_render_cost > 0
            && object_complexity.objectsCost + object_complexity.texturesCost > max_render_cost)
    {
        LL_DEBUGS("HUDdetail") << "HUD " << object_complexity.objectName << " complexity over limit,"
            << " HUD textures cost: " << object_complexity.texturesCost
            << " HUD objects cost: " << object_complexity.objectsCost << LL_ENDL;

        return WARN_COST;
    }
    else if (cmp_complexity.largeTexturesCount < object_complexity.largeTexturesCount
        && object_complexity.largeTexturesCount > max_oversized_count)
    {
        LL_DEBUGS("HUDdetail") << "HUD " << object_complexity.objectName << " contains to many large textures: "
            << object_complexity.largeTexturesCount << LL_ENDL;

        return WARN_HEAVY;
    }
    else if (cmp_complexity.texturesCount < object_complexity.texturesCount
        && object_complexity.texturesCount > max_textures_count)
    {
        LL_DEBUGS("HUDdetail") << "HUD " << object_complexity.objectName << " contains too many textures: "
            << object_complexity.texturesCount << LL_ENDL;

        return WARN_CRAMPED;
    }
    else if (cmp_complexity.objectsCount < object_complexity.objectsCount
        && object_complexity.objectsCount > max_objects_count)
    {
        LL_DEBUGS("HUDdetail") << "HUD " << object_complexity.objectName << " contains too many objects: "
            << object_complexity.objectsCount << LL_ENDL;

        return WARN_TEXTURES;
    }
    return WARN_NONE;
}

void LLHUDRenderNotifier::displayHUDNotification(EWarnLevel warn_type, LLUUID obj_id, std::string obj_name, std::string joint_name)
{
    static LLCachedControl<U32> pop_up_delay(gSavedSettings, "ComplexityChangesPopUpDelay", 300);
    static LLCachedControl<U32> expire_delay(gSavedSettings, "ShowMyComplexityChanges", 20);
    LLDate expire_date(LLDate::now().secondsSinceEpoch() + expire_delay);

    // Since we need working "ignoretext" there is no other way but to
    // use single notification while constructing it from multiple pieces
    LLSD reason_args;
    if (obj_id.isNull())
    {
        reason_args["HUD_DETAILS"] = LLTrans::getString("hud_description_total");
    }
    else
    {
        if (obj_name.empty())
        {
            LL_WARNS("HUDdetail") << "Object name not assigned" << LL_ENDL;
        }
        if (joint_name.empty())
        {
            std::string verb = "select?name=" + LLURI::escape(obj_name);
            reason_args["HUD_DETAILS"] = LLSLURL("inventory", obj_id, verb.c_str()).getSLURLString();
        }
        else
        {
            LLSD object_args;
            std::string verb = "select?name=" + LLURI::escape(obj_name);
            object_args["OBJ_NAME"] = LLSLURL("inventory", obj_id, verb.c_str()).getSLURLString();
            object_args["JNT_NAME"] = LLTrans::getString(joint_name);
            reason_args["HUD_DETAILS"] = LLTrans::getString("hud_name_with_joint", object_args);
        }
    }

    LLSD msg_args;
    msg_args["HUD_REASON"] = LLTrans::getString(e_hud_messages[warn_type], reason_args);

    mHUDNotificationPtr = LLNotifications::instance().add(LLNotification::Params()
        .name("HUDComplexityWarning")
        .expiry(expire_date)
        .substitutions(msg_args));
    mHUDPopUpDelayTimer.resetWithExpiry(pop_up_delay);
}

