local types = require './types'

export type PlayerProfile = types.PlayerProfile
export type ProfileCache = types.ProfileCache

return function(): ProfileCache
	local profile_by_player: { [Player]: PlayerProfile? } = {}

	local function get_profile(player: Player): PlayerProfile?
		return profile_by_player[player]
	end

	local function set_profile(player: Player, profile: PlayerProfile): ()
		profile_by_player[player] = profile
	end

	local function delete_profile(player: Player): ()
		profile_by_player[player] = nil
	end

	return table.freeze {
		get_profile = get_profile,
		set_profile = set_profile,
		delete_profile = delete_profile,
	}
end


local runtime = require '@self/runtime'
local types = require '@self/types'

export type ProfileData = types.ProfileData
export type PlayerProfile = types.PlayerProfile

return runtime.api


local types = require './types'

export type ProfilesApi = types.ProfilesApi
export type ProfilesApiDependencies = types.ProfilesApiDependencies

return function(dependencies: ProfilesApiDependencies): ProfilesApi
	local profile_cache = dependencies.profile_cache
	local is_player_active = dependencies.is_player_active
	local wait_for_profile_async = dependencies.wait_for_profile_async

	local function get_profile_async(player: Player): types.PlayerProfile?
		local cached_profile = profile_cache.get_profile(player)
		if cached_profile ~= nil then return cached_profile end

		if is_player_active(player) == false then return nil end

		return wait_for_profile_async(player)
	end

	local function get_profile_data_async(player: Player): types.ProfileData?
		local player_profile = get_profile_async(player)
		if player_profile == nil then return nil end

		return player_profile.Data :: types.ProfileData
	end

	return table.freeze {
		get_profile_async = get_profile_async,
		get_profile_data_async = get_profile_data_async,
	}
end


local Players = game:GetService 'Players'
local ServerScriptService = game:GetService 'ServerScriptService'

local ProfileStore = require(ServerScriptService.serverpkg.profilestore)

local create_cache = require './cache'
local create_public_api = require './public_api'
local create_session_service = require './session'
local create_store = require './store'
local create_wait_for_profile = require './wait_for_profile'
local runtime_types = require './runtime_types'
local types = require './types'

type ProfileCache = types.ProfileCache
type ProfilesApi = types.ProfilesApi
type PlayerSessionService = types.PlayerSessionService
type ProfilesRuntime = types.ProfilesRuntime
type StoreConfig = runtime_types.StoreConfig

local function is_player_active(player: Player): boolean
	return player.Parent == Players
end

local function kick_player_with_message(player: Player, message: string)
	player:Kick(message)
end

local function wait_for_next_profile_poll()
	task.wait()
end

local profile_cache: ProfileCache = create_cache()
local profile_store_config: StoreConfig = {
	profile_store_library = ProfileStore,
	profile_store_name = 'Profiles',
	default_profile_data = {},
}
local profile_store = create_store(profile_store_config)
local wait_for_profile_async = create_wait_for_profile {
	profile_cache = profile_cache,
	is_player_active = is_player_active,
	wait_for_next_profile_poll = wait_for_next_profile_poll,
}
local player_session_service: PlayerSessionService = create_session_service {
	profile_cache = profile_cache,
	is_player_active = is_player_active,
	kick_player_with_message = kick_player_with_message,
	profile_store = profile_store,
}
local profiles_api: ProfilesApi = create_public_api {
	profile_cache = profile_cache,
	is_player_active = is_player_active,
	wait_for_profile_async = wait_for_profile_async,
}

return table.freeze {
	api = profiles_api,
	cache = profile_cache,
	session = player_session_service,
} :: ProfilesRuntime


local ServerScriptService = game:GetService 'ServerScriptService'

local ProfileStore = require(ServerScriptService.serverpkg.profilestore)
local types = require './types'

export type ProfileData = types.ProfileData
export type PlayerStore = types.PlayerStore

export type ProfileStoreModule = typeof(ProfileStore)

export type StoreConfig = {
	profile_store_library: ProfileStoreModule,
	profile_store_name: string,
	default_profile_data: ProfileData,
}

return nil


local types = require './types'

export type ProfileCache = types.ProfileCache
export type PlayerSessionService = types.PlayerSessionService
export type PlayerSessionDependencies = types.PlayerSessionDependencies
export type PlayerProfile = types.PlayerProfile

return function(dependencies: PlayerSessionDependencies): PlayerSessionService
	local profile_cache = dependencies.profile_cache
	local is_player_active = dependencies.is_player_active
	local kick_player_with_message = dependencies.kick_player_with_message
	local profile_store = dependencies.profile_store

	local function handle_profile_session_end(player: Player): ()
		profile_cache.delete_profile(player)

		if is_player_active(player) == false then return end

		kick_player_with_message(
			player,
			'Profile session ended. Please rejoin.'
		)
	end

	local function configure_profile_session(
		player: Player,
		profile: PlayerProfile
	): ()
		profile:AddUserId(player.UserId)
		profile:Reconcile()
		profile.OnSessionEnd:Connect(function()
			handle_profile_session_end(player)
		end)
	end

	local function load_profile_async(player: Player): ()
		local profile_key = `{player.UserId}`
		local player_profile = profile_store:StartSessionAsync(profile_key, {
			Cancel = function()
				return is_player_active(player) == false
			end,
		})

		if player_profile == nil then
			if is_player_active(player) == false then return end

			kick_player_with_message(
				player,
				'Profile failed to load. Please rejoin.'
			)
			return
		end

		configure_profile_session(player, player_profile)

		if is_player_active(player) == false then
			player_profile:EndSession()
			return
		end

		profile_cache.set_profile(player, player_profile)
	end

	local function unload_profile(player: Player): ()
		local player_profile = profile_cache.get_profile(player)
		if player_profile == nil then return end

		player_profile:EndSession()
	end

	return table.freeze {
		load_profile_async = load_profile_async,
		unload_profile = unload_profile,
	}
end


local Players = game:GetService 'Players'

local runtime = require './runtime'

local player_session_service = runtime.session

for _, player in Players:GetPlayers() do
	task.spawn(player_session_service.load_profile_async, player)
end

Players.PlayerAdded:Connect(player_session_service.load_profile_async)
Players.PlayerRemoving:Connect(player_session_service.unload_profile)


local runtime_types = require './runtime_types'
local types = require './types'

export type ProfileData = types.ProfileData
export type PlayerStore = types.PlayerStore
export type StoreConfig = runtime_types.StoreConfig

return function(config: StoreConfig): PlayerStore
	local profile_store_library = config.profile_store_library
	local default_profile_data = config.default_profile_data
	local profile_store_name = config.profile_store_name

	local player_store = profile_store_library.New(
		profile_store_name,
		default_profile_data :: any
	) :: any

	return player_store :: PlayerStore
end


export type JsonValue =
	nil
	| boolean
	| buffer
	| number
	| string
	| { [string]: JsonValue }
	| { JsonValue }

export type ProfileData = { [string]: JsonValue }

export type ProfileSessionEndSignal = {
	Connect: (self: ProfileSessionEndSignal, callback: () -> ()) -> (),
}

export type PlayerProfile = {
	Data: ProfileData,
	AddUserId: (self: PlayerProfile, user_id: number) -> (),
	Reconcile: (self: PlayerProfile) -> (),
	EndSession: (self: PlayerProfile) -> (),
	OnSessionEnd: ProfileSessionEndSignal,
}

export type PlayerStore = {
	StartSessionAsync: (
		self: PlayerStore,
		profile_key: string,
		config: { Cancel: () -> boolean }
	) -> PlayerProfile?,
}

export type ProfileCache = {
	get_profile: (player: Player) -> PlayerProfile?,
	set_profile: (player: Player, profile: PlayerProfile) -> (),
	delete_profile: (player: Player) -> (),
}

export type WaitForProfile = (player: Player) -> PlayerProfile?

export type WaitForProfileDependencies = {
	profile_cache: ProfileCache,
	is_player_active: (player: Player) -> boolean,
	wait_for_next_profile_poll: () -> (),
}

export type ProfilesApi = {
	get_profile_async: (player: Player) -> PlayerProfile?,
	get_profile_data_async: (player: Player) -> ProfileData?,
}

export type ProfilesApiDependencies = {
	profile_cache: ProfileCache,
	is_player_active: (player: Player) -> boolean,
	wait_for_profile_async: WaitForProfile,
}

export type PlayerSessionService = {
	load_profile_async: (player: Player) -> (),
	unload_profile: (player: Player) -> (),
}

export type PlayerSessionDependencies = {
	profile_cache: ProfileCache,
	is_player_active: (player: Player) -> boolean,
	kick_player_with_message: (player: Player, message: string) -> (),
	profile_store: PlayerStore,
}

export type ProfilesRuntime = {
	api: ProfilesApi,
	cache: ProfileCache,
	session: PlayerSessionService,
}

return nil


local types = require './types'

export type ProfileCache = types.ProfileCache
export type WaitForProfile = types.WaitForProfile
export type WaitForProfileDependencies = types.WaitForProfileDependencies

return function(dependencies: WaitForProfileDependencies): WaitForProfile
	local profile_cache = dependencies.profile_cache
	local is_player_active = dependencies.is_player_active
	local wait_for_next_profile_poll = dependencies.wait_for_next_profile_poll

	local function wait_for_profile_async(player: Player): types.PlayerProfile?
		local cached_profile = profile_cache.get_profile(player)
		if cached_profile ~= nil then return cached_profile end

		if is_player_active(player) == false then return nil end

		while is_player_active(player) do
			local available_profile = profile_cache.get_profile(player)
			if available_profile ~= nil then return available_profile end

			wait_for_next_profile_poll()
		end

		return nil
	end

	return wait_for_profile_async
end


local roblox = require '@lune/roblox'

local next_user_id = 1000
local kick_message_list_by_player =
	setmetatable({} :: { [Instance]: { string } }, { __mode = 'k' })

local on_player_kick = function(player: Instance, ...: any)
	local message = select(1, ...) :: string
	local kick_message_list = kick_message_list_by_player[player]
	if kick_message_list == nil then
		kick_message_list = {}
		kick_message_list_by_player[player] = kick_message_list
	end

	table.insert(kick_message_list, message)
end

roblox.implementMethod('Player', 'Kick', on_player_kick :: any)

local function create_test_player(user_id: number?): Player
	local resolved_user_id = user_id
	if resolved_user_id == nil then
		resolved_user_id = next_user_id
		next_user_id += 1
	end

	local player = roblox.Instance.new 'Player'
	player.Name = `Player{resolved_user_id}`
	player.UserId = resolved_user_id

	return player
end

local function get_kick_message_list(player: Instance): { string }
	local kick_message_list = kick_message_list_by_player[player]
	if kick_message_list == nil then return {} end

	return kick_message_list
end

return table.freeze {
	create_test_player = create_test_player,
	get_kick_message_list = get_kick_message_list,
}


--!strict
local QuestObjective = require(script.Parent.QuestObjective)
local assertProperties = require(script.Parent.Parent.Functions.assertProperties)

export type QuestObjective = typeof(QuestObjective)

--[=[
    @class ObjectiveInfo
    @tag Class

    The ObjectiveInfo contains the static data for our quest objectives. This is where the developer
    should initiate the data for the objective as long as create the QuestObjective objects.

    ```lua
    local appleQuest = ObjectiveInfo.new {
        ObjectiveId = "CollectApples",
        Name = "Collect Apples",
        Description = "Collect %s apples",
    }

    local flowerQuest = ObjectiveInfo.new {
        ObjectiveId = "CollectFlowers",
        Name = "Collect Flowers",
        Description = "Collect %s flowers",
    }

    local appleObjective1 = appleQuest:NewObjective(5) -- Created an objective to collect 5 apples
    local appleObjective2 = appleQuest:NewObjective(3) -- Created an objective to collect 3 apples
    local appleObjective3 = appleQuest:NewObjective(7) -- Created an objective to collect 7 apples

    local flowerObjective1 = flowerQuest:NewObjective(5) -- Created an objective to collect 5 flowers
    local flowerObjective2 = flowerQuest:NewObjective(3) -- Created an objective to collect 3 flowers
    local flowerObjective3 = flowerQuest:NewObjective(7) -- Created an objective to collect 7 flowers
    ```
]=]
local ObjectiveInfo = {}
ObjectiveInfo.__index = ObjectiveInfo
ObjectiveInfo.__type = "ObjectiveInfo"
--[=[
    This is an ID to represent the objective. Should be used to identify the objective in the code.

    When we tell the server that player did X objective this is how it will determine if it should add or not to this objective.

    @prop ObjectiveId string
    @within ObjectiveInfo
]=]
ObjectiveInfo.ObjectiveId = "" :: string
--[=[
    The name of our objective. Can be used by the player or server to display the name of the objective
    that the player must complete

    @prop Name string
    @within ObjectiveInfo
]=]
ObjectiveInfo.Name = "" :: string
--[=[
    A more detailed description of our objective. Can be used by the developer to display the description

    @prop Description string
    @within ObjectiveInfo
]=]
ObjectiveInfo.Description = "" :: string

--[=[
    Constructor for ObjectiveInfo

    ```lua
    local appleQuest = ObjectiveInfo.new {
        ObjectiveId = "CollectApples",
        Name = "Collect Apples",
        Description = "Collect %s apples",
    }
    ```

    @param properties {[string]: any}

    @return ObjectiveInfo
]=]
function ObjectiveInfo.new(properties: {[string]: any}): ObjectiveInfo
    return assertProperties(properties, ObjectiveInfo)
end

--[=[
    Extends the Objective Info into a Quest Objective which is what we feed into our
    Quests

    ```lua
    local appleQuest = ObjectiveInfo.new {
        ObjectiveId = "CollectApples",
        Name = "Collect Apples",
        Description = "Collect %s apples",
    }

    local objective1 = appleQuest:NewObjective(5) -- Created an objective to collect 5 apples
    local objective2 = appleQuest:NewObjective(3) -- Created an objective to collect 3 apples
    local objective3 = appleQuest:NewObjective(7) -- Created an objective to collect 7 apples
    ```

    @return number
]=]
function ObjectiveInfo:NewObjective(target: number): QuestObjective
    return QuestObjective.new {
        TargetProgress = target,
        ObjectiveInfo = self,
    }
end

export type ObjectiveInfo = typeof(ObjectiveInfo)

return setmetatable(ObjectiveInfo, {
    __call = function(_, properties)
        return ObjectiveInfo.new(properties)
    end
})


--!strict
local assertProperties = require(script.Parent.Parent.Functions.assertProperties)
local QuestAcceptType = require(script.Parent.Parent.Enums.QuestAcceptType)
local QuestDeliverType = require(script.Parent.Parent.Enums.QuestDeliverType)
local QuestRepeatableType = require(script.Parent.Parent.Enums.QuestRepeatableType)
local QuestStatus = require(script.Parent.Parent.Enums.QuestStatus)
local Trove = require(script.Parent.Parent.Parent.Vendor.Trove)
local Signal = require(script.Parent.Parent.Parent.Vendor.Signal)
local QuestObjective = require(script.Parent.QuestObjective)
local QuestLifeCycle = require(script.Parent.QuestLifeCycle)
local QuestProgress = require(script.Parent.Parent.Structs.QuestProgress)
local QuestObjectiveProgress = require(script.Parent.Parent.Structs.QuestObjectiveProgress)

type Trove = Trove.Trove
type Signal = Signal.Signal
type QuestStatus = QuestStatus.QuestStatus
type QuestAcceptType = QuestAcceptType.QuestAcceptType
type QuestDeliverType = QuestDeliverType.QuestDeliverType
type QuestRepeatableType = QuestRepeatableType.QuestRepeatableType
type QuestObjective = QuestObjective.QuestObjective
type QuestLifeCycle = QuestLifeCycle.QuestLifeCycle
type QuestProgress = QuestProgress.QuestProgress
type QuestObjectiveProgress = QuestObjectiveProgress.QuestObjectiveProgress

local NO_OBJECTIVE: string = "There is no ObjectiveId %s in the Quest %s"

--[=[
    @class Quest
    @tag Class

    This is the main class of our quest system. Quest objects define exactly the kind of quest and how
    can a player complete it. When declaring a quest there are multiple properties that should
    be taken into consideration. On the following example we can see how could we, per example, make
    a quest to collect 5 apples.

    ```lua
    local appleQuest = ObjectiveInfo.new { -- We are creating an objective info object to store the data of our objective
        ObjectiveId = "CollectApples",
        Name = "Collect Apples",
        Description = "Collect %s apples",
    }

    local quest = Quest.new { -- We are creating a quest object with all the relevant data
        Name = "Collect Apples", -- The name of our quest
        Description = "Collect %s apples", -- The description that we will display to our user
        QuestId = "AppleCollection", -- A unique identifier to our quest
        QuestAcceptType = QuestAcceptType.Automatic, -- If the quest automatically gets accepted or rquires manual work
        QuestDeliverType = QuestDeliverType.Automatic, -- If the quest automatically gets delivered or requires manual work
        QuestRepeatableType = QuestRepeatableType.NonRepeatable, -- If the quest can be repeated or not
        QuestStart = -1, -- UTC time to define when the quest should become available (specially useful for event quests)
        QuestEnd = -1, -- UTC time to define when the quest should no longer be available (specially useful for event quests)
        RequiredQuests = {}, -- A list of quests that are required to be delivered before this quest can be started
        LifeCycles = {"AppleQuest"}, -- The lifecycles that will manage this quest's behavior
        QuestObjectives = {
            appleQuest:NewObjective(5)
        },
    }
    ```
]=]
local Quest = {}
Quest.__index = Quest
Quest.__type = "Quest"
--[=[
    Called whenever a quest objective gets completed

    ```lua
    quest.OnQuestObjectiveCompleted:Connect(function(objectiveId: string)
        print("Completed Objective: " .. objectiveId)
    end)
    ```

    @prop OnQuestObjectiveCompleted Signal
    @within Quest
]=]
Quest.OnQuestObjectiveCompleted = newproxy() :: Signal
--[=[
    Called whenever one of the quests objective changes the value

    ```lua
    quest.OnQuestObjectiveChanged:Connect(function(objectiveId: string, newAmount: number)
        print("Objective " .. objectiveId .. " has changed to " .. newAmount)
    end)
    ```

    @prop OnQuestObjectiveChanged Signal
    @within Quest
]=]
Quest.OnQuestObjectiveChanged = newproxy() :: Signal
--[=[
    Called when the quest gets completed

    ```lua
    quest.OnQuestCompleted:Connect(function()
        print("The quest got completed!")
    end)
    ```

    @prop OnQuestCompleted Signal
    @within Quest
]=]
Quest.OnQuestCompleted = newproxy() :: Signal
--[=[
    Called when the quest gets delivered

    ```lua
    quest.OnQuestDelivered:Connect(function()
        print("The quest got delivered!")
    end)
    ```

    @prop OnQuestDelivered Signal
    @within Quest
]=]
Quest.OnQuestDelivered = newproxy() :: Signal
--[=[
    Called when the quest gets cancelled

    ```lua
    quest.OnQuestCanceled:Connect()
        print("The quest got cancelled!")
    end)
    ```

    @prop OnQuestCanceled Signal
    @within Quest
]=]
Quest.OnQuestCanceled = newproxy() :: Signal
--[=[
    The name of the quest

    @prop Name string
    @within Quest
]=]
Quest.Name = "" :: string
--[=[
    The description of the quest

    @prop Description string
    @within Quest
]=]
Quest.Description = "" :: string
--[=[
    If the quest is disabled or not. If the quest is disabled it will
    be ignored when loaded into the system

    @prop Disabled boolean
    @within Quest
]=]
Quest.Disabled = false :: boolean
--[=[
    The quest ID. This must be a unique identifier for the quest

    @prop Name string
    @within Quest
]=]
Quest.QuestId = "" :: string
--[=[
    The type of the quest accepting system. This can be either automatic or manual

    @prop QuestAcceptType QuestAcceptType
    @within Quest
]=]
Quest.QuestAcceptType = QuestAcceptType.Automatic :: QuestAcceptType
--[=[
    The type of the quest delivering system. This can be either automatic or manual

    @prop QuestDeliverType QuestDeliverType
    @within Quest
]=]
Quest.QuestDeliverType = QuestDeliverType.Automatic :: QuestDeliverType
--[=[
    How many times can this quest be repeated

    @prop QuestRepeatableType QuestRepeatableType
    @within Quest
]=]
Quest.QuestRepeatableType = QuestRepeatableType.NonRepeatable :: QuestRepeatableType
--[=[
    UTC time to define when the quest should become available (specially useful for event quests)

    @prop QuestStart number
    @within Quest
]=]
Quest.QuestStart = -1 :: number
--[=[
    UTC time to define when the quest should no longer be available (specially useful for event quests)

    @prop QuestEnd number
    @within Quest
]=]
Quest.QuestEnd = -1 :: number
--[=[
    This is an array with all the required quest IDs in order for this quest to become available

    @prop RequiredQuests {string}
    @within Quest
]=]
Quest.RequiredQuests = {} :: {string}
--[=[
    This is an array with all the LifeCycles names that will manage this quest's behavior

    @prop LifeCycles {string}
    @within Quest
]=]
Quest.LifeCycles = {} :: {string}
--[=[
    An array with all the objectives required to complete this quest

    @prop QuestObjectives {QuestObjective}
    @within Quest
]=]
Quest.QuestObjectives = {} :: {QuestObjective}
--[=[
    This is a boolean that determines whether the player can repeat this
    quest after it has been completed or not

    @private
    @prop _CanRepeatQuest boolean
    @within Quest
]=]
Quest._CanRepeatQuest = false :: boolean
--[=[
    A hash map that stores and tracks all the quest objectives that are currently active in the quest

    @private
    @prop _QuestObjectives {[string]: QuestObjective}
    @within Quest
]=]
Quest._QuestObjectives = {} :: {[string]: QuestObjective}
--[=[
    Cached value of our quest progress

    @private
    @prop _QuestProgress QuestProgress
    @within Quest
]=]
Quest._QuestProgress = newproxy() :: QuestProgress
--[=[
    Our trove used to cleanup connections

    @private
    @prop _Trove Trove
    @within Quest
]=]
Quest._Trove = newproxy() :: Trove

--[=[
    Constructor for Quest

    ```lua
    local appleQuest = ObjectiveInfo.new {
        ObjectiveId = "CollectApples",
        Name = "Collect Apples",
        Description = "Collect %s apples",
    }

    local quest = Quest.new {
        Name = "Collect Apples",
        Description = "Collect %s apples",
        QuestId = "AppleCollection",
        QuestAcceptType = QuestAcceptType.Automatic,
        QuestDeliverType = QuestDeliverType.Automatic,
        QuestRepeatableType = QuestRepeatableType.NonRepeatable,
        QuestStart = 0,
        QuestEnd = 0,
        RequiredQuests = {},
        LifeCycles = {"AppleQuest"},
        QuestObjectives = {
            appleQuest:NewObjective(5)
        },
    }
    ```

    @param properties {[string]: any}

    @return Quest
]=]
function Quest.new(properties: {[string]: any}): Quest
    properties = properties or {}
    local self: Quest = assertProperties(properties, Quest)
    self.OnQuestObjectiveCompleted = Signal.new()
    self.OnQuestObjectiveChanged = Signal.new()
    self.OnQuestCompleted = Signal.new()
    self.OnQuestDelivered = Signal.new()
    self.OnQuestCanceled = Signal.new()

    if not properties["RequiredQuests"] then
        self.RequiredQuests = {}
    end
    if not properties["LifeCycles"] then
        self.LifeCycles = {}
    end
    if not properties["QuestObjectives"] then
        self.QuestObjectives = {}
    end

    self:_SetQuestProgress(QuestProgress {
        QuestObjectiveProgresses = {},
        QuestStatus = QuestStatus.NotStarted,
        CompletedCount = 0,
        FirstCompletedTick = 0,
        LastCompletedTick = 0,
    })
    self._Trove = Trove.new()
    self._Trove:Add(self.OnQuestObjectiveCompleted)
    self._Trove:Add(self.OnQuestObjectiveChanged)
    self._Trove:Add(self.OnQuestCompleted)
    self._Trove:Add(self.OnQuestDelivered)
    self._Trove:Add(self.OnQuestCanceled)

    return self
end

--[=[
    Checks if the objective with the given objectiveID has already been completed
    by the quest owner or not

    @param objectiveId string

    @return boolean
]=]
function Quest:IsObjectiveCompleted(objectiveId: string): boolean
    return self:GetObjective(objectiveId) == self:GetTargetObjective(objectiveId)
end

--[=[
    Gets the current status of the quest

    @return QuestStatus
]=]
function Quest:GetQuestStatus(): QuestStatus
    return self._QuestProgress.QuestStatus
end

--[=[
    Gets the amount of times that this quest has been completed.
    Will return 0 if it has never been completed

    @return number
]=]
function Quest:GetCompleteCount(): number
    return self._QuestProgress.CompletedCount
end

--[=[
    Gets the first UTC time that this quest was completed

    @return number
]=]
function Quest:GetFirstCompletedTick(): number
    return self._QuestProgress.FirstCompletedTick
end

--[=[
    Gets the last UTC time at which this quest was completed

    @return number
]=]
function Quest:GetLastCompletedTick(): number
    return self._QuestProgress.LastCompletedTick
end

--[=[
    Adds to the objective of the quest by the amount specified

    @param objectiveId string
    @param amount number

    @return ()
]=]
function Quest:AddObjective(objectiveId: string, amount: number): ()
    return self:SetObjective(objectiveId, self:GetObjective(objectiveId) + amount)
end

--[=[
    Removes to the objective by the amount specfiied

    @param objectiveId string
    @param amount number

    @return ()
]=]
function Quest:RemoveObjective(objectiveId: string, amount: number): ()
    return self:SetObjective(objectiveId, self:GetObjective(objectiveId) - amount)
end

--[=[
    Gets how long until the quest becomes available.
    It will return 0 if it is already available

    @return number
]=]
function Quest:GetTimeForAvailable(): number
    local serverTime: number = workspace:GetServerTimeNow()

    if serverTime < self:GetQuestStart() then
        return self:GetQuestStart() - serverTime
    end

    return 0
end

--[=[
    Will return how long until the quest becomes unavailable.
    It will return 0 if it is already unavailable

    @return number
]=]
function Quest:GetTimeForUnavailable(): number
    local serverTime: number = workspace:GetServerTimeNow()

    if serverTime < self:GetQuestEnd() then
        return self:GetQuestEnd() - serverTime
    end

    return 0
end

--[=[
    Gets the UTC time at which this quest should become available

    @return number
]=]
function Quest:GetQuestStart(): number
    return self.QuestStart
end

--[=[
    Gets the UTC time at which this quest should become disabled and no longer be available

    @return number
]=]
function Quest:GetQuestEnd(): number
    return self.QuestEnd
end

--[=[
    Sets the quest objective to the given new value

    @param objectiveId string
    @param newAmount number

    @return ()
]=]
function Quest:SetObjective(objectiveId: string, newAmount: number): ()
    local questObjective: QuestObjective? = self._QuestObjectives[objectiveId]

    if not questObjective then
        warn(string.format(NO_OBJECTIVE, objectiveId, self.Name))
        return
    end

    if questObjective:Set(newAmount) then
        self.OnQuestObjectiveChanged:Fire(objectiveId, self:GetObjective(objectiveId))
    end
end

--[=[
    Gets the time since the quest was completed

    @return number
]=]
function Quest:GetTimeSinceCompleted(): number
    return os.time() - self:GetLastCompletedTick()
end

--[=[
    Gets an objective value by its id

    @param objectiveId number

    @return number
]=]
function Quest:GetObjective(objectiveId: number): number
    local questObjective: QuestObjectiveProgress? = self:_GetObjectiveProgress(objectiveId)

    if not questObjective then
        return 0
    end

    return questObjective.CurrentProgress or 0
end

--[=[
    Gets the target objective by the id

    @param objectiveId string

    @return number
]=]
function Quest:GetTargetObjective(objectiveId: string): number
    local questObjective: QuestObjective = self:GetQuestObjective(objectiveId)

    if not questObjective then
        return 0
    end

    return questObjective:GetTargetProgress()
end

--[=[
    Returns a number with the amount of objectives that exist within
    this quest

    @return number
]=]
function Quest:GetQuestObjectivesCount(): number
    local counter: number = 0

    for _ in self:GetQuestObjectives() do
        counter += 1
    end

    return counter
end

--[=[
    Returns a number with the amount of **completed** objectives that exist within
    this quest

    @return number
]=]
function Quest:GetQuestObjectivesCompletedCount(): number
    local counter: number = 0

    for _, questObjective: QuestObjective in self:GetQuestObjectives() do
        if questObjective:IsCompleted() then
            counter += 1
        end
    end

    return counter
end

--[=[
    Returns an array of quest objectives for this given quest

    @return {[string]: QuestObjective} -- The index stands for the questId while the value stands for the QuestObjective class
]=]
function Quest:GetQuestObjectives(): {[string]: QuestObjective}
    return self._QuestObjectives

end

--[=[
    Gets the objective by its id

    @param objectiveId string

    @return QuestObjective?
]=]
function Quest:GetQuestObjective(objectiveId: string): QuestObjective?
    return self._QuestObjectives[objectiveId]
end

--[=[
    Sets the quest to complete if possible

    @return boolean
]=]
function Quest:Complete(): boolean
    if self:GetQuestStatus() ~= QuestStatus.InProgress then return false end

    local questProgress: QuestProgress = self:_GetQuestProgress()
    questProgress.CompletedCount += 1
    questProgress.LastCompletedTick = os.time()
    questProgress.QuestStatus = QuestStatus.Completed

    if questProgress.FirstCompletedTick < 0 then
        questProgress.FirstCompletedTick = questProgress.LastCompletedTick
    end

    self.OnQuestCompleted:Fire()

    if self.QuestDeliverType == QuestDeliverType.Automatic then
        -- The scheduler sometimes schedules the deliver before the complete, this ensures it will only schedule delivered afterwards
        task.delay(0, self.Deliver, self)
    end

    return true
end

--[=[
    Sets the quest to delivered if possible

    @return boolean
]=]
function Quest:Deliver(): boolean
    if self:GetQuestStatus() ~= QuestStatus.Completed then return false end

    self:_GetQuestProgress().QuestStatus = QuestStatus.Delivered
    self.OnQuestDelivered:Fire()
    return true
end

--[=[
    Cleans up our class

    @return ()
]=]
function Quest:Destroy(): ()
    self._Trove:Destroy()
end

--[=[
    Overrides and updates the quest progress. Should only be used when loading in
    player data

    @private
    @param newQuestProgress QuestProgress

    @return ()
]=]
function Quest:_SetQuestProgress(newQuestProgress: QuestProgress): ()
    for _, questObjective: QuestObjective in self._QuestObjectives do
        questObjective:Destroy()
    end
    self._QuestObjectives = {}

    for _, questObjective: QuestObjective in self.QuestObjectives do
        local objectiveId: string = questObjective.ObjectiveInfo.ObjectiveId
        local questObjectiveProgress: QuestObjectiveProgress? = newQuestProgress.QuestObjectiveProgresses[objectiveId]

        if not questObjectiveProgress then
            questObjectiveProgress = QuestObjectiveProgress {
                CurrentProgress = 0,
                Completed = false,
            }
        end

        newQuestProgress.QuestObjectiveProgresses[objectiveId] = questObjectiveProgress

        local newQuestObjective: QuestObjective = QuestObjective.new()
        newQuestObjective.ObjectiveInfo = questObjective.ObjectiveInfo
        newQuestObjective.TargetProgress = questObjective.TargetProgress
        newQuestObjective._QuestObjectiveProgress = questObjectiveProgress

        newQuestObjective.Completed:Connect(function()
            self.OnQuestObjectiveCompleted:Fire(objectiveId)
            self:_CheckProgress()
        end)

        self._QuestObjectives[objectiveId] = newQuestObjective
    end

    self._QuestProgress = newQuestProgress
end

--[=[
    Wrapper to get the objective progress quickly

    @private
    @param objectiveId string

    @return QuestObjectiveProgress?
]=]
function Quest:_GetObjectiveProgress(objectiveId: string): QuestObjectiveProgress?
    local questObjective: QuestObjectiveProgress? = self._QuestProgress.QuestObjectiveProgresses[objectiveId]

    if not questObjective then
        warn(string.format(NO_OBJECTIVE, objectiveId, self.Name))
    end

    return questObjective
end

--[=[
    Wrapper to get the quest progress

    @private

    @return QuestProgress
]=]
function Quest:_GetQuestProgress(): QuestProgress
    return self._QuestProgress
end

--[=[
    Checks if the quest is completed

    @private

    @return ()
]=]
function Quest:_CheckProgress(): ()
    if self:GetQuestStatus() ~= QuestStatus.InProgress then return end

    for _, questObjective: QuestObjective in self._QuestObjectives do
        if not questObjective:IsCompleted() then
            return
        end
    end

    self:Complete()
end

export type Quest = typeof(Quest)

return setmetatable(Quest, {
    __call = function(_, properties)
        return Quest.new(properties)
    end
})


--!strict
local QuestObjective = require(script.Parent.QuestObjective)
local QuestStatus = require(script.Parent.Parent.Enums.QuestStatus)
local Signal = require(script.Parent.Parent.Parent.Vendor.Signal)
local QuestAcceptType = require(script.Parent.Parent.Enums.QuestAcceptType)
local QuestDeliverType = require(script.Parent.Parent.Enums.QuestDeliverType)
local QuestRepeatableType = require(script.Parent.Parent.Enums.QuestRepeatableType)
local assertProperties = require(script.Parent.Parent.Functions.assertProperties)

type QuestObjective = QuestObjective.QuestObjective
type QuestStatus = QuestStatus.QuestStatus
type Signal = Signal.Signal
type QuestAcceptType = QuestAcceptType.QuestAcceptType
type QuestDeliverType = QuestDeliverType.QuestDeliverType
type QuestRepeatableType = QuestRepeatableType.QuestRepeatableType

type Quest = { -- Fake quest type for autocomplete candy
	AddObjective: (self: Quest, objectiveId: string, amount: number) -> (),
	Complete: (self: Quest) -> (),
	Deliver: (self: Quest) -> (),
	Description: string,
	GetCompleteCount: (self: Quest) -> number,
	GetFirstCompletedTick: (self: Quest) -> number,
	GetLastCompletedTick: (self: Quest) -> number,
	GetObjective: (self: Quest, objectiveId: string) -> number,
	GetQuestStatus: (self: Quest) -> QuestStatus,
	LifeCycles: {string},
	Name: string,
	OnQuestCanceled: Signal,
	OnQuestCompleted: Signal,
	OnQuestDelivered: Signal,
	OnQuestObjectiveChanged: Signal,
	QuestAcceptType: QuestAcceptType,
	QuestDeliverType: QuestDeliverType,
	QuestEnd: number,
	QuestId: string,
	QuestObjectives: {QuestObjective},
	QuestRepeatableType: QuestRepeatableType,
	QuestStart: number,
	RemoveObjective: (self: Quest, objectiveId: string, newAmount: number) -> (),
	RequiredQuests: {string},
	SetObjective: (self: Quest, objectiveId: string, newAmount: number) -> (),
}

--[=[
	@class QuestLifeCycle
	@tag Class

	QuestLifeCycles are classes that are used to give behavior to our quests. They get associated to a quest when it gets declared
	and will allow the developer to update world instances and other things when the quest is available, started, completed or delivered

	:::info

	The lifecycle runs on the side where you injected it meaning that if you load a lifecycle in the server-side it will be
	managed by the server while if you inject it in the client it will be managed by the client

	:::

	```lua
	local appleQuest = QuestLifeCycle.new {
		Name = "AppleQuest",
	}

	function appleQuest:OnInit()
		print("AppleQuest OnInit")
	end

	function appleQuest:OnStart()
		print("AppleQuest has been started!")
	end

	function appleQuest:OnComplete()
		print("AppleQuest has been completed!")
	end

	function appleQuest:OnDeliver()
		print("AppleQuest has been delivered!")
	end

	function appleQuest:OnObjectiveChange(objectiveId: string, newAmount: number)
		print("Objective " .. objectiveId .. " has been updated to " .. newAmount)
	end

	function appleQuest:OnDestroy()
		print("AppleQuest has been destroyed!")
	end
	```

]=]
local QuestLifeCycle = {}
QuestLifeCycle.__index = QuestLifeCycle
QuestLifeCycle.__type = "QuestLifeCycle"

--[=[
	The name of the lifecycle. This needs to be unique for each lifecycle

	@prop Name string
	@within QuestLifeCycle
]=]
QuestLifeCycle.Name = "" :: string
--[=[
	A reference to the player to which this lifecycle is associated

	@prop Player Player
	@within QuestLifeCycle
]=]
QuestLifeCycle.Player = newproxy() :: Player
--[=[
	A reference to the quest that this lifecycle is managing

	@prop Quest Quest
	@within QuestLifeCycle
]=]
QuestLifeCycle.Quest = newproxy() :: Quest

--[=[
	Constructor for Quest

	```lua
	local appleQuest = QuestLifeCycle.new {
		Name = "AppleQuest",
	}
	```

	@param properties {[string]: any}

	@return Quest
]=]
function QuestLifeCycle.new(properties: {[string]: any}): QuestLifeCycle
	return assertProperties(properties, QuestLifeCycle)
end

--[=[
	Called when the quest object gets created.

	If the player just joined the game and the quest is already in progress it will still call OnInit!

	:::info

	This is a virtual method. Meaning you can override it in your lifecycle

	:::

	@return ()
]=]
function QuestLifeCycle:OnInit(): ()

end

--[=[
	Called when the player quest starts the quest

	:::info

	This is a virtual method. Meaning you can override it in your lifecycle

	:::

	@return ()
]=]
function QuestLifeCycle:OnStart(): ()

end

--[=[
	Called when the quest gets completed

	:::info

	This is a virtual method. Meaning you can override it in your lifecycle

	:::

	@return ()
]=]
function QuestLifeCycle:OnComplete(): ()

end

--[=[
	Called when the quest gets delivered. Doesn't get called if the player joined the game
	and the quest was already delivered

	:::info

	This is a virtual method. Meaning you can override it in your lifecycle

	:::

	@return ()
]=]
function QuestLifeCycle:OnDeliver(): ()

end

--[=[
	Called when an objective of the quest gets updated

	:::info

	This is a virtual method. Meaning you can override it in your lifecycle

	:::

	@param _objectiveId string
	@param _newAmount number

	@return ()
]=]
function QuestLifeCycle:OnObjectiveChange(_objectiveId: string, _newAmount: number): ()

end

--[=[
	Called when the quest object gets destroyed. This happens when the player leaves the game or the quest gets removed

	:::info

	This is a virtual method. Meaning you can override it in your lifecycle

	:::

	@return ()
]=]
function QuestLifeCycle:OnDestroy(): ()

end

function QuestLifeCycle:OnCancel()

end

function QuestLifeCycle:Destroy()
	self:OnDestroy()
end

export type QuestLifeCycle = typeof(QuestLifeCycle)

return setmetatable(QuestLifeCycle, {
    __call = function(_, properties)
        return QuestLifeCycle.new(properties)
    end
})


--!strict
local QuestObjectiveProgress = require(script.Parent.Parent.Structs.QuestObjectiveProgress)
local assertProperties = require(script.Parent.Parent.Functions.assertProperties)
local Signal = require(script.Parent.Parent.Parent.Vendor.Signal)
local Trove = require(script.Parent.Parent.Parent.Vendor.Trove)

type Signal = Signal.Signal
type Trove = Trove.Trove
type QuestObjectiveProgress = QuestObjectiveProgress.QuestObjectiveProgress
type ObjectiveInfo = {
    New: ({[string]: any}) -> ObjectiveInfo,
    NewObjective: (self: ObjectiveInfo, target: number) -> (),

    ObjectiveId: string | {string},
    Name: string,
    Description: string,
}

--[=[
    @class QuestObjective
    @tag Class

    A quest objective is one of the steps that a player must complete in order to finish a quest

    It contains the data for the objective as well as some utility functions to allow us to
    manipulate the objective.

    This class shouldn't be created directly by the developer. Instead they should use the Objective
    info object and extend it with the NewObjective function.

    ```lua
    local appleQuest = ObjectiveInfo.new {
        ObjectiveId = "CollectApples",
        Name = "Collect Apples",
        Description = "Collect %s apples",
    }

    local objective1 = appleQuest:NewObjective(5) -- Created an objective to collect 5 apples
    local objective2 = appleQuest:NewObjective(3) -- Created an objective to collect 3 apples
    local objective3 = appleQuest:NewObjective(7) -- Created an objective to collect 7 apples
    ```
]=]
local QuestObjective = {}
QuestObjective.__type = "QuestObjective"
QuestObjective.__index = QuestObjective
--[=[
    A reference to the ObjectiveInfo object that this QuestObjective is based on

    @prop ObjectiveInfo ObjectiveInfo
    @within QuestObjective
]=]
QuestObjective.ObjectiveInfo = {}
--[=[
    The target progress required for the player complete this objective

    @prop TargetProgress number
    @within QuestObjective
]=]
QuestObjective.TargetProgress = 0 :: number
--[=[
    A Signal that fires whenever the quest objective is marked as completed

    ```lua
    local flowerQuest = ObjectiveInfo.new {
        ObjectiveId = "CollectFlowers",
        Name = "Collect Flowers",
        Description = "Collect %s flowers",
    }

    local appleObjective1 = appleQuest:NewObjective(5) -- Created an objective to collect 5 apples

    appleObjective1.Completed:Connect(function()
        print("Objective 1 has been completed!")
    end)
    ```

    @prop Completed Signal
    @within QuestObjective
]=]
QuestObjective.Completed = newproxy() :: Signal
--[=[
    A reference to our QuestObjectiveProgress to track the dynamic data of our objective

    @private

    @prop _QuestObjectiveProgress QuestObjectiveProgress
    @within QuestObjective
]=]
QuestObjective._QuestObjectiveProgress = newproxy() :: QuestObjectiveProgress
--[=[
    The Trove object from our QuestObjective that allows us to clearup the object

    @private

    @prop _Trove Trove
    @within QuestObjective
]=]
QuestObjective._Trove = newproxy() :: Trove

--[=[
    Constructor for QuestObjective

    ```lua
    QuestObjective.new {
        TargetProgress = 10, -- Creates a new objective with a target progress of 10
    }
    ```

    @param properties {[string]: any}

    @return QuestObjective
]=]
function QuestObjective.new(properties: {[string]: any}): QuestObjective
    local self: QuestObjective = assertProperties(properties, QuestObjective)
    self.Completed = Signal.new()
    self._Trove = Trove.new()
    self._QuestObjectiveProgress = QuestObjectiveProgress {
        CurrentProgress = 0,
        Completed = false,
    }

    return self
end

--[=[
    Gets the current target progress of our objective

    ```lua
     print(QuestObjective:Get()) -- 0
    QuestObjective:Set(5)
    print(QuestObjective:Get()) -- 5
    ```

    @return number
]=]
function QuestObjective:Get(): number
    return self._QuestObjectiveProgress.CurrentProgress
end

--[=[
    Gets the target to complete this objective

    ```lua
    local appleObjective1 = appleQuest:NewObjective(5) -- Created an objective to collect 5 apples
    print(appleObjective1:GetTargetProgress()) -- 5
    ```

    @return number
]=]
function QuestObjective:GetTargetProgress(): number
    return self.TargetProgress
end

--[=[
    Gets an id with the objective ID of the QuestObjective

    ```lua
    local flowerQuest = ObjectiveInfo.new {
        ObjectiveId = "CollectFlowers",
        Name = "Collect Flowers",
        Description = "Collect %s flowers",
    }

    local appleObjective1 = appleQuest:NewObjective(5) -- Created an objective to collect 5 apples

    print(appleObjective1:GetObjectiveId()) -- "CollectFlowers"
    ```

    @return string
]=]
function QuestObjective:GetObjectiveId(): string
    return self.ObjectiveInfo.ObjectiveId or ""
end

--[=[
    Gets an id with the objective ID of the QuestObjective

    ```lua
    local flowerQuest = ObjectiveInfo.new {
        ObjectiveId = "CollectFlowers",
        Name = "Collect Flowers",
        Description = "Collect %s flowers",
    }

    local appleObjective1 = appleQuest:NewObjective(5) -- Created an objective to collect 5 apples

    print(appleObjective1:GetName()) -- "Collect Flowers"
    ```

    @return string
]=]
function QuestObjective:GetName(): string
    return self.ObjectiveInfo.Name or ""
end

--[=[
    Gets an id with the objective ID of the QuestObjective

    ```lua
    local flowerQuest = ObjectiveInfo.new {
        ObjectiveId = "CollectFlowers",
        Name = "Collect Flowers",
        Description = "Collect %s flowers",
    }

    local appleObjective1 = appleQuest:NewObjective(5) -- Created an objective to collect 5 apples

    print(appleObjective1:GetDescription()) -- "Collect %s flowers"
    ```

    @return string
]=]
function QuestObjective:GetDescription(): string
    return self.ObjectiveInfo.Description or ""
end

--[=[
    Adds an amount to the current progress of the objective

    ```lua
     print(QuestObjective:Get()) -- 0
    QuestObjective:Add(1)
    print(QuestObjective:Get()) -- 1
    ```

    @param amount number

    @return boolean
]=]
function QuestObjective:Add(amount: number): boolean
    if self:IsCompleted() then return false end -- No point on updating

    return self:Set(self:Get() + amount)
end

--[=[
    Removes an amount to the current progress of the objective

    ```lua
     print(QuestObjective:Get()) -- 1
    QuestObjective:Remove(1)
    print(QuestObjective:Get()) -- 0
    ```

    @param amount number

    @return boolean
]=]
function QuestObjective:Remove(amount: number): boolean
    if self:IsCompleted() then return false end -- No point on updating

    return self:Set(self:Get() - amount)
end

--[=[
    Sets the current progress amount of our objective

    ```lua
     print(QuestObjective:Get()) -- 0
    QuestObjective:Set(1)
    print(QuestObjective:Get()) -- 1
    QuestObjective:Set(5)
    print(QuestObjective:Get()) -- 5
    ```

    @param newAmount number

    @return boolean
]=]
function QuestObjective:Set(newAmount: number): boolean
    if self:IsCompleted() then return false end -- No point on updating
    if self:Get() == newAmount then return false end -- Same value

    self._QuestObjectiveProgress.CurrentProgress = math.clamp(newAmount, 0, self.TargetProgress)

    if newAmount >= self.TargetProgress then
        self._QuestObjectiveProgress.Completed = true
        self.Completed:Fire()
    end

    return true
end

--[=[
    Checks if the quest has already been completed or not

    ```lua
    print(QuestObjective:IsCompleted()) -- false
    QuestObjective:Set(5)
     print(QuestObjective:IsCompleted()) -- true
    ```

    @return boolean
]=]
function QuestObjective:IsCompleted(): boolean
    return self._QuestObjectiveProgress.Completed
end

--[=[
    Destroys the QuestObjective and clears up the object

    ```lua
    QuestObjective:Destroy()
    ```

    @return ()
]=]
function QuestObjective:Destroy(): ()
    self._Trove:Destroy()
end

export type QuestObjective = typeof(QuestObjective)

return setmetatable(QuestObjective, {
    __call = function(_, properties)
        return QuestObjective.new(properties)
    end
})


local QuestStatus = require(script.Parent.Parent.Enums.QuestStatus)

return {
    [QuestStatus.InProgress] = "OnStart",
    [QuestStatus.Completed] = "OnComplete",
    [QuestStatus.Delivered] = "OnDeliver",
}


local QuestRepeatableType = require(script.Parent.Parent.Enums.QuestRepeatableType)

local function hoursToSeconds(hours: number): number
    return hours * 3600
end

return {
    [QuestRepeatableType.Custom] = 0,
    [QuestRepeatableType.Daily] = hoursToSeconds(23),
    [QuestRepeatableType.Weekly] = hoursToSeconds(167),
    [QuestRepeatableType.Infinite] = 0,
    [QuestRepeatableType.NonRepeatable] = 0,
}


return  {
    RoQuestAlreadyInit = "RoQuest has already been initialized",
    LoadDirectoryMixedType = "Cannot load directory with mixed file types. Make sure you only load Quests or only QuestLifeCycles",
    NoQuestById = "No quest with the id %s was found",
    DuplicateQuestId = "Duplicate quest id %s found",
    DuplicateLifeCycleName = "Duplicate life cycle name %s found",
    NoObjectiveId = "No objective with the id %s exists in any of the loaded quests",
    NoLifeCycleMethod = "No method %s exists in the life cycle %s",
    SetPlayerData = "When using RoQuest:SetPlayerData make sure to pass a PlayerQuestData struct\n\nlocal RoQuest = require(ReplicatedStorage.RoQuest).Server\nlocal PlayerQuestData = RoQuest.PlayerQuestData\nRoQuest:SetPlayerData(player, PlayerQuestData {})",
    WaitForLoad = "Wait For Load timed out. Traceback: \n%s",
}


--!strict
--[=[
    @class QuestAcceptType
    @tag Enum

    Sets if the quest should be accepted automatically or manually
]=]
local QuestAcceptType = {
    Automatic = "Automatic",
    Manual = "Manual",
}

--[=[
    @interface Status
    @within QuestAcceptType
    .Automatic "Automatic" -- The quest will automatically be accepted when the player meets the requirements
    .Manual "Manual" -- The quest will be accepted when the player manually accepts it
]=]

table.freeze(QuestAcceptType)

export type QuestAcceptType = typeof(QuestAcceptType)

return QuestAcceptType


--!strict
--[=[
    @class QuestDeliverType
    @tag Enum

    Sets if the quest should be delivered automatically or manually
]=]
local QuestDeliverType = {
    Automatic = "Automatic",
    Manual = "Manual",
}

--[=[
    @interface Status
    @within QuestDeliverType
    .Automatic "Automatic" -- The quest will automatically be delivered when completed
    .Manual "Manual" -- It requires to be manually delivered by the developer
]=]

table.freeze(QuestDeliverType)

export type QuestDeliverType = typeof(QuestDeliverType)

return QuestDeliverType


--!strict
--[=[
    @class QuestRepeatableType
    @tag Enum

    Sets how often and if it is possible to repeat a quest
]=]
local QuestRepeatableType = {
    NonRepeatable = "NonRepeatable",
    Infinite = "Infinite",
    Daily = "Daily",
    Weekly = "Weekly",
    Custom = "Custom",
}

--[=[
    @interface Status
    @within QuestRepeatableType
    .NonRepeatable "NonRepeatable" -- The quest can only be completed once
    .Infinite "Infinite" -- This means the quest can be repeated non-stop without any delay
    .Daily "Daily" -- Quest can be completed everyday
    .Weekly "Weekly" -- Quest can be completed weekly
    .Custom "Custom" -- Quest can only be completed when the developer sets it to be
]=]

table.freeze(QuestRepeatableType)

export type QuestRepeatableType = typeof(QuestRepeatableType)

return QuestRepeatableType


--!strict
--[=[
    @class QuestStatus
    @tag Enum

    Tracks the current status of the player's quest
]=]
local QuestStatus = {
    NotStarted = "NotStarted",
    InProgress = "InProgress",
    Completed = "Completed",
    Delivered = "Delivered",
}

--[=[
    @interface Status
    @within QuestStatus
    .NotStarted "NotStarted" -- The quest hasn't been initiated by the player
    .InProgress "InProgress" -- The quest is still in progress
    .Completed "Completed" -- The quest objectives have been fullfilled and the quest is now completed
    .Delivered "Delivered" -- Means the quest has been delivered and closed
]=]

table.freeze(QuestStatus)

export type QuestStatus = typeof(QuestStatus)

return QuestStatus


local function assertProperties(properties, class)
    properties = properties or {}

    for index: string, value: any in properties do
        if class[index] == nil then
            error(string.format("Property %s does not exist in class %s", index, class.__type))
        end

        if typeof(value) ~= typeof(class[index]) then
            print(value, class[index], class)
            error(string.format("Property %s is not of type %s in class %s", index, class[index], class.__type))
        end
    end

    return setmetatable(properties, class)
end

return assertProperties


local function createStruct(data)
    return setmetatable({}, {
        __call = function(_, properties)
            local dataTable = setmetatable({}, data)

            for index: string, value: any in pairs(data) do
                if properties[index] == nil then
                    if typeof(value) == "table" then
                        dataTable[index] = table.clone(value)
                    else
                        dataTable[index] = value
                    end
                    continue
                end

                dataTable[index] = properties[index]
            end

            return dataTable
        end
    })
end

return createStruct


local WarningMessages = require(script.Parent.Parent.Data.WarningMessages)
local Quest = require(script.Parent.Parent.Classes.Quest)
local QuestLifeCycle = require(script.Parent.Parent.Classes.QuestLifeCycle)

type Quest = Quest.Quest
type QuestLifeCycle = QuestLifeCycle.QuestLifeCycle

local LOAD_DIRECTORY_TYPES: {[string]: true} = {
	["Quest"] = true,
	["QuestLifeCycle"] = true,
}

--[[
    Loads all the quests and lifecycles from the directory and returns them in an array

	@private
	@server
	@param instancesToFilter {Instance}

	@return {Quest | QuestLifeCycle}
]]
local function loadDirectory(instancesToFilter: {Instance}): {Quest | QuestLifeCycle}
	local array: {Quest | QuestLifeCycle} = {}

	for _, childInstance: Instance in instancesToFilter do
		if not childInstance:IsA("ModuleScript") then
			continue
		end

		local requiredObject: any = require(childInstance)

		if LOAD_DIRECTORY_TYPES[requiredObject.__type] then
			if #array > 0 and array[1].__type ~= requiredObject.__type then
				error(WarningMessages.LoadDirectoryMixedType)
			end

			array[#array+1] = requiredObject
		end
	end

	return array
end

return loadDirectory


local Quest = require(script.Parent.Parent.Classes.Quest)

type Quest = Quest.Quest

local REPLICATE_PROPERTIES: {[string]: true} = {
    Name = true,
    Description = true,
    QuestId = true,
    QuestAcceptType = true,
    QuestDeliverType = true,
    QuestRepeatableType = true,
    QuestStart = true,
    QuestEnd = true,
    RequiredQuests = true,
    LifeCycles = true,
    QuestObjectives = true,
}

local function networkQuestParser(quest: Quest): any
    local parsedData: {[string]: any} = {}

    for propertyIndex: string in REPLICATE_PROPERTIES do
        parsedData[propertyIndex] = quest[propertyIndex]
    end

    return parsedData
end

return networkQuestParser


--!strict
local HEADER: string = "[RoQuest]: "

return function(message: string): ()
    warn(HEADER .. message, "\n", debug.traceback())
end


local QuestProgress = require(script.Parent.QuestProgress)
local createStruct = require(script.Parent.Parent.Functions.createStruct)

export type QuestProgress = QuestProgress.QuestProgress

--[=[
    @class PlayerQuestData
    @tag Struct

    ```csharp
    struct PlayerQuestData {
        InProgress: {[string]: QuestProgress},
        Completed: {[string]: QuestProgress},
        Delivered: {[string]: QuestProgress},
    }
    ```
]=]
local PlayerQuestData = {
    InProgress = {},
    Completed = {},
    Delivered = {},
}

--[=[
    Contains all the quests in progress by the Player

    @prop InProgress {[string]: QuestProgress}
    @within PlayerQuestData
]=]

--[=[
    Contains all the completed quests by the Player

    @prop Completed {[string]: QuestProgress}
    @within PlayerQuestData
]=]

--[=[
    Contains all the delivered quests by the Player

    @prop Delivered {[string]: QuestProgress}
    @within PlayerQuestData
]=]

export type PlayerQuestData = {
    InProgress: {[string]: QuestProgress},
    Completed: {[string]: QuestProgress},
    Delivered: {[string]: QuestProgress},
}

return createStruct(PlayerQuestData)


local createStruct = require(script.Parent.Parent.Functions.createStruct)

--[=[
    @class QuestObjectiveProgress
    @tag Struct

    ```csharp
    struct QuestObjectiveProgress {
        CurrentProgress: number,
        Completed: boolean
    }
    ```
]=]
local QuestObjectiveProgress = {
    CurrentProgress = 0,
    Completed = false,
}

--[=[
    Contains the data for the current progress of this objective

    @prop CurrentProgress number
    @within QuestObjectiveProgress
]=]

--[=[
    A flat to determine is the objective is complete or not

    @prop Completed boolean
    @within QuestObjectiveProgress
]=]

export type QuestObjectiveProgress = typeof(QuestObjectiveProgress)

return createStruct(QuestObjectiveProgress)


local QuestStatus = require(script.Parent.Parent.Enums.QuestStatus)
local createStruct = require(script.Parent.Parent.Functions.createStruct)
local QuestObjectiveProgress = require(script.Parent.QuestObjectiveProgress)

type QuestStatus = QuestStatus.QuestStatus
type QuestObjectiveProgress = QuestObjectiveProgress.QuestObjectiveProgress

--[=[
    @class QuestProgress
    @tag Struct

    ```csharp
    struct PlayerQuestData {
        QuestObjectiveProgresses: {[string]: QuestObjectiveProgress},
        QuestStatus: QuestStatus,
        CompletedCount: number,
        FirstCompletedTick: number,
        LastCompletedTick: number,
    }
    ```
]=]
local QuestProgress = {
    QuestObjectiveProgresses = {},
    QuestStatus = QuestStatus.NotStarted,
    CompletedCount = 0,
    FirstCompletedTick = -1,
    LastCompletedTick = -1,
}

--[=[
    A hashmap containing all the quest progress objectives of our quest

    @prop QuestObjectiveProgresses {[string]: QuestObjectiveProgress}
    @within QuestProgress
]=]

--[=[
    The current status of the quest. Wheter it is in progress, completed or delivered

    @prop QuestStatus QuestStatus
    @within QuestProgress
]=]

--[=[
    The amount of times this quest has been completed

    @prop CompletedCount number
    @within QuestProgress
]=]

--[=[
    The time date at which the quest was first completed

    @prop FirstCompletedTick number
    @within QuestProgress
]=]

--[=[
    The time date at which the quest was last completed

    @prop LastCompletedTick number
    @within QuestProgress
]=]

export type QuestProgress = {
    QuestObjectiveProgresses: {[string]: QuestObjectiveProgress},
    QuestStatus: QuestStatus,
    CompletedCount: number,
    FirstCompletedTick: number,
    LastCompletedTick: number,
}

return createStruct(QuestProgress)


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

local Remote = ReplicatedStorage:FindFirstChild("RedEvent") :: RemoteEvent
local IS_CLIENT = RunService:IsClient()

local Serdes = require(script.Parent.Serdes)

local Spawn = require(script.Parent.Parent.Util.Spawn)
local Promise = require(script.Parent.Parent.Util.Promise)
local Clock = require(script.Parent.Parent.Util.Clock)

local Event = {}

Event.Callbacks = {} :: { [string]: ((...any) -> ...any)? }
Event.Outgoing = {} :: any

if IS_CLIENT then
	Event.ActiveCalls = {}
end

function Event.Listen()
	debug.setmemorycategory("Red.Listen")

	if IS_CLIENT then
		Remote.OnClientEvent:Connect(function(SingleFire, MultipleFire, IncomingCall)
			debug.profilebegin("Red.Listen.Incoming")

			if SingleFire then
				debug.profilebegin("Red.Listen.Incoming.SingleFire")

				for EventId, Call in SingleFire do
					local Callback = Event.Callbacks[EventId]

					if Callback then
						if type(Call) == "table" then
							Spawn(Callback, unpack(Call))
						else
							Spawn(Callback, Call)
						end
					end
				end

				debug.profileend()
			end

			if MultipleFire then
				debug.profilebegin("Red.Listen.Incoming.Fire")

				for EventId, Calls in MultipleFire do
					local Callback = Event.Callbacks[EventId]

					if Callback then
						for _, Call in Calls do
							if type(Call) == "table" then
								Spawn(Callback, unpack(Call))
							else
								Spawn(Callback, Call)
							end
						end
					end
				end

				debug.profileend()
			end

			if IncomingCall then
				debug.profilebegin("Red.Listen.Incoming.Call")

				for _, Call in IncomingCall do
					local CallId = table.remove(Call, 1)
					local Success = table.remove(Call, 1)

					if Event.ActiveCalls[CallId] then
						if Success then
							Event.ActiveCalls[CallId].Resolve(unpack(Call))
						else
							Event.ActiveCalls[CallId].Reject(unpack(Call))
						end

						Event.ActiveCalls[CallId] = nil
					end
				end

				debug.profileend()
			end

			debug.profileend()
		end)

		Clock.new(1 / 60, function()
			debug.profilebegin("Red.Listen.Outgoing")

			if not next(Event.Outgoing) then
				return
			end

			local SingleFire = {}
			local SendSingleFire = false

			if Event.Outgoing[1] then
				for EventId, Calls in Event.Outgoing[1] do
					if #Calls == 1 then
						SingleFire[EventId] = Calls[1]
						Event.Outgoing[1][EventId] = nil

						SendSingleFire = true
					end
				end
			end

			Remote:FireServer(if SendSingleFire then SingleFire else nil, Event.Outgoing[1], Event.Outgoing[2])
			table.clear(Event.Outgoing)

			debug.profileend()
		end)
	else
		Remote.OnServerEvent:Connect(function(Player, SingleFire, MultipleFire, IncomingCall)
			debug.profilebegin("Red.Listen.Incoming")

			if SingleFire then
				debug.profilebegin("Red.Listen.Incoming.SingleFire")

				for EventId, Call in SingleFire do
					local Callback = Event.Callbacks[EventId]

					if Callback then
						if type(Call) == "table" then
							Spawn(Callback, Player, unpack(Call))
						else
							Spawn(Callback, Player, Call)
						end
					end
				end

				debug.profileend()
			end

			if MultipleFire then
				debug.profilebegin("Red.Listen.Incoming.MultipleFire")

				for EventId, Calls in MultipleFire do
					local Callback = Event.Callbacks[EventId]

					if Callback then
						for _, Call in Calls do
							if type(Call) == "table" then
								Spawn(Callback, Player, unpack(Call))
							else
								Spawn(Callback, Player, Call)
							end
						end
					end
				end

				debug.profileend()
			end

			if IncomingCall then
				debug.profilebegin("Red.Listen.Incoming.Call")

				for EventId, Calls in IncomingCall do
					if Event.Callbacks[EventId] then
						for _, Call in Calls do
							Spawn(function()
								if Event.Outgoing[Player] == nil then
									Event.Outgoing[Player] = {}
								end

								if Event.Outgoing[Player][2] == nil then
									Event.Outgoing[Player][2] = {}
								end

								local CallId = table.remove(Call, 1)

								table.insert(Event.Outgoing[Player][2], {
									CallId,
									pcall(Event.Callbacks[EventId] :: any, Player, unpack(Call)),
								})
							end)
						end
					else
						if Event.Outgoing[Player] == nil then
							Event.Outgoing[Player] = {}
						end

						if Event.Outgoing[Player][2] == nil then
							Event.Outgoing[Player][2] = {}
						end

						for _, Call in Calls do
							table.insert(Event.Outgoing[Player][2], {
								Call[1],
								false,
								"[RED] Event not found",
							})
						end
					end
				end

				debug.profileend()
			end

			debug.profileend()
		end)

		RunService.Heartbeat:Connect(function()
			debug.profilebegin("Red.Listen.Outgoing")

			for Player, Packets in Event.Outgoing do
				local SingleCall = {}
				local SendSingleCall = false

				if Packets[1] then
					for EventId, Calls in Packets[1] do
						if #Calls == 1 then
							SingleCall[EventId] = Calls[1]
							Packets[1][EventId] = nil

							SendSingleCall = true
						end
					end
				end

				Remote:FireClient(Player, if SendSingleCall then SingleCall else nil, Packets[1], Packets[2])
			end

			table.clear(Event.Outgoing)

			debug.profileend()
		end)
	end
end

function Event.AddQueue(Queue: { any }, Call: { any })
	local Length = #Call

	if Length == 1 then
		local Type = type(Call[1])

		if Type ~= "table" then
			table.insert(Queue, Call[1])
		else
			table.insert(Queue, Call)
		end
	else
		table.insert(Queue, Call)
	end
end

function Event.FireClient(Player: Player, EventName: string, ...)
	assert(not IS_CLIENT, "Event.FireClient can only be called from the server")

	local EventId = Serdes.IdentifierAsync(EventName)

	if Event.Outgoing[Player] == nil then
		Event.Outgoing[Player] = {}
	end

	if Event.Outgoing[Player][1] == nil then
		Event.Outgoing[Player][1] = {}
	end

	if Event.Outgoing[Player][1][EventId] == nil then
		Event.Outgoing[Player][1][EventId] = {}
	end

	Event.AddQueue(Event.Outgoing[Player][1][EventId], { ... })
end

function Event.FireServer(EventName: string, ...)
	assert(IS_CLIENT, "Event.FireServer can only be called on the client")

	local Args = { ... }

	return Serdes.Identifier(EventName):Then(function(EventId)
		if Event.Outgoing[1] == nil then
			Event.Outgoing[1] = {}
		end

		if Event.Outgoing[1][EventId] == nil then
			Event.Outgoing[1][EventId] = {}
		end

		Event.AddQueue(Event.Outgoing[1][EventId], Args)
	end)
end

function Event.Call(EventName: string, ...)
	assert(IS_CLIENT, "Event.Call can only be called on the client")

	local Args = { ... }

	return Promise.new(function(Resolve, Reject)
		local CallId = Serdes.OneTime()
		local EventId = Serdes.IdentifierAsync(EventName)

		if Event.Outgoing[2] == nil then
			Event.Outgoing[2] = {}
		end

		if Event.Outgoing[2][EventId] == nil then
			Event.Outgoing[2][EventId] = {}
		end

		table.insert(Args, 1, CallId)
		table.insert(Event.Outgoing[2][EventId], Args)

		Event.ActiveCalls[CallId] = {
			Resolve = Resolve,
			Reject = Reject,
		}
	end)
end

function Event.SetCallback(EventName: string, Callback: ((...any) -> any)?)
	return Serdes.Identifier(EventName):Then(function(EventId)
		Event.Callbacks[EventId] = Callback
	end)
end

return Event


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

local Event = ReplicatedStorage:WaitForChild("RedEvent")

local Promise = require(script.Parent.Parent.Util.Promise)

local Serdes = {}

Serdes.NextId = 0
Serdes.NextOT = 0

function Serdes.RegisterIdentifier(Name: string)
	assert(RunService:IsServer(), "RegisterIdentifier can only be called on the server")

	local Id = utf8.char(Serdes.NextId)
	Serdes.NextId += 1

	Event:SetAttribute(Name, Id)

	return Id
end

function Serdes.Identifier(Name: string)
	if RunService:IsClient() then
		return Promise.new(function(Resolve)
			if Event:GetAttribute(Name) ~= nil then
				Resolve(Event:GetAttribute(Name))
			else
				local Thread = task.delay(
					5,
					warn,
					`[Red.Serdes]: Retrieving identifier exceeded 5 seconds. Make sure '{Name}' is registered on the server.`
				)

				Event:GetAttributeChangedSignal(Name):Once(function()
					task.cancel(Thread)

					Resolve(Event:GetAttribute(Name))
				end)
			end
		end)
	else
		if Event:GetAttribute(Name) then
			return Promise.Resolve(Event:GetAttribute(Name))
		else
			return Promise.Resolve(Serdes.RegisterIdentifier(Name))
		end
	end
end

function Serdes.IdentifierAsync(Name: string)
	return Serdes.Identifier(Name):Await()
end

function Serdes.OneTime()
	Serdes.NextOT += 1

	if Serdes.NextOT == 0xFFFF + 1 then
		Serdes.NextOT = 0
	end

	return utf8.char(Serdes.NextOT)
end

return Serdes


local Spawn = require(script.Parent.Spawn)

type BinItem = Instance | RBXScriptConnection | () -> ...any

return function()
	local Bin: { BinItem } = {}

	return function(Item: BinItem)
		table.insert(Bin, Item)
	end, function()
		for _, Item in Bin do
			if typeof(Item) == "Instance" then
				Item:Destroy()
			elseif typeof(Item) == "RBXScriptConnection" then
				Item:Disconnect()
			elseif typeof(Item) == "function" then
				Spawn(Item)
			end
		end

		table.clear(Bin)
	end
end


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

local function MakeHeartbeatFunction(Clock: Clock)
	return function(Delta)
		Clock:Advance(Delta)
	end
end

local Clock = {}
Clock.__index = Clock

function Clock.Clock(Interval: number, Callback: () -> ())
	local self = setmetatable({}, Clock)

	self.Interval = Interval
	self.Callback = Callback
	self.Delta = 0

	self.Connection = RunService.Heartbeat:Connect(MakeHeartbeatFunction(self))

	return self
end

Clock.new = Clock.Clock

function Clock.Pause(self: Clock)
	if self.Connection then
		self.Connection:Disconnect()
	end
end

function Clock.Resume(self: Clock)
	if self.Connection.Connected then
		return
	end

	self.Connection = RunService.Heartbeat:Connect(MakeHeartbeatFunction(self))
end

function Clock.Advance(self: Clock, Delta: number)
	self.Delta += Delta

	if self.Delta >= self.Interval * 10 then
		local Skipped = math.floor(self.Delta / self.Interval)
		self.Delta -= Skipped * self.Interval

		if ReplicatedStorage:GetAttribute("RedDebug") then
			warn("[Red.Clock] Clock is falling behind! Skipped " .. Skipped .. " intervals.")
		end

		return
	end

	if self.Delta >= self.Interval then
		self.Delta -= self.Interval
		self.Callback()
	end
end

export type Clock = typeof(Clock.Clock(...))

return Clock


local CollectionService = game:GetService("CollectionService")

local Spawn = require(script.Parent.Spawn)

return function<T...>(Tag: string, Start: (Instance) -> T..., Stop: (T...) -> ()): () -> ()
	local InstanceMap = {}

	for _, Instance in CollectionService:GetTagged(Tag) do
		Spawn(function()
			InstanceMap[Instance] = { Start(Instance) }
		end)
	end

	local AddConnection = CollectionService:GetInstanceAddedSignal(Tag):Connect(function(Instance)
		InstanceMap[Instance] = { Start(Instance) }
	end)

	local RemoveConnection = CollectionService:GetInstanceRemovedSignal(Tag):Connect(function(Instance)
		local Value = InstanceMap[Instance]

		if Value then
			InstanceMap[Instance] = nil;
			(Stop :: any)(unpack(Value))
		end
	end)

	return function()
		AddConnection:Disconnect()
		RemoveConnection:Disconnect()

		for Instance, Value in InstanceMap do
			Spawn(Stop :: any, unpack(Value))
		end
	end
end


local Spawn = require(script.Parent.Spawn)

local Promise = {}
Promise.__index = Promise

function Promise.Promise(Callback: (Resolve: (...any) -> (), Reject: (...any) -> ()) -> ())
	local self = setmetatable({}, Promise)

	self.Status = "Pending"
	self.OnResolve = {} :: { (...any) -> () }
	self.OnReject = {} :: { (...any) -> () }

	self.Value = {} :: { any }

	self.Thread = nil :: thread?
	self.Thread = coroutine.create(function()
		xpcall(Callback, function(Error)
			(self :: any):_Reject(Error)
		end, function(...)
			(self :: any):_Resolve(...)
		end, function(...)
			(self :: any):_Reject(...)
		end)
	end)

	task.spawn(self.Thread :: thread)

	return self
end

Promise.new = Promise.Promise :: (Callback: (Resolve: (...any) -> (), Reject: (...any) -> ()) -> ()) -> Promise

function Promise.Resolve(...: any): Promise
	local self = setmetatable({}, Promise)

	self.Status = "Resolved"
	self.OnResolve = {} :: { (...any) -> () }
	self.OnReject = {} :: { (...any) -> () }
	self.Value = { ... } :: { any }
	self.Thread = nil :: thread?

	return self
end

function Promise.Reject(...: any): Promise
	local self = setmetatable({}, Promise)

	self.Status = "Rejected"
	self.OnResolve = {} :: { (...any) -> () }
	self.OnReject = {} :: { (...any) -> () }
	self.Value = { ... } :: { any }
	self.Thread = nil :: thread?

	return self
end

function Promise._Resolve(self: Promise, ...: any)
	assert(self.Status == "Pending", "Cannot resolve a promise that is not pending.")

	self.Status = "Resolved"
	self.Value = table.pack(...)

	for _, Callback in self.OnResolve do
		Spawn(Callback, ...)
	end

	task.defer(task.cancel, self.Thread :: thread)
end

function Promise._Reject(self: Promise, ...: any)
	assert(self.Status == "Pending", "Cannot reject a promise that is not pending.")

	self.Status = "Rejected"
	self.Value = table.pack(...)

	for _, Callback in self.OnReject do
		Spawn(Callback, ...)
	end

	task.defer(task.cancel, self.Thread :: thread)
end

function Promise.Then(self: Promise, OnResolve: ((...any) -> ...any)?, OnReject: ((...any) -> ...any)?): Promise
	return Promise.Promise(function(Resolve, Reject)
		local function PromiseResolutionProcedure(Value: Promise | any, ...: any)
			if type(Value) == "table" and getmetatable(Value) == Promise then
				if Value.Status == "Pending" then
					table.insert(Value.OnResolve, Resolve)
					table.insert(Value.OnReject, Reject)
				elseif Value.Status == "Resolved" then
					Resolve(Value.Value)
				elseif Value.Status == "Rejected" then
					Reject(Value.Value)
				end
			else
				Resolve(Value, ...)
			end
		end

		if self.Status == "Pending" then
			if OnResolve then
				table.insert(self.OnResolve, function(...)
					PromiseResolutionProcedure(OnResolve(...))
				end)
			else
				table.insert(self.OnResolve, PromiseResolutionProcedure)
			end

			if OnReject then
				table.insert(self.OnReject, function(...)
					PromiseResolutionProcedure(OnReject(...))
				end)
			else
				table.insert(self.OnReject, Reject)
			end
		elseif self.Status == "Resolved" then
			if OnResolve then
				PromiseResolutionProcedure(OnResolve(unpack(self.Value)))
			else
				Resolve(unpack(self.Value))
			end
		elseif self.Status == "Rejected" then
			if OnReject then
				PromiseResolutionProcedure(OnReject(unpack(self.Value)))
			else
				Reject(unpack(self.Value))
			end
		end
	end)
end

function Promise.Catch(self: Promise, OnReject: (...any) -> ())
	return self:Then(nil, OnReject)
end

function Promise.Finally(self: Promise, Finally: () -> ())
	return self:Then(function(...)
		Finally()
		return ...
	end, function(Error)
		Finally()
		error(Error)
	end)
end

function Promise.Await(self: Promise): ...any
	if self.Status == "Resolved" then
		return unpack(self.Value)
	elseif self.Status == "Rejected" then
		return error(unpack(self.Value))
	else
		local Current = coroutine.running()

		local function Resume()
			coroutine.resume(Current)
		end

		table.insert(self.OnResolve, Resume)
		table.insert(self.OnReject, Resume)

		coroutine.yield()

		if self.Status == "Resolved" then
			return unpack(self.Value)
		else
			return error(unpack(self.Value))
		end
	end
end

export type Promise = typeof(Promise.Promise(...))

return Promise


return function<T>(Limit: number, Interval: number)
	assert(Limit > 0, "Limit must be greater than 0")

	local CountMap = {} :: { [T]: number }
	local CountKeyless = 0

	return function(Key: T?)
		if Key then
			local Count = CountMap[Key]

			if Count == nil then
				Count = 0

				task.delay(Interval, function()
					CountMap[Key] = nil
				end)
			end

			if Count >= Limit then
				return false
			end

			CountMap[Key] = Count + 1
		else
			if CountKeyless == 0 then
				task.delay(Interval, function()
					CountKeyless = 0
				end)
			end

			if CountKeyless >= Limit then
				return false
			end

			CountKeyless = CountKeyless + 1
		end

		return true
	end
end


local Promise = require(script.Parent.Promise)
local Spawn = require(script.Parent.Spawn)

type SignalNode<T...> = {
	Next: SignalNode<T...>?,
	Callback: (T...) -> (),
}

export type Signal<T...> = {
	Root: SignalNode<T...>?,

	Connect: (self: Signal<T...>, Callback: (T...) -> ()) -> () -> (),
	Wait: (self: Signal<T...>) -> Promise.Promise,
	Fire: (self: Signal<T...>, T...) -> (),
	DisconnectAll: (self: Signal<T...>) -> (),
}

local Signal = {}
Signal.__index = Signal

function Signal.new<T...>(): Signal<T...>
	return setmetatable({
		Root = nil,
	}, Signal) :: any
end

function Signal.Connect<T...>(self: Signal<T...>, Callback: (T...) -> ()): () -> ()
	local Node = {
		Next = self.Root,
		Callback = Callback,
	}

	self.Root = Node

	return function()
		if self.Root == Node then
			self.Root = Node.Next
		else
			local Current = self.Root
			while Current do
				if Current.Next == Node then
					Current.Next = Node.Next
					break
				end
				Current = Current.Next
			end
		end
	end
end

function Signal.Wait<T...>(self: Signal<T...>): Promise.Promise
	return Promise.new(function(Resolve)
		local Disconnect

		Disconnect = self:Connect(function(...)
			Disconnect()
			Resolve(... :: any)
		end)
	end)
end

function Signal.Fire<T...>(self: Signal<T...>, ...: T...)
	local Current = self.Root
	while Current do
		Spawn(Current.Callback, ...)
		Current = Current.Next
	end
end

function Signal.DisconnectAll<T...>(self: Signal<T...>)
	self.Root = nil
end

return Signal


local FreeThread: thread? = nil

local function FunctionPasser(fn, ...)
	local AquiredThread = FreeThread
	FreeThread = nil
	fn(...)
	FreeThread = AquiredThread
end

local function Yielder()
	while true do
		FunctionPasser(coroutine.yield())
	end
end

return function<T...>(fn: (T...) -> (), ...: T...)
	if not FreeThread then
		FreeThread = coroutine.create(Yielder)
		coroutine.resume(FreeThread :: any)
	end

	task.spawn(FreeThread :: thread, fn, ...)
end


local Net = require(script.Net)

return {
	Server = Net.Server,
	Client = Net.Client,

	Collection = require(script.Util.Collection),
	Ratelimit = require(script.Util.Ratelimit),
	Promise = require(script.Util.Promise),
	Signal = require(script.Util.Signal),
	Clock = require(script.Util.Clock),
	Spawn = require(script.Util.Spawn),
	Bin = require(script.Util.Bin),
}


-- -----------------------------------------------------------------------------
--               Batched Yield-Safe Signal Implementation                     --
-- This is a Signal class which has effectively identical behavior to a       --
-- normal RBXScriptSignal, with the only difference being a couple extra      --
-- stack frames at the bottom of the stack trace when an error is thrown.     --
-- This implementation caches runner coroutines, so the ability to yield in   --
-- the signal handlers comes at minimal extra cost over a naive signal        --
-- implementation that either always or never spawns a thread.                --
--                                                                            --
-- License:                                                                   --
--   Licensed under the MIT license.                                          --
--                                                                            --
-- Authors:                                                                   --
--   stravant - July 31st, 2021 - Created the file.                           --
--   sleitnick - August 3rd, 2021 - Modified for Knit.                        --
-- -----------------------------------------------------------------------------

-- Signal types
export type Connection = {
	Disconnect: (self: Connection) -> (),
	Destroy: (self: Connection) -> (),
	Connected: boolean,
}

export type Signal<T...> = {
	Fire: (self: Signal<T...>, T...) -> (),
	FireDeferred: (self: Signal<T...>, T...) -> (),
	Connect: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
	Once: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
	DisconnectAll: (self: Signal<T...>) -> (),
	GetConnections: (self: Signal<T...>) -> { Connection },
	Destroy: (self: Signal<T...>) -> (),
	Wait: (self: Signal<T...>) -> T...,
}

-- The currently idle thread to run the next handler on
local freeRunnerThread = nil

-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquiredRunnerThread = freeRunnerThread
	freeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	freeRunnerThread = acquiredRunnerThread
end

-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
	acquireRunnerThreadAndCallEventHandler(...)
	while true do
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end

--[=[
	@within Signal
	@interface SignalConnection
	.Connected boolean
	.Disconnect (SignalConnection) -> ()

	Represents a connection to a signal.
	```lua
	local connection = signal:Connect(function() end)
	print(connection.Connected) --> true
	connection:Disconnect()
	print(connection.Connected) --> false
	```
]=]

-- Connection class
local Connection = {}
Connection.__index = Connection

function Connection:Disconnect()
	if not self.Connected then
		return
	end
	self.Connected = false

	-- Unhook the node, but DON'T clear it. That way any fire calls that are
	-- currently sitting on this node will be able to iterate forwards off of
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
	-- when no more fire calls are sitting on it.
	if self._signal._handlerListHead == self then
		self._signal._handlerListHead = self._next
	else
		local prev = self._signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end
end

Connection.Destroy = Connection.Disconnect

-- Make Connection strict
setmetatable(Connection, {
	__index = function(_tb, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

--[=[
	@within Signal
	@type ConnectionFn (...any) -> ()

	A function connected to a signal.
]=]

--[=[
	@class Signal
	@tag Vendor

	A Signal is a data structure that allows events to be dispatched
	and observed.

	This implementation is a direct copy of the de facto standard, [GoodSignal](https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063),
	with some added methods and typings.

	For example:
	```lua
	local signal = Signal.new()

	-- Subscribe to a signal:
	signal:Connect(function(msg)
		print("Got message:", msg)
	end)

	-- Dispatch an event:
	signal:Fire("Hello world!")
	```
]=]
local Signal = {}
Signal.__index = Signal

--[=[
	Constructs a new Signal

	@return Signal
]=]
function Signal.new<T...>(): Signal<T...>
	local self = setmetatable({
		_handlerListHead = false,
		_proxyHandler = nil,
		_yieldedThreads = nil,
	}, Signal)

	return self
end

--[=[
	Constructs a new Signal that wraps around an RBXScriptSignal.

	@param rbxScriptSignal RBXScriptSignal -- Existing RBXScriptSignal to wrap
	@return Signal

	For example:
	```lua
	local signal = Signal.Wrap(workspace.ChildAdded)
	signal:Connect(function(part) print(part.Name .. " added") end)
	Instance.new("Part").Parent = workspace
	```
]=]
function Signal.Wrap<T...>(rbxScriptSignal: RBXScriptSignal): Signal<T...>
	assert(
		typeof(rbxScriptSignal) == "RBXScriptSignal",
		"Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal)
	)

	local signal = Signal.new()
	signal._proxyHandler = rbxScriptSignal:Connect(function(...)
		signal:Fire(...)
	end)

	return signal
end

--[=[
	Checks if the given object is a Signal.

	@param obj any -- Object to check
	@return boolean -- `true` if the object is a Signal.
]=]
function Signal.Is(obj: any): boolean
	return type(obj) == "table" and getmetatable(obj) == Signal
end

--[=[
	@param fn ConnectionFn
	@return SignalConnection

	Connects a function to the signal, which will be called anytime the signal is fired.
	```lua
	signal:Connect(function(msg, num)
		print(msg, num)
	end)

	signal:Fire("Hello", 25)
	```
]=]
function Signal:Connect(fn)
	local connection = setmetatable({
		Connected = true,
		_signal = self,
		_fn = fn,
		_next = false,
	}, Connection)

	if self._handlerListHead then
		connection._next = self._handlerListHead
		self._handlerListHead = connection
	else
		self._handlerListHead = connection
	end

	return connection
end

--[=[
	@deprecated v1.3.0 -- Use `Signal:Once` instead.
	@param fn ConnectionFn
	@return SignalConnection
]=]
function Signal:ConnectOnce(fn)
	return self:Once(fn)
end

--[=[
	@param fn ConnectionFn
	@return SignalConnection

	Connects a function to the signal, which will be called the next time the signal fires. Once
	the connection is triggered, it will disconnect itself.
	```lua
	signal:Once(function(msg, num)
		print(msg, num)
	end)

	signal:Fire("Hello", 25)
	signal:Fire("This message will not go through", 10)
	```
]=]
function Signal:Once(fn)
	local connection
	local done = false

	connection = self:Connect(function(...)
		if done then
			return
		end

		done = true
		connection:Disconnect()
		fn(...)
	end)

	return connection
end

function Signal:GetConnections()
	local items = {}

	local item = self._handlerListHead
	while item do
		table.insert(items, item)
		item = item._next
	end

	return items
end

-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
--[=[
	Disconnects all connections from the signal.
	```lua
	signal:DisconnectAll()
	```
]=]
function Signal:DisconnectAll()
	local item = self._handlerListHead
	while item do
		item.Connected = false
		item = item._next
	end
	self._handlerListHead = false

	local yieldedThreads = rawget(self, "_yieldedThreads")
	if yieldedThreads then
		for thread in yieldedThreads do
			if coroutine.status(thread) == "suspended" then
				warn(debug.traceback(thread, "signal disconnected; yielded thread cancelled", 2))
				task.cancel(thread)
			end
		end
		table.clear(self._yieldedThreads)
	end
end

-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
--[=[
	@param ... any

	Fire the signal, which will call all of the connected functions with the given arguments.
	```lua
	signal:Fire("Hello")

	-- Any number of arguments can be fired:
	signal:Fire("Hello", 32, {Test = "Test"}, true)
	```
]=]
function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item.Connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end

--[=[
	@param ... any

	Same as `Fire`, but uses `task.defer` internally & doesn't take advantage of thread reuse.
	```lua
	signal:FireDeferred("Hello")
	```
]=]
function Signal:FireDeferred(...)
	local item = self._handlerListHead
	while item do
		local conn = item
		task.defer(function(...)
			if conn.Connected then
				conn._fn(...)
			end
		end, ...)
		item = item._next
	end
end

--[=[
	@return ... any
	@yields

	Yields the current thread until the signal is fired, and returns the arguments fired from the signal.
	Yielding the current thread is not always desirable. If the desire is to only capture the next event
	fired, using `Once` might be a better solution.
	```lua
	task.spawn(function()
		local msg, num = signal:Wait()
		print(msg, num) --> "Hello", 32
	end)
	signal:Fire("Hello", 32)
	```
]=]
function Signal:Wait()
	local yieldedThreads = rawget(self, "_yieldedThreads")
	if not yieldedThreads then
		yieldedThreads = {}
		rawset(self, "_yieldedThreads", yieldedThreads)
	end

	local thread = coroutine.running()
	yieldedThreads[thread] = true

	self:Once(function(...)
		yieldedThreads[thread] = nil
		task.spawn(thread, ...)
	end)

	return coroutine.yield()
end

--[=[
	Cleans up the signal.

	Technically, this is only necessary if the signal is created using
	`Signal.Wrap`. Connections should be properly GC'd once the signal
	is no longer referenced anywhere. However, it is still good practice
	to include ways to strictly clean up resources. Calling `Destroy`
	on a signal will also disconnect all connections immediately.
	```lua
	signal:Destroy()
	```
]=]
function Signal:Destroy()
	self:DisconnectAll()

	local proxyHandler = rawget(self, "_proxyHandler")
	if proxyHandler then
		proxyHandler:Disconnect()
	end
end

-- Make signal strict
setmetatable(Signal, {
	__index = function(_tb, key)
		error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

return table.freeze({
	new = Signal.new,
	Wrap = Signal.Wrap,
	Is = Signal.Is,
})