
local type = type;

SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP = 1;
SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM = 2;

ScrollingMessageFrameMixin = CreateFromMixins(FontableFrameMixin);

-- where ... is any extra user data
function ScrollingMessageFrameMixin:AddMessage(message, r, g, b, ...)
	if self.historyBuffer:PushFront(self:PackageEntry(message, r, g, b, ...)) then
		if self:GetScrollOffset() ~= 0 then
			self:ScrollUp();
		end
		self:MarkDisplayDirty();
	end
end

-- where ... is any extra user data
function ScrollingMessageFrameMixin:BackFillMessage(message, r, g, b, ...)
	if self.historyBuffer:PushBack(self:PackageEntry(message, r, g, b, ...)) then
		self:MarkDisplayDirty();
	end
end

function ScrollingMessageFrameMixin:GetNumMessages()
	return self.historyBuffer:GetNumElements();
end

function ScrollingMessageFrameMixin:GetMessageInfo(messageIndex)
	local messageInfo = self.historyBuffer:GetEntryAtIndex(self.historyBuffer:GetNumElements() - messageIndex + 1);
	if messageInfo then
		return self:UnpackageEntry(messageInfo);
	end
end

function ScrollingMessageFrameMixin:ForEachMessage(op)
	for i, entry in self.historyBuffer:EnumerateIndexedEntries() do
		if (op(entry)) then
			break;
		end
	end
end

function ScrollingMessageFrameMixin:RemoveMessagesByPredicate(predicate)
	local function Transform(entry)
		return self:UnpackageEntry(entry);
	end
	if self.historyBuffer:RemoveIf(predicate, Transform) then
		self:MarkDisplayDirty();
	end
end

-- Accepts a function that should return true if it wants to change the entries' color along with the new r, g, b values, false for no changes
--[[ Example function:
	local function ChangeRedToBlue(message, r, g, b, ...)
		if r == 1 and g == 0 and b == 0 then
			return true, 0, 0, 1;
		end
		return false; -- No change
	end

	scrollingMessageFrame:AdjustMessageColors(ChangeRedToBlue);
]]--
function ScrollingMessageFrameMixin:AdjustMessageColors(transformFunction)
	for i, entry in self.historyBuffer:EnumerateIndexedEntries() do
		local changeColor, newR, newG, newB = transformFunction(self:UnpackageEntry(entry));
		if changeColor then
			if (type(newR) == "number") or (type(newG) == "number") or (type(newB) == "number") then
				entry.r = newR;
				entry.g = newG;
				entry.b = newB;
				self:MarkDisplayDirty();
			end
		end
	end
end

-- Accepts a predicate that should return true if the entry should be transformed by the transformFunction
--[[
Example predicate:
	local function ShouldChangeToDeleted(message, r, g, b, ...)
		if #message > 30 then
			return true;
		end
	end

Example transformFunction:
	local function ChangeToDeleted(message, r, g, b, ...)
		return "This message has been deleted.", r, g, b, ...;
	end

	scrollingMessageFrame:TransformMessages(ShouldChangeToDeleted, ChangeToDeleted);
]]--
function ScrollingMessageFrameMixin:TransformMessages(predicate, transformFunction)
	local function Unpackage(entry)
		return self:UnpackageEntry(entry);
	end

	local function TransformEntry(...)
		return self:PackageEntry(transformFunction(...));
	end

	if self.historyBuffer:TransformIf(predicate, TransformEntry, Unpackage) then
		self:MarkDisplayDirty();
	end
end

function ScrollingMessageFrameMixin:ForEachVisibleLineText(op)
	-- Reverse order so the lines can be logically evaluated top-down.
	for index, visibleLine in ipairs_reverse(self.visibleLines) do
		op(visibleLine:GetText());
	end
end

function ScrollingMessageFrameMixin:ForEachMessageInfoText(op)
	for index = 1, self:GetNumMessages() do
		local messageText = self:GetMessageInfo(index);
		op(messageText);
	end
end

function ScrollingMessageFrameMixin:SetScrollAllowed(allowed)
	self.allowScroll = allowed;
end

function ScrollingMessageFrameMixin:IsScrollAllowed()
	return not not self.allowScroll;
end

function ScrollingMessageFrameMixin:ScrollByAmount(amount)
	if self:IsScrollAllowed() then
		self:SetScrollOffset(self:GetScrollOffset() + amount);
		self:ResetAllFadeTimes();
	end
end

function ScrollingMessageFrameMixin:ScrollUp()
	self:ScrollByAmount(1);
end

function ScrollingMessageFrameMixin:ScrollDown()
	self:ScrollByAmount(-1);
end

function ScrollingMessageFrameMixin:PageUp()
	self:ScrollByAmount(self:GetPagingScrollAmount());
end

function ScrollingMessageFrameMixin:PageDown()
	self:ScrollByAmount(-self:GetPagingScrollAmount());
end

function ScrollingMessageFrameMixin:ScrollToTop()
	self:SetScrollOffset(self:GetMaxScrollRange());
	self:ResetAllFadeTimes();
end

function ScrollingMessageFrameMixin:ScrollToBottom()
	self:SetScrollOffset(0);
	self:ResetAllFadeTimes();
end

function ScrollingMessageFrameMixin:AddOnDisplayRefreshedCallback(callback)
	table.insert(self.onDisplayRefreshedCallbacks, callback);
end

local function CallCallbacks(index, callback, messageFrame)
	callback(messageFrame);
end

function ScrollingMessageFrameMixin:CallOnDisplayRefreshed()
	secureexecuterange(self.onDisplayRefreshedCallbacks, CallCallbacks, self);
	self.isDisplayDirty = false;
end

function ScrollingMessageFrameMixin:SetOnScrollChangedCallback(onScrollChangedCallback)
	self.onScrollChangedCallback = onScrollChangedCallback;
end

function ScrollingMessageFrameMixin:GetOnScrollChangedCallback()
	return self.onScrollChangedCallback;
end

function ScrollingMessageFrameMixin:SetOnTextCopiedCallback(onTextCopiedCallback)
	self.onTextCopiedCallback = onTextCopiedCallback;
end

function ScrollingMessageFrameMixin:GetOnTextCopiedCallback()
	return self.onTextCopiedCallback;
end


function ScrollingMessageFrameMixin:SetOnLineRightClickedCallback(onLineRightClickedCallback)
	self.onLineRightClickedCallback = onLineRightClickedCallback;
end

function ScrollingMessageFrameMixin:GetOnLineRightClickedCallback()
	return self.onLineRightClickedCallback;
end

function ScrollingMessageFrameMixin:SetScrollOffset(offset)
	local newOffset = Clamp(offset, 0, self:GetMaxScrollRange());
	if newOffset ~= self.scrollOffset then
		self.scrollOffset = newOffset;
		self:MarkDisplayDirty();
		if self.onScrollChangedCallback then
			self.onScrollChangedCallback(self, self.scrollOffset);
		end
	end
end

function ScrollingMessageFrameMixin:GetScrollOffset()
	return self.scrollOffset;
end

function ScrollingMessageFrameMixin:GetMaxScrollRange()
	-- Warning! constrainRangeToText is only intended to be used by scrolling message frames with
	-- exclusively single unwrapped lines. This has the effect of removing the "empty space" by constraining
	-- the scroll range to prevent the text from being scrolled into a position beyond the last line.
	if self.constrainRangeToText then
		return math.max((self.historyBuffer:GetNumElements() - (self:GetNumVisibleLines() - 1)), 0);
	else
		return math.max(self.historyBuffer:GetNumElements() - 1, 0);
	end
end

function ScrollingMessageFrameMixin:GetNumVisibleLines()
	return #self.visibleLines;
end

function ScrollingMessageFrameMixin:AtTop()
	return self.scrollOffset == self:GetMaxScrollRange();
end

function ScrollingMessageFrameMixin:AtBottom()
	return self.scrollOffset == 0;
end

function ScrollingMessageFrameMixin:SetMaxLines(maxLines)
	if self:GetMaxLines() ~= maxLines then
		self.historyBuffer:SetMaxNumElements(maxLines);
		self:MarkDisplayDirty();
	end
end

function ScrollingMessageFrameMixin:GetMaxLines()
	return self.historyBuffer:GetMaxNumElements();
end

function ScrollingMessageFrameMixin:GetPagingScrollAmount()
	return math.max(self:GetNumVisibleLines() - 1, 1);
end

function ScrollingMessageFrameMixin:SetFading(shouldFadeAfterInactivity)
	shouldFadeAfterInactivity = not not shouldFadeAfterInactivity;
	if self.shouldFadeAfterInactivity ~= shouldFadeAfterInactivity then
		self.shouldFadeAfterInactivity = shouldFadeAfterInactivity;
		self:MarkDisplayDirty();
	end
end

function ScrollingMessageFrameMixin:GetFading()
	return self.shouldFadeAfterInactivity;
end

function ScrollingMessageFrameMixin:SetTimeVisible(timeVisibleSecs)
	if self.timeVisibleSecs ~= timeVisibleSecs then
		self.timeVisibleSecs = timeVisibleSecs;
		self:MarkDisplayDirty();
	end
end

function ScrollingMessageFrameMixin:GetTimeVisible()
	return self.timeVisibleSecs;
end

function ScrollingMessageFrameMixin:SetFadeDuration(fadeDurationSecs)
	if self.fadeDurationSecs ~= fadeDurationSecs then
		self.fadeDurationSecs = fadeDurationSecs;
		self:MarkDisplayDirty();
	end
end

function ScrollingMessageFrameMixin:GetFadeDuration()
	return self.fadeDurationSecs;
end

function ScrollingMessageFrameMixin:Clear()
	if not self.historyBuffer:IsEmpty() then
		self.historyBuffer:Clear();
		self:SetScrollOffset(0);
		self:MarkDisplayDirty();
	end
end

function ScrollingMessageFrameMixin:SetInsertMode(insertMode)
	if self:GetInsertMode() ~= insertMode then
		self.insertMode = insertMode;
		self:MarkLayoutDirty();
	end
end

function ScrollingMessageFrameMixin:GetInsertMode()
	return self.insertMode;
end

function ScrollingMessageFrameMixin:SetTextCopyable(textIsCopyable)
	if self:IsTextCopyable() ~= textIsCopyable then
		self.textIsCopyable = textIsCopyable;
		if not self:IsTextCopyable() then
			self:ResetSelectingText();
		end
	end
end

function ScrollingMessageFrameMixin:IsTextCopyable()
	return self.textIsCopyable;
end

function ScrollingMessageFrameMixin:IsSelectingText()
	return self.selectingCharacterIndex ~= nil;
end

function ScrollingMessageFrameMixin:ResetAllFadeTimes()
	local now = GetTime();
	self.overrideFadeTimestamp = now;
	self.oldestFadingLineTimestamp = now;
	self:MarkDisplayDirty();
end

-- "private" functions
function ScrollingMessageFrameMixin:OnPreLoad()
	self:InitializeFontableFrame("ScrollingMessageFrame");

	self.insertMode = SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM;
	self.historyBuffer = CreateCircularBuffer(120);
	self.shouldFadeAfterInactivity = true;
	self.timeVisibleSecs = 10.0;
	self.fadeDurationSecs = 3.0;
	self.scrollOffset = 0;
	self.overrideFadeTimestamp = 0;
	self.textIsCopyable = false;
	self.oldestFadingLineTimestamp = math.huge;

	self.visibleLines = {};
	self.onDisplayRefreshedCallbacks = {};

	self:RegisterEvent("COLOR_OVERRIDE_UPDATED");
	self:RegisterEvent("COLOR_OVERRIDES_RESET");
end

function ScrollingMessageFrameMixin:OnPostShow()
	self:RefreshIfNecessary();
end

function ScrollingMessageFrameMixin:OnPostHide()
	self:ResetSelectingText();
end

function ScrollingMessageFrameMixin:OnPostEvent(event, ...)
	if event == "COLOR_OVERRIDE_UPDATED" then
		local overrideType = ...;
		self:OnColorsUpdated();
	elseif event == "COLOR_OVERRIDES_RESET" then
		self:OnColorsUpdated();
	end
end

function ScrollingMessageFrameMixin:OnPostUpdate(elapsed)
	if self:IsSelectingText() then
		self:UpdateSelectingText();
	else
		self:RefreshIfNecessary();
		self:UpdateFading();
	end
end

function ScrollingMessageFrameMixin:OnPreSizeChanged()
	self:MarkLayoutDirty();
end

function ScrollingMessageFrameMixin:OnPostMouseDown(buttonName, inside)
	if (buttonName == "LeftButton") and self:IsTextCopyable() then
		self:ResetAllFadeTimes();
		self:RefreshIfNecessary();
		self:UpdateFading();

		local x, y = self:GetScaledCursorPosition();
		self.selectingCharacterIndex, self.selectingVisibleLineIndex = self:FindCharacterAndLineIndexAtCoordinate(x, y);
	end
end

function ScrollingMessageFrameMixin:OnPostMouseUp(buttonName, inside)
	if buttonName == "LeftButton" then
		if self:IsSelectingText() then
			local x, y = self:GetScaledCursorPosition();
			local selectedText = self:GatherSelectedText(x, y);

			local numCopied = nil;
			if selectedText then
				local REMOVE_MARKUP = true;
				numCopied = CopyToClipboard(selectedText, REMOVE_MARKUP);
			end

			self:ResetSelectingText();

			if numCopied and selectedText and self.onTextCopiedCallback then
				self.onTextCopiedCallback(self, selectedText, numCopied);
			end
		end
	elseif buttonName == "RightButton" then
		if self.onLineRightClickedCallback then
			local x, y = self:GetScaledCursorPosition();
			local _, visibleLineIndex = self:FindCharacterAndLineIndexAtCoordinate(x, y);
			local visibleLine = self.visibleLines[visibleLineIndex];
			self.onLineRightClickedCallback(self, visibleLineIndex, visibleLine:GetText());
		end
	end
end

function ScrollingMessageFrameMixin:OnColorsUpdated()
	for lineIndex, visibleLine in ipairs(self.visibleLines) do
		local messageIndex = lineIndex + self.scrollOffset;
		local messageInfo = self.historyBuffer:GetEntryAtIndex(messageIndex);
		if messageInfo then
			visibleLine:OnColorsUpdated();
		end
	end
end

function ScrollingMessageFrameMixin:ResetSelectingText()
	if self:IsSelectingText() then
		self:ResetAllFadeTimes();

		self.selectingCharacterIndex, self.selectingVisibleLineIndex = nil, nil;

		if self.highlightTexturePool then
			self.highlightTexturePool:ReleaseAll();
		end
	end
end

function ScrollingMessageFrameMixin:CalculateSelectingCharacterIndicesForVisibleLine(lineIndex, startLineIndex, endLineIndex, startCharacterIndex, endCharacterIndex)
	local visibleLine = self.visibleLines[lineIndex];
	if lineIndex == startLineIndex and lineIndex == endLineIndex then
		return math.min(startCharacterIndex, endCharacterIndex), math.max(startCharacterIndex, endCharacterIndex);
	elseif lineIndex == startLineIndex then
		if self:GetInsertMode() == SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP then
			if endLineIndex > startLineIndex then
				return startCharacterIndex, #visibleLine:GetText() + 1;
			else
				return 1, startCharacterIndex;
			end
		else
			if endLineIndex > startLineIndex then
				return 1, startCharacterIndex;
			else
				return startCharacterIndex, #visibleLine:GetText() + 1;
			end
		end
	elseif lineIndex == endLineIndex then
		if self:GetInsertMode() == SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP then
			if endLineIndex > startLineIndex then
				return 1, endCharacterIndex;
			else
				return endCharacterIndex, #visibleLine:GetText() + 1;
			end
		else
			if endLineIndex > startLineIndex then
				return endCharacterIndex, #visibleLine:GetText() + 1;
			else
				return 1, endCharacterIndex;
			end
		end
	end

	return 1, #visibleLine:GetText() + 1;
end

function ScrollingMessageFrameMixin:UpdateSelectingText()
	if self.highlightTexturePool then
		self.highlightTexturePool:ReleaseAll();
	end

	local x, y = self:GetScaledCursorPosition();
	local characterIndex, visibleLineIndex = self:FindCharacterAndLineIndexAtCoordinate(x, y);
	if characterIndex and (self.selectingCharacterIndex ~= characterIndex or self.selectingVisibleLineIndex ~= visibleLineIndex) then
		local startLineIndex, endLineIndex = self.selectingVisibleLineIndex, visibleLineIndex;
		local startCharacterIndex, endCharacterIndex = self.selectingCharacterIndex, characterIndex;

		for lineIndex = math.min(startLineIndex, endLineIndex), math.max(startLineIndex, endLineIndex) do
			local visibleLine = self.visibleLines[lineIndex];
			if visibleLine:GetText() then
				local selectingStartIndex, selectingEndIndex = self:CalculateSelectingCharacterIndicesForVisibleLine(lineIndex, startLineIndex, endLineIndex, startCharacterIndex, endCharacterIndex);
				local screenAreaTable = visibleLine:CalculateScreenAreaFromCharacterSpan(selectingStartIndex, selectingEndIndex);

				if screenAreaTable then
					for i, screenArea in ipairs(screenAreaTable) do
						local highlightTexture = self:AcquireHighlightTexture();

						highlightTexture:SetPoint("BOTTOMLEFT", visibleLine, "BOTTOMLEFT", screenArea.left, screenArea.bottom);
						highlightTexture:SetPoint("TOPRIGHT", visibleLine, "BOTTOMLEFT", screenArea.left + screenArea.width, screenArea.bottom + screenArea.height);

						highlightTexture:Show();
					end
				end
			end
		end
	end
end

function ScrollingMessageFrameMixin:GatherSelectedText(x, y)
	local characterIndex, visibleLineIndex = self:FindCharacterAndLineIndexAtCoordinate(x, y);
	if characterIndex and (self.selectingCharacterIndex ~= characterIndex or self.selectingVisibleLineIndex ~= visibleLineIndex) then
		local pendingText = {};
		local startLineIndex, endLineIndex = self.selectingVisibleLineIndex, visibleLineIndex;
		local startCharacterIndex, endCharacterIndex = self.selectingCharacterIndex, characterIndex - 1;

		local effectiveStartLineIndex, effectiveEndLineIndex, direction;
		if self:GetInsertMode() == SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP then
			effectiveStartLineIndex = math.min(startLineIndex, endLineIndex);
			effectiveEndLineIndex = math.max(startLineIndex, endLineIndex);
			direction = 1;
		else
			effectiveStartLineIndex = math.max(startLineIndex, endLineIndex);
			effectiveEndLineIndex = math.min(startLineIndex, endLineIndex);
			direction = -1;
		end

		for lineIndex = effectiveStartLineIndex, effectiveEndLineIndex, direction do
			local visibleLine = self.visibleLines[lineIndex];
			local text = visibleLine:GetText();
			if text then
				local selectingStartIndex, selectingEndIndex = self:CalculateSelectingCharacterIndicesForVisibleLine(lineIndex, startLineIndex, endLineIndex, startCharacterIndex, endCharacterIndex);
				local subText = text:sub(selectingStartIndex, selectingEndIndex);
				table.insert(pendingText, subText);
			end
		end

		return table.concat(pendingText, "\n");
	end
	return nil;
end

local function CalculateDistanceSqToLine(x, y, visibleLine)
	-- perimeter would be more accurate, but this seems to work well enough
	local cx, cy = visibleLine:GetCenter();
	return CalculateDistanceSq(x, y, cx, cy);
end

function ScrollingMessageFrameMixin:FindCharacterAndLineIndexAtCoordinate(x, y)
	local closestLineIndex, closestCharacterIndex, closestDistance;
	for lineIndex, visibleLine in ipairs(self.visibleLines) do
		local characterIndex, isInside = visibleLine:FindCharacterIndexAtCoordinate(x, y);
		if characterIndex then
			if isInside then
				return characterIndex, lineIndex;
			end

			local distanceToLine = CalculateDistanceSqToLine(x, y, visibleLine);
			if not closestDistance or distanceToLine < closestDistance then
				closestLineIndex = lineIndex;
				closestCharacterIndex = characterIndex;
				closestDistance = distanceToLine;
			end
		end
	end
	return closestCharacterIndex, closestLineIndex;
end

function ScrollingMessageFrameMixin:GetScaledCursorPosition()
	local scale = self:GetEffectiveScale();
	local x, y = GetCursorPosition();
	return x / scale, y / scale;
end

function ScrollingMessageFrameMixin:RefreshLayout()
	self.isLayoutDirty = false;

	if self.fontStringPool then
		self.fontStringPool:ReleaseAll();
	end

	local numVisibleLines = self:CalculateNumVisibleLines();

	local frameWidth = self:GetWidth();
	for lineIndex = 1, numVisibleLines do
		local fontString = self:AcquireFontString();
		self.visibleLines[lineIndex] = fontString;
		if lineIndex == 1 then
			if self:GetInsertMode() == SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP then
				fontString:SetPoint("TOPLEFT", self, "TOPLEFT");
			else
				fontString:SetPoint("BOTTOMLEFT", self, "BOTTOMLEFT");
			end
		else
			if self:GetInsertMode() == SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP then
				fontString:SetPoint("TOPLEFT", self.visibleLines[lineIndex - 1], "BOTTOMLEFT", 0, -fontString:GetSpacing());
			else
				fontString:SetPoint("BOTTOMLEFT", self.visibleLines[lineIndex - 1], "TOPLEFT", 0, fontString:GetSpacing());
			end
		end
		fontString:SetWidth(frameWidth);
	end

	for lineIndex = self:GetNumVisibleLines(), numVisibleLines, -1 do
		self.visibleLines[lineIndex] = nil;
	end

	self:MarkDisplayDirty();
end

function ScrollingMessageFrameMixin:RefreshIfNecessary()
	if self.isLayoutDirty then
		self:RefreshLayout();
	end
	if self.isDisplayDirty then
		self:RefreshDisplay();
	end
end

function ScrollingMessageFrameMixin:RefreshDisplay()
	if self:GetNumVisibleLines() == 0 then
		self:CallOnDisplayRefreshed();
		return;
	end

	local canFade = self:CanEffectivelyFade();
	local now = GetTime();

	local oldestFadingLineTimestamp = math.huge;

	local fontR, fontG, fontB = self:GetTextColor();
	for lineIndex, visibleLine in ipairs(self.visibleLines) do
		local messageIndex = lineIndex + self.scrollOffset;
		local messageInfo = self.historyBuffer:GetEntryAtIndex(messageIndex);
		if messageInfo then
			visibleLine.messageInfo = messageInfo;
			-- Re-initialize the fontstring to clear secret aspects, as fontstrings don't
			-- get released into the pool on display-only updates.
			self:InitializeFontString(visibleLine);
			visibleLine:SetText(messageInfo.message);
			visibleLine:SetTextColor(messageInfo.r or fontR, messageInfo.g or fontG, messageInfo.b or fontB);
			if canFade then
				local lineTimestamp = math.max(messageInfo.timestamp, self.overrideFadeTimestamp);
				local alpha = self:CalculateLineAlphaValueFromTimestamp(now, lineTimestamp);
				visibleLine:SetAlpha(alpha);
				visibleLine:SetShown(alpha > 0);

				if alpha > 0 then
					oldestFadingLineTimestamp = math.min(oldestFadingLineTimestamp, lineTimestamp);
				end
			else
				visibleLine:SetAlpha(1);
				visibleLine:Show();
			end
		else
			visibleLine.messageInfo = nil;
			visibleLine:Hide();
		end
	end

	self.oldestFadingLineTimestamp = oldestFadingLineTimestamp;

	self:CallOnDisplayRefreshed();
end

function ScrollingMessageFrameMixin:CalculateLineAlphaValueFromTimestamp(now, timestamp)
	local delta = now - timestamp;
	if delta <= self:GetTimeVisible() then
		return 1.0;
	end
	delta = delta - self:GetTimeVisible();

	if delta >= self:GetFadeDuration() then
		return 0.0;
	end

	return 1.0 - delta / self:GetFadeDuration();
end

function ScrollingMessageFrameMixin:CalculateNumVisibleLines()
	if not self:HasFontObject() then
		return 0;
	end

	local lineSpacing = self:CalculateLineSpacing();
	local height = self:GetHeight();
	if height and lineSpacing > 0 then
		return math.ceil(height / lineSpacing) + 1;
	end
	return 0;
end

function ScrollingMessageFrameMixin:MarkLayoutDirty()
	self.isLayoutDirty = true;
end

function ScrollingMessageFrameMixin:MarkDisplayDirty()
	self.isDisplayDirty = true;
end

function ScrollingMessageFrameMixin:AcquireFontString()
	if not self.fontStringPool then
		self.fontStringPool = CreateFontStringPool(self.FontStringContainer, "BACKGROUND", 0, nil, Pool_HideAndSetToDefaults);
	end

	local fontString = self.fontStringPool:Acquire();
	self:InitializeFontString(fontString);
	return fontString;
end

function ScrollingMessageFrameMixin:InitializeFontString(fontString)
	fontString:ClearText();
	fontString:SetFontObject(self:GetFontObject());
	fontString:SetNonSpaceWrap(true);
end

function ScrollingMessageFrameMixin:AcquireHighlightTexture()
	if not self.highlightTexturePool then
		self.highlightTexturePool = CreateTexturePool(self.FontStringContainer, "BACKGROUND", 1);
	end

	local texture = self.highlightTexturePool:Acquire();
	texture:SetColorTexture(.37, .37, .37);
	return texture;
end

function ScrollingMessageFrameMixin:CalculateLineSpacing()
	local fontString = self:AcquireFontString();
	local lineSpacing = fontString:GetLineHeight();
	self.fontStringPool:Release(fontString);
	return lineSpacing;
end

function ScrollingMessageFrameMixin:PackageEntry(message, r, g, b, ...)
	local extraDataCount = select("#", ...);
	local extraData;
	if extraDataCount > 0 then
		extraData = { ... };
		extraData.n = extraDataCount;
	end
	return { message = message, r = r, g = g, b = b, extraData = extraData, timestamp = GetTime(), };
end

function ScrollingMessageFrameMixin:UnpackageEntry(entry)
	if entry.extraData then
		return entry.message, entry.r, entry.g, entry.b, unpack(entry.extraData, 1, entry.extraData.n);
	end
	return entry.message, entry.r, entry.g, entry.b;
end

function ScrollingMessageFrameMixin:CanEffectivelyFade()
	return self.shouldFadeAfterInactivity and self:AtBottom() and not self:IsSelectingText();
end

function ScrollingMessageFrameMixin:UpdateFading()
	if not self:CanEffectivelyFade() then
		return;
	end

	local now = GetTime();

	local oldestFadingLineBeginsFading = self.oldestFadingLineTimestamp + self:GetTimeVisible();

	-- Skip updating every line unless lines are actively fading out because it's an expensive performance cost.
	-- If now is before when the oldest visible line begins fading, then fading is not occuring.
	-- If all lines are already faded out, then oldestFadingLineBeginsFading will be set to math.huge (infinity).
	if now < oldestFadingLineBeginsFading then
		return;
	end

	local oldestFadingLineTimestamp = math.huge;

	for lineIndex, visibleLine in ipairs(self.visibleLines) do
		if visibleLine.messageInfo then
			local lineTimestamp = math.max(visibleLine.messageInfo.timestamp, self.overrideFadeTimestamp);
			local alpha = self:CalculateLineAlphaValueFromTimestamp(now, lineTimestamp);
			visibleLine:SetAlpha(alpha);
			visibleLine:SetShown(alpha > 0);

			if alpha > 0 then
				oldestFadingLineTimestamp = math.min(oldestFadingLineTimestamp, lineTimestamp);
			end
		end
	end

	self.oldestFadingLineTimestamp = oldestFadingLineTimestamp;
end

function ScrollingMessageFrameMixin:OnFontObjectUpdated() -- override from FontableFrameMixin
	self:MarkLayoutDirty();
end

-- Public functions that mutate state on ScrollingMessageFrameMixin should
-- be exposed here to allow addons to customize message frames.
--
-- Note that all of these functions act as secure elevation barriers - ie.
-- calling them from tainted code will invoke the function as-if execution
-- were untainted. For function arguments, these are wrapped in closures that
-- taint when invoked.

ScrollingMessageFrameSecureMixin = {};

function ScrollingMessageFrameSecureMixin:AddMessage(message, r, g, b, ...)
	ScrollingMessageFrameMixin.AddMessage(self, message, r, g, b, ...);
end

function ScrollingMessageFrameSecureMixin:BackFillMessage(message, r, g, b, ...)
	ScrollingMessageFrameMixin.BackFillMessage(self, message, r, g, b, ...);
end

function ScrollingMessageFrameSecureMixin:RemoveMessagesByPredicate(predicate)
	ScrollingMessageFrameMixin.RemoveMessagesByPredicate(self, predicate);
end

function ScrollingMessageFrameSecureMixin:AdjustMessageColors(transformFunction)
	ScrollingMessageFrameMixin.AdjustMessageColors(self, transformFunction);
end

function ScrollingMessageFrameSecureMixin:TransformMessages(predicate, transformFunction)
	ScrollingMessageFrameMixin.TransformMessages(self, predicate, transformFunction);
end

function ScrollingMessageFrameSecureMixin:SetScrollAllowed(allowed)
	ScrollingMessageFrameMixin.SetScrollAllowed(self, allowed);
end

function ScrollingMessageFrameSecureMixin:ScrollByAmount(amount)
	ScrollingMessageFrameMixin.ScrollByAmount(self, amount);
end

function ScrollingMessageFrameSecureMixin:ScrollUp()
	ScrollingMessageFrameMixin.ScrollUp(self);
end

function ScrollingMessageFrameSecureMixin:ScrollDown()
	ScrollingMessageFrameMixin.ScrollDown(self);
end

function ScrollingMessageFrameSecureMixin:PageUp()
	ScrollingMessageFrameMixin.PageUp(self);
end

function ScrollingMessageFrameSecureMixin:PageDown()
	ScrollingMessageFrameMixin.PageDown(self);
end

function ScrollingMessageFrameSecureMixin:ScrollToTop()
	ScrollingMessageFrameMixin.ScrollToTop(self);
end

function ScrollingMessageFrameSecureMixin:ScrollToBottom()
	ScrollingMessageFrameMixin.ScrollToBottom(self);
end

function ScrollingMessageFrameSecureMixin:AddOnDisplayRefreshedCallback(callback)
	ScrollingMessageFrameMixin.AddOnDisplayRefreshedCallback(self, callback);
end

function ScrollingMessageFrameSecureMixin:SetOnScrollChangedCallback(onScrollChangedCallback)
	ScrollingMessageFrameMixin.SetOnScrollChangedCallback(self, onScrollChangedCallback);
end

function ScrollingMessageFrameSecureMixin:SetOnTextCopiedCallback(onTextCopiedCallback)
	ScrollingMessageFrameMixin.SetOnTextCopiedCallback(self, onTextCopiedCallback);
end

function ScrollingMessageFrameSecureMixin:SetOnLineRightClickedCallback(onLineRightClickedCallback)
	ScrollingMessageFrameMixin.SetOnLineRightClickedCallback(self, onLineRightClickedCallback);
end

function ScrollingMessageFrameSecureMixin:SetScrollOffset(offset)
	ScrollingMessageFrameMixin.SetScrollOffset(self, offset);
end

function ScrollingMessageFrameSecureMixin:SetMaxLines(maxLines)
	ScrollingMessageFrameMixin.SetMaxLines(self, maxLines);
end

function ScrollingMessageFrameSecureMixin:SetFading(shouldFadeAfterInactivity)
	ScrollingMessageFrameMixin.SetFading(self, shouldFadeAfterInactivity);
end

function ScrollingMessageFrameSecureMixin:SetTimeVisible(timeVisibleSecs)
	ScrollingMessageFrameMixin.SetTimeVisible(self, timeVisibleSecs);
end

function ScrollingMessageFrameSecureMixin:SetFadeDuration(fadeDurationSecs)
	ScrollingMessageFrameMixin.SetFadeDuration(self, fadeDurationSecs);
end

function ScrollingMessageFrameSecureMixin:Clear()
	ScrollingMessageFrameMixin.Clear(self);
end

function ScrollingMessageFrameSecureMixin:SetInsertMode(insertMode)
	ScrollingMessageFrameMixin.SetInsertMode(self, insertMode);
end

function ScrollingMessageFrameSecureMixin:SetTextCopyable(textIsCopyable)
	ScrollingMessageFrameMixin.SetTextCopyable(self, textIsCopyable);
end

function ScrollingMessageFrameSecureMixin:ResetAllFadeTimes()
	ScrollingMessageFrameMixin.ResetAllFadeTimes(self);
end

-- The following functions are "private" but we're allowing them to always
-- execute securely as they have limited scope (generally, any function
-- that accepts no parameters is safe to elevate).

function ScrollingMessageFrameSecureMixin:MarkLayoutDirty()
	ScrollingMessageFrameMixin.MarkLayoutDirty(self);
end

function ScrollingMessageFrameSecureMixin:MarkDisplayDirty()
	ScrollingMessageFrameMixin.MarkDisplayDirty(self);
end

function ScrollingMessageFrameSecureMixin:ResetSelectingText()
	ScrollingMessageFrameMixin.ResetSelectingText(self);
end

-- The following functions are inherited fron FontableFrameMixin.

function ScrollingMessageFrameSecureMixin:SetFontObject(fontObject)
	FontableFrameMixin.SetFontObject(self, fontObject);
end

function ScrollingMessageFrameSecureMixin:SetFont(font, fontHeight, fontFlags)
	FontableFrameMixin.SetFont(self, font, fontHeight, fontFlags);
end

function ScrollingMessageFrameSecureMixin:SetTextColor(r, g, b, a)
	FontableFrameMixin.SetTextColor(self, r, g, b, a);
end

function ScrollingMessageFrameSecureMixin:SetShadowColor(r, g, b, a)
	FontableFrameMixin.SetShadowColor(self, r, g, b, a);
end

function ScrollingMessageFrameSecureMixin:SetShadowOffset(offsetX, offsetY)
	FontableFrameMixin.SetShadowOffset(self, offsetX, offsetY);
end

function ScrollingMessageFrameSecureMixin:SetSpacing(spacing)
	FontableFrameMixin.SetSpacing(self, spacing);
end

function ScrollingMessageFrameSecureMixin:SetJustifyH(justifyH)
	FontableFrameMixin.SetJustifyH(self, justifyH);
end

function ScrollingMessageFrameSecureMixin:SetJustifyV(justifyV)
	FontableFrameMixin.SetJustifyV(self, justifyV);
end

function ScrollingMessageFrameSecureMixin:SetIndentedWordWrap(indentWordWrap)
	FontableFrameMixin.SetIndentedWordWrap(self, indentWordWrap);
end

function IsScrollingMessageFrame(object)
	-- Defined in the intrinsic's XML
	return object.isScrollingMessageFrame;
end
