-- Compiled with roblox-ts v2.3.0
local TS = _G[script]
local PlayersService = TS.import(script, TS.getModule(script, "@rbxts", "services")).Players
local Signal = TS.import(script, TS.getModule(script, "@rbxts", "beacon").out).Signal
local UIResolver = TS.import(script, script, "UIResolver").UIResolver
local _Timer = TS.import(script, script, "Timer")
local Timer = _Timer.Timer
local TimerPosition = _Timer.TimerPosition
local TimerType = _Timer.TimerType
local t = TS.import(script, TS.getModule(script, "@rbxts", "t").lib.ts).t
-- #region TYPE_RELATED
--[[
	*
	 * @category Prompt
	 * The PromptTypes that can be used when creating a new Prompt object.
	 
]]
local PromptType = {
	Custom = "Custom",
	Compact = "Compact",
	Choice = "Choice",
}
--[[
	*
	 * The PromptPayload that is sent during accepted fullfillment of the prompt.
	 * @category Prompt
	 * @example
	 * ```
	 * prompt.OnFulfill.Connect((accepted: boolean,payload?: PromptPayload) => {
	 *     if (accepted && payload) {
	 *         print(payload.promptContent);
	 *     }
	 * });
	 * ```
	 * 
	 * Assuming we had a Prompt with a ScrollingFrame as the Content
	 * and it contains 1 TextLabel named 'Money' and 1 TextBox named 'Nickname'
	 * 
	 * Your output would look like this
	 * ```
	 * {
	 *     ["Money"] = "350",
	 *     ["Nickname"] = "Jen"
	 * }
	 * ```
	 
]]
--[[
	*
	 * This is an enum of the TimeoutBehavior which contains behaviour for when the Prompt times out.
	 * @enum {number}
	 
]]
local TimeoutBehavior
do
	local _inverse = {}
	TimeoutBehavior = setmetatable({}, {
		__index = _inverse,
	})
	TimeoutBehavior.CancelOnTimeout = 0
	_inverse[0] = "CancelOnTimeout"
	TimeoutBehavior.RejectOnTimeout = 1
	_inverse[1] = "RejectOnTimeout"
end
--[[
	*
	 * PromptOptions allow you to configure the prompts behavior.
	 * @category Prompt
	 * @interface
	 
]]
-- #endregion
local promptChoice = script:FindFirstChild("PromptInstances"):FindFirstChild("Prompt_Choice")
local promptCompact = script:FindFirstChild("PromptInstances"):FindFirstChild("Prompt_Compact")
local player = PlayersService.LocalPlayer
-- #region MODULE_FUNCTIONS
--* Creates the default UIListLayout inserted into Prompt._UI.content if it's a ScrollingFrame. 
local function createDefaultUIListLayout()
	local listLayout = Instance.new("UIListLayout")
	listLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
	listLayout.VerticalAlignment = Enum.VerticalAlignment.Top
	listLayout.ItemLineAlignment = Enum.ItemLineAlignment.Center
	listLayout.Padding = UDim.new(0.025, 0)
	listLayout.Wraps = true
	listLayout.SortOrder = Enum.SortOrder.LayoutOrder
	listLayout.FillDirection = Enum.FillDirection.Vertical
	return listLayout
end
--* Creates the default message label used for the Prompt.Message property. 
local function createDefaultMessageLabel(text, bgColor)
	local messageLabel = Instance.new("TextLabel")
	messageLabel.BackgroundColor3 = bgColor or Color3.fromRGB(60, 60, 60)
	messageLabel.Name = "Message"
	messageLabel.Size = UDim2.new(1, 0, 0.2, 0)
	messageLabel.Text = text
	messageLabel.TextColor3 = Color3.new(255, 255, 255)
	messageLabel.TextStrokeTransparency = 0.5
	messageLabel.TextStrokeColor3 = Color3.new(0, 0, 0)
	messageLabel.BorderSizePixel = 0
	return messageLabel
end
--[[
	*
	 * Gets the lowest layout order of all child elements.
	 * @param element - The parent element to find the lowest layout order from
	 * @returns - A number of the lowest layout order or if no elements are present 0 is returned.
	 
]]
local function getLowestLayoutOrder(element)
	local lowestOrder = nil
	for _, child in element:GetChildren() do
		if not child:IsA("GuiObject") then
			continue
		end
		if not (lowestOrder ~= 0 and lowestOrder == lowestOrder and lowestOrder) then
			lowestOrder = child.LayoutOrder
		end
		if child.LayoutOrder < lowestOrder then
			lowestOrder = child.LayoutOrder
		end
	end
	return if lowestOrder ~= nil then lowestOrder else 0
end
--* Extracts content data from UI elements currently supports TextLabel & TextBox Instances.
local extractDataFromContent
local function extractDataFromElement(element, contentPayload)
	if element:IsA("Frame") or element:IsA("ScrollingFrame") then
		extractDataFromContent(element, contentPayload)
	elseif element:IsA("TextLabel") or element:IsA("TextBox") then
		return element.Text
	end
	return nil
end
--* Extracts content data from a ScrollingFrame or Frame Instance children. 
function extractDataFromContent(content, contentPayload)
	for _, child in content:GetChildren() do
		if not child:IsA("GuiObject") then
			continue
		end
		local data = extractDataFromElement(child, contentPayload)
		if data ~= "" and data then
			local _contentPayload = contentPayload
			local _name = child.Name
			_contentPayload[_name] = data
		end
	end
end
local function getPromptsScreenGui()
	local playerGui = player:FindFirstChildWhichIsA("PlayerGui")
	if not playerGui then
		error(`\{Promptifier\}: No PlayerGui can be found.`)
	end
	local _promptsScreenUI = playerGui:FindFirstChild("Prompts")
	if not _promptsScreenUI then
		_promptsScreenUI = Instance.new("ScreenGui")
		_promptsScreenUI.Name = "Prompts"
		_promptsScreenUI.ResetOnSpawn = false
		-- Try to have it so the prompts are drawn above every other UI
		_promptsScreenUI.DisplayOrder = 5000
		_promptsScreenUI.Parent = playerGui
	end
	return _promptsScreenUI
end
-- #endregion
--[[
	*
	 * The main class used to create & use Prompts.
	 * @category Prompt
	 * @example
	 * const prompt: Prompt = new Prompt(PromptType.Choice,"ExamplePrompt","Example message") || new Prompt(PromptType.Compact,"ExamplePrompt","Example message") || new Prompt(PromptType.Custom,"ExamplePrompt","Example message");
	 * prompt.OnFulfill.Connect((accepted: boolean,payload: PromptPayload) => {
	 *     if (accepted && payload) {
	 *         if (payload.PromptContent.has("SomeInstanceName")) // Do something with data
	 *     }
	 *     prompt.Destroy();
	 * });
	 * 
	 * prompt.OnCancel.Connect((reason?: string) => {
	 *     warn("Prompt was cancelled for reason: " + reason);
	 *     // Other logic upon cancel
	 *     prompt.Destroy();
	 * });
	 * 
	 *
	 * // Tips:
	 * 
	 * // - You don't have to destroy the prompt, after it's first prompt it can be reused.
	 * 
	 * // - A prompt message that is passed into the contructor is used to display a text label automatically for you with text inside of it
	 * //   TODO: It will later be accessible through a function that will allow you to change some properties.
	 *
	 
]]
local Prompt
do
	Prompt = setmetatable({}, {
		__tostring = function()
			return "Prompt"
		end,
	})
	Prompt.__index = Prompt
	function Prompt.new(...)
		local self = setmetatable({}, Prompt)
		return self:constructor(...) or self
	end
	function Prompt:constructor(promptType, title, message, UI)
		self.TimeOut = 0
		self.Options = {
			destroyOnTimeout = true,
			timeoutBehavior = TimeoutBehavior.RejectOnTimeout,
		}
		self.OnFulfill = Signal.new()
		self.OnCancel = Signal.new()
		self._triggered = false
		self._cancelled = false
		self._destroyed = false
		self._UIConnections = {}
		self.Title = title
		self.Message = message
		if promptType == PromptType.Custom then
			-- If the prompt is custom validate it
			if not UI then
				self:__error("UIResolver must be given when using custom prompt type.")
			end
			UI:Validate()
			self._UI = UI
		elseif promptType == PromptType.Compact then
			local _promptCompact = promptCompact:Clone()
			local resolver = UIResolver.new()
			local isResolvable = resolver:Resolve({
				bg = _promptCompact,
				title = _promptCompact.Title,
				content = _promptCompact.Content,
				acceptBtn = _promptCompact.ConfirmBtn,
				declineBtn = _promptCompact.CloseBtn,
			})
			if not isResolvable then
				self:__error("Failed to resolve UI Structure for prompt 'Compact' type.")
			end
			self._UI = resolver
		elseif promptType == PromptType.Choice then
			local _promptChoice = promptChoice:Clone()
			local resolver = UIResolver.new()
			local isResolvable = resolver:Resolve({
				bg = _promptChoice,
				title = _promptChoice.Title,
				content = _promptChoice.Content,
				acceptBtn = _promptChoice.YBtn,
				declineBtn = _promptChoice.NBtn,
			})
			if not isResolvable then
				self:__error("Failed to resolve UI Structure for prompt 'Choice' type.")
			end
			self._UI = resolver
		else
			self:__error(`Unknown PromptType: '{promptType}'`)
		end
		self._type = promptType
		self._UI.Title.Text = self.Title
		if self._UI.Content:IsA("ScrollingFrame") then
			-- Check for a UIListLayout adding one if it doesn't exist
			if not self._UI.Content:FindFirstChildOfClass("UIListLayout") then
				local listLayout = createDefaultUIListLayout()
				listLayout.Parent = self._UI.Content
			end
		end
		-- Assign this UI to the Prompts ScreenGui
		if not Prompt._promptsScreenUI then
			Prompt._promptsScreenUI = getPromptsScreenGui()
		end
		self._UI.BG.Parent = Prompt._promptsScreenUI
	end
	function Prompt:Trigger()
		if self._destroyed then
			return nil
		end
		-- Only trigger if not already triggered
		if self._triggered then
			return nil
		end
		self._cancelled = false
		self._triggered = true
		-- Adjust the ZIndex for this Prompt
		self._UI.BG.ZIndex = #Prompt._promptsScreenUI:GetChildren() + 1
		self._UI:ReassignZIndex()
		self._UI.Title.Text = self.Title
		-- #region PROMPT_MESSAGE
		-- Check if a message is present for this prompt
		local _value = self.Message
		if _value ~= "" and _value then
			local msgInstance = self._UI.Content:FindFirstChild("Message")
			-- If an instance is found but it's not a text label then rename that instance
			if msgInstance and not t.instanceIsA("TextLabel")(msgInstance) then
				self:__warn(`Prompt '{self.Title}' An Instance named 'Message' is in the Prompt UI and it is a reserved child instance. It will be renamed to Message1.`)
				msgInstance.Name = "Message1"
				msgInstance = nil
			end
			local msgLabel = nil
			if not msgInstance then
				-- Add a text label to the content to represent the message
				msgLabel = createDefaultMessageLabel(self.Message, self._UI.Content.BackgroundColor3)
				msgLabel.LayoutOrder = getLowestLayoutOrder(self._UI.Content) - 1
				msgLabel.Parent = self._UI.Content
			else
				msgLabel = msgInstance
			end
			msgLabel.Text = self.Message
		end
		-- #endregion
		-- For each GuiObject in content; assign ZIndex + 1
		local contextZIndex = self._UI.Content.ZIndex + 1
		local _exp = self._UI.Content:GetChildren()
		-- ▼ ReadonlyArray.filter ▼
		local _newValue = {}
		local _callback = function(child)
			return child:IsA("GuiObject")
		end
		local _length = 0
		for _k, _v in _exp do
			if _callback(_v, _k - 1, _exp) == true then
				_length += 1
				_newValue[_length] = _v
			end
		end
		-- ▲ ReadonlyArray.filter ▲
		-- ▼ ReadonlyArray.forEach ▼
		local _callback_1 = function(child)
			child.ZIndex = contextZIndex
			return child.ZIndex
		end
		for _k, _v in _newValue do
			_callback_1(_v, _k - 1, _newValue)
		end
		-- ▲ ReadonlyArray.forEach ▲
		-- #region UI_CONNECTION
		local __UIConnections = self._UIConnections
		local _arg0 = self._UI.AcceptBtn.MouseButton1Click:Connect(function()
			-- Clean the UI connections connections since we reestablish every trigger
			self:cleanConnections()
			local promptPayload = {
				prompt = self,
				promptContent = {},
			}
			extractDataFromContent(self._UI.Content, promptPayload.promptContent)
			-- If the Prompt is not validated or cancelled during validation then return.
			if self.Validator then
				local validated = self.Validator(promptPayload)
				if not validated then
					warn("Prompt could not be validated.")
					return nil
				end
				if self._cancelled then
					return nil
				end
			end
			self._UI.BG.Visible = false
			self._triggered = false
			self.OnFulfill:Fire(true, promptPayload)
		end)
		table.insert(__UIConnections, _arg0)
		-- Listen for when the prompt is declined
		local __UIConnections_1 = self._UIConnections
		local _arg0_1 = self._UI.DeclineBtn.MouseButton1Click:Connect(function()
			-- Clean the UI connections connections since we reestablish every trigger
			self:cleanConnections()
			self._UI.BG.Visible = false
			self._triggered = false
			self.OnFulfill:Fire(false)
		end)
		table.insert(__UIConnections_1, _arg0_1)
		-- #endregion
		-- #region TIMEOUT_HANDLER
		if self.TimeOut > 1 then
			-- Create a timer for this prompt
			if not self._timer then
				-- If no timer is assigned but a time out is present then create the defaults
				if self._type == PromptType.Choice then
					self._timer = Timer.new(TimerType.Bar, self.TimeOut)
					self._timer.ParentBorderSize = self._UI.BG.BorderSizePixel
					self._timer:SetPosition(TimerPosition.Bottom)
				elseif self._type == PromptType.Compact then
					self._timer = Timer.new(TimerType.Digit, self.TimeOut)
					self._timer.ParentBorderSize = self._UI.BG.BorderSizePixel
					self._timer:SetPosition(TimerPosition.BottomLeft)
				else
					self:__warn(`Prompt '{self.Title}' is a PromptType.Custom type and will not be timed out since no timer was assigned.`)
				end
			end
			-- If a timer is present; start the timer
			if self._timer then
				self._timer:SetZIndex(self._UI.BG.ZIndex)
				self._timer._timeUI.Parent = self._UI.BG
				self._timer:Set(self.TimeOut)
				task.defer(function()
					return self:startPromptTimer()
				end)
			end
		end
		-- #endregion
		self._UI.BG.Visible = true
	end
	function Prompt:SetTimer(timer)
		self._timer = timer
	end
	function Prompt:Cancel(reason)
		if self._destroyed then
			return nil
		end
		self._triggered = false
		self._cancelled = true
		self:cleanConnections()
		self.OnCancel:Fire(reason)
	end
	function Prompt:Destroy()
		self:cleanConnections()
		if self._timer then
			self._timer:Destroy()
			self._timer = nil
		end
		if self._UI.BG then
			self._UI.BG:Destroy()
			self._UI.BG = nil
		end
		self._UI = nil
		self._destroyed = true
	end
	function Prompt:cleanConnections()
		local _exp = self._UIConnections
		-- ▼ ReadonlyArray.forEach ▼
		local _callback = function(conn)
			return conn:Disconnect()
		end
		for _k, _v in _exp do
			_callback(_v, _k - 1, _exp)
		end
		-- ▲ ReadonlyArray.forEach ▲
		table.clear(self._UIConnections)
	end
	function Prompt:startPromptTimer()
		local initial = os.time()
		while not self._destroyed and self._triggered and not self._cancelled do
			if self._timer then
				self._timer:Decrement()
			end
			-- Check if the prompt has timed out
			if os.difftime(os.time(), initial) >= self.TimeOut then
				self._UI.BG.Visible = false
				if self.Options.timeoutBehavior == TimeoutBehavior.CancelOnTimeout then
					self.OnCancel:Fire("Prompt has timed out.")
				else
					self._triggered = false
					self.OnFulfill:Fire(false)
				end
				if self.Options.destroyOnTimeout then
					-- Destroy the Prompt since it has timed out.
					self:Destroy()
				end
				break
			end
			task.wait(1)
		end
	end
	function Prompt:__warn(...)
		local params = { ... }
		warn(`\{Promptifier\}: `, unpack(params))
	end
	function Prompt:__error(message, level)
		if not (level ~= 0 and level == level and level) then
			level = 1
		end
		local _message = message
		if typeof(_message) == "string" then
			error(`\{Promptifier\}: {message}`, level + 1)
		else
			error(message, level)
		end
	end
	Prompt.ClassName = "Prompt"
end
return {
	PromptType = PromptType,
	Prompt = Prompt,
	UIResolver = UIResolver,
	promptChoice = promptChoice,
	promptCompact = promptCompact,
	TimeoutBehavior = TimeoutBehavior,
}
