--!nonstrict
local ToastRoot = script.Parent
local DialogRoot = ToastRoot.Parent
local AppRoot = DialogRoot.Parent
local UIBloxRoot = AppRoot.Parent
local Packages = UIBloxRoot.Parent

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

local SlidingDirection = require(UIBloxRoot.Core.Animation.Enum.SlidingDirection)
local SlidingContainer = require(UIBloxRoot.Core.Animation.SlidingContainer)
local validateColorInfo = require(UIBloxRoot.Core.Style.Validator.validateColorInfo)
local StateTable = require(UIBloxRoot.StateTable.StateTable)
local ButtonType = require(AppRoot.Button.Enum.ButtonType)

local AnimationState = require(ToastRoot.Enum.AnimationState)
local InformativeToast = require(ToastRoot.InformativeToast)
local InteractiveToast = require(ToastRoot.InteractiveToast)
local ToastContainer = require(ToastRoot.ToastContainer)

local Toast = Roact.PureComponent:extend("Toast")

Toast.validateProps = t.strictInterface({
	-- AnchorPoint of the `ToastContainer`.
	anchorPoint = t.optional(t.Vector2),
	-- Toast duration before auto-disappear, `nil` will make the toast keep showing until `show` is `false`.
	duration = t.optional(t.number),
	-- LayoutOrder of toast page.
	layoutOrder = t.optional(t.integer),
	-- Position of `ToastContainer` in the whole page.
	position = t.optional(t.UDim2),
	-- Control if we want to force toast to show or hide immediately. Could be used when we want the toast to slide up for some reason.
	-- For example when route changes or user interacts with other components, etc. And also could control the toast to slide down and show up again.
	show = t.optional(t.boolean),
	-- Size of `ToastContainer`, height is dynamic based on text length.
	size = t.optional(t.UDim2),
	springOptions = t.optional(t.table),
	-- A table of information in the Toast. This prop has only one required field: toastTile.
	toastContent = t.strictInterface({
		-- Text of the button.
		-- This prop will cause the accompanying `onActivated` prop to be triggered on button press rather than on toast press.
		buttonText = t.optional(t.string),
		-- Icon image color style, will be assigned with default one in `ToastIcon` if unspecified here.
		-- This prop change will trigger toast to slide up and update the content then slide down.
		iconColorStyle = t.optional(validateColorInfo),
		-- Icon image in the toast, could be nil if we want the toast without icon. Support both ImageSet compatible table or string directory
		-- or a function that returns another RoactElement to render in place of an image.
		-- This prop change will trigger toast to slide up and update the content then slide down.
		iconImage = t.optional(t.union(t.table, t.string, t.callback)),
		-- Size of the icon image in the toast. This prop change will trigger toast to slide up and update the content then slide down.
		iconSize = t.optional(t.Vector2),
		-- A Roact children table of icon image to customize toast icon.
		iconChildren = t.optional(t.table),
		-- Callback when toast is Activated. Would be called when toast is activated and sliding up animation is done.
		-- This prop change will trigger toast to slide up and update the content then slide down.
		onActivated = t.optional(t.callback),
		-- Callback when toast is Appeared. Would be called when sliding down animation is done.
		-- This prop change will trigger toast to slide up and update the content then slide down.
		onAppeared = t.optional(t.callback),
		-- Callback when toast is dismissed. Would be called when sliding up animation is done. Wouldn't be called if the toast is activated.
		-- This prop change will trigger toast to slide up and update the content then slide down.
		onDismissed = t.optional(t.callback),
		-- Control if toast would be dismissed when user swipes up. Default is true.
		-- This prop change will trigger toast to slide up and update the content then slide down.
		swipeUpDismiss = t.optional(t.boolean),
		-- Subtitle text to display in the toast. Subtitle TextLabel won't be rendered if the string is `nil`.
		-- This prop change will trigger toast to slide up and update the content then slide down.
		toastSubtitle = t.optional(t.string),
		-- optional sequence number so you can, if you want to, display the same
		-- toast message twice in a row (Without this Toast would 'eat'
		-- the second, matching message without notifying the caller about it being
		-- dismissed or activated or whatever)
		sequenceNumber = t.optional(t.integer),
		-- Title text to display in the toast. Should be localized. This prop change will trigger toast to slide up and update the content then slide down.
		toastTitle = t.string,
		-- Changes which button component gets displayed
		buttonType = t.optional(ButtonType.isEnumValue),
		-- Callback when toast is swiped to dismiss.
		onTouchSwipeUp = t.optional(t.callback),
	}),
})

Toast.defaultProps = {
	anchorPoint = Vector2.new(0.5, 0),
	position = UDim2.new(0.5, 0, 0, 20),
	show = true,
}

local function toastContentEqual(toastContent1, toastContent2)
	if
		toastContent1.iconColorStyle ~= toastContent2.iconColorStyle
		or toastContent1.iconImage ~= toastContent2.iconImage
		or toastContent1.iconSize ~= toastContent2.iconSize
		or toastContent1.iconChildren ~= toastContent2.iconChildren
		or toastContent1.onActivated ~= toastContent2.onActivated
		or toastContent1.onDismissed ~= toastContent2.onDismissed
		or toastContent1.sequenceNumber ~= toastContent2.sequenceNumber
		or toastContent1.swipeUpDismiss ~= toastContent2.swipeUpDismiss
		or toastContent1.toastSubtitle ~= toastContent2.toastSubtitle
		or toastContent1.toastTitle ~= toastContent2.toastTitle
	then
		return false
	end
	return true
end

function Toast:init()
	self.isMounted = false

	self.currentToastContent = self.props.toastContent

	self.onActivated = function()
		self.stateTable.events.Activated({
			activated = true,
		})
	end

	self.onAppeared = function()
		if self.currentToastContent.onAppeared then
			self.currentToastContent.onAppeared()
		end
		local duration = self.props.duration
		if duration and duration > 0 then
			local currentToastContent = self.currentToastContent
			delay(duration, function()
				if currentToastContent == self.currentToastContent then
					self.stateTable.events.AutoDismiss()
				end
			end)
		end
	end

	self.onComplete = function()
		local duration = self.props.duration

		if self.state.currentState == AnimationState.Appearing and duration and duration <= 0 then
			self.stateTable.events.AutoDismiss()
		else
			self.stateTable.events.AnimationComplete()
		end
	end

	self.onDisappeared = function()
		if self.state.context.activated then
			if self.currentToastContent.onActivated then
				self.currentToastContent.onActivated()
			end
		else
			if self.currentToastContent.onDismissed then
				self.currentToastContent.onDismissed()
			end
		end
	end

	self.onTouchSwipe = function(_, swipeDir)
		if swipeDir == Enum.SwipeDirection.Up then
			self.stateTable.events.ForceDismiss()
			if self.currentToastContent.onTouchSwipeUp then
				self.currentToastContent.onTouchSwipeUp()
			end
		end
	end

	self.renderInteractiveToast = function(props)
		return Roact.createElement(InteractiveToast, props)
	end

	self.renderInformativeToast = function(props)
		return Roact.createElement(InformativeToast, props)
	end

	self.setContext = function(_, _, data)
		return data
	end

	self.updateToastContent = function()
		if self.currentToastContent ~= self.props.toastContent then
			self.currentToastContent = self.props.toastContent
			if self.props.show then
				-- Show next toast content
				self.stateTable.events.ForceAppear({
					activated = false,
				})
			end
		end
	end

	local initialState = AnimationState.Disappeared
	self.state = {
		currentState = initialState,
		context = {
			activated = false,
		},
	}

	local stateTableName = string.format("Animated(%s)", tostring(self))
	self.stateTable = StateTable.new(stateTableName, initialState, {}, {
		[AnimationState.Appearing] = {
			AnimationComplete = { nextState = AnimationState.Appeared, action = self.onAppeared },
			AutoDismiss = { nextState = AnimationState.Disappearing, action = self.onAppeared },
			ContentChanged = { nextState = AnimationState.Disappearing },
			ForceDismiss = { nextState = AnimationState.Disappearing },
		},
		[AnimationState.Appeared] = {
			Activated = { nextState = AnimationState.Disappearing, action = self.setContext },
			AutoDismiss = { nextState = AnimationState.Disappearing },
			ContentChanged = { nextState = AnimationState.Disappearing },
			ForceDismiss = { nextState = AnimationState.Disappearing },
		},
		[AnimationState.Disappearing] = {
			AnimationComplete = { nextState = AnimationState.Disappeared, action = self.onDisappeared },
		},
		[AnimationState.Disappeared] = {
			ContentChanged = { nextState = AnimationState.Appearing, action = self.updateToastContent },
			ForceAppear = { nextState = AnimationState.Appearing, action = self.setContext },
		},
	})

	self.stateTable:onStateChange(function(oldState, newState, updatedContext)
		if self.isMounted and oldState ~= newState then
			self:setState({
				currentState = newState,
				context = updatedContext,
			})
		end
	end)
end

function Toast:isShowing()
	return self.state.currentState == AnimationState.Appearing or self.state.currentState == AnimationState.Appeared
end

function Toast:render()
	local onActivated = self.currentToastContent.onActivated
	local swipeUpDismiss = self.currentToastContent.swipeUpDismiss
	if swipeUpDismiss == nil then
		swipeUpDismiss = true
	end
	return Roact.createElement(SlidingContainer, {
		show = self:isShowing(),
		layoutOrder = self.props.layoutOrder,
		onComplete = self.onComplete,
		slidingDirection = SlidingDirection.Down,
		springOptions = self.props.springOptions,
	}, {
		ToastContainer = Roact.createElement(ToastContainer, {
			anchorPoint = self.props.anchorPoint,
			buttonText = self.currentToastContent.buttonText,
			position = self.props.position,
			size = self.props.size,
			-- Toast content props
			iconColorStyle = self.currentToastContent.iconColorStyle,
			iconImage = self.currentToastContent.iconImage,
			iconSize = self.currentToastContent.iconSize,
			iconChildren = self.currentToastContent.iconChildren,
			onActivated = onActivated and self.onActivated,
			onTouchSwipe = swipeUpDismiss and self.onTouchSwipe,
			renderToast = onActivated and self.renderInteractiveToast or self.renderInformativeToast,
			toastSubtitle = self.currentToastContent.toastSubtitle,
			toastTitle = self.currentToastContent.toastTitle,
			buttonType = self.currentToastContent.buttonType,
		}),
	})
end

function Toast:didMount()
	self.isMounted = true
	if self.props.show then
		self.stateTable.events.ForceAppear({
			activated = false,
		})
	end
end

function Toast:willUnmount()
	self.isMounted = false
end

function Toast:didUpdate(oldProps, oldState)
	if oldProps.show ~= self.props.show then
		if self.props.show then
			self.stateTable.events.ForceAppear({
				activated = false,
			})
		else
			self.stateTable.events.ForceDismiss()
		end
	end
	if not toastContentEqual(oldProps.toastContent, self.props.toastContent) then
		-- Toast content updated, need to force dismiss current toast and show the new one
		self.stateTable.events.ContentChanged({
			activated = false,
		})
	end
	if oldState.currentState ~= self.state.currentState and self.state.currentState == AnimationState.Disappeared then
		self.updateToastContent()
	end
end

return Toast
