--!strict

local PartyManager = {}

--=========================
-- // SERVICES
--=========================

local Players = game:GetService("Players")

local HttpService = game:GetService("HttpService")

--=========================
-- // MODULES
--=========================

PartyManager.Modules = {
    Settings = require(script.Parent.Parent.helpers.Settings),
	QueueManager = require(script.Parent.QueueManager)
}

--=========================
-- // DATA
--=========================

PartyManager.Parties = {}

PartyManager.PlayerLookup = {}

--=========================
-- // PRIVATE API
--=========================

-- SetupPlayerLookup(): Maintains a lookup table of UserId -> Player
local function SetupPlayerLookup(): ()
    for _, player in Players:GetPlayers() do
        PartyManager.PlayerLookup[player.UserId] = player
    end

    Players.PlayerAdded:Connect(function(player)
        PartyManager.PlayerLookup[player.UserId] = player
    end)

    Players.PlayerRemoving:Connect(function(player)
        PartyManager.PlayerLookup[player.UserId] = nil
    end)
end

-- CleanupInit(): Handle players leaving if they're in a party
local function CleanupInit(): ()
    Players.PlayerRemoving:Connect(function(player)
        if PartyManager.Parties[player] then
            local party = PartyManager.Parties[player]

            local isInMatch = false

            for _, member in { player, table.unpack(party.Members) } do
                if PartyManager.Modules.QueueManager.IsPlayerQueued(member) then
                    isInMatch = true
                    break
                end
            end

            if not isInMatch then
                PartyManager.DeleteParty(player)
            end

            return
        end

        local leader = PartyManager.GetLeader(player)

        if leader then
            PartyManager.RemovePlayer(leader, player)
        end
    end)
end

--=========================
-- // PUBLIC API
--=========================

-- CreateParty(): Creates a party with a unique ID
-- @param LeaderPlayer: The player who created the party
-- @param Members: The members of the party
-- @return string: PartyID
function PartyManager.CreateParty(LeaderPlayer: Player, Members: { Player }): string
    if PartyManager.Parties[LeaderPlayer] then
        warn("CreateParty(): Leader already has a party")
        return ""
    end

    local partyId = HttpService:GenerateGUID(false)

    PartyManager.Parties[LeaderPlayer] = {
        PartyID = partyId,
        Members = Members
    }

    return partyId
end

-- QueueParty(): Queues all members of a party
-- @param LeaderPlayer: The player who created the party
-- @param mode: The mode the player should be queued to
-- @param subMode: The Sub-Mode the player should be queued to
-- @param ELO?: Optional ELO the party has
function PartyManager.QueueParty(LeaderPlayer: Player, mode: string, subMode: string, ELO: number?): ()
    local party = PartyManager.Parties[LeaderPlayer]

    if not party then
        warn("QueueParty(): LeaderPlayer not found")
        return
    end

    local matchSize = PartyManager.Modules.Settings.SubModes[subMode] and PartyManager.Modules.Settings.SubModes[subMode].MatchSize or 2
    local maxPartySize = math.floor(matchSize / 2)
    local totalPartySize = #party.Members + 1

    if totalPartySize > maxPartySize then
        warn(`QueueParty(): Party size {totalPartySize} exceeds max {maxPartySize} for {subMode}`)
        return
    end

    for _, member in { LeaderPlayer, table.unpack(party.Members) } do
        if PartyManager.Modules.QueueManager.IsPlayerQueued(member) then
            warn(`QueueParty(): {member.Name} is already queued, cannot queue party`)
            return
        end
    end

    PartyManager.Modules.QueueManager.QueueParty(LeaderPlayer, party.Members, mode, subMode, party.PartyID, ELO)
end

-- StopParty(): Stop queues all members of a party
-- @param LeaderPlayer: The player who created the party
function PartyManager.StopParty(LeaderPlayer: Player): ()
    local party = PartyManager.Parties[LeaderPlayer]

    if not party then
        warn("StopParty(): LeaderPlayer not found")
        return
    end

    PartyManager.Modules.QueueManager.StopQueue({ LeaderPlayer, table.unpack(party.Members) })
end

-- DeleteParty(): Deletes a party, also stopping everyone currently queueing (Including the leader)
-- @param LeaderPlayer: The player who created the party
function PartyManager.DeleteParty(LeaderPlayer: Player): ()
    local party = PartyManager.Parties[LeaderPlayer]

    if not party then
        warn("DeleteParty(): LeaderPlayer not found")
        return
    end

    PartyManager.Modules.QueueManager.StopQueue({ LeaderPlayer, table.unpack(party.Members) })

    PartyManager.Parties[LeaderPlayer] = nil
end

--=========================
-- // MEMBER API
--=========================

-- AddPlayer(): Adds a player to a leaders party
-- @param LeaderPlayer: The player who created the party
-- @param NewMember: The player to add to the party
function PartyManager.AddPlayer(LeaderPlayer: Player, NewMember: Player): ()
	local party = PartyManager.Parties[LeaderPlayer]

    if not party then
		warn("AddPlayer(): LeaderPlayer not found")
		return
	end

	if table.find(party.Members, NewMember) then
		warn("AddPlayer(): Player already in the party")
		return
	end

	if LeaderPlayer == NewMember then
		warn("AddPlayer(): Cannot add leader as member")
		return
	end

	table.insert(party.Members, NewMember)
end

-- RemovePlayer(): Removes a player from a leaders party. If the leader is the only one in the party, it deletes the party
-- @param LeaderPlayer: The player who created the party
-- @param Member: The player to remove from the party
function PartyManager.RemovePlayer(LeaderPlayer: Player, Member: Player): ()
	local party = PartyManager.Parties[LeaderPlayer]

	if not party then
		warn("RemovePlayer(): LeaderPlayer not found")
		return
	end

	local index = table.find(party.Members, Member)

    if index then
        table.remove(party.Members, index)
    end

	if #party.Members == 0 then
		PartyManager.DeleteParty(LeaderPlayer)
	end
end

--=========================
-- // UTILITY API
--=========================

-- GetPartyMembers(): Returns the members of a leaders party
-- @param LeaderPlayer: The player who created the party
-- @return { Player }?: The members
function PartyManager.GetPartyMembers(LeaderPlayer: Player): { Player }?
	local party = PartyManager.Parties[LeaderPlayer]

	return party and party.Members or nil
end

-- IsInParty(): Checks if a player is in any party
-- @param PlayerToCheck: The player to check
-- @return boolean: If they're in the party
function PartyManager.IsInParty(PlayerToCheck: Player): boolean
	if PartyManager.Parties[PlayerToCheck] then
		return true
	end

	for _, partyData in PartyManager.Parties do
		if table.find(partyData.Members, PlayerToCheck) then
			return true
		end
	end

	return false
end

-- GetLeader(): Returns the leader of the party a player belongs to
-- @param PlayerToCheck: The player to check
-- @return Player?: The leader, if found
function PartyManager.GetLeader(PlayerToCheck: Player): Player?
	if PartyManager.Parties[PlayerToCheck] then
		return PlayerToCheck
	end

	for leader, partyData in PartyManager.Parties do
		if table.find(partyData.Members, PlayerToCheck) then
			return leader
		end
	end

	return nil
end

-- GetPartySize(): Returns total size of a leaders party (leader + members)
-- @param LeaderPlayer: The player who created the party
-- @return number: The party size
function PartyManager.GetPartySize(LeaderPlayer: Player): number
	local party = PartyManager.Parties[LeaderPlayer]

    return party and (#party.Members + 1) or 1
end

-- CreateTeamsFromPartyMap(): Automatically creates parties from a server's party data
-- @param serverParties: The party data for this server
function PartyManager.CreateTeamsFromPartyMap(serverParties: { [string]: { Members: { number }, Leader: number? } }): ()
	if not serverParties then
		warn("CreateTeamsFromPartyMap(): No party data provided")
		return
	end

	for partyId, partyData in serverParties do
		if not partyData.Leader then
			warn("CreateTeamsFromPartyMap(): Party missing leader", partyId)
			continue
		end

		local leaderPlayer = PartyManager.PlayerLookup[partyData.Leader]

		if not leaderPlayer then
			warn("CreateTeamsFromPartyMap(): Leader not found", partyData.Leader)
			continue
		end

		local members = {}

		for _, userId in partyData.Members do
			local player = PartyManager.PlayerLookup[userId]

			if player and player ~= leaderPlayer then
				table.insert(members, player)
			end
		end

		PartyManager.Parties[leaderPlayer] = {
			PartyID = partyId,
			Members = members
		}
	end
end

--=========================
-- // CLEANUP
--=========================

-- Cleanup(): Cleans up all of the parties
function PartyManager.Cleanup(): ()
    PartyManager.Parties = {}
end

--=========================
-- // INIT
--=========================

SetupPlayerLookup()

CleanupInit()

return PartyManager


--!strict

local QueueManager = {}

--=========================
-- // SERVICES
--=========================

local MemoryStoreService = game:GetService("MemoryStoreService")
local HttpService = game:GetService("HttpService")

--=========================
-- // TYPES
--=========================

local Types = require(script.Parent.Parent.helpers.Types)

--=========================
-- // MODULES
--=========================

QueueManager.Modules = {
	Settings = require(script.Parent.Parent.helpers.Settings),
	SharedData = require(script.Parent.SharedData),
	PlayerLocation = require(script.Parent.Parent.helpers.PlayerLocation)
}

--=========================
-- // PRIVATE API
--=========================

-- generatePlayerQueueData(): Returns data based on multiple parameters
-- @param player: The Player object to generate queue data for
-- @param partyID?: The ID of the party, if any
-- @param isLeader: If the player is the leader of their party
-- @param continent: The Continent of the player (Use PlayerLocation)
-- @param ELO: The ELO of the player
-- @return key, encoded? (JSON Encoded PlayerQueueData)
local function generatePlayerQueueData(player: Player, partyID: string?, isLeader: boolean, continent: string, ELO: number): (string, string?)
	local timestamp = tick()
	local key = HttpService:GenerateGUID(false)

	local data: Types.PlayerQueueData = {
        PartyID = partyID,
        IsPartyLeader = isLeader,
		UserId = player.UserId,
		ELO = ELO,
		JoinTime = timestamp,
		ServerId = QueueManager.Modules.Settings.Information.SERVER_ID,
		Continent = continent
	}

	local success, encoded = pcall(HttpService.JSONEncode, HttpService, data)

	if not success then
		warn("generatePlayerQueueData(): Failed to JSONEncode player queue data for " .. tostring(player.UserId) .. ": " .. encoded)
		return key, nil
	end

	return key, encoded
end

--=========================
-- // PUBLIC API
--=========================

-- QueuePlayer(): Queues the player(s) up into matchmaking
-- @param playerOrList: The player(s) to queue. Put a list of players to bulk queue them or just add one player
-- @param mode: The mode the player should be queued to (Match.QueueManager.Modules.Settings.Modes)
-- @param subMode: The Sub-Mode the player should be queued to (Keys of Match.QueueManager.Modules.Settings.SubModes)
-- @param ELO?: Optional ELO the player has
function QueueManager.QueuePlayer(playerOrList: Player | { Player }, mode: string, subMode: string, ELO: number?): ()
	assert(table.find(QueueManager.Modules.Settings.Modes, mode), "'mode' doesn't exist in Settings.Modes")
	assert(QueueManager.Modules.Settings.SubModes[subMode], "'subMode' doesn't exist in Settings.SubModes")

	local players: { Player } = typeof(playerOrList) == "Instance" and { playerOrList } or playerOrList :: { Player }

	for _, player in players do
		if QueueManager.Modules.SharedData.playerQueueKeys[player.UserId] then
			warn("QueuePlayer(): " .. player.Name .. " is already in a queue")
			continue
		end

		local playerContinent = QueueManager.Modules.PlayerLocation.GetPlayerContinent(player)

        if not playerContinent then
            warn(`QueuePlayer(): Couldn't find {player}'s continent, skipping`)
            continue
        end

		local key, data = generatePlayerQueueData(player, nil, false, playerContinent, ELO or 0)

		local success, err = pcall(function()
			QueueManager.GetQueue(mode, subMode):SetAsync(key, data, QueueManager.Modules.Settings.Matches.MATCHMAKING_DURATION)
		end)

		if success then
			QueueManager.Modules.SharedData.playerQueueKeys[player.UserId] = QueueManager.Modules.SharedData.playerQueueKeys[player.UserId] or {}
			QueueManager.Modules.SharedData.playerQueueKeys[player.UserId][mode] = QueueManager.Modules.SharedData.playerQueueKeys[player.UserId][mode] or {}
			QueueManager.Modules.SharedData.playerQueueKeys[player.UserId][mode][subMode] = key

			if not QueueManager.Modules.SharedData.localQueueCounts[mode] then
				QueueManager.Modules.SharedData.localQueueCounts[mode] = {}
			end
			QueueManager.Modules.SharedData.localQueueCounts[mode][subMode] = (QueueManager.Modules.SharedData.localQueueCounts[mode][subMode] or 0) + 1

			print("QueuePlayer(): Queued player", player.Name, "for", mode, subMode, "mode. Region:", playerContinent)
		else
			warn("QueuePlayer(): Failed to queue player:", err)
            continue
		end
	end
end

-- QueueParty(): Queues a party into matchmaking
-- @param leaderPlayer: The player who created the party
-- @param partyMembers: The party members
-- @param mode: The mode the player should be queued to (Match.QueueManager.Modules.Settings.Modes)
-- @param subMode: The Sub-Mode the player should be queued to (Keys of Match.QueueManager.Modules.Settings.SubModes)
-- @param partyID: The party ID from PartyManager
-- @param ELO?: Optional ELO the player has
function QueueManager.QueueParty(leaderPlayer: Player, partyMembers: { Player }, mode: string, subMode: string, partyID: string, ELO: number?): ()
	if QueueManager.IsPlayerQueued(leaderPlayer) then
		warn("QueueParty(): Leader already queued")
		return
	end

	assert(#partyMembers > 0, "QueueParty(): Party must have at least one member")
	assert(partyID, "QueueParty(): partyID is required")

	local leaderContinent = QueueManager.Modules.PlayerLocation.GetPlayerContinent(leaderPlayer)

	if not leaderContinent then
		warn(`QueueParty(): Couldn't find {leaderPlayer.Name}'s continent, aborting`)
		return
	end

	local queuedPlayers = {}
	local failedPlayers = {}

	for _, player in { leaderPlayer, table.unpack(partyMembers) } do
		local isLeader = player == leaderPlayer

		local key, data = generatePlayerQueueData(player, partyID, isLeader, leaderContinent, ELO or 0)

		if not data then
			warn(`QueueParty(): Failed to generate data for {player.Name}`)
			table.insert(failedPlayers, player)
			continue
		end

		local success, err = pcall(function()
			QueueManager.GetQueue(mode, subMode):SetAsync(key, data, QueueManager.Modules.Settings.Matches.MATCHMAKING_DURATION)
		end)

		if success then
			QueueManager.Modules.SharedData.playerQueueKeys[player.UserId] = QueueManager.Modules.SharedData.playerQueueKeys[player.UserId] or {}
			QueueManager.Modules.SharedData.playerQueueKeys[player.UserId][mode] = QueueManager.Modules.SharedData.playerQueueKeys[player.UserId][mode] or {}
			QueueManager.Modules.SharedData.playerQueueKeys[player.UserId][mode][subMode] = key

			QueueManager.Modules.SharedData.localQueueCounts[mode] = QueueManager.Modules.SharedData.localQueueCounts[mode] or {}
			QueueManager.Modules.SharedData.localQueueCounts[mode][subMode] = (QueueManager.Modules.SharedData.localQueueCounts[mode][subMode] or 0) + 1

			table.insert(queuedPlayers, player)
		else
			warn(`QueueParty(): Failed to queue {player.Name}: {err}`)
			table.insert(failedPlayers, player)
		end
	end

	if #failedPlayers > 0 then
		warn(`QueueParty(): {#failedPlayers} players failed to queue, rolling back party`)

		for _, player in queuedPlayers do
			QueueManager.StopQueue(player)
		end

		return
	end

	print((`QueueParty(): Queued party led by %s with %d members for %s / %s`):format(
		leaderPlayer.Name,
		#partyMembers,
		mode,
		subMode
	))
end

-- StopQueue(): Removes the player(s) from all queues across every mode and subMode
-- @param playerOrList: The player(s) to remove from queues. Pass a single player or a list of players for bulk removal
-- @return boolean: True if all players were successfully removed from their queues, false if any failures occurred
function QueueManager.StopQueue(playerOrList: Player | { Player }): boolean
	local players: { Player } = typeof(playerOrList) == "Instance" and { playerOrList } or playerOrList :: { Player }
	local allSuccess = true
	local lockMap = QueueManager.GetLockMap()

	for _, player in players do
		if not player or not player.UserId then
			allSuccess = false
			continue
		end

		local playerQueues = QueueManager.Modules.SharedData.playerQueueKeys[player.UserId]

		if not playerQueues then
			pcall(function()
				lockMap:RemoveAsync(tostring(player.UserId))
			end)

			QueueManager.Modules.SharedData.playerQueueKeys[player.UserId] = nil

			continue
		end

		for mode, subModes in playerQueues do
			for subMode, keyToRemove in subModes do
				local success = pcall(function()
					QueueManager.GetQueue(mode, subMode):RemoveAsync(keyToRemove)
				end)

				if not success then
					warn("StopQueue(): Failed to remove from queue:", mode, subMode)
					allSuccess = false
					continue
				end

				if not QueueManager.Modules.SharedData.localQueueCounts[mode] then
					QueueManager.Modules.SharedData.localQueueCounts[mode] = {}
				end
				QueueManager.Modules.SharedData.localQueueCounts[mode][subMode] = math.max(0, (QueueManager.Modules.SharedData.localQueueCounts[mode][subMode] or 0) - 1)
			end
		end

		pcall(function()
			lockMap:RemoveAsync(tostring(player.UserId))
		end)

		QueueManager.Modules.SharedData.playerQueueKeys[player.UserId] = nil
	end

	return allSuccess
end

--=========================
-- // QUEUE DATA API
--=========================

-- IsPlayerQueued(): Checks if a player is queued in a certain subMode's mode
-- @param player: The player to check
-- @param mode: The mode the player should be queued to (Match.QueueManager.Modules.Settings.Modes)
-- @param subMode: The Sub-Mode the player should be queued to (Keys of Match.QueueManager.Modules.Settings.SubModes)
-- @return boolean: If the player is queued or not
function QueueManager.IsPlayerQueued(player: Player, mode: string?, subMode: string?): boolean
	local queues = QueueManager.Modules.SharedData.playerQueueKeys[player.UserId]

	if not queues then
		return false
	end

	if mode and subMode then
		return queues[mode] and queues[mode][subMode] ~= nil
	end

	return next(queues) ~= nil
end

-- GetQueuedPlayers(): Get all of the queued players in a subMode's mode
-- @param mode: The mode the player should be queued to (Match.QueueManager.Modules.Settings.Modes)
-- @param subMode: The Sub-Mode the player should be queued to (Keys of Match.QueueManager.Modules.Settings.SubModes)
-- @return { number }: A list of UserIds of the queued players
function QueueManager.GetQueuedPlayers(mode: string, subMode: string): { number }
	local players = {}
	local queue = QueueManager.GetQueue(mode, subMode)
	local matchSize = QueueManager.GetMatchSize(subMode)
	local batchSize = math.clamp(matchSize :: number * QueueManager.Modules.Settings.Matches.BATCH_SIZE, 1, 200)

	local success, result = pcall(function()
		return queue:GetRangeAsync(Enum.SortDirection.Ascending, batchSize)
	end)

	if success and result then
		for _, entry in result do
			local ok, data = pcall(HttpService.JSONDecode, HttpService, entry.value)

			if ok and data and data.UserId then
				table.insert(players, data.UserId)
			else
				warn("GetQueuedPlayers(): Skipped bad queue entry: " .. entry.key)
			end
		end
	else
		warn("GetQueuedPlayers(): Failed to get queue range for " .. mode .. "-" .. subMode)
	end

	return players
end

-- GetPlayerJoinTime(): Gets the players join time in a subMode's mode
-- @param player: The player to check
-- @param mode: The mode the player should be queued to (Match.QueueManager.Modules.Settings.Modes)
-- @param subMode: The Sub-Mode the player should be queued to (Keys of Match.QueueManager.Modules.Settings.SubModes)
-- @return number?: The join time of the player if found, otherwise nil
function QueueManager.GetPlayerJoinTime(player: Player, mode: string, subMode: string): number?
	local key = QueueManager.Modules.SharedData.playerQueueKeys[player.UserId]
		and QueueManager.Modules.SharedData.playerQueueKeys[player.UserId][mode]
		and QueueManager.Modules.SharedData.playerQueueKeys[player.UserId][mode][subMode]

	if not key then return nil end

	local queue = QueueManager.GetQueue(mode, subMode)

	local success, value = pcall(function()
		return queue:GetAsync(key)
	end)

	if success and value then
		local ok, data = pcall(HttpService.JSONDecode, HttpService, value)

		if ok and data and data.JoinTime then
			return data.JoinTime
		else
			warn("GetPlayerJoinTime(): Failed to decode join time for " .. tostring(player.UserId))
		end
	else
		warn("GetPlayerJoinTime(): Failed to get async for key: " .. key)
	end

	return nil
end

-- GetQueueCount(): Gets the local queue count in a subMode's mode
-- @param mode: The mode the player should be queued to (Match.QueueManager.Modules.Settings.Modes)
-- @param subMode: The Sub-Mode the player should be queued to (Keys of Match.QueueManager.Modules.Settings.SubModes)
-- @return number: The total number of players in the queue
function QueueManager.GetQueueCount(mode: string, subMode: string): number
	if not QueueManager.Modules.SharedData.localQueueCounts[mode] then
		return 0
	end

	return QueueManager.Modules.SharedData.localQueueCounts[mode][subMode] or 0
end

--=========================
-- // UTILITY API
--=========================

-- GetMatchSize(): Returns the total number of players required for a match in the given subMode
-- @param subMode: The subMode
-- @return number: totalPlayers
function QueueManager.GetMatchSize(subMode: string): number
	if QueueManager.Modules.Settings.SubModes[subMode] then
		return QueueManager.Modules.Settings.SubModes[subMode].totalPlayers
	end

	warn("GetMatchSize(): Unknown subMode:", subMode, "- using default size of 2")

	return 2
end

-- GetQueue(): Returns the MemoryStoreService SortedMap based on the mode and subMode
-- @param mode: The mode to get
-- @param subMode: The subMode of the mode
-- @return MemoryStoreSortedMap
function QueueManager.GetQueue(mode: string, subMode: string): MemoryStoreSortedMap
	if not QueueManager.Modules.SharedData.queueCache[mode] then
    	QueueManager.Modules.SharedData.queueCache[mode] = {}
	end

	if not QueueManager.Modules.SharedData.queueCache[mode][subMode] then
		QueueManager.Modules.SharedData.queueCache[mode][subMode] = {
			queue = MemoryStoreService:GetSortedMap("MatchQueue_" .. mode .. "_" .. subMode),
			lastUsed = tick()
		}
	else
		QueueManager.Modules.SharedData.queueCache[mode][subMode].lastUsed = tick()
	end

	return QueueManager.Modules.SharedData.queueCache[mode][subMode].queue
end

-- GetLockMap(): Returns MatchLocks
-- @return MemoryStoreSortedMap
function QueueManager.GetLockMap(): MemoryStoreSortedMap
	return MemoryStoreService:GetSortedMap("MatchLocks")
end

-- RunHeartbeat(): Runs cache cleanup on heartbeat
function QueueManager.RunHeartbeat(): ()
	local now = tick()

	for mode, subModes in QueueManager.Modules.SharedData.queueCache do
		for subMode, cacheData in subModes do
			if now - cacheData.lastUsed > QueueManager.Modules.Settings.Matches.CACHE_CLEANUP_INTERVAL then
				QueueManager.Modules.SharedData.queueCache[mode][subMode] = nil
			end
		end

		if next(QueueManager.Modules.SharedData.queueCache[mode]) == nil then
			QueueManager.Modules.SharedData.queueCache[mode] = nil
		end
	end

	QueueManager.Modules.SharedData.lastCacheCleanup = now
end

--=========================
-- // CLEANUP
--=========================

-- Cleanup(): Cleans up queue-specific data
function QueueManager.Cleanup(): ()
	QueueManager.Modules.SharedData.queueCache = {}
	QueueManager.Modules.SharedData.localQueueCounts = {}
	QueueManager.Modules.SharedData.playerQueueKeys = {}
end

return QueueManager


--!strict

--=========================
-- // MODULES
--=========================

local Signal = require(script.Parent.Parent.helpers.Signal)

--=========================
-- // TYPES
--=========================

local Types = require(script.Parent.Parent.helpers.Types)

--=========================
-- // MAIN
--=========================

local SharedData = {
	playerQueueKeys = {},
	localQueueCounts = {},
	isProcessingMatch = {},
	matchmakingTasks = {},
	queueCache = {},
	messagingConnections = {},
	lastCacheCleanup = 0,
	heartbeatConnection = nil,
	signal = Signal.new()
} :: Types.MatchData

return SharedData


--!strict

local TeleportHandler = {}

--=========================
-- // SERVICES
--=========================

local Players = game:GetService("Players")
local TeleportService = game:GetService("TeleportService")

--=========================
-- // TYPES
--=========================

local Types = require(script.Parent.Parent.helpers.Types)

--=========================
-- // MODULES
--=========================

TeleportHandler.Modules = {
	Settings = require(script.Parent.Parent.helpers.Settings),
	QueueManager = require(script.Parent.QueueManager)
}

--=========================
-- // PUBLIC API
--=========================

-- HandleTeleportRequest(): Handles a teleport request by teleporting players to a reserved private server for a cross-server match, with retries and backoff
-- @param data: Types.TeleportRequestData
function TeleportHandler.HandleTeleportRequest(data: Types.TeleportRequestData): ()
	local serverId = TeleportHandler.Modules.Settings.Information.SERVER_ID
	local playerIds = data.PlayersForServer[serverId]

	if not playerIds then return end

	local playersToTeleport = {}

	for _, userId in playerIds do
		local player = Players:GetPlayerByUserId(userId)

		if player then
			table.insert(playersToTeleport, player)
		end
	end

	if #playersToTeleport == 0 then return end

	print("HandleTeleportRequest(): Teleporting", #playersToTeleport, "players to cross-server match")

	local maxRetries = TeleportHandler.Modules.Settings.Matches.TELEPORT_RETRIES

	for attempt = 1, maxRetries do
		if #playersToTeleport == 0 then return end

		local success, err = pcall(function()
			TeleportService:TeleportToPrivateServer(
				data.PlaceId,
				data.ServerCode,
				playersToTeleport,
				nil,
				{ MatchId = data.MatchId, Mode = data.Mode, SubMode = data.SubMode, Parties = data.PartyMapping[serverId] }
			)
		end)

		if success then
			print(`HandleTeleportRequest(): Successfully teleported {#playersToTeleport} players on attempt {attempt}`)
			return
		else
			print(`HandleTeleportRequest(): Attempt {attempt} failed: {err}, retrying {#playersToTeleport} players`)
		end

		local stillValid = {}

		for _, player in playersToTeleport do
			if player.Parent == Players then
				table.insert(stillValid, player)
			end
		end

		playersToTeleport = stillValid

		if attempt < maxRetries and #playersToTeleport > 0 then
			task.wait(0.5 * (1 + attempt * 0.5))
		end
	end

	if #playersToTeleport > 0 then
		warn(`HandleTeleportRequest(): Failed to teleport {#playersToTeleport} players after {maxRetries} attempts`)

		local failedIds = {}

		for _, p in playersToTeleport do
			table.insert(failedIds, p.UserId)
		end

		TeleportHandler.UnlockPlayers(failedIds)
	end
end

-- ReserveMatchServer(): Reserves a server for the match
-- @return string?: Server code on success, nil on failure
function TeleportHandler.ReserveMatchServer(): string?
	local success, result = pcall(function()
		return TeleportService:ReserveServer(TeleportHandler.Modules.Settings.Information.PLACE_ID)
	end)

	if not success or not result then
		warn("ReserveMatchServer(): Failed to reserve server:", result)
		return nil
	end

	return result
end

-- UnlockPlayers(): Unlocks players in the lockMap to allow re-queuing or other operations
-- @param playerIds: The UserIDs of players to unlock
function TeleportHandler.UnlockPlayers(playerIds: { number }): ()
	local lockMap = TeleportHandler.Modules.QueueManager.GetLockMap()

	for _, userId in playerIds do
		pcall(function()
			lockMap:RemoveAsync(tostring(userId))
		end)
	end
end

return TeleportHandler


-- 7kayoh
-- Demo.lua
-- 07 February 2022

-- Tests out every UI component in the library

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local LocalPlayer = Players.LocalPlayer
local PlayerGui = LocalPlayer.PlayerGui
local Packages = ReplicatedStorage.Packages

local Lydie = require(Packages.Lydie)
local Fusion = require(Packages.Fusion)

local Components = Lydie.Components
local Modules = Lydie.Modules

local Scheme = Modules.Scheme
local Icons = Modules.Icons

local AppTabBar = Components.Views.AppTabBar
local BottomAppBar = Components.Views.BottomAppBar
local TopAppBar = Components.Views.TopAppBar
local Window = Components.Views.Window

local AppBarButton = Components.Controls.AppBarButton
local AppTabBarButton = Components.Controls.AppTabBarButton
local Checkbox = Components.Controls.Checkbox
local IconButton = Components.Controls.IconButton
local IconTextButton = Components.Controls.IconTextButton
local TextButton = Components.Controls.TextButton
local Toggle = Components.Controls.Toggle
local TextField = Components.Controls.TextField
local Slider = Components.Controls.Slider
local ColorPicker = Components.Controls.ColorPicker

local themeIcon = Fusion.Value(Icons.sun)
local windowVisible = Fusion.Value(true)

local tabStates = {
	Home = Fusion.Value(true),
	Settings = Fusion.Value(false),
	About = Fusion.Value(false),
}

local accentNames = {
	"Red", "Green", "Blue", "Amber", "Orange", "Pink", "Purple", "Indigo", "Teal", "Mint",
}

local function tabOnClick(state)
	state:set(true)
	for _, tabState in pairs(tabStates) do
		if tabState ~= state then
			tabState:set(false)
		end
	end
end

Fusion.New("ScreenGui")({
	Parent = PlayerGui,

	[Fusion.Children] = {
		Window({
			AspectRatio = Fusion.Value(1.5),
			Visible = windowVisible,

			[Fusion.Children] = {
				TopAppBar({
					Icon = Icons.info,
					Subtitle = Fusion.Value("Subtitle"),
					Title = Fusion.Value("Top App Bar"),
					ZIndex = 2,

					[Fusion.Children] = {
						Fusion.New("UIPadding")({
							PaddingRight = UDim.new(0, 5),
						}),

						AppBarButton({
							Icon = Icons["x-circle"],
							LayoutOrder = 2,

							OnClick = function()
								windowVisible:set(false)
							end,
						}),

						AppBarButton({
							Icon = themeIcon,
							LayoutOrder = 1,

							OnClick = function()
								if Scheme.Current == Scheme.Schemes.Dark then
									themeIcon:set(Icons.moon)
									Scheme.Switch(Scheme.Schemes.Light)
								elseif Scheme.Current == Scheme.Schemes.Light then
									themeIcon:set(Icons.sun)
									Scheme.Switch(Scheme.Schemes.Dark)
								end
							end,
						}),
					},
				}),

				Fusion.New("Frame")({
					BackgroundTransparency = 1,

					Position = UDim2.fromOffset(0, 50),
					Size = UDim2.new(1, 0, 1, -100),

					[Fusion.Children] = {
						Fusion.New("UIListLayout")({
							FillDirection = Enum.FillDirection.Vertical,
							HorizontalAlignment = Enum.HorizontalAlignment.Center,
							Padding = UDim.new(0, 15),
							SortOrder = Enum.SortOrder.LayoutOrder,
							VerticalAlignment = Enum.VerticalAlignment.Center,
						}),

						Fusion.New("Frame")({
							BackgroundTransparency = 1,

							Size = UDim2.new(1, 0, 0, 50),

							[Fusion.Children] = {
								Fusion.New("UIListLayout")({
									FillDirection = Enum.FillDirection.Horizontal,
									HorizontalAlignment = Enum.HorizontalAlignment.Center,
									Padding = UDim.new(0, 15),
									SortOrder = Enum.SortOrder.LayoutOrder,
									VerticalAlignment = Enum.VerticalAlignment.Center,
								}),

								TextButton({
									Text = Fusion.Value("Random Theme Color"),

									OnClick = function()
										print("TextButton Clicked")
										Scheme.AccentName:set(accentNames[math.random(1, #accentNames)])
									end,
								}),

								IconTextButton({
									Icon = Icons.info,
									Text = Fusion.Value("Random Scheme"),

									OnClick = function()
										print("IconTextButton Clicked")
										local tbl = {"Dark", "Light", "SolarizedDark"}
										Scheme.Switch(tbl[math.random(1, 3)])
									end,
								}),
							},
						}),

						Fusion.New("Frame")({
							BackgroundTransparency = 1,
							LayoutOrder = 2,

							Size = UDim2.new(1, 0, 0, 50),

							[Fusion.Children] = {
								Fusion.New("UIListLayout")({
									FillDirection = Enum.FillDirection.Horizontal,
									HorizontalAlignment = Enum.HorizontalAlignment.Center,
									Padding = UDim.new(0, 15),
									SortOrder = Enum.SortOrder.LayoutOrder,
									VerticalAlignment = Enum.VerticalAlignment.Center,
								}),

								IconTextButton({
									BackgroundColor = Scheme.GetAnimated(Scheme.Accent),
									BackgroundOpacity = 0,
									ForegroundColor = Scheme.GetAnimated(Scheme.Elevation[2]),
									Icon = Icons.info,
									Text = Fusion.Value("IconTextButton"),

									OnClick = function()
										print("IconTextButton Clicked")
									end,
								}),

								IconButton({
									BackgroundColor = Scheme.GetAnimated(Scheme.Accent),
									BackgroundOpacity = 0,
									ForegroundColor = Scheme.GetAnimated(Scheme.Elevation[2]),
									Icon = Icons["alert-circle"],
									LayoutOrder = 2,

									OnClick = function()
										print("IconButton Clicked")
									end,
								}),

								IconButton({
									BackgroundColor = Scheme.GetAnimated(Scheme.Accent),
									Icon = Icons.info,
									LayoutOrder = 3,

									OnClick = function()
										print("IconButton Clicked")
									end,
								}),
							},
						}),

						Fusion.New("Frame")({
							BackgroundTransparency = 1,
							LayoutOrder = 3,

							Size = UDim2.new(1, 0, 0, 24),

							[Fusion.Children] = {
								Fusion.New("UIListLayout")({
									FillDirection = Enum.FillDirection.Horizontal,
									HorizontalAlignment = Enum.HorizontalAlignment.Center,
									Padding = UDim.new(0, 15),
									SortOrder = Enum.SortOrder.LayoutOrder,
									VerticalAlignment = Enum.VerticalAlignment.Center,
								}),

								Toggle({
									OnClick = function(state)
										state:set(not state:get())
										print("Toggle Clicked", state:get())
									end,
								}),

								Checkbox({
									LayoutOrder = 2,

									OnClick = function(state)
										state:set(not state:get())
										print("Checkbox Clicked", state:get())
									end,
								}),

								TextField({
									LayoutOrder = 3,

									OnTextChanged = function(text)
										print("Changed to: " .. text)
									end,
								}),
							},
						}),

						Fusion.New("Frame")({
							BackgroundTransparency = 1,
							LayoutOrder = 4,

							Size = UDim2.new(1, 0, 0, 24),

							[Fusion.Children] = {
								Fusion.New("UIListLayout")({
									FillDirection = Enum.FillDirection.Horizontal,
									HorizontalAlignment = Enum.HorizontalAlignment.Center,
									Padding = UDim.new(0, 15),
									SortOrder = Enum.SortOrder.LayoutOrder,
									VerticalAlignment = Enum.VerticalAlignment.Center,
								}),

								Slider({}),
							},
						}),

						Fusion.Hydrate(ColorPicker {}) {
							Size = UDim2.fromOffset(150, 0),
						}
					},
				}),

				BottomAppBar({
					ZIndex = 2,

					[Fusion.Children] = {
						AppTabBar({
							[Fusion.Children] = {
								AppTabBarButton({
									Icon = Icons.home,
									State = tabStates.Home,
									OnClick = tabOnClick,
								}),

								AppTabBarButton({
									Icon = Icons.sliders,
									State = tabStates.Settings,
									OnClick = tabOnClick,
								}),

								AppTabBarButton({
									Icon = Icons.info,
									State = tabStates.About,
									OnClick = tabOnClick,
								}),
							},
						}),
					},
				}),
			},
		}),
	},
})

if Scheme.Current == Scheme.Schemes.Dark then
	themeIcon:set(Icons.sun)
else
	themeIcon:set(Icons.moon)
end


-- 7kayoh
-- AppBarButton.lua
-- 20 February 2022

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules

local Icons = require(Modules.Icons)
local Scheme = require(Modules.Scheme)
local IconButton = require(Components.Controls.IconButton)

return function(props)
	return IconButton({
		_iconSize = UDim2.fromScale(0.5, 0.5),
		BackgroundOpacity = 1,
		Icon = props.Icon or Icons["loader-2"],
		LayoutOrder = props.LayoutOrder,
		ForegroundColor = Scheme.GetAnimated(Scheme.Color.Text.Secondary),

		Size = UDim2.fromScale(0.8, 0.8),

		OnClick = props.OnClick,
	})
end


-- 7kayoh
-- AppTabBarButton.lua
-- 21 February 2022

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Constants = require(Modules.Constants)
local Icons = require(Modules.Icons)
local Scheme = require(Modules.Scheme)
local IconButton = require(Components.Controls.IconButton)

return function(props)
	props.State = props.State or Fusion.Value(false)
	return IconButton({
		_iconSize = UDim2.fromScale(0.5, 0.5),
		BackgroundOpacity = 1,
		Icon = props.Icon or Icons["loader-2"],
		LayoutOrder = props.LayoutOrder,
		ForegroundColor = Scheme.GetAnimated(Fusion.Computed(function()
			return props.State:get() and Scheme.Accent:get() or Scheme.Color.Text.Secondary:get()
		end)),

		Size = UDim2.fromScale(0.8, 0.8),

		OnClick = function()
			if props.OnClick then
				props.OnClick(props.State)
			end
		end,

		[Fusion.Children] = {
			Fusion.New("ImageLabel")({
				BackgroundTransparency = 1,
				Image = Constants.Rounded.Id,
				ImageColor3 = Scheme.GetAnimated(Scheme.Accent),
				ScaleType = Enum.ScaleType.Slice,
				SliceCenter = Constants.Rounded.Center,
				SliceScale = Constants.RoundedValues[8],
				Visible = props.State,

				Position = UDim2.new(0, 0, 1.2, -6),
				Size = UDim2.new(1, 0, 0, 8),
			}),
		},
	})
end


-- 7kayoh
-- TextButton.lua
-- 10 February 2022

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Constants = require(Modules.Constants)
local Scheme = require(Modules.Scheme)

local Children = Fusion.Children
local New = Fusion.New
local OnEvent = Fusion.OnEvent

return function(props)
	local layerOpacity = Fusion.Value(1)

	return New("ImageButton")({
		BackgroundTransparency = 1,
		Image = Constants.Rounded.Id,
		ImageColor3 = props.BackgroundColor or Scheme.GetAnimated(Scheme.Color.Text.Primary),
		ImageTransparency = props.BackgroundOpacity or 0.85,
		ScaleType = Enum.ScaleType.Slice,
		SliceCenter = Constants.Rounded.Center,
		SliceScale = props.RoundedValue or Constants.RoundedValues[8],
		LayoutOrder = props.LayoutOrder or 0,
		ZIndex = props.ZIndex,

		AnchorPoint = props.AnchorPoint or Vector2.new(0, 0),
		Position = props.Position or UDim2.fromScale(0, 0),
		Size = props.Size or UDim2.fromOffset(200, 50),

		[Children] = {
			New("ImageLabel")({
				BackgroundTransparency = 1,
				Image = Constants.Rounded.Id,
				ImageColor3 = Color3.new(),
				ImageTransparency = Fusion.Tween(layerOpacity, TweenInfo.new(0.15)),
				LayoutOrder = props.LayoutOrder or 1,
				ScaleType = Enum.ScaleType.Slice,
				SliceCenter = Constants.Rounded.Center,
				SliceScale = props.RoundedValue or Constants.RoundedValues[8],
				Visible = true,
				ZIndex = 2,

				Size = UDim2.fromScale(1, 1),
			}),

			props[Children],
		},

		[OnEvent("MouseEnter")] = function()
			layerOpacity:set(0.85)
		end,

		[OnEvent("MouseLeave")] = function()
			layerOpacity:set(1)
		end,

		[OnEvent("MouseButton1Down")] = function()
			layerOpacity:set(0.7)
			if props.OnClickDown then
				props.OnClickDown()
			end
		end,

		[OnEvent("InputEnded")] = function(input)
			if table.find({Enum.UserInputType.MouseButton1, Enum.UserInputType.Touch}, input.UserInputType) then
				layerOpacity:set(1)
				if props.OnClick then
					props.OnClick()
				end
			end
		end,
	})
end


-- 7kayoh
-- Checkbox.lua
-- 24 March 2022

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Icons = require(Modules.Icons)
local Scheme = require(Modules.Scheme)
local BaseButton = require(Components.Controls.BaseButton)

local Children = Fusion.Children
local New = Fusion.New
local Value = Fusion.Value
local Computed = Fusion.Computed

return function(props)
	props.Toggled = Value(false)

	local component = BaseButton({
		BackgroundColor = Scheme.GetAnimated(Computed(function()
			if props.Toggled:get() then
				return Scheme.Accent:get()
			else
				return Scheme.Color.Text.Primary:get()
			end
		end)),
		BackgroundOpacity = Scheme.GetAnimated(Computed(function()
			if props.Toggled:get() then
				return 0
			else
				return 0.6
			end
		end)),
		ZIndex = props.ZIndex or 1,

		Size = UDim2.fromOffset(24, 24),
		Position = props.Position or UDim2.fromOffset(0, 0),

		[Children] = {
			New("ImageLabel")({
				BackgroundTransparency = 1,
				Image = Icons.check,
				ImageColor3 = Scheme.GetAnimated(Scheme.Elevation[2]),
				ImageTransparency = Scheme.GetAnimated(Fusion.Computed(function()
					return props.Toggled:get() and 0 or 1
				end)),

				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.fromScale(0.5, 0.5),
				Size = UDim2.fromScale(0.7, 0.7),
			}),
		},

		OnClick = function()
			if props.OnClick then
				props.OnClick(props.Toggled)
			else
				props.Toggled:set(not props.Toggled:get())
			end
		end,
	})

	return component
end


-- 7kayoh
-- ColorPicker.lua
-- 24 Apr 2024

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Slider = require(Components.Controls.Slider)
local Constants = require(Modules.Constants)
local Scheme = require(Modules.Scheme)

local Children = Fusion.Children
local Computed = Fusion.Computed
local Hydrate = Fusion.Hydrate
local New = Fusion.New
local Value = Fusion.Value

return function(props)
    local hueValue = props.HueValue or Value(0)
    local satValue = props.SatValue or Value(0)
    local briValue = props.BriValue or Value(0)
    local colorValue = Computed(function()
        return Color3.fromHSV(hueValue:get(), satValue:get(), briValue:get())
    end)

    local function pickerSlider(text, color, value, layoutOrder)
        local sliderComp = Slider {
            GradientColor = color,
            Value = value,
            Stepper = Value(1),
        }

        return New "Frame" {
            BackgroundTransparency = 1,
            AutomaticSize = Enum.AutomaticSize.Y,
            Size = UDim2.fromScale(1, 0),
            LayoutOrder = layoutOrder,

            [Children] = {
                New "UIListLayout" {
                    Padding = UDim.new(0, 20),
                    FillDirection = Enum.FillDirection.Horizontal,
                    SortOrder = Enum.SortOrder.LayoutOrder,
                    VerticalAlignment = Enum.VerticalAlignment.Center,
                    HorizontalAlignment = Enum.HorizontalAlignment.Center,
                },

                New "UIPadding" {
                    PaddingRight = UDim.new(0, 5),
                },

                New "TextLabel" {
                    BackgroundTransparency = 1,
                    AutomaticSize = Enum.AutomaticSize.XY,
                    Font = Scheme.Typography.Secondary.Font,
                    Text = text,
                    TextColor3 = Scheme.GetAnimated(Scheme.Color.Text.Secondary),
                    TextXAlignment = Enum.TextXAlignment.Right,
                    TextSize = Scheme.Typography.Secondary.Size,
                    LayoutOrder = 1,
                },

                Hydrate(sliderComp) {
                    ImageColor3 = Color3.new(1, 1, 1),
                    ImageTransparency = 0,
                    Size = UDim2.new(1, -45, 0, 8),
                    LayoutOrder = 2
                }
            }
        }
    end

    return New "Frame" {
        AutomaticSize = Enum.AutomaticSize.Y,
        BackgroundTransparency = 1,

        [Children] = {
            New "UIListLayout" {
                Padding = UDim.new(0, 15),
                SortOrder = Enum.SortOrder.LayoutOrder,
            },

            New "ImageLabel" {
                BackgroundTransparency = 1,
                Image = Constants.Rounded.Id,
                ImageColor3 = colorValue,
                ScaleType = Enum.ScaleType.Slice,
                SliceCenter = Constants.Rounded.Center,
                SliceScale = Constants.RoundedValues[8],

                Size = UDim2.new(1, 0, 0, 80),
                LayoutOrder = 1,
            },

            pickerSlider("H", Computed(function()
                return ColorSequence.new({
                    ColorSequenceKeypoint.new(0, Color3.fromHSV(0, satValue:get(), briValue:get())),
                    ColorSequenceKeypoint.new(0.166, Color3.fromHSV(0.166, satValue:get(), briValue:get())),
                    ColorSequenceKeypoint.new(0.333, Color3.fromHSV(0.333, satValue:get(), briValue:get())),
                    ColorSequenceKeypoint.new(0.5, Color3.fromHSV(0.5, satValue:get(), briValue:get())),
                    ColorSequenceKeypoint.new(0.666, Color3.fromHSV(0.666, satValue:get(), briValue:get())),
                    ColorSequenceKeypoint.new(0.833, Color3.fromHSV(0.833, satValue:get(), briValue:get())),
                    ColorSequenceKeypoint.new(1, Color3.fromHSV(1, satValue:get(), briValue:get()))
                })
            end), hueValue, 2),

            pickerSlider("S", Computed(function()
                return ColorSequence.new({
                    ColorSequenceKeypoint.new(0, Color3.fromHSV(0, 0, briValue:get())),
                    ColorSequenceKeypoint.new(1, Color3.fromHSV(hueValue:get(), 1, briValue:get())),
                })
            end), satValue, 3),

            pickerSlider("V", Computed(function()
                return ColorSequence.new({
                    ColorSequenceKeypoint.new(0, Color3.fromHSV(0, 0, 0)),
                    ColorSequenceKeypoint.new(1, Color3.fromHSV(hueValue:get(), satValue:get(), 1)),
                })
            end), briValue, 4),
        }
    }
end


-- 7kayoh
-- IconButton.lua
-- 17 March 2022

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Scheme = require(Modules.Scheme)

local BaseButton = require(Components.Controls.BaseButton)

local New = Fusion.New
local Children = Fusion.Children

return function(props)
	props[Children] = props[Children] or {}

	table.insert(
		props[Children],
		New("UIAspectRatioConstraint")({
			AspectRatio = 1,
		})
	)

	table.insert(
		props[Children],
		New("ImageLabel")({
			BackgroundTransparency = 1,
			Image = props.Icon,
			ImageColor3 = props.ForegroundColor or props.BackgroundColor or Scheme.GetAnimated(
				Scheme.Color.Text.Primary
			),

			AnchorPoint = Vector2.new(0.5, 0.5),
			Position = UDim2.fromScale(0.5, 0.5),
			Size = props._iconSize or UDim2.fromScale(0.45, 0.45),
		})
	)

	return BaseButton(props)
end


-- 7kayoh
-- IconTextButton.lua
-- 17 March 2022
local TextService = game:GetService("TextService")

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Scheme = require(Modules.Scheme)
local BaseButton = require(Components.Controls.BaseButton)

local New = Fusion.New
local Children = Fusion.Children

return function(props)
	props[Children] = {
		New("Frame")({
			BackgroundTransparency = 1,

			Size = UDim2.fromScale(1, 1),

			[Children] = {
				New("UIListLayout")({
					Padding = UDim.new(0, 10),
					FillDirection = Enum.FillDirection.Horizontal,
					HorizontalAlignment = Enum.HorizontalAlignment.Center,
					VerticalAlignment = Enum.VerticalAlignment.Center,
					SortOrder = Enum.SortOrder.LayoutOrder,
				}),

				New("ImageLabel")({
					BackgroundTransparency = 1,
					Image = props.Icon,
					ImageColor3 = props.ForegroundColor or props.BackgroundColor or Scheme.GetAnimated(
						Scheme.Color.Text.Primary
					),
					LayoutOrder = props.IconLayoutOrder or 0,

					Size = UDim2.fromScale(0.45, 0.45),

					[Children] = {
						New("UIAspectRatioConstraint")({
							AspectRatio = 1,
						}),
					},
				}),

				New("TextLabel")({
					BackgroundTransparency = 1,
					Font = Scheme.Typography.Button.Font,
					LayoutOrder = 1,
					Text = props.Text,
					TextColor3 = props.ForegroundColor or props.BackgroundColor or Scheme.GetAnimated(
						Scheme.Color.Text.Primary
					),
					TextSize = Scheme.Typography.Button.Size,

					Size = Fusion.Computed(function()
						local textSize = TextService:GetTextSize(
							props.Text:get(),
							Scheme.Typography.Button.Size:get(),
							Scheme.Typography.Button.Font:get(),
							Vector2.new(math.huge, math.huge)
						)

						return UDim2.fromOffset(textSize.X, textSize.Y)
					end),
				}),
			},
		}),
	}

	return BaseButton(props)
end


-- 7kayoh
-- Slider.lua
-- 2 Apr 2022

local UserInputService = game:GetService("UserInputService")

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Scheme = require(Modules.Scheme)
local Constants = require(Modules.Constants)
local BaseButton = require(Components.Controls.BaseButton)
local ToolTip = require(Components.Views.ToolTip)

local New = Fusion.New
local Children = Fusion.Children
local Value = Fusion.Value
local Computed = Fusion.Computed
local Observer = Fusion.Observer
local Tween = Fusion.Tween

return function(props)
	local isListening = Value(false)
	props.Value = props.Value or Value(0)
	props.Stepper = props.Stepper or Value(5)
	local bar = BaseButton({
		Size = UDim2.new(0, 200, 0, 8),
		RoundedValue = 1,

		[Children] = {
			New "UISizeConstraint" {
				MaxSize = Vector2.new(math.huge, 8),
				MinSize = Vector2.new(0, 8),
			},

			New "UIGradient" {
				Color = props.GradientColor,
			},

			BaseButton({
				BackgroundColor = Scheme.GetAnimated(Scheme.Accent),
				BackgroundOpacity = 0,
				ZIndex = 3,

				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = Computed(function()
					return UDim2.fromScale(props.Value:get(), 0.5)
				end),
				Size = UDim2.new(3, 0, 3, 0),
				RoundedValue = 1,

				[Children] = {
					New("UIAspectRatioConstraint")({
						AspectRatio = 1,
					}),

					New("ImageLabel")({
						BackgroundTransparency = 1,
						ImageColor3 = Scheme.GetAnimated(Scheme.Elevation[4]),
						Image = Constants.Rounded.Id,
						ScaleType = Enum.ScaleType.Slice,
						SliceCenter = Constants.Rounded.Center,
						SliceScale = 1,

						AnchorPoint = Vector2.new(0.5, 0.5),
						Position = UDim2.fromScale(0.5, 0.5),
						Size = Tween(Computed(function()
                            if isListening:get() then
                                return UDim2.fromScale(0.6, 0.6)
                            else
                                return UDim2.fromScale(0.7, 0.7)
                            end
                        end), TweenInfo.new(0.3, Enum.EasingStyle.Back)),
					}),

					ToolTip({
						Text = props.Value,
						Visible = isListening,
					}),
				},

				OnClickDown = function()
					isListening:set(true)
				end,
				OnClick = function()
					isListening:set(false)
				end,
			}),
		},

		OnClickDown = function()
			isListening:set(true)
		end,
		OnClick = function()
			isListening:set(false)
		end,
	})

	local function getClosestStep(value)
		local step = props.Stepper:get()
		local closest = math.floor(value / step) * step
		return closest / 100
	end

	local function update(input)
		if isListening:get() then
			local percentage
			if
				table.find(
					{ Enum.UserInputType.MouseMovement, Enum.UserInputType.Touch, Enum.UserInputType.MouseButton1 },
					input.UserInputType
				)
			then
				local delta = input.Position.X - bar.AbsolutePosition.X
				percentage = delta / bar.AbsoluteSize.X
			end
			if percentage then
				percentage = getClosestStep(math.clamp(percentage, 0, 1) * 100)
				props.Value:set(percentage)
			end
		end
	end

	local connection = UserInputService.InputChanged:Connect(update)
	local connection2 = UserInputService.InputBegan:Connect(update)
	local disconnectObserver = Observer(props.Value):onChange(function()
		props.Value:set(getClosestStep(math.clamp(props.Value:get(), 0, 1) * 100))
	end)

	bar.Destroying:Connect(function()
		connection:Disconnect()
		connection2:Disconnect()
		disconnectObserver()
	end)

	return bar
end


-- 7kayoh
-- TextButton.lua
-- 17 March 2022

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Scheme = require(Modules.Scheme)
local BaseButton = require(Components.Controls.BaseButton)

local New = Fusion.New
local Children = Fusion.Children

return function(props)
	props[Children] = {
		New("TextLabel")({
			BackgroundTransparency = 1,
			Font = Scheme.Typography.Button.Font,
			Text = props.Text,
			TextColor3 = props.ForegroundColor or props.BackgroundColor or Scheme.GetAnimated(
				Scheme.Color.Text.Primary
			),
			TextSize = Scheme.Typography.Button.Size,

			AnchorPoint = Vector2.new(0.5, 0.5),
			Position = UDim2.fromScale(0.5, 0.5),
			Size = UDim2.fromScale(0, 1),
		}),
	}

	return BaseButton(props)
end


local TextService = game:GetService("TextService")
local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Constants = require(Modules.Constants)
local Scheme = require(Modules.Scheme)

local Children = Fusion.Children
local Computed = Fusion.Computed
local New = Fusion.New
local OnEvent = Fusion.OnEvent
local OnChange = Fusion.OnChange
local Value = Fusion.Value

return function(props)
	local buttonState = Value(0)
	local canvasPosition = Value(Vector2.new(0, 0))
	props.ObservedText = props.ObservedText or Value(props.Text or "")

	local padding = Computed(function()
		if buttonState:get() == 2 then
			return UDim.new(0, 2)
		else
			return UDim.new(0, 1)
		end
	end)

	local textBox = Value(New("TextBox")({
		BackgroundTransparency = 1,
		ClearTextOnFocus = false,
		Font = Scheme.Typography.Body.Font,
		PlaceholderText = props.Placeholder or "Click to type",
		Text = props.Text or "",
		TextColor3 = Scheme.GetAnimated(Scheme.Color.Text.Primary),
		TextSize = Scheme.Typography.Body.Size,
		TextXAlignment = Enum.TextXAlignment.Left,
		PlaceholderColor3 = Scheme.GetAnimated(Scheme.Color.Text.Tertiary),

		Size = UDim2.new(1, 0, 1, 0),

		[OnChange("Text")] = function(text)
			props.ObservedText:set(text)
			if props.OnTextChanged then
				props.OnTextChanged(text)
			end
		end,

		[OnChange("CursorPosition")] = function(value)
			canvasPosition:set(
				TextService:GetTextSize(
					string.sub(props.ObservedText:get(), 1, value),
					Scheme.Typography.Body.Size:get(),
					Scheme.Typography.Body.Font:get(),
					Vector2.new(math.huge, math.huge)
				)
			)
		end,

		[OnEvent("FocusLost")] = function()
			buttonState:set(0)
		end,

		[OnEvent("Focused")] = function()
			buttonState:set(2)
		end,
	}))

	return New("ImageButton")({
		BackgroundTransparency = 1,
		Image = Constants.Rounded.Id,
		ImageColor3 = Scheme.GetAnimated(Computed(function()
			if buttonState:get() == 1 then
				return Scheme.Color.Text.Secondary:get()
			elseif buttonState:get() == 2 then
				return props.ForegroundColor and props.ForegroundColor:get() or Scheme.Accent:get()
            else
                return Scheme.Color.Text.Tertiary:get()
            end
		end)),
		ScaleType = Enum.ScaleType.Slice,
		SliceCenter = Constants.Rounded.Center,
		SliceScale = Constants.RoundedValues[8],
		ZIndex = props.ZIndex or 1,
		LayoutOrder = props.LayoutOrder or 1,

		Size = props.Size or UDim2.fromOffset(200, 42),
		Position = props.Position or UDim2.fromScale(0, 0),

		[Children] = {
			New("UIPadding")({
				PaddingTop = padding,
				PaddingLeft = padding,
				PaddingRight = padding,
				PaddingBottom = padding,
			}),

			New("ImageLabel")({
				BackgroundTransparency = 1,
				Image = Constants.Rounded.Id,
				ImageColor3 = Scheme.GetAnimated(Scheme.Elevation[2]),
				ScaleType = Enum.ScaleType.Slice,
				SliceCenter = Constants.Rounded.Center,
				SliceScale = Computed(function()
					if buttonState:get() == 2 then
						return Constants.RoundedValues[6]
					else
						return Constants.RoundedValues[7]
					end
				end),
				ClipsDescendants = true,

				Size = UDim2.new(1, 0, 1, 0),

				[Children] = {
					New("ScrollingFrame")({
						BackgroundTransparency = 1,
						ScrollBarThickness = 0,
						ScrollingDirection = Enum.ScrollingDirection.X,
						ClipsDescendants = false,

						CanvasPosition = Fusion.Tween(canvasPosition, TweenInfo.new(0.1, Enum.EasingStyle.Quint)),
						CanvasSize = Fusion.Tween(
							Computed(function()
								return UDim2.fromOffset(canvasPosition:get().X, canvasPosition:get().Y)
							end),
							TweenInfo.new(0.1, Enum.EasingStyle.Quint)
						),
						Position = Computed(function()
							return UDim2.fromOffset(Scheme.Typography.Body.Size:get(), 0)
						end),
						Size = Computed(function()
							return UDim2.new(1, -Scheme.Typography.Body.Size:get() * 2, 1, 0)
						end),

						[Children] = textBox,
					}),
				},
			}),
		},

		[OnEvent("MouseEnter")] = function()
			if not textBox:get():IsFocused() then
				buttonState:set(1)
			end
		end,

		[OnEvent("MouseLeave")] = function()
			if not textBox:get():IsFocused() then
				buttonState:set(0)
			end
		end,

		[OnEvent("MouseButton1Up")] = function()
			if not textBox:get():IsFocused() then
				textBox:get():CaptureFocus()
			else
				buttonState:set(0)
			end
			if props.OnClick then
				props.OnClick()
			end
		end,
	})
end


-- 7kayoh
-- Toggle.lua
-- 18 February 2022

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Constants = require(Modules.Constants)
local Scheme = require(Modules.Scheme)
local BaseButton = require(Components.Controls.BaseButton)

local Children = Fusion.Children
local New = Fusion.New
local Value = Fusion.Value
local Computed = Fusion.Computed
local Tween = Fusion.Tween

return function(props)
	props.Toggled = Value(false)

	local component = BaseButton({
		BackgroundColor = Scheme.GetAnimated(Computed(function()
			if props.Toggled:get() then
				return Scheme.Accent:get()
			else
				return Scheme.Color.Text.Primary:get()
			end
		end)),
		BackgroundOpacity = Scheme.GetAnimated(Computed(function()
			if props.Toggled:get() then
				return 0
			else
				return 0.6
			end
		end)),
		RoundedValue = 1,
		ZIndex = props.ZIndex or 1,

		Size = UDim2.fromOffset(41, 24),
		Position = props.Position or UDim2.fromOffset(0, 0),

		[Children] = {
			New("Frame")({
				BackgroundTransparency = 1,

				Size = UDim2.fromScale(1, 1),

				[Children] = {
					New("ImageLabel")({
						BackgroundTransparency = 1,
						Image = Constants.Rounded.Id,
						ImageColor3 = Scheme.GetAnimated(Scheme.Elevation[2]),

						Size = UDim2.new(1, -6, 1, -6),
						Position = Tween(
							Computed(function()
								if props.Toggled:get() then
									return UDim2.new(0.5, 0, 0, 3)
								else
									return UDim2.new(0, 3, 0, 3)
								end
							end),
							TweenInfo.new(0.15, Enum.EasingStyle.Quint)
						),

						[Children] = {
							New("UIAspectRatioConstraint")({
								AspectRatio = 1,
							}),
						},
					}),
				},
			}),
		},

		OnClick = function()
			if props.OnClick then
				props.OnClick(props.Toggled)
			else
				props.Toggled:set(not props.Toggled:get())
			end
		end,
	})

	return component
end


-- 7kayoh
-- Divider.lua
-- 03 February 2022

--[[

    Divider {
        AnchorPoint: Vector2?,
        Size: UDim2?,
        Position: UDim2?,
        ZIndex: number?
    } -> Frame

--]]
local Components = script.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Scheme = require(Modules.Scheme)

local New = Fusion.New

return function(props)
	return New("Frame")({
		Name = "Divider",

		BackgroundColor3 = Scheme.GetAnimated(Scheme.Color.Divider),
		ZIndex = props.ZIndex or 1,

		Size = props.Size or UDim2.new(1, 0, 0, 1),
		Position = props.Position or UDim2.new(0, 0, 0, 0),
		AnchorPoint = props.AnchorPoint or Vector2.new(0, 0),
	})
end


-- 7kayoh
-- BottomAppTabBar.lua
-- 21 February 2022

local Components = script.Parent.Parent
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)

local Children = Fusion.Children
local New = Fusion.New

return function(props)
	return New("Frame")({
		BackgroundTransparency = 1,
		ZIndex = props.ZIndex or 1,

		Size = UDim2.fromScale(1, 1),
		ClipsDescendants = true,

		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				VerticalAlignment = Enum.VerticalAlignment.Center,
			}),

			props[Children],
		},
	})
end


-- 7kayoh
-- BottomAppBar.lua
-- 07 February 2022

--[[

    BottomAppBar {
        ZIndex: number?,

        [Fusion.Children]: {Instances}
    } -> Frame

--]]

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)

local Constants = require(Modules.Constants)
local Scheme = require(Modules.Scheme)
local Divider = require(Components.Divider)

local Children = Fusion.Children
local New = Fusion.New

return function(props)
	return New("Frame")({
		BackgroundTransparency = 1,
		ZIndex = props.ZIndex or 1,

		AnchorPoint = Vector2.new(0, 1),
		Position = UDim2.fromScale(0, 1),
		Size = UDim2.new(1, 0, 0, 50),

		[Children] = {
			Divider({
				ZIndex = 2,
			}),

			Content = New("ImageLabel")({
				BackgroundTransparency = 1,
				Image = Constants.BottomRounded.Id,
				ImageColor3 = Scheme.GetAnimated(Scheme.Elevation[4]),
				ScaleType = Enum.ScaleType.Slice,
				SliceCenter = Constants.BottomRounded.Center,
				SliceScale = Constants.RoundedValues[8],

				Size = UDim2.fromScale(1, 1),

				[Children] = props[Children],
			}),
		},
	})
end


-- 7kayoh
-- ToolTip.lua
-- 4 April 2022

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Constants = require(Modules.Constants)
local Scheme = require(Modules.Scheme)
local Shadow = require(Components.Shadow)

local Children = Fusion.Children
local New = Fusion.New
local Value = Fusion.Value
local Computed = Fusion.Computed
local OnChange = Fusion.OnChange

return function(props)
	local textSize = Value(Vector2.new(0, 0))
	local component = New("Frame")({
		BackgroundTransparency = 1,
		Visible = props.Visible or true,

		AnchorPoint = props.AnchorPoint or Vector2.new(0.5, 1),
		Position = props.Position or UDim2.new(0.5, 0, 0, -6),
		Size = Computed(function()
			return UDim2.fromOffset(textSize:get().X + 14, textSize:get().Y + 8)
		end),

		[Children] = {
			New("ImageLabel")({
				BackgroundTransparency = 1,
				Image = Constants.Rounded.Id,
				ImageColor3 = Scheme.GetAnimated(Scheme.Color.Divider),
				ScaleType = Enum.ScaleType.Slice,
				SliceCenter = Constants.Rounded.Center,
				SliceScale = Constants.RoundedValues[8],

				Size = UDim2.fromScale(1, 1),

				[Children] = {
					New("ImageLabel")({
						BackgroundTransparency = 1,
						Image = Constants.Rounded.Id,
						ImageColor3 = Scheme.GetAnimated(Scheme.Elevation[4]),
						ScaleType = Enum.ScaleType.Slice,
						SliceCenter = Constants.Rounded.Center,
						SliceScale = Constants.RoundedValues[7],

						AnchorPoint = Vector2.new(0.5, 0.5),
						Position = UDim2.fromScale(0.5, 0.5),
						Size = UDim2.new(1, -2, 1, -2),
					}),

					New("TextLabel")({
						BackgroundTransparency = 1,
						Font = Scheme.Typography.Secondary.Font,
						Text = props.Text,
						TextColor3 = Scheme.GetAnimated(Scheme.Color.Text.Primary),
						TextSize = Scheme.Typography.Secondary.Size,
						TextWrapped = false,
						TextXAlignment = Enum.TextXAlignment.Center,
						TextYAlignment = Enum.TextYAlignment.Center,
						ZIndex = 2,

						Size = UDim2.fromScale(1, 1),

						[OnChange("TextBounds")] = function(newValue)
							textSize:set(Vector2.new(newValue.X, newValue.Y))
						end,
					}),
				},
			}),

			Shadow({
				Elevation = Value(Constants.Elevation[2]),
				ZIndex = 0,
			}),
		},
	})

	textSize:set(component.ImageLabel.TextLabel.TextBounds)
	return component
end


-- 7kayoh
-- TopAppbar.lua
-- 03 February 2022

--[[

    TopAppBar {
        Icon: Value<string>|string,
        Title: Value<string>|string,
        Subtitle: Value<string>|string,
        ZIndex: number?,

        [Fusion.Children]: {Instances}
    } -> Frame

--]]

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Constants = require(Modules.Constants)
local Scheme = require(Modules.Scheme)
local Divider = require(Components.Divider)

local Children = Fusion.Children
local Computed = Fusion.Computed
local New = Fusion.New

return function(props)
	props.Subtitle = props.Subtitle or Fusion.Value("")

	return New("Frame")({
		BackgroundTransparency = 1,
		ZIndex = props.ZIndex or 1,

		Size = UDim2.new(1, 0, 0, 50),

		[Children] = {
			Divider({
				ZIndex = 2,

				AnchorPoint = Vector2.new(0, 1),
				Position = UDim2.fromScale(0, 1),
			}),

			Content = New("ImageLabel")({
				BackgroundTransparency = 1,
				Image = Constants.TopRounded.Id,
				ImageColor3 = Scheme.GetAnimated(Scheme.Elevation[4]),
				ScaleType = Enum.ScaleType.Slice,
				SliceCenter = Constants.TopRounded.Center,
				SliceScale = Constants.RoundedValues[8],

				Size = UDim2.fromScale(1, 1),

				[Children] = {
					Left = New("Frame")({
						BackgroundTransparency = 1,

						Size = UDim2.fromScale(1, 1),

						[Children] = {
							New("UIListLayout")({
								FillDirection = Enum.FillDirection.Horizontal,
								SortOrder = Enum.SortOrder.LayoutOrder,
							}),

							Icon = New("Frame")({
								BackgroundTransparency = 1,

								Size = UDim2.fromScale(1, 1),

								[Children] = {
									New("UIAspectRatioConstraint")({
										AspectRatio = 1,
									}),

									New("ImageLabel")({
										BackgroundTransparency = 1,
										Image = props.Icon,
										ImageColor3 = Scheme.GetAnimated(Scheme.Color.Text.Primary),
										ScaleType = Enum.ScaleType.Fit,

										AnchorPoint = Vector2.new(0.5, 0.5),
										Position = UDim2.fromScale(0.5, 0.5),
										Size = UDim2.fromScale(0.4, 0.4),
									}),
								},
							}),

							Text = New("Frame")({
								BackgroundTransparency = 1,
								LayoutOrder = 2,

								Size = UDim2.fromScale(0, 1),

								[Children] = {
									New("UIListLayout")({
										FillDirection = Enum.FillDirection.Vertical,
										VerticalAlignment = Enum.VerticalAlignment.Center,
									}),

									Title = New("TextLabel")({
										BackgroundTransparency = 1,
										Font = Scheme.Typography.Header.Font,
										Text = props.Title,
										TextColor3 = Scheme.GetAnimated(Scheme.Color.Text.Primary),
										TextSize = Scheme.Typography.Header.Size,
										TextXAlignment = Enum.TextXAlignment.Left,

										Size = Computed(function()
											return UDim2.fromOffset(0, Scheme.Typography.Header.Size:get())
										end),
									}),

									Subtitle = New("TextLabel")({
										BackgroundTransparency = 1,
										Font = Scheme.Typography.Subheader.Font,
										LayoutOrder = 2,
										Text = props.Subtitle,
										TextColor3 = Scheme.GetAnimated(Scheme.Color.Text.Primary),
										TextSize = Scheme.Typography.Subheader.Size,
										TextXAlignment = Enum.TextXAlignment.Left,
										Visible = Computed(function()
											return props.Subtitle:get() ~= ""
										end),

										Size = Computed(function()
											return UDim2.fromOffset(0, Scheme.Typography.Subheader.Size:get())
										end),
									}),
								},
							}),
						},
					}),

					Right = New("Frame")({
						BackgroundTransparency = 1,

						Size = UDim2.fromScale(1, 1),

						[Children] = {
							New("UIListLayout")({
								FillDirection = Enum.FillDirection.Horizontal,
								HorizontalAlignment = Enum.HorizontalAlignment.Right,
								VerticalAlignment = Enum.VerticalAlignment.Center,
								SortOrder = Enum.SortOrder.LayoutOrder,
							}),

							props[Children],
						},
					}),
				},
			}),
		},
	})
end


-- 7kayoh
-- Window.lua
-- 03 February 2022

--[[

    Window {
        Size: Value<UDim2>|UDim2?,
        Position: Value<UDim2>|UDim2?,
        ZIndex: Value<number>|number?,
		Visible: Value<boolean>|boolean?,

        [Fusion.Children]: {Instances}
    } -> Frame

--]]

local Components = script.Parent.Parent
local Modules = Components.Parent.Modules
local Lydie = Components.Parent

local Fusion = require(Lydie.Parent.Fusion)
local Constants = require(Modules.Constants)
local Scheme = require(Modules.Scheme)
local Shadow = require(Components.Shadow)

local Children = Fusion.Children
local New = Fusion.New
local Value = Fusion.Value

return function(props)
	return New("Frame")({
		Name = "Window",

		BackgroundTransparency = 1,
		ZIndex = props.ZIndex or 1,
		Visible = props.Visible or true,

		AnchorPoint = Vector2.new(0.5, 0.5),
		Position = props.Position or UDim2.fromScale(0.5, 0.5),
		Size = props.Size or UDim2.fromOffset(700, 700),

		[Children] = {
			Fusion.Computed(function()
				if props.AspectRatio and props.AspectRatio:get() > 0 then
					return New("UIAspectRatioConstraint")({
						AspectRatio = props.AspectRatio:get() or 1.5,
						AspectType = Enum.AspectType.ScaleWithParentSize,
						DominantAxis = Enum.DominantAxis.Width,
					})
				else
					return nil
				end
			end),

			New("ImageLabel")({
				BackgroundTransparency = 1,
				Image = Constants.Rounded.Id,
				ImageColor3 = Scheme.GetAnimated(Scheme.Elevation[2]),
				ScaleType = Enum.ScaleType.Slice,
				SliceCenter = Constants.Rounded.Center,
				SliceScale = Constants.RoundedValues[8],

				Size = UDim2.fromScale(1, 1),

				[Children] = props[Children],
			}),

			Shadow({
				Elevation = Value(Constants.Elevation[4]),
				ZIndex = 0,
			}),
		},
	})
end


return {
    Controls = {
        AppBarButton = require(script.Controls.AppBarButton),
        AppTabBarButton = require(script.Controls.AppTabBarButton),
        BaseButton = require(script.Controls.BaseButton),
        Checkbox = require(script.Controls.Checkbox),
        ColorPicker = require(script.Controls.ColorPicker),
        IconButton = require(script.Controls.IconButton),
        Icons = require(script.Controls.Icons),
        IconTextButton = require(script.Controls.IconTextButton),
        Slider = require(script.Controls.Slider),
        TextButton = require(script.Controls.TextButton),
        TextField = require(script.Controls.TextField),
        Toggle = require(script.Controls.Toggle),
    },

    Views = {
        AppTabBar = require(script.Views.AppTabBar),
        BottomAppBar = require(script.Views.BottomAppBar),
        ToolTip = require(script.Views.ToolTip),
        TopAppBar = require(script.Views.TopAppBar),
        Window = require(script.Views.Window)
    },

    Divider = require(script.Divider),
    Shadow = require(script.Shadow),
}


local EnumList = require(script.Parent.EnumList)

local rect256 = Rect.new(Vector2.new(256, 256), Vector2.new(256, 256))

return {
	BottomRounded = EnumList.new("BottomRounded", {
		Center = rect256,
		Id = "rbxassetid://8858987793",
	}),

	Rounded = EnumList.new("Rounded", {
		Center = rect256,
		Id = "rbxassetid://8858983293",
	}),

	TopRounded = EnumList.new("TopRounded", {
		Center = rect256,
		Id = "rbxassetid://8858987141",
	}),

	RoundedValues = EnumList.new("RoundedValues", {
		[8] = 0.03,
		[7] = 0.025,
		[6] = 0.02,
	}),

	Elevation = EnumList.new("Elevation", {
		[0] = 0,
		[1] = 1,
		[2] = 2,
		[3] = 3,
		[4] = 4,
		[6] = 6,
		[8] = 8,
		[9] = 9,
		[12] = 12,
		[16] = 16,
	}),

	ShadowImage = "rbxassetid://1316045217",
}


-- bubba_biga
-- EnumList.lua
-- 17 January 2022

--[[
    EnumList.new(name: string, enumTable: table {[string]: any}) --> Enum
	EnumList.IsA(self: any) --> boolean
	EnumList.GetEnums() --> table
	[EnumList instance only]:
	EnumList:IsEnumItem(enumItem: any) --> boolean
	EnumList:GetEnumItems() --> table {[string]: any}
	EnumList:Destroy() --> void
]]

local EnumList = { _enums = {} }

function EnumList._createEnumItems(self, enumsTable)
	local enumItems = {}

	for enumItem, value in pairs(enumsTable) do
		enumItems[enumItem] = {
			Name = enumItem,
			Value = value,
			Enum = self,
		}
	end

	return enumItems
end

function EnumList.new(name, enumsTable)
	local self = setmetatable({
		Name = name,
		_index = #EnumList._enums + 1,
	}, EnumList)

	self._enumItems = EnumList._createEnumItems(self, enumsTable)
	table.insert(EnumList._enums, self._index)
	return self
end

function EnumList.IsA(self)
	return getmetatable(self) == EnumList
end

function EnumList.GetEnums()
	return EnumList._enums
end

function EnumList:IsEnumItem(enumItem)
	return typeof(enumItem) == "table" and enumItem.Enum == self
end

function EnumList:GetEnumItems()
	return self._enumItems
end

function EnumList:Destroy()
	table.remove(EnumList._enums, self._index)
	setmetatable(self, nil)
end

function EnumList:__index(key)
	if EnumList[key] then
		return EnumList[key]
	end

	local value = self._enumItems[key]

	if value then
		return value.Value
	else
		error(("\"%s\" is not a valid EnumItem of Enum \"%s\""):format(tostring(key), self.Name))
	end
end

return EnumList


-- 7kayoh
-- Dark.lua
-- 03 February 2022
return {
	Elevation = {
		Color3.fromRGB(0, 0, 0),
		Color3.fromRGB(20, 20, 20),
		Color3.fromRGB(25, 25, 25),
		Color3.fromRGB(30, 30, 30),
	},
	Typography = {
		HeroLarge = { Font = Enum.Font.GothamBold, Size = 28 },
		Hero = { Font = Enum.Font.GothamMedium, Size = 25 },
		Header = { Font = Enum.Font.GothamBold, Size = 14 },
		Subheader = { Font = Enum.Font.Gotham, Size = 14 },
		Body = { Font = Enum.Font.Gotham, Size = 16 },
		Button = { Font = Enum.Font.GothamBold, Size = 16 },
		Secondary = { Font = Enum.Font.GothamMedium, Size = 14 },
		Caption = { Font = Enum.Font.GothamBold, Size = 14 },
	},
	Color = {
		Red = Color3.fromRGB(199, 35, 35),
		Green = Color3.fromRGB(74, 224, 106),
		Blue = Color3.fromRGB(93, 162, 241),
		Amber = Color3.fromRGB(255, 201, 85),
		Orange = Color3.fromRGB(255, 127, 77),
		Pink = Color3.fromRGB(255, 134, 200),
		Purple = Color3.fromRGB(188, 146, 255),
		Indigo = Color3.fromRGB(131, 104, 255),
		Teal = Color3.fromRGB(70, 243, 188),
		Mint = Color3.fromRGB(42, 223, 211),
		Mono = {
			Color3.fromRGB(0, 0, 0),
			Color3.fromRGB(25, 25, 25),
			Color3.fromRGB(50, 50, 50),
			Color3.fromRGB(75, 75, 75),
			Color3.fromRGB(150, 150, 150),
			Color3.fromRGB(200, 200, 200),
			Color3.fromRGB(225, 225, 225),
			Color3.fromRGB(240, 240, 240),
			Color3.fromRGB(255, 255, 255),
		},
		Text = {
			Primary = Color3.fromRGB(220, 220, 220),
			Secondary = Color3.fromRGB(150, 150, 150),
			Tertiary = Color3.fromRGB(100, 100, 100),
		},
		Divider = Color3.fromRGB(60, 60, 60),
	},
}


-- 7kayoh
-- Light.lua
-- 03 February 2022
return {
	Elevation = {
		Color3.fromRGB(200, 200, 200),
		Color3.fromRGB(245, 245, 245),
		Color3.fromRGB(250, 250, 255),
		Color3.fromRGB(255, 255, 255),
	},
	Typography = {
		HeroLarge = { Font = Enum.Font.GothamBold, Size = 28 },
		Hero = { Font = Enum.Font.GothamMedium, Size = 25 },
		Header = { Font = Enum.Font.GothamBold, Size = 14 },
		Subheader = { Font = Enum.Font.Gotham, Size = 14 },
		Body = { Font = Enum.Font.Gotham, Size = 16 },
		Button = { Font = Enum.Font.GothamBold, Size = 16 },
		Secondary = { Font = Enum.Font.GothamMedium, Size = 14 },
		Caption = { Font = Enum.Font.GothamBold, Size = 14 },
	},
	Color = {
		Red = Color3.fromRGB(255, 47, 57),
		Green = Color3.fromRGB(19, 175, 53),
		Blue = Color3.fromRGB(20, 103, 199),
		Amber = Color3.fromRGB(177, 123, 7),
		Orange = Color3.fromRGB(255, 79, 10),
		Pink = Color3.fromRGB(255, 10, 145),
		Purple = Color3.fromRGB(125, 45, 255),
		Indigo = Color3.fromRGB(83, 45, 255),
		Teal = Color3.fromRGB(13, 107, 77),
		Mint = Color3.fromRGB(0, 138, 128),
		Mono = {
			Color3.fromRGB(0, 0, 0),
			Color3.fromRGB(25, 25, 25),
			Color3.fromRGB(50, 50, 50),
			Color3.fromRGB(75, 75, 75),
			Color3.fromRGB(150, 150, 150),
			Color3.fromRGB(200, 200, 200),
			Color3.fromRGB(225, 225, 225),
			Color3.fromRGB(240, 240, 240),
			Color3.fromRGB(255, 255, 255),
		},
		Text = {
			Primary = Color3.fromRGB(0, 0, 0),
			Secondary = Color3.fromRGB(100, 100, 100),
			Tertiary = Color3.fromRGB(150, 150, 150),
		},
		Divider = Color3.fromRGB(200, 200, 200),
	},
}


-- 7kayoh
-- SolarizedDark.lua
-- 03 February 2022
return {
	Elevation = {
		Color3.fromHex("002b36"),
		Color3.fromHex("002b36"),
		Color3.fromHex("073642"),
		Color3.fromHex("073642"),
	},
	Typography = {
		HeroLarge = { Font = Enum.Font.Ubuntu, Size = 28 },
		Hero = { Font = Enum.Font.Ubuntu, Size = 25 },
		Header = { Font = Enum.Font.Ubuntu, Size = 14 },
		Subheader = { Font = Enum.Font.Ubuntu, Size = 14 },
		Body = { Font = Enum.Font.Ubuntu, Size = 16 },
		Button = { Font = Enum.Font.Ubuntu, Size = 16 },
		Secondary = { Font = Enum.Font.Ubuntu, Size = 14 },
		Caption = { Font = Enum.Font.Ubuntu, Size = 14 },
	},
	Color = {
		Red = Color3.fromHex("dc322f"),
		Green = Color3.fromHex("859900"),
		Blue = Color3.fromHex("268bd2"),
		Amber = Color3.fromHex("b58900"),
		Orange = Color3.fromHex("cb4b16"),
		Pink = Color3.fromRGB(255, 51, 170),
		Purple = Color3.fromHex("6c71c4"),
		Indigo = Color3.fromHex("d33682"),
		Teal = Color3.fromRGB(70, 243, 188),
		Mint = Color3.fromRGB(42, 223, 211),
		Mono = {
			Color3.fromRGB(0, 0, 0),
			Color3.fromRGB(25, 25, 25),
			Color3.fromRGB(50, 50, 50),
			Color3.fromRGB(75, 75, 75),
			Color3.fromRGB(150, 150, 150),
			Color3.fromRGB(200, 200, 200),
			Color3.fromRGB(225, 225, 225),
			Color3.fromRGB(240, 240, 240),
			Color3.fromRGB(255, 255, 255),
		},
		Text = {
			Primary = Color3.fromHex("fdf6e3"),
			Secondary = Color3.fromHex("eee8d5"),
			Tertiary = Color3.fromHex("fdf6e3"),
		},
		Divider = Color3.fromHex("657b83"),
	},
}


-- bubba_biga
-- Scheme.lua
-- 03 February 2022

--[[
    Scheme.Schemes: EnumList
	Scheme.Current: string

    Scheme.Switch(enumScheme: string) --> void
]]

-- :( why wally
local Scheme = {} :: {
	Switch: (string) -> (),
	SetAccent: (Color3|Fusion.Value<Color3>) -> (),
	_updateScheme: ({ any }, { any }) -> (),
	_initializeStates: ({ any }, { any }) -> (),
	GetAnimated: (Fusion.Value<any>) -> (Tween<Fusion.Value<any>>),
	_initialize: () -> (),
	[any]: any,
}

local Modules = script.Parent
local Lydie = Modules.Parent

local Fusion = require(Lydie.Parent.Fusion)
local EnumList = require(Modules.EnumList)

local Value = Fusion.Value
local Computed = Fusion.Computed

local schemes = script.Schemes:GetChildren()

function Scheme.Switch(enumTheme)
	for _, theme in ipairs(schemes) do
		if theme.Name ~= enumTheme then
			continue
		end

		Scheme.Current = enumTheme
		Scheme._updateStates(require(theme), Scheme)
	end
end

function Scheme.SetAccent(value)
	Scheme.Accent = typeof(value) == "Color3" and Value(value) or value
end

function Scheme._updateStates(newScheme, tabl)
	for key, data in pairs(newScheme) do
		if typeof(data) == "table" then
			Scheme._updateStates(data, tabl[key])
		else
			tabl[key]:set(data)
		end
	end
end

function Scheme._initializeStates(scheme, tabl)
	for key, data in pairs(scheme) do
		if typeof(data) == "table" then
			tabl[key] = {}
			Scheme._initializeStates(data, tabl[key])
		else
			tabl[key] = Value(data)
		end
	end
end

function Scheme.GetAnimated(state)
	return Fusion.Tween(state, TweenInfo.new(0.15, Enum.EasingStyle.Linear))
end

function Scheme._initialize()
	local schemeEnumItems = {}

	for _, scheme in ipairs(schemes) do
		schemeEnumItems[scheme.Name] = scheme.Name
	end

	Scheme.Schemes = EnumList.new("Scheme", schemeEnumItems)
	Scheme._initializeStates(require(schemes[1]), Scheme)

	-- Switch to light theme as that is the default theme, we still want
	-- our states to represent some theme!
	Scheme.Switch(Scheme.Schemes.Light)
	Scheme.AccentName = Value("Pink")
	Scheme.Accent = Computed(function()
		return Scheme.Color[Scheme.AccentName:get()]:get()
	end)
end

Scheme._initialize()

return Scheme


return {
    Scheme = require(script.Scheme),
    Constants = require(script.Constants),
    EnumList = require(script.EnumList),
    Icons = require(script.Icons),
    SharedStates = require(script.SharedStates)
}


return {
  Components = require(script.Components),
  Modules = require(script.Modules),
}


--!strict
-- Plugin that exposes the ColorPane API script for testing

local ColorPane = script.Parent.ColorPane
require(ColorPane)(plugin, "CPTester")

local APIScript = ColorPane.API
APIScript.Parent = game:GetService("Workspace")
APIScript.Archivable = false

plugin.Unloading:Connect(function()
    APIScript:Destroy()
end)