CoreScriptsRoot/Modules/Server/ClientChat/TransparencyTweener.lua
--	// FileName: TransparencyTweener.lua
--	// Written by: Xsitsu
--	// Description: Data structure for tweening transparency of a group of objects as one unit.

local module = {}

local RunService = game:GetService("RunService")
--////////////////////////////// Include
--//////////////////////////////////////
local modulesFolder = script.Parent

--// Can't use ClassMaker in it's current state since this uses custom __index and __newindex.
--// Maybe in the future I'll expand that to be more powerful, but it's alright like this for now.

--////////////////////////////// Details
--//////////////////////////////////////
local metatable = {}
metatable.__ClassName = "TransparencyTweener"

metatable.__tostring = function(tbl)
	return tbl.__ClassName .. ": " .. tbl.MemoryLocation
end

metatable.__metatable = "The metatable is locked"
metatable.__index = function(tbl, index, value)
	if rawget(tbl, index) then return rawget(tbl, index) end
	if rawget(metatable, index) then return rawget(metatable, index) end

	if (index == "Transparency") then
		return rawget(tbl, "InternalLastTweenPercentage")
	end

	error(index .. " is not a valid member of " .. tbl.__ClassName)
end
metatable.__newindex = function(tbl, index, value)
	if (index == "Transparency") then
		tbl.InternalLastTweenPercentage = value
		tbl:SetPropertiesToTweenPercentage(value)
		return
	end

	error(index .. " is not a valid member of " .. tbl.__ClassName)
end


--////////////////////////////// Methods
--//////////////////////////////////////
function metatable:Dump()
	local str = tostring(self)

	for tweenObject, objectProperties in pairs(self.TweenObjects) do
		local addStr = " | "
		if (type(tweenObject) == "table") then
			addStr = addStr .. "{{" .. tweenObject:Dump() .. "}}"
		elseif (type(tweenObject) == "userdata") then
			addStr = addStr .. tweenObject.Name .. "/" .. (tweenObject.Parent and tweenObject.Parent.Name or nil)
		end

		for propertyName, baseValue in pairs(objectProperties) do
			addStr =  addStr .. " [" .. propertyName .. "=" .. string.sub(tostring(baseValue), 1, 4) .. "]"
		end

		str = str .. addStr
	end

	return str
end

function metatable:OutputTest()
	print("Test Output for {={" .. self:Dump() .. "}=}")
	for tweenObject, objectProperties in pairs(self.TweenObjects) do
		print("TweenObject:", tweenObject)
		for propertyName, baseValue in pairs(objectProperties) do
			print("\t[" .. propertyName .. "=" .. string.sub(tostring(baseValue), 1, 4) .. "] Actual:" .. tweenObject[propertyName])
		end
	end
end

function metatable:RegisterTweenObjectProperty(objectValue, propertyName, baseValue)
	baseValue = baseValue or objectValue[propertyName]

	if (not self.TweenObjects[objectValue]) then
		self.TweenObjects[objectValue] = {}
	end

	self.TweenObjects[objectValue][propertyName] = baseValue

	self:SetObjectPropertyToPercentValue(objectValue, propertyName, baseValue, self.InternalLastTweenPercentage)
end

function metatable:UnregisterTweenObject(objectValue)
	self.TweenObjects[objectValue] = nil
end

function metatable:SetObjectPropertyToPercentValue(objectValue, propertyName, baseValue, percentValue)
	local tweenOverValue = 1 - baseValue
	local actualValue = baseValue + (tweenOverValue * percentValue)
	objectValue[propertyName] = actualValue
end

function metatable:SetPropertiesToTweenPercentage(percentValue)
	for tweenObject, objectProperties in pairs(self.TweenObjects) do
		for propertyName, baseValue in pairs(objectProperties) do
			self:SetObjectPropertyToPercentValue(tweenObject, propertyName, baseValue, percentValue)
		end
	end
end

function metatable:Tween(duration, targetPercentage, startingPercentage)
	if (self.Tweening) then
		self.QueuedTween = {duration,  targetPercentage, startingPercentage}
		self.TweenIsQueued = true
		self:CancelTween()
		return
	end
	self.Tweening = true

	local vStartingPercentage = startingPercentage

	if (vStartingPercentage) then
		self:SetPropertiesToTweenPercentage(vStartingPercentage)
	else
		vStartingPercentage = self.InternalLastTweenPercentage
	end

	local startTime = tick()
	local endTime = startTime + duration
	local tweeningOverPercentage = targetPercentage - vStartingPercentage

	local percentComplete = 0
	spawn(function()
		local now = tick()
		while(now < endTime and not self.Canceled) do
			percentComplete = math.min(math.max((now - startTime) / duration, 0), 1)
			local percentValue = vStartingPercentage + (tweeningOverPercentage * percentComplete)
			self.InternalLastTweenPercentage = percentValue
			self:SetPropertiesToTweenPercentage(percentValue)

			RunService.RenderStepped:wait()
			now = tick()
		end

		if (not self.Canceled) then
			self.InternalLastTweenPercentage = targetPercentage
			self:SetPropertiesToTweenPercentage(targetPercentage)

		else
			self.Canceled = false

			if (self.TweenIsQueued) then
				self.TweenIsQueued = false
				self.Tweening = false
				self:Tween(unpack(self.QueuedTween))
				return
			end
		end

		self.Tweening = false
	end)
end

function metatable:CancelTween()
	self.Canceled = true
end

--///////////////////////// Constructors
--//////////////////////////////////////
function module.new()
	local obj = {}
	obj.MemoryLocation = tostring(obj):match("[0123456789ABCDEF]+")

	--// We do not want to hold strong references to objects.
	obj.TweenObjects = setmetatable({}, {__mode = "k"})

	--// Transparency is a property that doesn't actually exist.
	--// The index of 'Transparency' is used for reading from and
	--// writing to InternalLastTweenPercentage. This needs to be
	--// done through metatables so we can also get the behavior
	--// of calling the method 'SetPropertiesToTweenPercentage'
	--// automatically when a new value is set.

	obj.Transparency = nil
	obj.InternalLastTweenPercentage = 0
	obj.Tweening = false
	obj.Canceled = false

	obj.TweenIsQueued = false
	obj.QueuedTween = {}

	obj = setmetatable(obj, metatable)

	return obj
end

return module
