-------------------------------------------------------------------------------
-- Title: Mik's Scrolling Battle Text Default Animation Styles
-- Author: Mikord
-------------------------------------------------------------------------------

-------------------------------------------------------------------------------
-- Imports.
-------------------------------------------------------------------------------

-- Local references to various functions for faster access.
local math_floor = math.floor
local math_ceil = math.ceil
local math_random = math.random
local math_max = math.max
local math_abs = math.abs
local math_sqrt = math.sqrt


-------------------------------------------------------------------------------
-- Private constants.
-------------------------------------------------------------------------------

-- Sticky animation style constants.
local POW_FADE_IN_TIME = 0.17
local POW_DISPLAY_TIME = 1.5
local POW_FADE_OUT_TIME = 0.5
local POW_TEXT_DELTA = 0.7
local JIGGLE_DELAY_TIME = 0.05

-- Static animation style constants.
local STATIC_DISPLAY_TIME = 3.15

-- Angled animation style constants.
local ANGLED_HORIZONTAL_PHASE_TIME = 1
local ANGLED_FADE_OUT_TIME = 0.5
local ANGLED_WIDTH_PERCENT = 0.85


-- Default movement speed. (260 pixels every 3 seconds)
local MOVEMENT_SPEED = (3 / 260)

-- Minimum amount of space allowed between two strings.
local MIN_VERTICAL_SPACING = 8
local MIN_HORIZONTAL_SPACING = 10


-------------------------------------------------------------------------------
-- Private variables.
-------------------------------------------------------------------------------

-- Prevent tainting global _.
local _

-- Values used for previous events.
local lastAngledFinishPositionY = {}
local lastAngledDirection = {}
local lastHorizontalPositionY = {}
local lastHorizontalDirection = {}



-------------------------------------------------------------------------------
-- Pow Sticky functions.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Animates the passed display event using using the normal pow style.
-- ****************************************************************************
local function AnimatePowNormal(displayEvent, animationProgress)
	local fadeInPercent = POW_FADE_IN_TIME / displayEvent.scrollTime

	-- Scale the text height.
	if animationProgress <= fadeInPercent then
		displayEvent.fontString:SetTextHeight(displayEvent.fontSize * (1 + ((1 - animationProgress / fadeInPercent) * POW_TEXT_DELTA)))

	-- Reset the font properties to normal.
	else
		local fontPath, _, fontOutline = displayEvent.fontString:GetFont()
		displayEvent.fontString:SetFont(fontPath, displayEvent.fontSize, fontOutline)
	end
end


-- ****************************************************************************
-- Animates the passed display event using using the jiggle pow style.
-- ****************************************************************************
local function AnimatePowJiggle(displayEvent, animationProgress)
	local fadeInPercent = POW_FADE_IN_TIME / displayEvent.scrollTime

	-- Scale the text height.
	if animationProgress <= fadeInPercent then
		displayEvent.fontString:SetTextHeight(displayEvent.fontSize * (1 + ((1 - animationProgress / fadeInPercent) * POW_TEXT_DELTA)))
		return

	-- Jiggle the text around and reset the font properties to normal.
	elseif animationProgress <= displayEvent.fadePercent then
		local elapsedTime = displayEvent.elapsedTime
		if elapsedTime - displayEvent.timeLastJiggled > JIGGLE_DELAY_TIME then
			displayEvent.positionX = displayEvent.originalPositionX + math_random(-1, 1)
			displayEvent.positionY = displayEvent.originalPositionY + math_random(-1, 1)
			displayEvent.timeLastJiggled = elapsedTime
		end

		local fontPath, _, fontOutline = displayEvent.fontString:GetFont()
		displayEvent.fontString:SetFont(fontPath, displayEvent.fontSize, fontOutline)
	end
end


-- ****************************************************************************
-- Initialize the passed display event and reposition the ones that are
-- currently animating in the scroll area to prevent overlaps.
-- ****************************************************************************
local function InitPow(newDisplayEvent, activeDisplayEvents, direction, behavior)
	-- Calculate how long the animation should take by only scaling the display period and
	-- set the percent to start the fade out.
	local animationSpeed = newDisplayEvent.animationSpeed
	local scrollTime = POW_FADE_IN_TIME + (POW_DISPLAY_TIME / animationSpeed) + POW_FADE_OUT_TIME
	newDisplayEvent.scrollTime = scrollTime * animationSpeed
	newDisplayEvent.fadePercent = (POW_FADE_IN_TIME + (POW_DISPLAY_TIME / animationSpeed)) / scrollTime


	-- Choose the correct animation function.
	newDisplayEvent.animationHandler = (behavior == "Jiggle") and AnimatePowJiggle or AnimatePowNormal

	-- Set the new event's starting position.
	local anchorPoint = newDisplayEvent.anchorPoint
	if anchorPoint == "BOTTOMLEFT" then
		newDisplayEvent.positionX = 0
	elseif anchorPoint == "BOTTOM" then
		newDisplayEvent.positionX = newDisplayEvent.scrollWidth / 2
	elseif anchorPoint == "BOTTOMRIGHT" then
		newDisplayEvent.positionX = newDisplayEvent.scrollWidth
	end
	newDisplayEvent.positionY = newDisplayEvent.scrollHeight / 2

	-- Save the original x and y positions for calculating the jiggle effect.
	newDisplayEvent.originalPositionX = newDisplayEvent.positionX
	newDisplayEvent.originalPositionY = newDisplayEvent.positionY
	newDisplayEvent.timeLastJiggled = 0

	-- Get the number of sticky display events that are currently animating.
	local numActiveAnimations = #activeDisplayEvents

	-- Exit if there is no need to check for collisions.
	if (numActiveAnimations == 0) then return end


	-- Check if the text is scrolling down.
	if direction == "Down" then
		-- Get the middle sticky.
		local middleSticky = math_floor((numActiveAnimations + 2) / 2)

		-- Set the middle sticky to the center of the scroll area.
		activeDisplayEvents[middleSticky].originalPositionY = newDisplayEvent.scrollHeight / 2
		activeDisplayEvents[middleSticky].positionY = activeDisplayEvents[middleSticky].originalPositionY

		-- Loop backwards from the middle sticky and move the animating display events so they don't collide.
		for x = middleSticky - 1, 1, -1 do
			activeDisplayEvents[x].originalPositionY = activeDisplayEvents[x + 1].originalPositionY - activeDisplayEvents[x].fontSize - MIN_VERTICAL_SPACING
			activeDisplayEvents[x].positionY = activeDisplayEvents[x].originalPositionY
		end

		-- Loop forwards from the middle sticky and move the animating display events so they don't collide.
		for x = middleSticky + 1, numActiveAnimations do
			activeDisplayEvents[x].originalPositionY = activeDisplayEvents[x - 1].originalPositionY + activeDisplayEvents[x - 1].fontSize + MIN_VERTICAL_SPACING
			activeDisplayEvents[x].positionY = activeDisplayEvents[x].originalPositionY
		end

		-- Move the new display event so it doesn't collide.
		newDisplayEvent.originalPositionY = activeDisplayEvents[numActiveAnimations].originalPositionY + activeDisplayEvents[numActiveAnimations].fontSize + MIN_VERTICAL_SPACING
		newDisplayEvent.positionY = newDisplayEvent.originalPositionY

	-- Text is scrolling up.
	else
		-- Get the middle sticky.
		local middleSticky = math_ceil(numActiveAnimations / 2)

		-- Set the middle sticky to the center of the scroll area.
		activeDisplayEvents[middleSticky].originalPositionY = newDisplayEvent.scrollHeight / 2
		activeDisplayEvents[middleSticky].positionY = activeDisplayEvents[middleSticky].originalPositionY

		-- Loop backwards from the middle sticky and move the animating display events so they don't collide.
		for x = middleSticky - 1, 1, -1 do
			activeDisplayEvents[x].originalPositionY = activeDisplayEvents[x + 1].originalPositionY + activeDisplayEvents[x + 1].fontSize + MIN_VERTICAL_SPACING
			activeDisplayEvents[x].positionY = activeDisplayEvents[x].originalPositionY
		end

		-- Loop forwards from the middle sticky and move the animating display events so they don't collide.
		for x = middleSticky + 1, numActiveAnimations do
			activeDisplayEvents[x].originalPositionY = activeDisplayEvents[x - 1].originalPositionY - activeDisplayEvents[x].fontSize - MIN_VERTICAL_SPACING
			activeDisplayEvents[x].positionY = activeDisplayEvents[x].originalPositionY
		end

		-- Move the new display event so it doesn't collide.
		newDisplayEvent.originalPositionY = activeDisplayEvents[numActiveAnimations].originalPositionY - activeDisplayEvents[numActiveAnimations].fontSize - MIN_VERTICAL_SPACING
		newDisplayEvent.positionY = newDisplayEvent.originalPositionY
	end
end


-------------------------------------------------------------------------------
-- Angled scroll functions.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Scrolls the passed display event angled to the left and upwards.
-- ****************************************************************************
local function ScrollLeftAngledUp(displayEvent, animationProgress)
	local linePhasePercent = displayEvent.linePhasePercent
	local horizontalPhasePercent = displayEvent.horizontalPhasePercent

	-- Move the event in an angled line.
	if animationProgress <= linePhasePercent then
		-- Calculate how far along the current phase is and set the x and y positions accordingly.
		local phaseProgress = animationProgress / linePhasePercent
		displayEvent.positionX = displayEvent.scrollWidth - (displayEvent.startPositionX + (displayEvent.finishPositionX - displayEvent.startPositionX) * phaseProgress)
		displayEvent.positionY = displayEvent.finishPositionY * phaseProgress

	-- Wait a bit at the finish position.
	elseif animationProgress <= horizontalPhasePercent then
		displayEvent.positionX = displayEvent.scrollWidth - displayEvent.finishPositionX
		displayEvent.positionY = displayEvent.finishPositionY

	-- Move the event horizontally to outer edge.
	else
		-- Calculate how far along the current phase is and set the x and y positions accordingly.
		local phaseProgress = (animationProgress - horizontalPhasePercent) / (1 - horizontalPhasePercent)
		displayEvent.positionX = displayEvent.scrollWidth - (displayEvent.finishPositionX + ((displayEvent.scrollWidth - displayEvent.finishPositionX) * phaseProgress))
	end
end


-- ****************************************************************************
-- Scrolls the passed display event angled to the left and downwards.
-- ****************************************************************************
local function ScrollLeftAngledDown(displayEvent, animationProgress)
	local linePhasePercent = displayEvent.linePhasePercent
	local horizontalPhasePercent = displayEvent.horizontalPhasePercent

	-- Move the event in an angled line.
	if animationProgress <= linePhasePercent then
		-- Calculate how far along the current phase is and set the x and y positions accordingly.
		local phaseProgress = animationProgress / linePhasePercent
		displayEvent.positionX = displayEvent.scrollWidth - (displayEvent.startPositionX + (displayEvent.finishPositionX - displayEvent.startPositionX) * phaseProgress)
		displayEvent.positionY = displayEvent.scrollHeight - displayEvent.finishPositionY * phaseProgress

	-- Wait a bit at the finish position.
	elseif animationProgress <= horizontalPhasePercent then
		displayEvent.positionX = displayEvent.scrollWidth - displayEvent.finishPositionX
		displayEvent.positionY = displayEvent.scrollHeight - displayEvent.finishPositionY

	-- Move the event horizontally to outer edge.
	else
		-- Calculate how far along the current phase is and set the x and y positions accordingly.
		local phaseProgress = (animationProgress - horizontalPhasePercent) / (1 - horizontalPhasePercent)
		displayEvent.positionX = displayEvent.scrollWidth - (displayEvent.finishPositionX + ((displayEvent.scrollWidth - displayEvent.finishPositionX) * phaseProgress))
	end
end


-- ****************************************************************************
-- Scrolls the passed display event angled to the right and upwards.
-- ****************************************************************************
local function ScrollRightAngledUp(displayEvent, animationProgress)
	local linePhasePercent = displayEvent.linePhasePercent
	local horizontalPhasePercent = displayEvent.horizontalPhasePercent

	-- Move the event in an angled line.
	if animationProgress <= linePhasePercent then
		-- Calculate how far along the current phase is and set the x and y positions accordingly.
		local phaseProgress = animationProgress / linePhasePercent
		displayEvent.positionX = displayEvent.startPositionX + (displayEvent.finishPositionX - displayEvent.startPositionX) * phaseProgress
		displayEvent.positionY = displayEvent.finishPositionY * phaseProgress

	-- Wait a bit at the finish position.
	elseif animationProgress <= horizontalPhasePercent then
		displayEvent.positionX = displayEvent.finishPositionX
		displayEvent.positionY = displayEvent.finishPositionY

	-- Move the event horizontally to outer edge.
	else
		-- Calculate how far along the current phase is and set the x and y positions accordingly.
		local phaseProgress = (animationProgress - horizontalPhasePercent) / (1 - horizontalPhasePercent)
		displayEvent.positionX = displayEvent.finishPositionX + ((displayEvent.scrollWidth - displayEvent.finishPositionX) * phaseProgress)
	end
end


-- ****************************************************************************
-- Scrolls the passed display event angled to the right and downwards.
-- ****************************************************************************
local function ScrollRightAngledDown(displayEvent, animationProgress)
	local linePhasePercent = displayEvent.linePhasePercent
	local horizontalPhasePercent = displayEvent.horizontalPhasePercent

	-- Move the event in an angled line.
	if animationProgress <= linePhasePercent then
		-- Calculate how far along the current phase is and set the x and y positions accordingly.
		local phaseProgress = animationProgress / linePhasePercent
		displayEvent.positionX = displayEvent.startPositionX + (displayEvent.finishPositionX - displayEvent.startPositionX) * phaseProgress
		displayEvent.positionY = displayEvent.scrollHeight - displayEvent.finishPositionY * phaseProgress

	-- Wait a bit at the finish position.
	elseif animationProgress <= horizontalPhasePercent then
		displayEvent.positionX = displayEvent.finishPositionX
		displayEvent.positionY = displayEvent.scrollHeight - displayEvent.finishPositionY

	-- Move the event horizontally to outer edge.
	else
		-- Calculate how far along the current phase is and set the x and y positions accordingly.
		local phaseProgress = (animationProgress - horizontalPhasePercent) / (1 - horizontalPhasePercent)
		displayEvent.positionX = displayEvent.finishPositionX + ((displayEvent.scrollWidth - displayEvent.finishPositionX) * phaseProgress)
	end
end


-- ****************************************************************************
-- Initialize the passed display event and reposition the ones that are
-- currently animating in the scroll area to prevent overlaps.
-- ****************************************************************************
local function InitAngled(newDisplayEvent, activeDisplayEvents, direction, behavior)
	-- Modify the direction and anchor if the direction is alternating.
	local startPositionX = 0
	local anchorPoint = newDisplayEvent.anchorPoint
	if direction ~= "Left" and direction ~= "Right" then
		-- Select direction and anchor point based on the last event.
		direction = (lastAngledDirection[activeDisplayEvents] == "Left") and "Right" or "Left"
		lastAngledDirection[activeDisplayEvents] = direction
		anchorPoint = (direction == "Left") and "BOTTOMRIGHT" or "BOTTOMLEFT"
		newDisplayEvent.anchorPoint = anchorPoint

		-- Start at the scroll area's mid point.
		startPositionX = newDisplayEvent.scrollWidth / 2
	end

	-- Choose correct animation function.
	if direction == "Right" then
		newDisplayEvent.animationHandler = (behavior == "AngleDown") and ScrollRightAngledDown or ScrollRightAngledUp
	else
		newDisplayEvent.animationHandler = (behavior == "AngleDown") and ScrollLeftAngledDown or ScrollLeftAngledUp
	end

	-- Calculate the y finish position based on the last event.
	local finishPositionY
	finishPositionY = lastAngledFinishPositionY[activeDisplayEvents] or newDisplayEvent.scrollHeight
	finishPositionY = finishPositionY - newDisplayEvent.fontSize - MIN_VERTICAL_SPACING
	if finishPositionY < 0 then
		finishPositionY = newDisplayEvent.scrollHeight
	end

	-- Calculate how long the animation should take based on the distance the text has to travel.
	local animationSpeed = newDisplayEvent.animationSpeed
	local finishPositionX = newDisplayEvent.scrollWidth * ANGLED_WIDTH_PERCENT
	local linePhaseTime = math_sqrt((finishPositionX - startPositionX) * (finishPositionX - startPositionX) + finishPositionY * finishPositionY) * MOVEMENT_SPEED
	local scrollTime = ((linePhaseTime + ANGLED_HORIZONTAL_PHASE_TIME) / animationSpeed) + ANGLED_FADE_OUT_TIME
	newDisplayEvent.scrollTime = scrollTime * animationSpeed
	newDisplayEvent.linePhasePercent = linePhaseTime / animationSpeed / scrollTime
	newDisplayEvent.horizontalPhasePercent = ((linePhaseTime + ANGLED_HORIZONTAL_PHASE_TIME) / animationSpeed) / scrollTime
	newDisplayEvent.fadePercent = 1 - (ANGLED_FADE_OUT_TIME / scrollTime)

	-- Initialize the new event's x and y positions.
	newDisplayEvent.positionX = startPositionX
	newDisplayEvent.positionY = 0
	newDisplayEvent.startPositionX = startPositionX
	newDisplayEvent.finishPositionX = newDisplayEvent.scrollWidth * ANGLED_WIDTH_PERCENT
	newDisplayEvent.finishPositionY = finishPositionY

	lastAngledFinishPositionY[activeDisplayEvents] = finishPositionY
end


-------------------------------------------------------------------------------
-- Straight scroll functions.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Scrolls the passed display event upwards.
-- ****************************************************************************
local function ScrollUp(displayEvent, animationProgress)
	-- Set the y position based on the percent completed.
	displayEvent.positionY = displayEvent.scrollHeight * animationProgress
end


-- ****************************************************************************
-- Scrolls the passed display event downwards.
-- ****************************************************************************
local function ScrollDown(displayEvent, animationProgress)
	-- Set the y position based on the percent completed.
	displayEvent.positionY = displayEvent.scrollHeight - displayEvent.scrollHeight * animationProgress
end


-- ****************************************************************************
-- Initialize the passed display event and reposition the ones that are
-- currently scrolling in the scroll area to prevent overlaps.
-- ****************************************************************************
local function InitStraight(newDisplayEvent, activeDisplayEvents, direction, behavior)
	-- Calculate how long the animation should take based on the height of the scroll area.
	newDisplayEvent.scrollTime = newDisplayEvent.scrollHeight * MOVEMENT_SPEED

	-- Set the new event's starting X position.
	local anchorPoint = newDisplayEvent.anchorPoint
	if anchorPoint == "BOTTOMLEFT" then
		newDisplayEvent.positionX = 0
	elseif anchorPoint == "BOTTOM" then
		newDisplayEvent.positionX = newDisplayEvent.scrollWidth / 2
	elseif anchorPoint == "BOTTOMRIGHT" then
		newDisplayEvent.positionX = newDisplayEvent.scrollWidth
	end

	-- Get the number of display events that are currently scrolling for this style.
	local numActiveAnimations = #activeDisplayEvents

	-- Scroll text down.
	if direction == "Down" then
		-- Choose the correct animation function.
		newDisplayEvent.animationHandler = ScrollDown

		-- Exit if there is no need to check for collisions.
		if numActiveAnimations == 0 then
			return
		end

		-- Scale the per pixel time based on the animation speed.
		local perPixelTime = MOVEMENT_SPEED / newDisplayEvent.animationSpeed
		local currentDisplayEvent = newDisplayEvent
		local prevDisplayEvent, topTimeCurrent

		-- Move events that are colliding.
		for x = numActiveAnimations, 1, -1 do
			prevDisplayEvent = activeDisplayEvents[x]

			-- Calculate the elapsed time for the top point of the current display event.
			topTimeCurrent = currentDisplayEvent.elapsedTime + (currentDisplayEvent.fontSize + MIN_VERTICAL_SPACING) * perPixelTime

			-- Adjust the elapsed time of the previous display event if the current one is colliding with it.
			if prevDisplayEvent.elapsedTime < topTimeCurrent then
				prevDisplayEvent.elapsedTime = topTimeCurrent
			else
				-- Don't continue checking if there is no need.
				break
			end

			currentDisplayEvent = prevDisplayEvent
		end

	-- Scroll text up.
	else
		-- Choose the correct animation function.
		newDisplayEvent.animationHandler = ScrollUp

		-- Exit if there is no need to check for collisions.
		if numActiveAnimations == 0 then
			return
		end

		-- Scale the per pixel time based on the animation speed.
		local perPixelTime = MOVEMENT_SPEED / newDisplayEvent.animationSpeed
		local currentDisplayEvent = newDisplayEvent
		local prevDisplayEvent, topTimePrev

		-- Move events that are colliding.
		for x = numActiveAnimations, 1, -1 do
			prevDisplayEvent = activeDisplayEvents[x]

			-- Calculate the elapsed time for the top point of the previous display event.
			topTimePrev = prevDisplayEvent.elapsedTime - (prevDisplayEvent.fontSize + MIN_VERTICAL_SPACING) * perPixelTime

			-- Adjust the elapsed time of the previous display event if the current one is colliding with it.
			if topTimePrev < currentDisplayEvent.elapsedTime then
				prevDisplayEvent.elapsedTime = currentDisplayEvent.elapsedTime + (prevDisplayEvent.fontSize + MIN_VERTICAL_SPACING) * perPixelTime
			else
				-- Exit if there is no need to continue checking for collisions.
				return
			end

			currentDisplayEvent = prevDisplayEvent
		end
	end -- Direction.
end


-------------------------------------------------------------------------------
-- Parabola scroll functions.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Scrolls the passed display event in a left parabola upwards.
-- ****************************************************************************
local function ScrollLeftParabolaUp(displayEvent, animationProgress)
	-- Leverage the scroll up logic.
	ScrollUp(displayEvent, animationProgress)

	-- Calculate the new x position based on equation of a parabola.
	-- Equation of a parabola at vertex 0,0: x = y^2 / 4a
	local y = displayEvent.positionY - displayEvent.midPoint
	displayEvent.positionX = (y * y) / displayEvent.fourA
end


-- ****************************************************************************
-- Scrolls the passed display event in a left parabola downwards.
-- ****************************************************************************
local function ScrollLeftParabolaDown(displayEvent, animationProgress)
	-- Leverage the scroll down logic.
	ScrollDown(displayEvent, animationProgress)

	-- Calculate the new x position based on equation of a parabola.
	-- Equation of a parabola at vertex 0,0: x = y^2 / 4a
	local y = displayEvent.positionY - displayEvent.midPoint
	displayEvent.positionX = (y * y) / displayEvent.fourA
end


-- ****************************************************************************
-- Scrolls the passed display event in a right parabola upwards.
-- ****************************************************************************
local function ScrollRightParabolaUp(displayEvent, animationProgress)
	-- Leverage the scroll up logic.
	ScrollUp(displayEvent, animationProgress)

	-- Calculate the new x position based on equation of a parabola.
	-- Equation of a parabola at vertex 0,0: x = y^2 / 4a
	local y = displayEvent.positionY - displayEvent.midPoint
	displayEvent.positionX = displayEvent.scrollWidth - ((y * y) / displayEvent.fourA)
end


-- ****************************************************************************
-- Scrolls the passed display event in a right parabola downwards.
-- ****************************************************************************
local function ScrollRightParabolaDown(displayEvent, animationProgress)
	-- Leverage the scroll down logic.
	ScrollDown(displayEvent, animationProgress)

	-- Calculate the new x position based on equation of a parabola.
	-- Equation of a parabola at vertex 0,0: x = y^2 / 4a
	local y = displayEvent.positionY - displayEvent.midPoint
	displayEvent.positionX = displayEvent.scrollWidth - ((y * y) / displayEvent.fourA)
end


-- ****************************************************************************
-- Initialize the passed display event and reposition the ones that are
-- currently scrolling in the scroll area to prevent overlaps.
-- ****************************************************************************
local function InitParabola(newDisplayEvent, activeDisplayEvents, direction, behavior)
	-- Leverage the straight logic.
	InitStraight(newDisplayEvent, activeDisplayEvents, direction, behavior)

	-- Choose correction animation function.
	if direction == "Down" then
		newDisplayEvent.animationHandler = (behavior == "CurvedRight") and ScrollRightParabolaDown or ScrollLeftParabolaDown
	else
		newDisplayEvent.animationHandler = (behavior == "CurvedRight") and ScrollRightParabolaUp or ScrollLeftParabolaUp
	end

	-- Calculate the scroll area midpoint.
	local midPoint = newDisplayEvent.scrollHeight / 2
	newDisplayEvent.midPoint = midPoint

	-- Calculate the parabola focal point.
	-- Equation of a parabola at vertex 0,0: x = y^2 / 4a
	newDisplayEvent.fourA = (midPoint * midPoint) / newDisplayEvent.scrollWidth
end


-------------------------------------------------------------------------------
-- Horizontal scroll functions.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Scrolls the passed display event horizontally left.
-- ****************************************************************************
local function ScrollLeft(displayEvent, animationProgress)
	-- Set the x position based on the percent completed..
	displayEvent.positionX = displayEvent.scrollWidth - displayEvent.scrollWidth * animationProgress
end


-- ****************************************************************************
-- Scrolls the passed display event horizontally right.
-- ****************************************************************************
local function ScrollRight(displayEvent, animationProgress)
	-- Set the x position based on the percent completed..
	displayEvent.positionX = displayEvent.scrollWidth * animationProgress
end


-- ****************************************************************************
-- Reposition the passed display events to prevent overlaps.
-- ****************************************************************************
local function RepositionHorizontalRight(currentDisplayEvent, activeDisplayEvents, startEvent)
	-- Scale the per pixel time based on the animation speed.
	local perPixelTime = MOVEMENT_SPEED / currentDisplayEvent.animationSpeed

	-- Get the top and bottom points of the current display events.
	local topCurrent = currentDisplayEvent.positionY + currentDisplayEvent.fontSize
	local bottomCurrent = currentDisplayEvent.positionY

	local prevDisplayEvent, topPrev, bottomPrev
	local leftTimePrev, rightTimeCurrent
	for x = startEvent, 1, -1 do
		-- Get the top and bottom points of the previous display event.
		prevDisplayEvent = activeDisplayEvents[x]
		topPrev = prevDisplayEvent.positionY + prevDisplayEvent.fontSize
		bottomPrev = prevDisplayEvent.positionY

		-- Check for a vertical collision.
		if (topCurrent >= bottomPrev and topCurrent <= topPrev) or (bottomCurrent >= bottomPrev and bottomCurrent <= topPrev) then
			-- Calculate the elapsed time for the left and right points.
			leftTimePrev = prevDisplayEvent.elapsedTime + (prevDisplayEvent.offsetLeft or 0) * perPixelTime
			rightTimeCurrent = currentDisplayEvent.elapsedTime + ((currentDisplayEvent.offsetRight or 0) + MIN_HORIZONTAL_SPACING) * perPixelTime

			-- Adjust the elapsed time of the previous display event if the current one is colliding with it.
			if leftTimePrev <= rightTimeCurrent then
				prevDisplayEvent.elapsedTime = rightTimeCurrent + math_abs((prevDisplayEvent.offsetLeft or 0) * perPixelTime)

				-- Move events that are now colliding as a result of moving this one.
				RepositionHorizontalRight(prevDisplayEvent, activeDisplayEvents, x - 1)
			end -- Horizontal collision.
		end -- Vertical collision.
	end
end


-- ****************************************************************************
-- Reposition the passed display events to prevent overlaps.
-- ****************************************************************************
local function RepositionHorizontalLeft(currentDisplayEvent, activeDisplayEvents, startEvent)
	-- Scale the per pixel time based on the animation speed.
	local perPixelTime = MOVEMENT_SPEED / currentDisplayEvent.animationSpeed

	-- Get the top and bottom points of the current display events.
	local topCurrent = currentDisplayEvent.positionY + currentDisplayEvent.fontSize
	local bottomCurrent = currentDisplayEvent.positionY

	local prevDisplayEvent, topPrev, bottomPrev
	local rightTimePrev, leftTimeCurrent
	for x = startEvent, 1, -1 do
		-- Get the top and bottom points of the previous display event.
		prevDisplayEvent = activeDisplayEvents[x]
		topPrev = prevDisplayEvent.positionY + prevDisplayEvent.fontSize
		bottomPrev = prevDisplayEvent.positionY

		-- Check for a vertical collision.
		if (topCurrent >= bottomPrev and topCurrent <= topPrev) or (bottomCurrent >= bottomPrev and bottomCurrent <= topPrev) then
			-- Calculate the elapsed time for the left and right points.
			rightTimePrev = prevDisplayEvent.elapsedTime - ((prevDisplayEvent.offsetRight or 0) + MIN_HORIZONTAL_SPACING) * perPixelTime
			leftTimeCurrent = currentDisplayEvent.elapsedTime - (currentDisplayEvent.offsetLeft or 0) * perPixelTime

			-- Adjust the elapsed time of the previous display event if the current one is colliding with it.
			if rightTimePrev <= leftTimeCurrent then
				prevDisplayEvent.elapsedTime = leftTimeCurrent + ((prevDisplayEvent.offsetRight or 0) + MIN_HORIZONTAL_SPACING) * perPixelTime

				-- Move events that are now colliding as a result of moving this one.
				RepositionHorizontalLeft(prevDisplayEvent, activeDisplayEvents, x - 1)
			end -- Horizontal collision.
		end -- Vertical collision.
	end
end


-- ****************************************************************************
-- Initialize the passed display event and reposition the ones that are
-- currently scrolling in the scroll area to prevent overlaps.
-- ****************************************************************************
local function InitHorizontal(newDisplayEvent, activeDisplayEvents, direction, behavior)
	-- Calculate how long the animation should take based on the width of the scroll area.
	newDisplayEvent.scrollTime = newDisplayEvent.scrollWidth * MOVEMENT_SPEED

	-- Modify the direction and anchor if the direction is alternating.
	local anchorPoint = newDisplayEvent.anchorPoint
	if direction ~= "Left" and direction ~= "Right" then
		-- Select direction and anchor point based on the last event.
		direction = (lastHorizontalDirection[activeDisplayEvents] == "Left") and "Right" or "Left"
		lastHorizontalDirection[activeDisplayEvents] = direction
		anchorPoint = (direction == "Left") and "BOTTOMRIGHT" or "BOTTOMLEFT"
		newDisplayEvent.anchorPoint = anchorPoint

		-- Start at the scroll area's mid point.
		newDisplayEvent.elapsedTime = newDisplayEvent.scrollTime / 2
	end

	-- Calculate the left and right offsets from the anchor point.
	local fontStringWidth = newDisplayEvent.fontString:GetStringWidth()
	if anchorPoint == "BOTTOMLEFT" then
		newDisplayEvent.offsetLeft = 0
		newDisplayEvent.offsetRight = fontStringWidth
	elseif anchorPoint == "BOTTOM" then
		local halfWidth = fontStringWidth / 2
		newDisplayEvent.offsetLeft = -halfWidth
		newDisplayEvent.offsetRight = halfWidth
	elseif anchorPoint == "BOTTOMRIGHT" then
		newDisplayEvent.offsetLeft = -fontStringWidth
		newDisplayEvent.offsetRight = 0
	end


	-- Check if the text is growing down.
	local positionY
	if behavior == "GrowDown" then
		-- Calculate the y position based on the last event.
		positionY = lastHorizontalPositionY[activeDisplayEvents] or newDisplayEvent.scrollHeight
		positionY = positionY - newDisplayEvent.fontSize - MIN_VERTICAL_SPACING
		if positionY < 0 then
			positionY = newDisplayEvent.scrollHeight
		end

	-- Text is growing up.
	else
		-- Calculate the y position based on the last event.
		positionY = lastHorizontalPositionY[activeDisplayEvents] or 0
		positionY = positionY + newDisplayEvent.fontSize + MIN_VERTICAL_SPACING
		if positionY > newDisplayEvent.scrollHeight then
			positionY = 0
		end
	end

	-- Set the y position to the calculated value and save it for the next event.
	newDisplayEvent.positionY = positionY
	lastHorizontalPositionY[activeDisplayEvents] = positionY

	-- Get the number of display events that are currently scrolling for this style.
	local numActiveAnimations = #activeDisplayEvents

	-- Scroll text right.
	if direction == "Right" then
		-- Choose the correct animation function.
		newDisplayEvent.animationHandler = ScrollRight

		-- Exit if there is no need to check for collisions.
		if numActiveAnimations == 0 then
			return
		end

		-- Move events that are colliding.
		RepositionHorizontalRight(newDisplayEvent, activeDisplayEvents, numActiveAnimations)

	-- Scroll text left.
	else
		-- Choose the correct animation function.
		newDisplayEvent.animationHandler = ScrollLeft

		-- Exit if there is no need to check for collisions.
		if numActiveAnimations == 0 then
			return
		end

		-- Move events that are colliding.
		RepositionHorizontalLeft(newDisplayEvent, activeDisplayEvents, numActiveAnimations)
	end
end


-------------------------------------------------------------------------------
-- Static scroll functions.
-------------------------------------------------------------------------------

-- ****************************************************************************
-- Animates the passed display event using the static style.
-- ****************************************************************************
local function ScrollStatic(displayEvent, animationProgress)
	-- Nothing needs to be done.
end


-- ****************************************************************************
-- Initialize the passed display event and reposition the ones that are
-- currently scrolling in the scroll area to prevent overlaps.
-- ****************************************************************************
local function InitStatic(newDisplayEvent, activeDisplayEvents, direction, behavior)
	-- Set how long the animation should take.
	newDisplayEvent.scrollTime = STATIC_DISPLAY_TIME

	-- Set the animation function.
	newDisplayEvent.animationHandler = ScrollStatic

	-- Set the new event's starting X position.
	local anchorPoint = newDisplayEvent.anchorPoint
	if anchorPoint == "BOTTOMLEFT" then
		newDisplayEvent.positionX = 0
	elseif anchorPoint == "BOTTOM" then
		newDisplayEvent.positionX = newDisplayEvent.scrollWidth / 2
	elseif anchorPoint == "BOTTOMRIGHT" then
		newDisplayEvent.positionX = newDisplayEvent.scrollWidth
	end

	-- Get the number of display events that are currently animating for this style.
	local numActiveAnimations = #activeDisplayEvents
	local positionY

	-- Static display is growing downwards.
	if direction == "Down" then
		positionY = newDisplayEvent.scrollHeight

		-- Offset the new display event correctly if there are already animating events.
		if numActiveAnimations > 0 then
			-- Set the next y position to after the last display event.
			positionY = activeDisplayEvents[numActiveAnimations].positionY - newDisplayEvent.fontSize - MIN_VERTICAL_SPACING

			-- Wrap the y position if it is outside the scroll area's height.
			if positionY < 0 then
				positionY = newDisplayEvent.scrollHeight
			end
		end

	-- Static display is growing upwards.
	else
		positionY = 0

		-- Offset the new display event correctly if there are already animating events.
		if numActiveAnimations > 0 then
			-- Set the next y position to before the last display event.
			positionY = activeDisplayEvents[numActiveAnimations].positionY + newDisplayEvent.fontSize + MIN_VERTICAL_SPACING

			-- Wrap the y position if it is outside the scroll area's height.
			if positionY > newDisplayEvent.scrollHeight then
				positionY = 0
			end
		end
	end


	-- Check if there are already animating events.
	if numActiveAnimations > 0 then
		-- Get the top and bottom points of the new display event.
		local topNew = positionY + newDisplayEvent.fontSize
		local bottomNew = positionY

		-- Loop through all the old display events to force old animations that the new one overlaps to complete.
		local oldDisplayEvent, topOld, bottomOld
		for x = 1, numActiveAnimations - 1 do
			-- Get the top and bottom points of the old display event.
			oldDisplayEvent = activeDisplayEvents[x]
			bottomOld = oldDisplayEvent.positionY
			topOld = bottomOld + oldDisplayEvent.fontSize

			-- Force the old animation to complete if the new display event is overlapping it.
			if (topNew >= bottomOld and topNew <= topOld) or (bottomNew >= bottomOld and bottomNew <= topOld) then
				oldDisplayEvent.elapsedTime = oldDisplayEvent.scrollTime
			end
		end
	end

	-- Set the y position to the calculated value.
	newDisplayEvent.positionY = positionY
end


-------------------------------------------------------------------------------
-- Initialization.
-------------------------------------------------------------------------------

-- Register the default animation styles.
MikSBT.RegisterAnimationStyle("Angled", InitAngled, "Alternate;Left;Right", "AngleUp;AngleDown")
MikSBT.RegisterAnimationStyle("Straight", InitStraight, "Up;Down", nil)
MikSBT.RegisterAnimationStyle("Parabola", InitParabola, "Up;Down", "CurvedLeft;CurvedRight")
MikSBT.RegisterAnimationStyle("Horizontal", InitHorizontal, "Alternate;Left;Right", "GrowUp;GrowDown")
MikSBT.RegisterAnimationStyle("Static", InitStatic, "Up;Down", nil)


-- Register the default sticky animation styles.
MikSBT.RegisterStickyAnimationStyle("Pow", InitPow, "Up;Down", "Normal;Jiggle")
MikSBT.RegisterStickyAnimationStyle("Static", InitStatic, "Up;Down", nil)
