-- Builds a set of runnable Tweens from a table of instances and values
local function BuildTweens(root, tweenTable, origin)
	local tweens = {}
	
	local initialFrame
	local initialPos, initialRot, originPos, originRot
	local center
	if origin ~= nil then
		if root:IsA("Model") then
			center = root.PrimaryPart
			if center == nil then
				error("Root Model must have a PrimaryPart to use SetOrigin.")
			end
		elseif root:IsA("BasePart") then
			center = root
		else
			error("Root must be a BasePart or Model to use SetOrigin.")
		end
		initialFrame = center.CFrame:inverse()
		initialPos = initialFrame.Position
		initialRot = initialFrame - initialPos
		originPos = origin.Position
		originRot = origin - originPos
	end

	VisitAllInstances(root, tweenTable, function(instance, props)
		for prop, values in pairs(props) do
			if origin and instance:IsA("BasePart") then
				local newValue = ApplyOrigin(prop, values.InitialValue, initialFrame, origin)
				if newValue then
					values.InitialValue = newValue
				end
			end

			local keyframes = values.Keyframes
			local lastTime = 0
			for _, keyframe in ipairs(keyframes) do
				if origin and instance:IsA("BasePart") then
					local newValue = ApplyOrigin(prop, keyframe.Value, initialFrame, origin)
					if newValue then
						keyframe.Value = newValue
					end
				end

				local tweenInfo = TweenInfo.new(keyframe.Time - lastTime,
					keyframe.EasingStyle,keyframe.EasingDirection, 0, false, 0)
				local propTable = {
					[prop] = keyframe.Value
				}
				local tween = TweenService:Create(instance, tweenInfo, propTable)
				table.insert(tweens, {
					Tween = tween,
					DelayTime = lastTime,
				})
				lastTime = keyframe.Time
			end
		end
	end)

	return tweens
end

local TweenUtilities = {}
TweenUtilities.__index = TweenUtilities

function TweenUtilities.new(root, tweenTable)
	local self = {
		running = false,
		root = root,
		tweenTable = tweenTable,
		tweens = BuildTweens(root, tweenTable),
		origin = nil,
	}
	setmetatable(self, TweenUtilities)
	return self
end

function TweenUtilities:ResetValues()
	if self.origin then
		if self.root:IsA("Model") then
			if self.root.PrimaryPart == nil then
				error("Root Model must have a PrimaryPart to use SetOrigin.")
			end
			self.root:SetPrimaryPartCFrame(self.origin)
		elseif self.root:IsA("BasePart") then
			self.root.CFrame = self.origin
		else
			error("Root must be a BasePart or Model to use SetOrigin.")
		end
	end

	VisitAllInstances(self.root, self.tweenTable, function(instance, props)
		for prop, values in pairs(props) do
			local initialValue = values.InitialValue
			instance[prop] = initialValue
		end
	end)
end

function TweenUtilities:SetOrigin(origin)
	if self.running then
		error("Cannot SetOrigin while the animation is running.")
	end
	self.origin = origin
	self.tweens = BuildTweens(self.root, self.tweenTable, origin)
end

function TweenUtilities:PlayTweens(callback)
	if self.running then
		self:PauseTweens()
	end
	self.running = true

	local delayedTweens = {}
	for _, props in pairs(self.tweens) do
		local tween = props.Tween
		local delayTime = props.DelayTime
		if delayTime == 0 then
			tween:Play()
		else
			table.insert(delayedTweens, {
				Tween = tween,
				DelayTime = delayTime,
				Playing = false,
			})
		end
	end

	local heartbeatConnection
	local start = tick()
	local tweensDone = false
	heartbeatConnection = RunService.Heartbeat:Connect(function()
		local now = tick() - start
		if not self.running then
			self.running = false
			heartbeatConnection:Disconnect()
			if callback then
				callback()
			end
			return
		end
		if tweensDone then
			local allDone = true
			for _, props in pairs(self.tweens) do
				local tween = props.Tween
				if tween.PlaybackState == Enum.PlaybackState.Playing then
					allDone = false
				end
			end
			if allDone then
				self.running = false
				heartbeatConnection:Disconnect()
				if callback then
					callback()
				end
				return
			end
		else
			local allPlaying = true
			for _, props in pairs(delayedTweens) do
				if not props.Playing then
					if now > props.DelayTime then
						props.Playing = true
						props.Tween:Play()
					else
						allPlaying = false
					end
				end
			end
			tweensDone = allPlaying
		end
	end)
end

function TweenUtilities:PauseTweens()
	self.running = false
	for _, props in pairs(self.tweens) do
		local tween = props.Tween
		tween:Cancel()
	end
end

return TweenUtilities