Quantcast
-- Author      : Christopher Tse
-- Create Date : 3/28/2020 1:37:28 PM

local ADDON_NAME, Import = ...;

local pool = {}

Import.ChatBubblePool = {};
local ChatBubblePool = Import.ChatBubblePool

local function setChatBubbleWidth(chatBubbleBg, parent, width)
	chatBubbleBg:SetPoint("TOPLEFT",parent,"TOP",-width/2,16);
	chatBubbleBg:SetPoint("BOTTOMLEFT",parent,"BOTTOM",-width/2,-16);
	chatBubbleBg:SetWidth(width);
end

local function adjustChatBubbleWidth(chatBubble)
	local editBox = chatBubble.editBox;
	local strWidth = editBox.stringMeasure:GetStringWidth();
	local bg = editBox.background;
	local padding = bg.padding;
	local nameBox = chatBubble.nameBox
	local nameBoxWidth = nameBox:GetFullWidth();
	local minWidth = 64;
	if ( nameBoxWidth ~= nil) then
		local nameBoxMargin = nameBox.margin.L + nameBox.margin.R - nameBox.padding.L - 5;
		minWidth = max(64, nameBoxWidth + nameBoxMargin);
	end
	local maxWidth = chatBubble:GetWidth()
	if ( strWidth < minWidth ) then
		setChatBubbleWidth(bg, editBox, minWidth + padding);
	elseif ( minWidth < strWidth and strWidth < maxWidth ) then
		setChatBubbleWidth(bg, editBox, strWidth + padding);
	else
		setChatBubbleWidth(bg, editBox, maxWidth + padding);
	end
end

local function adjustNameBoxWidth(chatBubble)
	local nameBox = chatBubble.nameBox;
	local nameBoxBg = nameBox.background;
	local strWidth = nameBox.stringMeasure:GetStringWidth();
	local minWidth = 32;
	local padding = nameBox.padding.L + nameBox.padding.R;
	--The max width usually won't be reached because of the character limit on the name box
	local maxWidth = chatBubble:GetWidth() - padding - nameBox.margin.L
	if ( strWidth < minWidth ) then
		nameBoxBg:SetWidth(minWidth + padding);
	elseif ( minWidth < strWidth and strWidth < maxWidth ) then
		nameBoxBg:SetWidth(strWidth + padding);
	else
		nameBoxBg:SetWidth(maxWidth + padding);
	end
end

local function pickNameColor(chatBubble)
	local r, g, b = chatBubble:GetNameColor()
	ColorPickerFrame.hasOpacity = false;
	ColorPickerFrame.func = function(self) chatBubble:SetNameColor(ColorPickerFrame:GetColorRGB()) end;
	ColorPickerFrame.cancelFunc = function(self) chatBubble:SetNameColor(r,g,b) end;
	ColorPickerFrame:SetColorRGB(r,g,b);
	ColorPickerFrame:Show();
end

local function closeBubble(chatBubble)
	chatBubble:Hide();
	chatBubble:SetMessage("");
	chatBubble:SetName("");

	chatBubble.nameBox:SetAlpha(0.01)
	chatBubble:ClearAllPoints();
	chatBubble:ResetNameColor();
	chatBubble:SetPoint("TOP",WorldFrame,"CENTER",0,-chatBubble.center.y);
	chatBubble.tail:Reset();
	chatBubble.isAvailable = true;
end

local function getClosestEdge(tail,bubble,cursorX,cursorY)
	local centerX, centerY = bubble:GetCenter()
	local bubbleGradient = bubble:GetHeight() / bubble:GetWidth();
	--This calculates the vector from the center of the bubble to the cursor co-oridinates.
	local localCursorX, localCursorY = cursorX - centerX, cursorY - centerY;
	if localCursorX >= 0 and localCursorX * bubbleGradient > math.abs(localCursorY) then
		return "RIGHT", "TOPLEFT", "TOPRIGHT";
	elseif localCursorX < 0 and -localCursorX * bubbleGradient > math.abs(localCursorY) then
		return "LEFT", "TOPRIGHT", "TOPLEFT";
	elseif localCursorY >= 0 and localCursorY > math.abs(localCursorX) * bubbleGradient then
		return "TOP", "BOTTOMLEFT","TOPLEFT";
	else
		return "BOTTOM", "TOPLEFT", "BOTTOMLEFT";
	end
end

local function getAnchoringPointCoords(bubble,anchoringPoint)
	if anchoringPoint == "BOTTOMLEFT" then
		return { x=bubble:GetLeft(), y=bubble:GetBottom() };
	elseif anchoringPoint == "BOTTOMRIGHT" then
		return { x=bubble:GetRight(), y=bubble:GetBottom() };
	elseif anchoringPoint == "TOPLEFT" then
		return { x=bubble:GetLeft(), y=bubble:GetTop() };
	else
		return { x=bubble:GetRight(), y=bubble:GetTop() };
	end
end

local function addVector(a, b)
	return { x=a.x + b.x, y=a.y + b.y };
end

local function subtractVector(a, b)
	return { x=a.x - b.x, y=a.y - b.y};
end

local function closeBubbles()
	for _, bubble in pairs(pool) do
		closeBubble(bubble);
	end
end

local function hideBubbles()
	for _, bubble in pairs(pool) do
		bubble:Hide()
	end
end

local function showBubbles()
	for _, bubble in pairs(pool) do
		if not bubble.isAvailable then
			bubble:Show()
		end
	end
end

local function moveTail(tail)
	--Note: Since the chat bubble is anchored to the WorldFrame, we shouldn't adjust for UIParent's scale

	--Vectors Used
	-- Cursor - The cursor's current X and Y screen coords
	-- origPoint - Vector of tail's original location relative to its old anchoring point.
	-- anchoringPointCoords - Vector of new anchoring point in world coords
	-- oldAnchoringPointCoords - Vector of old anchoring point in world coords
	-- origPointWorldCoords - Vector of tail's original location in world coords.

	local cursorX, cursorY = GetCursorPosition();
	local origPoint = tail.origPoint;
	local bubble = tail:GetParent();
	local tailWidth, tailHeight = tail:GetWidth(), tail:GetHeight();
	local bubbleWidth, bubbleHeight = bubble:GetWidth(), bubble:GetHeight();
	local closestEdge, point, anchoringPoint = getClosestEdge(tail, bubble, cursorX, cursorY);
	local anchoringPointCoords = getAnchoringPointCoords(bubble, anchoringPoint);
	local oldAnchoringPointCoords = getAnchoringPointCoords(bubble, origPoint.relativeP);
	local origPointWorldCoords = addVector(origPoint, oldAnchoringPointCoords);

	--In this part, we basically only move the x or y based on which side it is on.
	--The other axis should remain at "0" (plus some offset to align the texture)
	--This is so the tail remains attached to the bubble.

	--We also bound the new X/Y so it doesn't go beyond the edge of the bubble.
	--The minimum X/Y variable determines how much space to leave at the end of the bubble.
	if closestEdge == "BOTTOM" or closestEdge == "TOP" then
		local cursorOffset = cursorX - tail.origCursorLoc.x;
		local newXinWorldCoords = origPointWorldCoords.x + cursorOffset;
		local newX = newXinWorldCoords - anchoringPointCoords.x
		if newX < tail.minX  then
			newX = tail.minX
		elseif newX > bubbleWidth - tailWidth - tail.minX then
			newX = bubbleWidth - tailWidth - tail.minX
		end
		local yOffset = 0;
		if closestEdge == "BOTTOM" then
			yOffset = tail.bottomOffset;
			tail.tex:SetRotation(0);
		else
			yOffset = tail.topOffset;
			tail.tex:SetRotation(math.pi)
		end
		tail:ClearAllPoints();
		tail:SetPoint(point,bubble,anchoringPoint,newX,yOffset);
	else
		local cursorOffset = cursorY - tail.origCursorLoc.y
		local newYinWorldCoords = origPointWorldCoords.y + cursorOffset
		local newY = -(newYinWorldCoords - anchoringPointCoords.y);
		if newY < tail.minY then
			newY = tail.minY
		elseif newY > bubbleHeight - tailHeight - tail.minY then
			newY = bubbleHeight - tailHeight - tail.minY
		end
		local xOffset = 0;
		if closestEdge == "LEFT" then
			xOffset = tail.leftOffset;
			tail.tex:SetRotation( math.pi * 1.5 );
		else
			xOffset = tail.rightOffset;
			tail.tex:SetRotation( math.pi * 0.5 );
		end
		tail:ClearAllPoints();
		tail:SetPoint(point,bubble,anchoringPoint,xOffset,-newY);
	end
	tail.side = closestEdge
end

local function startMovingTail(self, button)
	if button == "LeftButton" then
		local origCursor = {};
		local origPoint = {};
		origCursor.x, origCursor.y = GetCursorPosition();
		origPoint.p, origPoint.relative, origPoint.relativeP, origPoint.x, origPoint.y = self:GetPoint(1);
		self.origPoint = origPoint;
		self.origCursorLoc = origCursor;
		self:SetScript("OnUpdate",moveTail);
	end
end

local function stopMovingTail(self,button)
	if button == "LeftButton" then
		self:SetScript("OnUpdate",nil);
	end
end

local function checkTailBounds(chatBubble)
	local tail = chatBubble.tail;
	local point, chatBubbleBg, relativePoint, x, y = tail:GetPoint(1);
	if tail.side == "RIGHT" or tail.side == "LEFT" then
		y = -y; --Reverse Y because the point goes from top down.
		local maxY = chatBubbleBg:GetHeight() - tail:GetHeight() - tail.minY
		if ( y > maxY ) then
			tail:SetPoint(point, chatBubbleBg, relativePoint, x, -maxY);
		end
	else
		local maxX = chatBubbleBg:GetWidth() - tail:GetWidth() - tail.minX;
		if ( x > maxX ) then
			tail:SetPoint(point, chatBubbleBg, relativePoint, maxX, y);
		end
	end
end

local function clearFocusAndSelection(editBox)
	editBox:ClearFocus()
	editBox:HighlightText(0,0);
end

function ChatBubblePool.getChatBubble()
	for index, chatBubble in ipairs(pool) do
		if chatBubble.isAvailable then
			chatBubble:Show()
			chatBubble.isAvailable = false;
			return chatBubble
		end
	end

	-- If we got here, there isn't any available chat bubble so create a new one
	local frameName = "RPChatBubble" .. #pool

	local newChatBubble = CreateFrame("Frame",frameName,nil)
	newChatBubble:SetWidth(300)
	newChatBubble:SetHeight(300)
	newChatBubble:SetMovable(true)
	newChatBubble:SetFrameStrata("HIGH");
	newChatBubble:SetFrameLevel(20);
	newChatBubble.isAvailable = false
	table.insert(pool, newChatBubble);

	--tail:EnableMouse(true)
	--tail:SetMovable(true)
	--tail:RegisterForDrag("LeftButton")
	--tail:SetScript("OnDragStart", function(self) self:StartMoving() end)
	--tail:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)

	local editBox = CreateFrame("EditBox",frameName.."-EditBox",newChatBubble);
	editBox:SetPoint("TOPLEFT",newChatBubble);
	editBox:SetPoint("TOPRIGHT",newChatBubble);
	editBox:SetMultiLine(true);
	editBox:SetAutoFocus(false);
	editBox:SetFontObject("ChatBubbleFont");
	editBox:SetJustifyH("CENTER");
	editBox:SetScript("OnEnterPressed", function(self) if IsShiftKeyDown() then self:Insert("\n") else self:ClearFocus() end end);
	editBox:SetScript("OnEscapePressed",clearFocusAndSelection);
	--Apparently, the below code stops the user from being able to change the cursor location
	--editBox:EnableMouse(true)
	--editBox:SetScript("OnMouseDown", function(self) newChatBubble:StartMoving() end )
	--editBox:SetScript("OnMouseUp", function(self) newChatBubble:StopMovingOrSizing() end )

	newChatBubble.editBox = editBox;

	local chatBubbleBackground = CreateFrame("Button",frameName.."Background",editBox, "BackdropTemplate");
	chatBubbleBackground:SetBackdrop({
		bgFile="Interface\\Tooltips\\CHATBUBBLE-BACKGROUND.BLP",
		edgeFile="Interface\\Tooltips\\CHATBUBBLE-BACKDROP.BLP",
		tile=true, tileSize=16, edgeSize=16,
		insets={left=16, right=16, top=16, bottom=16}
	})
	chatBubbleBackground:SetPoint("TOPLEFT",editBox,"CENTER",-16,16);
	chatBubbleBackground:SetPoint("BOTTOMLEFT",editBox,"CENTER",-16,-16);
	chatBubbleBackground.padding = 32;
	chatBubbleBackground:SetWidth(64 + chatBubbleBackground.padding);
	chatBubbleBackground:SetFrameLevel(10);
	chatBubbleBackground:SetScript("OnMouseDown", function(self) newChatBubble:StartMoving() end )
	chatBubbleBackground:SetScript("OnMouseUp", function(self) newChatBubble:StopMovingOrSizing() end )
	chatBubbleBackground:SetScript("OnClick", function(self) editBox:SetFocus() end);
	editBox.background = chatBubbleBackground;

	--This part of the code makes the editbox and the background grow up to 300px as the text grows.
	--We use an invisible FontString to measure the length of the text inside the edit box.
	editBox.stringMeasure = editBox:CreateFontString(nil,"OVERLAY","ChatBubbleFont");
	editBox.stringMeasure:SetAlpha(0);
	editBox:SetScript("OnTextChanged", function(self)
	    editBox.stringMeasure:SetText(self:GetText());
		adjustChatBubbleWidth(newChatBubble);
		checkTailBounds(newChatBubble);
	end)

	--This is a hack that centers the newChatBubble using the center of the editbox
	newChatBubble.center = { x=chatBubbleBackground:GetWidth()/2, y=chatBubbleBackground:GetHeight()/2 };
	newChatBubble:SetPoint("TOP",WorldFrame,"CENTER",0,newChatBubble.center.y);


	local closeButton = CreateFrame("Button",frameName.."-CloseButton",chatBubbleBackground,"UIPanelCloseButton")
	closeButton:SetFrameLevel("21");
	closeButton:SetPoint("CENTER",chatBubbleBackground,"TOPRIGHT",-4,-4);
	closeButton:SetScript("OnClick",function(self) closeBubble(newChatBubble) end);
	closeButton:SetScript("OnEnter",function(self) closeButton:SetAlpha(1) end);
	closeButton:SetScript("OnLeave",function(self) closeButton:SetAlpha(0) end);
	closeButton:SetAlpha(0);

	local nameBoxFrame = CreateFrame("Frame",frameName.."-NameBoxFrame",newChatBubble)
	nameBoxFrame:SetSize(250,18);
	nameBoxFrame:SetPoint("BOTTOMLEFT",chatBubbleBackground,"TOPLEFT");

	local nameBox = CreateFrame("EditBox",frameName.."-NameBox",nameBoxFrame);
	nameBox:SetFontObject("GameFontNormal");
	nameBox:SetMaxLetters(25);
	nameBox.margin = {L=10, R=0, T=4, D=4};
	nameBox.padding = {L=10, R=10};
	nameBox:SetPoint("BOTTOMRIGHT",nameBoxFrame,-nameBox.margin.R,nameBox.margin.D);
	nameBox:SetPoint("TOPLEFT",nameBoxFrame,nameBox.margin.L,-nameBox.margin.T);
	nameBox:SetAutoFocus(false);
	nameBox:SetMultiLine(true); --It's not actually multiline, but this stops the name from scrolling off if the user selects too much of the text.
								--The max letters should prevent the edit box from ever reaching more than one line
	nameBox:SetScript("OnEnterPressed",clearFocusAndSelection);
	nameBox:SetScript("OnTabPressed",function(self) editBox:SetFocus() end);
	nameBox:SetScript("OnEscapePressed",clearFocusAndSelection);
	nameBox:SetAlpha(0);
	nameBox:SetScript("OnEditFocusGained", function(self) self:SetAlpha(1); nameBox:SetPropagateKeyboardInput(false); end);
	nameBox:SetScript("OnEditFocusLost", function(self) if self:GetText() == "" then self:SetAlpha(0.01); end; nameBox:SetPropagateKeyboardInput(true); end);
	--nameBox:SetScript("OnClick", function(self) nameBox:SetFocus() end);
	nameBox:SetScript("OnEnter", function(self) if nameBox:GetText() == "" and not nameBox:HasFocus() then nameBox:SetAlpha(0.5) end end);
	nameBox:SetScript("OnLeave", function(self) if nameBox:GetText() == "" and not nameBox:HasFocus() then nameBox:SetAlpha(0) end end);
	newChatBubble.nameBox = nameBox;

	local nameBoxBackground = CreateFrame("Button",frameName.."-NameBoxBackground",nameBox);
	local paddingL = nameBox.padding.L;
	nameBoxBackground:SetPoint("BOTTOMLEFT",nameBox,"BOTTOMLEFT",-paddingL,-nameBox.margin.D)
	nameBoxBackground:SetPoint("TOPLEFT",nameBox,"TOPLEFT",-paddingL,nameBox.margin.T + 12);
	nameBoxBackground:SetWidth(32);
	nameBoxBackground:SetFrameLevel("12");
	nameBoxBackground:SetScript("OnClick", function(self) nameBox:SetFocus() end);
	nameBoxBackground:SetScript("OnMouseDown", function(self) newChatBubble:StartMoving() end )
	nameBoxBackground:SetScript("OnMouseUp", function(self) newChatBubble:StopMovingOrSizing() end )
	nameBoxBackground:SetScript("OnEnter", function(self) if nameBox:GetText() == "" and not nameBox:HasFocus() then nameBox:SetAlpha(0.5) end end);
	nameBoxBackground:SetScript("OnLeave", function(self) if nameBox:GetText() == "" and not nameBox:HasFocus() then nameBox:SetAlpha(0) end end);
	nameBox.background = nameBoxBackground

	nameBox.stringMeasure = nameBox:CreateFontString(nil,"OVERLAY","GameFontNormal");
	--nameBox.stringMeasure:SetAlpha(0);
	nameBox.GetFullWidth = function(self)  return nameBoxBackground:GetWidth() end;
	nameBox:SetScript("OnTextChanged", function(self)
		nameBox.stringMeasure:SetText(self:GetText());
		adjustNameBoxWidth(newChatBubble)
		adjustChatBubbleWidth(newChatBubble)
	end);

	local midTex = nameBoxBackground:CreateTexture("nameBoxBackgroundTex-middle","BACKGROUND");
	midTex:SetTexture("Interface/CHATFRAME/ChatFrameTab-BGMid.blp");
	midTex:SetPoint("TOPLEFT",16,0);
	midTex:SetPoint("BOTTOMRIGHT",-16,0);
	local leftTex = nameBoxBackground:CreateTexture("nameBoxBackgroundTex-left","BACKGROUND");
	leftTex:SetTexture("Interface/CHATFRAME/ChatFrameTab-BGLeft.blp");
	leftTex:SetPoint("TOPRIGHT",midTex,"TOPLEFT");
	leftTex:SetPoint("BOTTOMRIGHT",midTex,"BOTTOMLEFT");
	local rightTex = nameBoxBackground:CreateTexture("nameBoxBackgroundTex-right","BACKGROUND");
	rightTex:SetTexture("Interface/CHATFRAME/ChatFrameTab-BGRight.blp");
	rightTex:SetPoint("TOPLEFT",midTex,"TOPRIGHT");
	rightTex:SetPoint("BOTTOMLEFT",midTex,"BOTTOMRIGHT");

	local nameBoxColorPicker = CreateFrame("Button",frameName.."-ColorPickerButton",newChatBubble);
	nameBoxColorPicker:SetSize(16,16);
	nameBoxColorPicker:SetFrameLevel("25") -- Needs to be higher than the EditBox to override it
	nameBox.colorPickerTex = nameBoxColorPicker:CreateTexture(frameName.."-ColorPickerButton-color","ARTWORK")
	nameBox.colorPickerTex:SetPoint("TOPLEFT",2,-2);
	nameBox.colorPickerTex:SetPoint("BOTTOMRIGHT",-2,2);
	nameBox.colorPickerTex:SetColorTexture(nameBox:GetTextColor());
	local cpBorderTex = nameBoxColorPicker:CreateTexture(frameName.."-ColorPickerButton-border","BORDER");
	cpBorderTex:SetAllPoints();
	cpBorderTex:SetColorTexture(0.1,0.1,0.1);
	nameBoxColorPicker:SetPoint("BOTTOMLEFT",nameBoxBackground,"BOTTOMRIGHT");
	nameBoxColorPicker:SetAlpha(0.01);
	nameBoxColorPicker:EnableMouse(false);
	nameBoxColorPicker:SetScript("OnEnter", function(self) if nameBox:GetText() ~= "" then self:SetAlpha(1); end; end);
	nameBoxColorPicker:SetScript("OnLeave", function(self) self:SetAlpha(0.01) end);
	nameBoxColorPicker:SetScript("OnClick", function(self) if nameBox:GetText() ~= "" then pickNameColor(newChatBubble) end; end);

	local tail = CreateFrame("Frame",frameName.."-tail",chatBubbleBackground)
	tail:SetSize(16,16)
	tail.tex = tail:CreateTexture(frameName.."-tailTexture","BACKGROUND");
	tail.tex:SetTexture("Interface\\Tooltips\\CHATBUBBLE-TAIL.BLP");
	tail.tex:SetAllPoints();
	tail.bottomOffset = 5;
	tail.topOffset = -4;
	tail.leftOffset = 4;
	tail.rightOffset = -5;
	tail.minX = 8;
	tail.minY = 8;
	tail:SetPoint("TOPLEFT",chatBubbleBackground,"BOTTOMLEFT",tail.minX,tail.bottomOffset)
	tail.side = "BOTTOM";
	tail.Reset = function(self)
		self.tex:SetRotation(0);
		self:ClearAllPoints();
		self:SetPoint("TOPLEFT",chatBubbleBackground,"BOTTOMLEFT",tail.minX,tail.bottomOffset);
		self.tail = "BOTTOM"
	end

	local chatBubbleTailCatcher = CreateFrame("Button",frameName.."-tailButtonCatcher",tail);
	chatBubbleTailCatcher:SetAllPoints();
	chatBubbleTailCatcher:SetScript("OnMouseDown",function(self, button) startMovingTail(tail, button) end);
	chatBubbleTailCatcher:SetScript("OnMouseUp",function(self, button) stopMovingTail(tail, button) end);
	chatBubbleTailCatcher:SetFrameLevel(50);

	newChatBubble.tail = tail;

	--Functions for outside use
	newChatBubble.GetName = nameBox.GetText;
	newChatBubble.SetName = function(self,name)
		if (name == nil) then name = ""; end;
		nameBox:SetText(name);
		if (name ~= "" ) then
			nameBox:SetAlpha(1)
		else
			nameBox:SetAlpha(0);
		end;
	end;
	newChatBubble.GetMessage = editBox.GetText;
	newChatBubble.SetMessage = function(self,message) editBox:SetText(message) end;
	newChatBubble.GetNameColor = function(self) return nameBox:GetTextColor() end;
	newChatBubble.SetNameColor = function(self,r,g,b) nameBox:SetTextColor(r,g,b); nameBox.colorPickerTex:SetColorTexture(r,g,b) end;
	newChatBubble.GetTextColor = function(self) return editBox:GetTextColor() end;
	newChatBubble.SetTextColor = function(self,r,g,b) editBox:SetTextColor(r,g,b) end;

	local origR,origG,origB = nameBox:GetTextColor();
	newChatBubble.ResetNameColor = function(self) self:SetNameColor(origR,origG,origB); end;

	return newChatBubble
end

local function doEvent(self, event, ...)
	if event == "PLAY_MOVIE" then
		closeBubbles()
	elseif event == "CINEMATIC_START" then
		hideBubbles()
	elseif event == "CINEMATIC_STOP" then
		showBubbles()
	end
end

local frame = CreateFrame("FRAME","ChatBubbleEventHandler");
frame:RegisterEvent("PLAY_MOVIE");
frame:RegisterEvent("CINEMATIC_START");
frame:RegisterEvent("CINEMATIC_STOP");
frame:SetScript("OnEvent",doEvent);