--!nonstrict
--[[
	Creates a Roact component which will automatically animate.
	The animations are created with Otter springs.

	Example:
		To create a frame whose position animates up / down:
		AnimatedFrame = Roact.createElement(SpringAnimatedItem.AnimatedFrame, {
			regularProps = {
				Size = ...,
				BackgroundColor3 = ...,
			},
			animatedValues = {
				positionY = goal,
			},
			mapValuesToProps = function(values)
				return {
					Position = UDim2.new(0, 0, 0, values.positionY),
				}
			end,
		})
		Whenever goal changes, the frame will animate toward the new position.
]]

local Packages = script.Parent.Parent.Parent

local Otter = require(Packages.Otter)
local Roact = require(Packages.Roact)
local Cryo = require(Packages.Cryo)
local t = require(Packages.t)

local PropTypes = t.intersection(
	t.strictInterface({
		-- Values in this table will be animated with Otter.
		animatedValues = t.table,

		-- This function describes how the animated values should be converted to
		-- Roblox properties.
		mapValuesToProps = t.callback,

		-- Options to pass to Otter's spring configuration
		springOptions = t.optional(t.table),

		-- Called when the animation is complete
		onComplete = t.optional(t.callback),

		-- Props to pass to the inner component.
		regularProps = t.table,

		-- Children passed in by Roact
		[Roact.Children] = t.optional(t.table),
	}),
	function(props)
		if props[Roact.Children] ~= nil and props.regularProps[Roact.Children] ~= nil then
			return false,
				"Children must be specified in one place, but the [Roact.Children] key was found"
					.. " in both props and props.regularProps on SpringAnimatedItem."
		end

		return true
	end
)

local SpringAnimatedItem = {}

function SpringAnimatedItem.wrap(component)
	local AnimatedComponent = Roact.PureComponent:extend(string.format("SpringAnimatedItem(%s)", tostring(component)))

	AnimatedComponent.defaultProps = {
		regularProps = {},
	}

	function AnimatedComponent:init()
		self.ref = self.props.regularProps[Roact.Ref] or Roact.createRef()

		self.applyAnimatedValues = function(values)
			local rbx = self.ref.current
			if rbx == nil then
				return
			end

			local mapValuesToProps = self.props.mapValuesToProps

			local rbxProps = mapValuesToProps(values)
			for key, value in pairs(rbxProps) do
				rbx[key] = value
			end
		end

		self.onComplete = function()
			if self.props.onComplete then
				self.props.onComplete()
			end
		end

		self.motor = nil
	end

	function AnimatedComponent:didMount()
		local animatedValues = self.props.animatedValues

		-- Apply initial values
		self.applyAnimatedValues(animatedValues)

		-- Set up motor
		self.motor = Otter.createGroupMotor(animatedValues)
		self.motor:onStep(function(newValues)
			self.applyAnimatedValues(newValues)
		end)
		self.motor:onComplete(self.onComplete)
		self.motor:start()
	end

	function AnimatedComponent:willUpdate(newProps)
		if
			self.props.regularProps[Roact.Ref] ~= newProps.regularProps[Roact.Ref]
			and newProps.regularProps[Roact.Ref] ~= nil
		then
			self.ref = newProps.regularProps[Roact.Ref]
		end
	end

	function AnimatedComponent:render()
		assert(PropTypes(self.props))

		local regularProps = self.props.regularProps
		local props = Cryo.Dictionary.join(regularProps, {
			[Roact.Ref] = self.ref,
			[Roact.Children] = self.props[Roact.Children],
		})

		return Roact.createElement(component, props)
	end

	function AnimatedComponent:didUpdate(oldProps)
		-- If the animatedValues changed, set new goals for the motor so they animate.
		if self.props.animatedValues ~= oldProps.animatedValues then
			local goals = {}
			local needsMotorUpdate = false
			for key, newValue in pairs(self.props.animatedValues) do
				local springOptions = self.props.springOptions -- nil means default
				goals[key] = Otter.spring(newValue, springOptions)
				if newValue ~= oldProps.animatedValues[key] then
					needsMotorUpdate = true
				end
			end
			if needsMotorUpdate then
				self.motor:setGoal(goals)
			end
		end
	end

	function AnimatedComponent:willUnmount()
		if self.motor then
			self.motor:destroy()
			self.motor = nil
		end
	end

	return AnimatedComponent
end

SpringAnimatedItem.AnimatedFrame = SpringAnimatedItem.wrap("Frame")
SpringAnimatedItem.AnimatedScrollingFrame = SpringAnimatedItem.wrap("ScrollingFrame")
SpringAnimatedItem.AnimatedImageLabel = SpringAnimatedItem.wrap("ImageLabel")
SpringAnimatedItem.AnimatedTextButton = SpringAnimatedItem.wrap("TextButton")
SpringAnimatedItem.AnimatedUIScale = SpringAnimatedItem.wrap("UIScale")

return SpringAnimatedItem
