
/*
 * Copyright 2011-2022 Cuberite Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// CompositeChat.cpp

// Implements the cCompositeChat class used to wrap a chat message with multiple parts (text, url, cmd)

#include "Globals.h"
#include "CompositeChat.h"
#include "ClientHandle.h"
#include "JsonUtils.h"





////////////////////////////////////////////////////////////////////////////////
// cCompositeChat:

cCompositeChat::cCompositeChat(void) :
	m_MessageType(mtCustom)
{
}





cCompositeChat::cCompositeChat(const AString & a_ParseText, eMessageType a_MessageType) :
	m_MessageType(a_MessageType)
{
	ParseText(a_ParseText);
}





void cCompositeChat::Clear(void)
{
	m_Parts.clear();
}





void cCompositeChat::AddTextPart(const AString & a_Message, const AString & a_Style)
{
	m_Parts.push_back(TextPart{{ a_Message, a_Style, {} } });
}





void cCompositeChat::AddClientTranslatedPart(const AString & a_TranslationID, const AStringVector & a_Parameters, const AString & a_Style)
{
	m_Parts.push_back(ClientTranslatedPart{{ a_TranslationID, a_Style, {} }, a_Parameters });
}





void cCompositeChat::AddUrlPart(const AString & a_Text, const AString & a_Url, const AString & a_Style)
{
	m_Parts.push_back(UrlPart{{ a_Text, a_Style, {} }, a_Url });
}





void cCompositeChat::AddRunCommandPart(const AString & a_Text, const AString & a_Command, const AString & a_Style)
{
	m_Parts.push_back(RunCommandPart{{{ a_Text, a_Style, {} }, a_Command } });
}





void cCompositeChat::AddSuggestCommandPart(const AString & a_Text, const AString & a_SuggestedCommand, const AString & a_Style)
{
	m_Parts.push_back(SuggestCommandPart{{{ a_Text, a_Style, {} }, a_SuggestedCommand } });
}





void cCompositeChat::AddShowAchievementPart(const AString & a_PlayerName, const AString & a_Achievement, const AString & a_Style)
{
	m_Parts.push_back(ShowAchievementPart{{ a_Achievement, a_Style, {} }, a_PlayerName });
}




/**
* Parse the input message to add colors or link then add it to the object.
*
* It detects every & of the message and the next character for it to colorize.
* It detect : in the text to detect link structures.
*
* @param a_ParseText The input text to parse
*/
void cCompositeChat::ParseText(const AString & a_ParseText)
{
	size_t len = a_ParseText.length();
	size_t cursor = 0;
	AString CurrentStyle;
	AString CurrentText;

	for (size_t i = 0; i < len; i++)
	{
		switch (a_ParseText[i])
		{
			case '&':  //< Color code
			{
				if ((i != 0) && (a_ParseText[i-1] == '\\'))
				{
					CurrentText.append(a_ParseText, cursor, i - cursor - 1).append("&");
					AddTextPart(CurrentText, CurrentStyle);
					CurrentText.clear();
					cursor = ++i;
					continue;
				}

				if (cursor < i)
				{
					CurrentText.append(a_ParseText, cursor, i - cursor);
					AddTextPart(CurrentText, CurrentStyle);
					CurrentText.clear();
				}
				i++;
				cursor = i + 1;

				if (a_ParseText[i] == 'r')
				{
					CurrentStyle = "";
				}
				else
				{
					CurrentStyle.push_back(a_ParseText[i]);
				}
				break;
			}

			case ':':
			{
				static const constexpr std::array<std::string_view, 2> LinkPrefixes =
				{
					{
						"http",
						"https"
					}
				};
				for (const auto & Prefix : LinkPrefixes)
				{
					size_t PrefixLen = Prefix.size();
					if (
						(i >= cursor + PrefixLen) &&  // There is enough space in front of the colon for the prefix
						(std::string_view(a_ParseText).substr(i - PrefixLen, PrefixLen) == Prefix)  // the prefix matches
					)
					{
						// Add everything before this as a text part:
						if (i > cursor+ PrefixLen)
						{
							CurrentText.append(a_ParseText.c_str() + cursor, i - cursor - PrefixLen);
							cursor= i - PrefixLen;
						}
						if (!CurrentText.empty())
						{
							AddTextPart(CurrentText, CurrentStyle);
							CurrentText.clear();
						}

						// Go till the last non-whitespace char in the text:
						for (; i < len; i++)
						{
							if (isspace(a_ParseText[i]))
							{
								break;
							}
						}
						AddUrlPart(a_ParseText.substr(cursor, i - cursor), a_ParseText.substr(cursor, i - cursor), CurrentStyle);
						cursor = i;
						break;
					}
				}  // for Prefix - LinkPrefix[]
				break;
			}  // case ':'
		}  // switch (a_ParseText[i])
	}  // for i - a_ParseText[]
	if (cursor < len)
	{
		CurrentText.clear();
		CurrentText.append(a_ParseText, cursor, len - cursor);
		AddTextPart(CurrentText, CurrentStyle);
	}
}





void cCompositeChat::SetMessageType(eMessageType a_MessageType, const AString & a_AdditionalMessageTypeData)
{
	m_MessageType = a_MessageType;
	m_AdditionalMessageTypeData = a_AdditionalMessageTypeData;
}





void cCompositeChat::UnderlineUrls(void)
{
	for (auto & Part : m_Parts)
	{
		std::visit(OverloadedVariantAccess
		{
			[](TextPart &             a_Part) {  },
			[](ClientTranslatedPart & a_Part) {  },
			[](UrlPart &              a_Part) { a_Part.Style += 'n'; },
			[](RunCommandPart &       a_Part) {  },
			[](SuggestCommandPart &   a_Part) {  },
			[](ShowAchievementPart &  a_Part) {  },
		}, Part);
	}
}





AString cCompositeChat::ExtractText(void) const
{
	AString Msg;
	for (const auto & Part : m_Parts)
	{
		std::visit(OverloadedVariantAccess
		{
			[&Msg](const TextPart &             a_Part) { Msg.append(a_Part.Text); },
			[&Msg](const ClientTranslatedPart & a_Part) { Msg.append(a_Part.Text); },
			[&Msg](const UrlPart &              a_Part) { Msg.append(a_Part.Url); },
			[&Msg](const RunCommandPart &       a_Part) { Msg.append(a_Part.Text); },
			[&Msg](const SuggestCommandPart &   a_Part) { Msg.append(a_Part.Text); },
			[    ](const ShowAchievementPart &  a_Part) {  },
		}, Part);
	}
	return Msg;
}





eLogLevel cCompositeChat::MessageTypeToLogLevel(eMessageType a_MessageType)
{
	switch (a_MessageType)
	{
		case mtCustom:         return eLogLevel::Regular;
		case mtFailure:        return eLogLevel::Warning;
		case mtInformation:    return eLogLevel::Info;
		case mtSuccess:        return eLogLevel::Regular;
		case mtWarning:        return eLogLevel::Warning;
		case mtFatal:          return eLogLevel::Error;
		case mtDeath:          return eLogLevel::Regular;
		case mtPrivateMessage: return eLogLevel::Regular;
		case mtJoin:           return eLogLevel::Regular;
		case mtLeave:          return eLogLevel::Regular;
		case mtMaxPlusOne: break;
	}
	ASSERT(!"Unhandled MessageType");
	return eLogLevel::Error;
}





AString cCompositeChat::CreateJsonString(bool a_ShouldUseChatPrefixes) const
{
	Json::Value Message;
	Message["text"] = cClientHandle::FormatMessageType(a_ShouldUseChatPrefixes, GetMessageType(), GetAdditionalMessageTypeData());  // The client crashes without this field being present
	for (const auto & Part : m_Parts)
	{
		Json::Value JsonPart;
		std::visit(OverloadedVariantAccess
		{
			[this, &JsonPart](const TextPart & a_Part)
			{
				JsonPart["text"] = a_Part.Text;
				AddChatPartStyle(JsonPart, a_Part.Style);
			},
			[this, &JsonPart](const ClientTranslatedPart & a_Part)
			{
				JsonPart["translate"] = a_Part.Text;
				Json::Value With;
				for (const auto & Parameter : a_Part.Parameters)
				{
					With.append(Parameter);
				}
				if (!a_Part.Parameters.empty())
				{
					JsonPart["with"] = With;
				}
				AddChatPartStyle(JsonPart, a_Part.Style);
			},
			[this, &JsonPart](const UrlPart & a_Part)
			{
				JsonPart["text"] = a_Part.Text;
				Json::Value Url;
				Url["action"] = "open_url";
				Url["value"] = a_Part.Url;
				JsonPart["clickEvent"] = Url;
				AddChatPartStyle(JsonPart, a_Part.Style);
			},
			[this, &JsonPart](const RunCommandPart & a_Part)
			{
				JsonPart["text"] = a_Part.Text;
				Json::Value Cmd;
				Cmd["action"] = "run_command";
				Cmd["value"] = a_Part.Command;
				JsonPart["clickEvent"] = Cmd;
				AddChatPartStyle(JsonPart, a_Part.Style);
			},
			[this, &JsonPart](const SuggestCommandPart & a_Part)
			{
				JsonPart["text"] = a_Part.Text;
				Json::Value Cmd;
				Cmd["action"] = "suggest_command";
				Cmd["value"] = a_Part.Command;
				JsonPart["clickEvent"] = Cmd;
				AddChatPartStyle(JsonPart, a_Part.Style);
			},
			[this, &JsonPart](const ShowAchievementPart & a_Part)
			{
				JsonPart["translate"] = "chat.type.achievement";

				Json::Value Ach;
				Ach["action"] = "show_achievement";
				Ach["value"] = a_Part.Text;

				Json::Value AchColourAndName;
				AchColourAndName["color"] = "green";
				AchColourAndName["translate"] = a_Part.Text;
				AchColourAndName["hoverEvent"] = Ach;

				Json::Value Extra;
				Extra.append(AchColourAndName);

				Json::Value Name;
				Name["text"] = a_Part.PlayerName;

				Json::Value With;
				With.append(Name);
				With.append(Extra);

				JsonPart["with"] = With;
				AddChatPartStyle(JsonPart, a_Part.Style);
			},
		}, Part);
		Message["extra"].append(JsonPart);
	}  // for itr - Parts[]

	#if 1
		// Serialize as machine-readable string (no whitespace):
		return JsonUtils::WriteFastString(Message);
	#else
		// Serialize as human-readable string (pretty-printed):
		return JsonUtils::WriteStyledString(msg);
	#endif
}





void cCompositeChat::AddChatPartStyle(Json::Value & a_Value, const AString & a_PartStyle) const
{
	size_t len = a_PartStyle.length();
	for (size_t i = 0; i < len; i++)
	{
		switch (a_PartStyle[i])
		{
			case 'k': a_Value["obfuscated"] = Json::Value(true); break;
			case 'l': a_Value["bold"] = Json::Value(true); break;
			case 's':  // Deprecated
				LOGERROR("Value s in AddChatPartStyle() is deprecated");
			case 'm': a_Value["strikethrough"] = Json::Value(true); break;
			case 'u':  // Deprecated
				LOGERROR("Value u in AddChatPartStyle() is deprecated");
			case 'n': a_Value["underlined"] = Json::Value(true); break;
			case 'i':  // Deprecated
				LOGERROR("Value i in AddChatPartStyle() is deprecated");
			case 'o': a_Value["italic"] = Json::Value(true); break;
			case '0': a_Value["color"] = Json::Value("black"); break;
			case '1': a_Value["color"] = Json::Value("dark_blue"); break;
			case '2': a_Value["color"] = Json::Value("dark_green"); break;
			case '3': a_Value["color"] = Json::Value("dark_aqua"); break;
			case '4': a_Value["color"] = Json::Value("dark_red"); break;
			case '5': a_Value["color"] = Json::Value("dark_purple"); break;
			case '6': a_Value["color"] = Json::Value("gold"); break;
			case '7': a_Value["color"] = Json::Value("gray"); break;
			case '8': a_Value["color"] = Json::Value("dark_gray"); break;
			case '9': a_Value["color"] = Json::Value("blue"); break;
			case 'a': a_Value["color"] = Json::Value("green"); break;
			case 'b': a_Value["color"] = Json::Value("aqua"); break;
			case 'c': a_Value["color"] = Json::Value("red"); break;
			case 'd': a_Value["color"] = Json::Value("light_purple"); break;
			case 'e': a_Value["color"] = Json::Value("yellow"); break;
			case 'f': a_Value["color"] = Json::Value("white"); break;

		}  // switch (Style[i])
	}  // for i - a_PartStyle[]
}
