--!strict
local UIS = game:GetService("UserInputService")

type ActionValidator = (input: InputObject) -> boolean
type ActionValidator2 = (input: InputObject | Enum.UserInputState) -> boolean

local IMInput = {
	HOVER_PROMPT_START = 1,
	HOVER_PROMPT_END = 2,
	SPLIT = 3,
	CONTEXT_MENU_OPEN = 5,
	MULTI_CONTEXT_BOUNDS_CHECK = 6,
	DRAG_START = 7,
	DRAG_MOVE = 8,
	DRAG_END = 9,
	ROOT_MENU_BOUNDS_CHECK = 10,

	INPUT_NONE = 0,
	INPUT_TOUCH = 1,
	INPUT_MOUSE = 2,
}

local TOUCH_ENABLED = UIS.TouchEnabled
local MOUSE_ENABLED = UIS.MouseEnabled

local function validateInputHoverPromptStart(input: InputObject)
	if MOUSE_ENABLED then
		return input.UserInputType == Enum.UserInputType.MouseMovement
	end
	if TOUCH_ENABLED then
		return input.UserInputType == Enum.UserInputType.Touch and input.UserInputState == Enum.UserInputState.Change
	end

	return false
end

local function validateInputHoverPromptEnd(input: InputObject)
	if MOUSE_ENABLED then
		return input.UserInputType == Enum.UserInputType.MouseMovement
	end
	if TOUCH_ENABLED then
		return input.UserInputType == Enum.UserInputType.Touch and
			(input.UserInputState == Enum.UserInputState.End or input.UserInputState == Enum.UserInputState.Change)
	end

	return false
end

local function validateInputSplit(input: InputObject | Enum.UserInputState)
	local validated = false
	if MOUSE_ENABLED then
		validated = (input::InputObject).UserInputType == Enum.UserInputType.MouseButton1 and UIS:IsKeyDown(Enum.KeyCode.LeftControl)
	elseif TOUCH_ENABLED then
		validated = (input::Enum.UserInputState) == Enum.UserInputState.Begin
	end

	return validated
end

local function validateInputContextMenuOpen(input: InputObject)
	if MOUSE_ENABLED then
		return input.UserInputType == Enum.UserInputType.MouseButton2
	end
	if TOUCH_ENABLED then
		return input.UserInputType == Enum.UserInputType.Touch and input.UserInputState == Enum.UserInputState.Begin
	end

	return false
end

local function validateInputRootMenuBoundsCheck(input: InputObject)
	if MOUSE_ENABLED then
		return input.UserInputType == Enum.UserInputType.MouseButton1
	end
	if TOUCH_ENABLED then
		return input.UserInputType == Enum.UserInputType.Touch and input.UserInputState == Enum.UserInputState.Begin
	end

	return false
end

local function validateInputMultiContextBoundsCheck(input: InputObject)
	if MOUSE_ENABLED then
		return input.UserInputType == Enum.UserInputType.MouseMovement
	end

	return false
end

local function validateInputDragStart(input: InputObject)
	local validated = false
	if MOUSE_ENABLED then
		validated = input.UserInputType == Enum.UserInputType.MouseButton1
	end
	if TOUCH_ENABLED then
		validated = input.UserInputType == Enum.UserInputType.Touch and input.UserInputState == Enum.UserInputState.Begin
	end

	return validated
end

local function validateInputDragMove(input: InputObject)
	local validated = false
	if MOUSE_ENABLED then
		validated = input.UserInputType == Enum.UserInputType.MouseMovement
	end
	if TOUCH_ENABLED then
		validated = input.UserInputType == Enum.UserInputType.Touch and input.UserInputState == Enum.UserInputState.Change
	end

	return validated
end

local function validateInputDragEnd(input: InputObject)
	local validated = false
	if MOUSE_ENABLED then
		validated = input.UserInputType == Enum.UserInputType.MouseButton1
	end
	if TOUCH_ENABLED then
		validated = input.UserInputType == Enum.UserInputType.Touch and input.UserInputState == Enum.UserInputState.End
	end

	return validated
end

local ACTIONS: { [number]: ActionValidator | ActionValidator2 } = {
	[IMInput.HOVER_PROMPT_START] = validateInputHoverPromptStart,
	[IMInput.HOVER_PROMPT_END] = validateInputHoverPromptEnd,
	[IMInput.SPLIT] = validateInputSplit,
	[IMInput.CONTEXT_MENU_OPEN] = validateInputContextMenuOpen,
	[IMInput.MULTI_CONTEXT_BOUNDS_CHECK] = validateInputMultiContextBoundsCheck,
	[IMInput.ROOT_MENU_BOUNDS_CHECK] = validateInputRootMenuBoundsCheck,
	[IMInput.DRAG_START] = validateInputDragStart,
	[IMInput.DRAG_MOVE] = validateInputDragMove,
	[IMInput.DRAG_END] = validateInputDragEnd,
}

--[[
	Returns true if the input for the specified action is active.
]]
function IMInput.IsInputActive(action: number, input: InputObject | Enum.UserInputState): boolean
	if action == IMInput.SPLIT then
		return (ACTIONS[action]::ActionValidator2)(input)
	end

	return (ACTIONS[action]::ActionValidator)(input::InputObject)
end

function IMInput.GetInputType(): number
	if TOUCH_ENABLED then return IMInput.INPUT_TOUCH end
	if MOUSE_ENABLED then return IMInput.INPUT_MOUSE end

	return IMInput.INPUT_NONE
end

return IMInput


--!strict
local StaticContainer = {}

local Players = game:GetService("Players")
local player = Players.LocalPlayer::Player
local playerGui = player.PlayerGui
local Constants = require(script.Parent.IMConstants)

export type ActivatedCallbackStatic = (item: any, filledGui: GuiObject, containerIndex: number) -> ()

type ItemPositionInfo = {
	SlotIndex: number,
	ContainerIndex: number,
	ContainerName: string
}

type GridLayoutParams = {
	CellPadding: UDim2?,
	CellSize: UDim2?,
	FillDirection: Enum.FillDirection?,
	FillDirectionMaxCells: number?,
	StartCorner: Enum.StartCorner?,
	HorizontalAlignment: Enum.HorizontalAlignment?,
	VerticalAlignment: Enum.VerticalAlignment?,
	ParentContainer: GuiObject
}

type HoverPromptConfigs = {
	GuiTemplate: GuiObject,
	FixedPosition: boolean,
	Offset: Vector2?,
	RenderFunc: (item: any, hoverPromptGui: GuiObject, positionInfo: ItemPositionInfo) -> (() -> ())?,
}

type _HoverPromptInfo = {
	_TEMPLATE: GuiObject,
	_OFFSET: UDim2,
	_FIXED_POSITION: boolean,
	_RENDER_FUNC: (item: any, gui: GuiObject, positionInfo: ItemPositionInfo) -> (() -> ())?,
	_GUI: GuiObject?,
	_INSET: UDim2?,
	_OPEN_HOVER_PROMPT_AFTER_DRAGGING: boolean,
    _RENDER_FUNC_CLEANUP: (() -> ())?,
}

export type StaticContainer = {
    --[[
        Define the layout of the container, which is determined using a UIGridLayout.
        At minimum, the `ParentContainer` field must be defined; it is the gui object
        that the UIGridLayout will be parented to.

            type GridLayoutParams = {
                CellPadding: UDim2?,
                CellSize: UDim2?,
                FillDirection: Enum.FillDirection?,
                FillDirectionMaxCells: number?,
                StartCorner: Enum.StartCorner?,
                HorizontalAlignment: Enum.HorizontalAlignment?,
                VerticalAlignment: Enum.VerticalAlignment?,
                ParentContainer: GuiObject
            }
    ]]
	DefineLayout: (self: StaticContainer, params: GridLayoutParams) -> (),

    --[[
        Define how empty slots are rendered, providing a gui template and a render function.
        The render function should only handle UI logic.

            type StaticContainerEmptyConfigs = {
                GuiTemplate: GuiObject,
                RenderFunc: ((
                    emptyGui: GuiObject,
                    containerIndex: number
                ) -> ())?
            }
    ]]
	DefineEmptySlot: (self: StaticContainer, configs: StaticContainerEmptyConfigs) -> (),

    --[[
        Define how filled slots are rendered, providing a gui template and a render function.
        The render function should only handle UI logic.

            type StaticContainerFilledConfigs = {
                GuiTemplate: GuiObject,
                RenderFunc: (
                    item: any,
                    filledGui: GuiObject,
                    containerIndex: number,
                    ...any
                ) -> ()
            }
    ]]
	DefineFilledSlot: (self: StaticContainer, configs: StaticContainerFilledConfigs) -> (),

    --[[
        Define a callback that gets called whenever an item is activated in this container.
        An item is activated when it is left-clicked (mobile), double-tapped (mobile, nonhotbar),
        or tapped (mobile, tapped)

            type ActivatedCallbackStatic = (item: any, filledGui: GuiObject, containerIndex: number) -> ()
    ]]
	DefineActivatedCallback: (self: StaticContainer, callback: ActivatedCallbackStatic) -> (),

    --[[
        Takes in a table that define how you want to configure your hover prompt. The GuiTemplate
        defines the hover prompt gui to use for displaying the hover information. The RenderFunc defines
        how you display the item information to the gui. It passes in the item data and the hover prompt gui.

        The GuiTmeplate is shared across all items; it's a singleton. So be sure to manage your properties
        appropriately. For example, if hovering over a rare item changes the color to blue, then you'd probably
        change the color back to normal when hovering over a normal item.

        You can provide just the GuiTemplate and RenderFunc and ignore the other fields if you wish to use
        the default values. It will default to FixedPosition = false and Offset = UDim2.new(0, 20, 0, 0).

        If FixedPosition = true, the hover prompt will be position to the top right corner of the
        inventory item. The offset will apply relative to the top right corner. If no offset is
        provided, it will default to UDim2.new(0, 0, 0, 0).

        If FixedPosition = false, the hover prompt will follow the mouse as long as it's moving within
        the inventory item. The offset will apply relative to the mouse position.
    ]]
	DefineHoverPrompt: (self: StaticContainer, configs: HoverPromptConfigs) -> (),

    DefineOpen: (self: StaticContainer, func: (parentContainer: GuiObject, ...any) -> ()) -> (),

    DefineClose: (self: StaticContainer, func: (parentContainer: GuiObject, ...any) -> ()) -> (),

    --[[
        Opens the static container's inventory; renders onto the screen.
    ]]
    Open: (self: StaticContainer, ...any) -> (),

    --[[
        Closes the static inventory's inventory; unrenders from the screen.
    ]]
    Close: (self: StaticContainer, ...any) -> (),

    --[[
        Calls the filled slot's render function defined in DefineFilledSlot().
        This function does nothing if it's an empty slot.
    ]]
    RerenderItem: (self: StaticContainer, containerIndex: number, ...any) -> (),
}

type StaticContainerEmptyConfigs = {
    GuiTemplate: GuiObject,
	RenderFunc: ((emptyGui: GuiObject, containerIndex: number) -> ())?
}

type StaticContainerFilledConfigs = {
    GuiTemplate: GuiObject,
    RenderFunc: (item: any, filledGui: GuiObject, containerIndex: number, ...any) -> ()
}

export type _StaticContainer = {
    _type: string,
    _opened: boolean,
    _name: string,
    _capacity: number,
    _data: { any },
    _slotGuiObjs: { GuiObject },
    _hoverPromptParent: ScreenGui,
    _PARENT_CONTAINER: GuiObject,
    _GRID_LAYOUT: UIGridLayout,
    _EMPTY_TEMPLATE: GuiObject,
    _HOVER_PROMPT_SCREEN_GUI: ScreenGui,
    _FILLED_TEMPLATE: GuiObject,
    _RENDER_EMPTY_FUNC: ((emptyGui: GuiObject, containerIndex: number) -> ())?,
    _RENDER_FILLED_FUNC: (item: any, filledGui: GuiObject, containerIndex: number, ...any) -> (),

    _HOVER_INFO: _HoverPromptInfo,

    _CALLBACKS: {
        _ON_OPENED_FUNC: (parentContainer: GuiObject, ...any) -> (),
        _ON_CLOSED_FUNC: (parentContainer: GuiObject, ...any) -> (),
        _ACTIVATED_FUNC: ActivatedCallbackStatic,
    }
} & StaticContainer

local DEFAULT_CELL_PADDING = Constants.DEFAULT_CELL_PADDING
local DEFAULT_CELL_SIZE = Constants.DEFAULT_CELL_SIZE
local DEFAULT_FILL_DIRECTION = Constants.DEFAULT_FILL_DIRECTION
local DEFAULT_FILL_DIRECTION_MAX_CELLS = Constants.DEFAULT_FILL_DIRECTION_MAX_CELLS
local DEFAULT_START_CORNER = Constants.DEFAULT_START_CORNER
local DEFAULT_H_ALIGN = Constants.DEFAULT_H_ALIGN
local DEFAULT_V_ALIGN = Constants.DEFAULT_V_ALIGN

function StaticContainer.NewStaticContainer(name: string, capacity: number): StaticContainer
    local _props = {
        _name = name,
        _capacity = capacity,
        _opened = false,
        _CALLBACKS = {},
        _slotGuiObjs = {},
        _type = "Static",

        DefineLayout = function(self: _StaticContainer, params: GridLayoutParams)
            if (params.ParentContainer::GuiObject?) == nil then error("[Inventory]: A parent GUI container must be provided for grid layout.") end
			if not params.ParentContainer:IsA("GuiObject") then error("[Inventory]: The provided parent GUI container must be a GuiObject.") end

			local layout = Instance.new("UIGridLayout")
			layout.Name = Constants.GRID_LAYOUT_NAME
			layout.CellPadding = params.CellPadding or DEFAULT_CELL_PADDING
			layout.CellSize = params.CellSize or DEFAULT_CELL_SIZE
			layout.FillDirection = params.FillDirection or DEFAULT_FILL_DIRECTION
			layout.FillDirectionMaxCells = params.FillDirectionMaxCells or DEFAULT_FILL_DIRECTION_MAX_CELLS
			layout.StartCorner = params.StartCorner or DEFAULT_START_CORNER
			layout.HorizontalAlignment = params.HorizontalAlignment or DEFAULT_H_ALIGN
			layout.VerticalAlignment = params.VerticalAlignment or DEFAULT_V_ALIGN
			layout.SortOrder = Enum.SortOrder.LayoutOrder

			self._PARENT_CONTAINER = params.ParentContainer
			self._GRID_LAYOUT = layout
        end,

        DefineEmptySlot = function(self: _StaticContainer, configs: StaticContainerEmptyConfigs)
            self._EMPTY_TEMPLATE = configs.GuiTemplate
            self._RENDER_EMPTY_FUNC = configs.RenderFunc
        end,

        DefineFilledSlot = function(self: _StaticContainer, configs: StaticContainerFilledConfigs)
            self._FILLED_TEMPLATE = configs.GuiTemplate
			self._RENDER_FILLED_FUNC = configs.RenderFunc
        end,

        DefineActivatedCallback = function(self: _StaticContainer, callback: ActivatedCallbackStatic)
			self._CALLBACKS._ACTIVATED_FUNC = callback
		end,

        DefineHoverPrompt = function(self: _StaticContainer, configs: HoverPromptConfigs)
            local offset: UDim2
			if configs.Offset then
				offset = UDim2.fromOffset(configs.Offset.X, configs.Offset.Y)
			else
				offset = UDim2.fromOffset(0, 0)
			end

			self._HOVER_INFO = {
				_TEMPLATE = configs.GuiTemplate,
				_FIXED_POSITION = configs.FixedPosition or false,
				_OFFSET = offset,
				_RENDER_FUNC = configs.RenderFunc,
				_OPEN_HOVER_PROMPT_AFTER_DRAGGING = false
			}

            local staticContainerHoverPromptScreenGui = Instance.new("ScreenGui")
            staticContainerHoverPromptScreenGui.Name = "HOVER_PROMPT_"..self._name
            staticContainerHoverPromptScreenGui.ResetOnSpawn = false
            staticContainerHoverPromptScreenGui.DisplayOrder = 100
            staticContainerHoverPromptScreenGui.Parent = playerGui

            self._HOVER_PROMPT_SCREEN_GUI = staticContainerHoverPromptScreenGui
        end,

        DefineOpen = function(self: _StaticContainer, func: (parentContainer: GuiObject, ...any) -> ())
            self._CALLBACKS._ON_OPENED_FUNC = func
        end,

        DefineClose = function(self: _StaticContainer, func: (parentContainer: GuiObject, ...any) -> ())
            self._CALLBACKS._ON_CLOSED_FUNC = func
        end,

        Open = function(self: _StaticContainer, ...: any)
            if self._opened then return end
            self._opened = true
            self._CALLBACKS._ON_OPENED_FUNC(self._PARENT_CONTAINER, ...)
        end,

        Close = function(self: _StaticContainer, ...: any)
            if not self._opened then return end
            self._opened = false
            self._CALLBACKS._ON_CLOSED_FUNC(self._PARENT_CONTAINER, ...)
        end,

        RerenderItem = function(self: _StaticContainer, containerIndex: number, ...: any)
            local item = self._data[containerIndex]
            if not item then return end

            local slotGui = self._slotGuiObjs[containerIndex]::{ [string]: GuiObject } & GuiObject
            local filledGui = slotGui[Constants.FILLED_GUI_NAME]
            self._RENDER_FILLED_FUNC(item, filledGui, containerIndex, ...)
        end,
    }

    return _props::StaticContainer
end

local function createStaticContainerEmptySlot(staticContainer: _StaticContainer, containerIndex: number)
    local gui = staticContainer._EMPTY_TEMPLATE:Clone()
	if staticContainer._RENDER_EMPTY_FUNC then
		staticContainer._RENDER_EMPTY_FUNC(gui, containerIndex)
	end
	gui.Name = Constants.SLOT_NAME_PREFIX..tostring(containerIndex)
	gui.LayoutOrder = containerIndex
	gui.Visible = true
	gui.Parent = staticContainer._PARENT_CONTAINER

	return gui
end

local function createStaticContainerFilledSlot(staticContainer: _StaticContainer, item: any, containerIndex: number): (GuiObject, GuiObject)
    local slot = createStaticContainerEmptySlot(staticContainer, containerIndex)

	local filledSlotGui = staticContainer._FILLED_TEMPLATE:Clone()
	filledSlotGui.Visible = true
    staticContainer._RENDER_FILLED_FUNC(item, filledSlotGui, containerIndex)
	filledSlotGui.Name = Constants.FILLED_GUI_NAME
	filledSlotGui.Parent = slot

	return filledSlotGui, slot
end

function StaticContainer.InitStaticContainer(staticContainer: _StaticContainer, staticContainerData: { any },
    onFilledSlotCreatedCallback: (filledGui: GuiObject, containerIndex: number) -> ()
)
    if #staticContainerData > staticContainer._capacity then
        error("the provided inventory data length exceeds the static container capacity.")
    end

    for i, item in staticContainerData do
        if not item then
            local slotGui = createStaticContainerEmptySlot(staticContainer, i)
            table.insert(staticContainer._slotGuiObjs, slotGui)
        else
            local filledGui, slotGui = createStaticContainerFilledSlot(staticContainer, item, i)
            table.insert(staticContainer._slotGuiObjs, slotGui)
            onFilledSlotCreatedCallback(filledGui, i)
        end
    end

    staticContainer._data = staticContainerData
    staticContainer._GRID_LAYOUT.Parent = staticContainer._PARENT_CONTAINER
end

function StaticContainer.DestroyStaticContainer(staticContainer: _StaticContainer)
    for i, v in staticContainer._slotGuiObjs do
        v:Destroy()
    end
    staticContainer._HOVER_PROMPT_SCREEN_GUI:Destroy()
    table.clear(staticContainer._slotGuiObjs)
    table.clear(staticContainer::{ [string]: any })
end

return StaticContainer


local IMUserType = {}

-- This is for if you want better typechecking for your items (ITEM_TYPE)
-- Define the schema of your type here. This type should represent what each item looks like. You can store anything you want
-- The current type is just used as an example
export type USER_ITEM_TYPE = { -- DON'T CHANGE THE NAME OF THE TYPE
	ItemData: {
		ItemID: number,
		Quantity: number,
		Metadata: { [string]: any }
	},
	ItemName: string,
	ItemDescription: string,
	ItemType: string,
	Instance: Instance?,

	IsEquipped: boolean?
}

return IMUserType


--!strict
local Utility = {}

local GuiService = game:GetService("GuiService")

export type Table = { [any]: any }

function Utility.GetGuiInsetY(guiInsetIgnored: boolean): number
	return guiInsetIgnored and GuiService:GetGuiInset().Y or 0
end

function Utility.DeepCopyTable(t: Table)
	local copy = {}

	for k,v in pairs(t) do
		if type(v) == "table" then
			copy[k] = Utility.DeepCopyTable(v::Table)
		else
			copy[k] = v
		end
	end
	return copy
end

function Utility.DeepTableClear(t: Table)
	for k,v in pairs(t) do
		if type(v) == "table" then
			Utility.DeepTableClear(v::Table)
		end
		t[k] = nil
	end
end

function Utility.IsPointWithinBoundingBox(
	px: number, py: number,
	minX: number, minY: number,
	maxX: number, maxY: number): boolean
	return px >= minX and px <= maxX and py >= minY and py <= maxY
end

function Utility.IsMouseWithinGui(mx: number, my: number, gui: GuiObject): boolean
	local pos = gui.AbsolutePosition
	local size = gui.AbsoluteSize

	return mx >= pos.X and mx <= pos.X + size.X and my >= pos.Y and my <= pos.Y + size.Y
end

return Utility


-- see .lune/test.luau
-- selene: allow(unused_variable)
return function<T>(value: T, expected: T): boolean
	return nil :: any
end


-- see .lune/test.luau
-- selene: allow(unused_variable)
return function(name: string, fn: () -> ()) end


-- see .lune/test.luau

export type TupleFunctions = {
	map: (...any) -> ...any,
	other: () -> ...any,
	assertEq: (...any) -> (),
	assertEqMapped: (...any) -> (),
	assertEqOther: (...any) -> (),
	assertEqOtherMapped: (...any) -> (),
}

-- selene: allow(unused_variable)
return function(f: (tuple: TupleFunctions, ...any) -> ())
	return nil :: any
end


local Option = require("../option/lib")
local std = require("../std/lib")
local threadpool = require("../threadpool/lib")

local task = std.task

export type Future<T> = typeof(setmetatable(
	{} :: {
		_f: boolean,
		_v: T,
		_w: { thread },
		_c: { (T) -> () },
	},
	{} :: FutureImpl
))

export type FutureImpl = {
	__index: FutureImpl,

	new: <T>(value: T) -> Future<T>,
	never: () -> Future<any>,
	spawn: <T, U...>(f: (U...) -> T, U...) -> Future<T>,
	pending: <T>() -> (Future<T>, (T) -> ()),
	fn: <T, V...>(f: (V...) -> T) -> (V...) -> Future<T>,

	all: <T>(futures: { Future<T> }) -> Future<{ T }>,
	race: <T>(futures: { Future<T> }) -> Future<T>,

	isFinished: <T>(self: Future<T>) -> boolean,
	isPending: <T>(self: Future<T>) -> boolean,
	now: <T>(self: Future<T>) -> Option.Option<T>,
	await: <T>(self: Future<T>) -> T,
	after: <T>(self: Future<T>, f: (T) -> ()) -> (),
}

--[=[
  @class Future

  A future represents a value that does not exist yet, but will exist at some
  point in the future.

  Futures allow you to more easily compose asynchronous operations.
]=]
local Future = {} :: FutureImpl
Future.__index = Future

--[=[
  Creates a finished future with the given value.
]=]
function Future.new(value)
	local self = setmetatable({ _f = true, _v = value, _w = {}, _c = {} }, Future)
	return self
end

--[=[
  Creates a future that will never finish.
]=]
function Future.never()
	return setmetatable({ _f = false, _w = {}, _c = {} }, Future)
end

local function finish<T>(fut: Future<T>, value: T)
	fut._f = true
	fut._v = value

	for _, thread in fut._w do
		if coroutine.status(thread) == "suspended" then
			task.spawn(thread, value)
		end
	end

	for _, callback in fut._c do
		threadpool.spawn(callback, value)
	end
end

--[=[
	Creates a future and spawns the given function in a new thread, once the
	function finishes, the future resolves with the result of the function.
]=]
function Future.spawn(f, ...)
	local self = Future.never()

	local function inner(self: Future<any>, f: (...any) -> any, ...)
		finish(self, f(...))
	end
	threadpool.spawn(inner, self, f, ...)

	return self
end

--[=[
	Creates a future that is pending, and returns the future and a function that
	can be used to resolve the future with a value.
]=]
function Future.pending()
	local fut = Future.never()
	return fut, function(value)
		finish(fut, value)
	end
end

--[=[
	Returns a function that, when called, will spawn the given function in a new
	thread and returns a future that resolves with the result of the function.
]=]
function Future.fn(f)
	return function(...)
		return Future.spawn(f, ...)
	end
end

--[=[
	Takes an array of futures and returns a new future that will finish once all
	of the given futures have finished. The future will resolve with an array
	containing the results of the given futures.
]=]
function Future.all(futures)
	local function inner(futures: { Future<any> })
		local result = table.create(#futures)
		for _, fut: any in futures do
			table.insert(result, fut:await())
		end
		return result
	end

	return Future.spawn(inner, futures) :: any
end

--[=[
	Takes an array of futures and returns a new future that will finish once any
	of the given futures have finished. The future will resolve with the result
	of the first future to finish.

	The result of the other futures will be discarded.
]=]
function Future.race(futures)
	local self = Future.never()
	local done = false

	for _, fut: any in futures do
		fut:after(function(val)
			if done then
				return
			end

			done = true
			finish(self, val)
		end)
	end

	return self
end

--[=[
  Returns `true` if the future has finished, `false` otherwise.
]=]
function Future.isFinished(self)
	return self._f
end

--[=[
  Returns `true` if the future is still pending, `false` otherwise.
]=]
function Future.isPending(self)
	return not self._f
end

--[=[
	Returns the value of the future, if it has finished. If the future is still
	pending, the function will return `None`.
]=]
function Future.now(self)
	if self._f then
		return Option.Some(self._v :: any)
	else
		return Option.None
	end
end

--[=[
	Yields the current thread until the future has finished, then returns the
	value of the future.

	If the future has already finished, the function will return immediately.
]=]
function Future.await(self)
	if self._f then
		return self._v
	end

	local thread = coroutine.running()
	table.insert(self._w, thread)
	return coroutine.yield()
end

--[=[
	Calls the given callback once the future has finished. If the future has
	already finished, the callback will be called immediately.
]=]
function Future.after(self, f)
	if self._f then
		f(self._v)
	else
		table.insert(self._c, f)
	end
end

return Future


local assertEq = require("@testing/assertEq")
local describe = require("@testing/describe")
local test = require("@testing/test")

local task = require("@lune/task")

local Future = require("./lib")

describe("new", function()
	test("creates a finished future with the given value", function()
		local fut = Future.new(5)
		assertEq(fut._f, true)
		assertEq(fut._v, 5)
	end)
end)

describe("never", function()
	test("creates a pending future", function()
		local fut = Future.never()
		assertEq(fut._f, false)
		assertEq(fut._v, nil)
	end)
end)

describe("spawn", function()
	test("spawns the function and returns the result", function()
		local thread = coroutine.running()
		local spawned

		local fut = Future.spawn(function()
			spawned = coroutine.running()
			coroutine.yield()
			return 5
		end)

		assertEq(fut._f, false)
		assertEq(spawned ~= nil, true)
		assertEq(thread ~= spawned, true)
		task.spawn(spawned)
		assertEq(fut._f, true)
		assertEq(fut._v, 5)
	end)
end)

describe("pending", function()
	test("resolves when the resolve function is callled", function()
		local fut: Future.Future<number?>, resolve = Future.pending()
		assertEq(fut._f, false)
		assertEq(fut._v, nil)

		resolve(5)
		assertEq(fut._f, true)
		assertEq(fut._v, 5)
	end)
end)

describe("fn", function()
	test("creates a function that spawns a future", function()
		local f = Future.fn(function(a: number, b: number)
			return a + b
		end)

		local fut = f(1, 2)
		assertEq(fut._f, true)
		assertEq(fut._v, 3)
	end)
end)

describe("all", function()
	test("creates a future that resolves when all futures have resolved", function()
		local spawned = {}
		local function returnValue(value)
			spawned[value] = coroutine.running()
			coroutine.yield()
			return value
		end

		local futs = {}
		for i = 1, 10 do
			futs[i] = Future.spawn(returnValue, i)
		end

		local fut = Future.all(futs)
		for i = 1, 10 do
			assertEq(fut._f, false)
			task.spawn(spawned[i])

			if i < 10 then
				assertEq(fut._f, false)
			else
				assertEq(fut._f, true)
				for j = 1, 10 do
					assertEq(fut._v[j], j)
				end
			end
		end
	end)
end)

describe("race", function()
	test("resolves with the first future to resolve", function()
		local spawned = {}
		local function returnValue(value)
			spawned[value] = coroutine.running()
			coroutine.yield()
			return value
		end

		local random = math.random(1, 10)
		local futs = {}
		for i = 1, 10 do
			futs[i] = Future.spawn(returnValue, i)
		end

		local fut = Future.race(futs)
		assertEq(fut._f, false)
		task.spawn(spawned[random])
		assertEq(fut._f, true)
		assertEq(fut._v, random)

		for i = 1, 10 do
			if i ~= random then
				task.spawn(spawned[i])
				assertEq(fut._f, true)
				assertEq(fut._v, random)
			end
		end
	end)
end)

describe("isFinished", function()
	test("returns whether a future is finished", function()
		assertEq(Future.new(5):isFinished(), true)
		assertEq(Future.never():isFinished(), false)
	end)
end)

describe("isPending", function()
	test("returns whether a future is pending", function()
		assertEq(Future.new(5):isPending(), false)
		assertEq(Future.never():isPending(), true)
	end)
end)

describe("now", function()
	test("returns Some if the future is finished", function()
		assertEq(Future.new(5):now():unwrap(), 5)
	end)

	test("returns None if the future is pending", function()
		assertEq(Future.never():now():isNone(), true)
	end)
end)

describe("await", function()
	test("returns the value if the future is finished", function()
		assertEq(Future.new(5):await(), 5)
	end)

	test("yields if the future is pending", function()
		local done = false
		local spawned
		task.spawn(function()
			local fut = Future.spawn(function()
				spawned = coroutine.running()
				coroutine.yield()
				return 5
			end)

			assertEq(fut:await(), 5)
			done = true
		end)

		assertEq(done, false)
		task.spawn(spawned)
		assertEq(done, true)
	end)
end)

describe("after", function()
	test("calls the callback immediately if finished", function()
		local done = false
		Future.new(5):after(function(v)
			assertEq(v, 5)
			done = true
		end)

		assertEq(done, true)
	end)

	test("calls the callback once the future is finished", function()
		local done = false
		local spawned

		local fut = Future.spawn(function()
			spawned = coroutine.running()
			coroutine.yield()
			return 5
		end)

		fut:after(function(v)
			assertEq(v, 5)
			done = true
		end)

		assertEq(done, false)
		task.spawn(spawned)
		assertEq(done, true)
	end)
end)


export type Option<T...> = typeof(setmetatable(
	{} :: {
		-- value to "store" T...
		-- this value doesn't exist at runtime, and is only used for typechecking
		_T: (T...) -> (),

		_n: number,
	},
	{} :: OptionImpl
))

export type OptionImpl = {
	__index: OptionImpl,
	__tostring: <T0...>(self: Option<T0...>) -> string,
	__eq: <T1...>(self: Option<T1...>, other: Option<T1...>) -> boolean,

	None: Option<...any>,
	Some: <T3...>(T3...) -> Option<T3...>,
	from: <T4>(value: T4?) -> Option<T4>,
	is: (value: unknown) -> boolean,
	isSome: <T5...>(self: Option<T5...>) -> boolean,
	isSomeAnd: <T6...>(self: Option<T6...>, f: (T6...) -> boolean) -> boolean,
	isNone: <T7...>(self: Option<T7...>) -> boolean,
	expect: <T8...>(self: Option<T8...>, message: string) -> T8...,
	unwrap: <T9...>(self: Option<T9...>) -> T9...,
	unwrapOr: <T10...>(self: Option<T10...>, T10...) -> T10...,
	unwrapOrElse: <T11...>(self: Option<T11...>, f: () -> T11...) -> T11...,
	unwrapOrNil: <T>(self: Option<T>) -> T?,
	map: <T12..., U0...>(self: Option<T12...>, f: (T12...) -> U0...) -> Option<U0...>,
	mapOr: <U, T13...>(self: Option<T13...>, default: U, f: (T13...) -> U) -> U,
	mapOrRest: <T14..., U1...>(self: Option<T14...>, f: (T14...) -> U1..., U1...) -> U1...,
	mapOrElse: <T15..., U2...>(self: Option<T15...>, default: () -> U2..., f: (T15...) -> U2...) -> U2...,
	andOpt: <T16..., U3...>(self: Option<T16...>, other: Option<U3...>) -> Option<U3...>,
	andThen: <T17..., U4...>(self: Option<T17...>, f: (T17...) -> Option<U4...>) -> Option<U4...>,
	filter: <T18...>(self: Option<T18...>, f: (T18...) -> boolean) -> Option<T18...>,
	orOpt: <T19...>(self: Option<T19...>, other: Option<T19...>) -> Option<T19...>,
	orElse: <T20...>(self: Option<T20...>, f: () -> Option<T20...>) -> Option<T20...>,
	xor: <T21...>(self: Option<T21...>, other: Option<T21...>) -> Option<T21...>,
	match: <T22..., U5...>(self: Option<T22...>, f: { Some: (T22...) -> U5..., None: () -> U5... }) -> U5...,
	join: OptionJoin,
}

-- can't join two type packs
-- stylua: ignore
type OptionJoin =
			(<T1, U1...>(self: Option<T1>, other: Option<U1...>) -> Option<(T1, U1...)>)
		& (<T1, T2, U2...>(self: Option<T1, T2>, other: Option<U2...>) -> Option<(T1, T2, U2...)>)
		& (<T1, T2, T3, U3...>(self: Option<T1, T2, T3>, other: Option<U3...>) -> Option<(T1, T2, T3, U3...)>)
		& (<T1, T2, T3, T4, U4...>(self: Option<T1, T2, T3, T4>, other: Option<U4...>) -> Option<(T1, T2, T3, T4, U4...)>)
		& (<T1, T2, T3, T4, T5, U5...>(self: Option<T1, T2, T3, T4, T5>, other: Option<U5...>) -> Option<(T1, T2, T3, T4, T5, U5...)>)
		& (<T1, T2, T3, T4, T5, T6, U6...>(self: Option<T1, T2, T3, T4, T5, T6>, other: Option<U6...>) -> Option<(T1, T2, T3, T4, T5, T6, U6...)>)
		& (<T1, T2, T3, T4, T5, T6, T7, U7...>(self: Option<T1, T2, T3, T4, T5, T6, T7>, other: Option<U7...>) -> Option<(T1, T2, T3, T4, T5, T6, T7, U7...)>)
		& (<T1, T2, T3, T4, T5, T6, T7, T8, U8...>(self: Option<T1, T2, T3, T4, T5, T6, T7, T8>, other: Option<U8...>) -> Option<(T1, T2, T3, T4, T5, T6, T7, T8, U8...)>)
		& (<T1, T2, T3, T4, T5, T6, T7, T8, T9, U9...>(self: Option<T1, T2, T3, T4, T5, T6, T7, T8, T9>, other: Option<U9...>) -> Option<(T1, T2, T3, T4, T5, T6, T7, T8, T9, U9...)>)
		& (<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, U10...>(self: Option<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>, other: Option<U10...>) -> Option<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, U10...)>)
		& (<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, U11...>(self: Option<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>, other: Option<U11...>) -> Option<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, U11...)>)
		& (<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, U12...>(self: Option<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>, other: Option<U12...>) -> Option<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, U12...)>)

--[=[
  @class Option

  Represents an optional value: `Some` and contains a value, or `None`
  and does not contain a value.

  Options can be used as an alternative to `nil` to represent the
  absence of a value. They help you avoid bugs by requiring you to
  handle the case where a value might be missing.
]=]
local Option = {} :: OptionImpl
Option.__index = Option

--[=[
  The `Option` that does not contain a value.
]=]
Option.None = table.freeze(setmetatable({ _n = 0 }, Option) :: any)

--[=[
  Creates a new `Some` with the given value.
]=]
function Option.Some(...)
	local n = select("#", ...)
	if n == 0 then
		error("Option.Some() called with no arguments", 2)
	end
	local self = setmetatable({ _n = n, ... }, Option)
	return table.freeze(self) :: any
end

--[=[
  Converts the given value to an `Option`, returning `None` if the
  value is `nil`.
]=]
function Option.from(value)
	if value == nil then
		return Option.None
	else
		return Option.Some(value)
	end
end

--[=[
	Returns `true` if the given value is an `Option`.
]=]
function Option.is(value)
	return typeof(value) == "table" and getmetatable(value :: any) == Option
end

--[=[
  Returns `true` if the option is a `Some` value.
]=]
function Option.isSome(self)
	return self._n ~= 0
end

--[=[
	Returns `true` if the option is a `Some` value and the value
	satisfies the given predicate.
]=]
function Option.isSomeAnd(self, f)
	return self._n ~= 0 and f(unpack(self :: any))
end

--[=[
  Returns `true` if the option is a `None` value.
]=]
function Option.isNone(self): boolean
	return self._n == 0
end

--[=[
  Returns the value of the option if it is `Some`, otherwise throws
  an error with the given message.
]=]
function Option.expect(self, message)
	if self._n == 0 then
		error(message, 2)
	end
	return unpack(self :: any)
end

--[=[
  Returns the value of the option if it is `Some`, otherwise throws
  an error.
]=]
function Option.unwrap(self)
	if self._n == 0 then
		error("called `Option.unwrap()` on a `None` value", 2)
	end
	return unpack(self :: any)
end

--[=[
  Returns the value of the option if it is `Some`, otherwise returns
  the given default value.
]=]
function Option.unwrapOr(self, ...)
	if self._n == 0 then
		return ...
	end
	return unpack(self :: any)
end

--[=[
  Returns the value of the option if it is `Some`, otherwise returns
  the result of calling the given function.
]=]
function Option.unwrapOrElse(self, f)
	if self._n == 0 then
		return f()
	end
	return unpack(self :: any)
end

--[=[
	Returns the value of the option if it is `Some`, otherwise returns
	`nil`. Only works with `Option`s that contain a single value.
]=]
function Option.unwrapOrNil(self)
	return self[1]
end

--[=[
  Returns the result of the given function wrapped in an `Option` if
  the option is `Some`, otherwise returns `None`.
]=]
function Option.map(self, f)
	if self._n == 0 then
		return Option.None
	end
	return Option.Some(f(unpack(self :: any)))
end

--[=[
  Returns the result of the given function if the option is `Some`,
  otherwise returns the given default value.
]=]
function Option.mapOr(self, default, f)
	if self._n == 0 then
		return default
	end
	return f(unpack(self :: any))
end

--[=[
	Returns the result of the given function if the option is `Some`,
	otherwise returns the rest of the arguments.
]=]
function Option.mapOrRest(self, f, ...)
	if self._n == 0 then
		return ...
	end
	return f(unpack(self :: any))
end

--[=[
  Returns the result of the given function if the option is `Some`,
  otherwise returns the result of calling the default function.
]=]
function Option.mapOrElse(self, default, f)
	if self._n == 0 then
		return default()
	end
	return f(unpack(self :: any))
end

--[=[
  Returns `None` if the option is `None`, otherwise returns the other option.
]=]
function Option.andOpt(self, other)
	if self._n == 0 then
		return Option.None
	end
	return other
end

--[=[
  Returns `None` if the option is `None`, otherwise calls the given
  function with the value of the option and returns the result.
]=]
function Option.andThen(self, f)
	if self._n == 0 then
		return Option.None
	end
	return f(unpack(self :: any))
end

--[=[
  Returns `None` if the option is `None`, otherwise calls the given
  function with the value:
   - if the function returns `true`, returns the original option
   - if the function returns `false`, returns `None`
]=]
function Option.filter(self, f)
	if self._n == 0 then
		return Option.None
	end
	if f(unpack(self :: any)) then
		return self
	end
	return Option.None
end

--[=[
  Returns the option if it is `Some`, otherwise returns
  the other option.
]=]
function Option.orOpt(self, other)
	if self._n ~= 0 then
		return self
	end
	return other
end

--[=[
  Returns the option if it is `Some`, otherwise returns
  the result of calling the given function.
]=]
function Option.orElse(self, f)
	if self._n ~= 0 then
		return self
	end
	return f()
end

--[=[
  Returns `Some` if exactly one of the options is `Some`, otherwise
  returns `None`.
]=]
function Option.xor(self, other)
	if self._n == 0 then
		return other
	end
	if other._n == 0 then
		return self
	end
	return Option.None
end

--[=[
	Takes a table containing a `Some` and `None` function. If the option
	is a `Some`, calls the `Some` function with the value of the option,
	otherwise calls the `None` function. The result of the function call
	is returned.
]=]
function Option.match(self, f)
	if self._n ~= 0 then
		return f.Some(unpack(self :: any))
	end
	return f.None()
end

Option.join = function(self, other)
	if self._n == 0 or other._n == 0 then
		return Option.None
	end
	return Option.Some(unpack(self), unpack(other))
end :: any

if _G.UTIL_TARGET == "lune" then
	-- use `stdio.format` on Lune which pretty-prints tables

	local stdio = require("@lune/stdio")

	function Option.__tostring(self)
		if self._n ~= 0 then
			local s = ""
			for i = 1, self._n do
				if i > 1 then
					s ..= ", "
				end

				local v = self[i]
				if typeof(v) == "string" then
					s ..= string.format("%q", v)
				else
					s ..= stdio.format(v)
				end
			end

			return `Option::Some({s})`
		else
			return "Option::None"
		end
	end
else
	function Option.__tostring(self)
		if self._n ~= 0 then
			local s = ""
			for i = 1, self._n do
				if i > 1 then
					s ..= ", "
				end

				local v = self[i]
				if typeof(v) == "string" then
					s ..= string.format("%q", v)
				else
					s ..= tostring(v)
				end
			end

			return `Option::Some({s})`
		else
			return "Option::None"
		end
	end
end

--[=[
	Returns `true` if the given value is an `Option` and contains the
	same value as this option, `false` otherwise.
]=]
function Option.__eq(self, other)
	if Option.is(other) then
		if self._n == 0 and other._n == 0 then
			return true
		elseif self._n == 1 and other._n == 1 then
			return self[1] == other[1]
		elseif self._n == other._n then
			for i = 1, self._n do
				if self[i] ~= other[i] then
					return false
				end
			end
			return true
		end
	end
	return false
end

return Option


local stdio = require("@lune/stdio")

local assertEq = require("@testing/assertEq")
local describe = require("@testing/describe")
local randomTuple = require("@testing/randomTuple")
local test = require("@testing/test")

local Option = require("./lib")

describe("Some", function()
	test("returns an option containing the given value", function()
		randomTuple(function(tuple, ...)
			local opt = Option.Some(...)
			tuple.assertEq(unpack(opt :: any))
		end)
	end)

	test("errors if no arguments are given", function()
		local success, err = pcall(Option.Some)
		assertEq(success, false)
		assertEq(err :: any, "Option.Some() called with no arguments")
	end)
end)

describe("from", function()
	test("returns Some if the value is not nil", function()
		local option = Option.from(1)
		assertEq(option, Option.Some(1))
	end)

	test("returns None if the value is nil", function()
		local option = Option.from(nil)
		assertEq(option, Option.None)
	end)
end)

describe("is", function()
	test("returns true when given an option", function()
		assertEq(Option.is(Option.Some(5, "hello")), true)
		assertEq(Option.is(Option.None), true)
	end)

	test("returns false when given a non-option", function()
		assertEq(Option.is(5), false)
		assertEq(Option.is("hello"), false)
		assertEq(Option.is({}), false)
		assertEq(Option.is(function() end), false)
	end)
end)

describe("isSome", function()
	test("returns true if the option is Some", function()
		assertEq(Option.Some(5, "hello"):isSome(), true)
	end)

	test("returns false if the option is None", function()
		assertEq(Option.None:isSome(), false)
	end)
end)

describe("isSomeAnd", function()
	test("returns true if the option is Some and the predicate is true", function()
		randomTuple(function(tuple, ...)
			assertEq(
				Option.Some(...):isSomeAnd(function(...)
					tuple.assertEq(...)
					return true
				end),
				true
			)
		end)
	end)

	test("returns false if the option is Some and the predicate is false", function()
		randomTuple(function(tuple, ...)
			assertEq(
				Option.Some(...):isSomeAnd(function(...)
					tuple.assertEq(...)
					return false
				end),
				false
			)
		end)
	end)

	test("returns false if the option is None", function()
		assertEq(
			Option.None:isSomeAnd(function()
				return true
			end),
			false
		)
	end)
end)

describe("isNone", function()
	test("returns true if the option is None", function()
		assertEq(Option.None:isNone(), true)
	end)

	test("returns false if the option is Some", function()
		assertEq(Option.Some(5, "hello"):isNone(), false)
	end)
end)

describe("expect", function()
	test("returns the value if the option is Some", function()
		randomTuple(function(tuple, ...)
			tuple.assertEq(Option.Some(...):expect("option should be some"))
		end)
	end)

	test("errors if the option is None", function()
		local success, err = pcall(Option.None.expect, Option.None, "hello")
		assertEq(success, false)
		assertEq(err, "hello")
	end)
end)

describe("unwrap", function()
	test("returns the value if the option is Some", function()
		randomTuple(function(tuple, ...)
			tuple.assertEq(Option.Some(...):unwrap())
		end)
	end)

	test("errors if the option is None", function()
		local success, err = pcall(Option.None.unwrap, Option.None)
		assertEq(success, false)
		assertEq(err, "called `Option.unwrap()` on a `None` value")
	end)
end)

describe("unwrapOr", function()
	test("returns the value if the option is Some", function()
		randomTuple(function(tuple, ...)
			tuple.assertEq(Option.Some(...):unwrapOr(tuple.other()))
		end)
	end)

	test("returns the default value if the option is None", function()
		randomTuple(function(tuple, ...)
			tuple.assertEq(Option.None:unwrapOr(...))
		end)
	end)
end)

describe("unwrapOrElse", function()
	test("returns the value if the option is Some", function()
		randomTuple(function(tuple, ...)
			tuple.assertEq(Option.Some(...):unwrapOrElse(tuple.other))
		end)
	end)

	test("returns the default value if the option is None", function()
		randomTuple(function(tuple)
			tuple.assertEqOther(Option.None:unwrapOrElse(tuple.other))
		end)
	end)
end)

describe("map", function()
	test("returns None if the option is None", function()
		randomTuple(function(tuple)
			assertEq(Option.None:map(tuple.map), Option.None)
		end)
	end)

	test("returns Some if the option is Some", function()
		randomTuple(function(tuple, ...)
			assertEq(Option.Some(...):map(tuple.map), Option.Some(tuple.map(...)))
		end)
	end)
end)

describe("mapOr", function()
	test("returns the default value if the option is None", function()
		assertEq(
			Option.None:mapOr(10, function(value: number)
				return value + 1
			end),
			10
		)
	end)

	test("returns the result of the function if the option is Some", function()
		assertEq(
			Option.Some(5):mapOr(10, function(value: number)
				return value + 1
			end),
			6
		)
	end)
end)

describe("mapOrRest", function()
	test("returns the default value if the option is None", function()
		randomTuple(function(tuple, ...)
			tuple.assertEq(Option.None:mapOrRest(tuple.map, ...))
		end)
	end)

	test("returns the result of the function if the option is Some", function()
		randomTuple(function(tuple, ...)
			tuple.assertEqMapped(Option.Some(...):mapOrRest(tuple.map, tuple.other()))
		end)
	end)
end)

describe("mapOrElse", function()
	test("returns the result of the default function if the option is None", function()
		randomTuple(function(tuple)
			tuple.assertEqOther(Option.None:mapOrElse(tuple.other, tuple.map))
		end)
	end)

	test("returns the result of the function if the option is Some", function()
		randomTuple(function(tuple, ...)
			tuple.assertEqMapped(Option.Some(...):mapOrElse(tuple.other, tuple.map))
		end)
	end)
end)

describe("andOpt", function()
	test("returns None if the option is None", function()
		randomTuple(function(_, ...)
			assertEq(Option.None:andOpt(Option.Some(...)), Option.None)
		end)
	end)

	test("returns None if the other option is None", function()
		randomTuple(function(_, ...)
			assertEq(Option.Some(...):andOpt(Option.None), Option.None)
		end)
	end)

	test("returns None if both options are none", function()
		assertEq(Option.None:andOpt(Option.None), Option.None)
	end)

	test("returns the other option if both options are Some", function()
		randomTuple(function(tuple, ...)
			assertEq(Option.Some(...):andOpt(Option.Some(tuple.other())), Option.Some(tuple.other()))
		end)
	end)
end)

describe("andThen", function()
	test("returns None if the option is None", function()
		randomTuple(function(tuple)
			assertEq(
				Option.None:andThen(function(...)
					return Option.Some(tuple.map(...))
				end),
				Option.None
			)
		end)
	end)

	test("returns None if the function returns None", function()
		randomTuple(function(_, ...)
			assertEq(
				Option.Some(...):andThen(function()
					return Option.None
				end),
				Option.None
			)
		end)
	end)

	test("returns Some if the function returns Some", function()
		randomTuple(function(tuple, ...)
			assertEq(
				Option.Some(...):andThen(function(...)
					return Option.Some(tuple.map(...))
				end),
				Option.Some(tuple.map(...))
			)
		end)
	end)
end)

describe("filter", function()
	test("returns None if the option is None", function()
		assertEq(
			Option.None:filter(function()
				return true
			end),
			Option.None
		)
	end)

	test("returns None if the predicate returns false", function()
		randomTuple(function(_, ...)
			assertEq(
				Option.Some(...):filter(function()
					return false
				end),
				Option.None
			)
		end)
	end)

	test("returns Some if the predicate returns true", function()
		randomTuple(function(_, ...)
			assertEq(
				Option.Some(...):filter(function()
					return true
				end),
				Option.Some(...)
			)
		end)
	end)
end)

describe("orOpt", function()
	test("returns the other option if the option is None", function()
		randomTuple(function(_, ...)
			assertEq(Option.None:orOpt(Option.Some(...)), Option.Some(...))
		end)
	end)

	test("returns the option if the other option is None", function()
		randomTuple(function(_, ...)
			assertEq(Option.Some(...):orOpt(Option.None), Option.Some(...))
		end)
	end)

	test("returns None if both options are none", function()
		assertEq(Option.None:andOpt(Option.None), Option.None)
	end)

	test("returns the option if both options are Some", function()
		randomTuple(function(tuple, ...)
			assertEq(Option.Some(...):orOpt(Option.Some(tuple.other())), Option.Some(...))
		end)
	end)
end)

describe("orElse", function()
	test("returns the result of the function if the option is None", function()
		randomTuple(function(tuple)
			assertEq(
				(Option.None :: Option.Option<number>):orElse(function()
					return Option.Some(tuple.other())
				end),
				Option.Some(tuple.other())
			)
		end)
	end)

	test("returns the option if the option is Some", function()
		randomTuple(function(tuple, ...)
			assertEq(
				Option.Some(...):orElse(function()
					return Option.Some(tuple)
				end),
				Option.Some(...)
			)
		end)
	end)
end)

describe("xor", function()
	test("returns None if both options are None", function()
		assertEq(Option.None:xor(Option.None), Option.None)
	end)

	test("returns Some if only one option is None", function()
		randomTuple(function(_, ...)
			assertEq(Option.None:xor(Option.Some(...)), Option.Some(...))
			assertEq(Option.Some(...):xor(Option.None), Option.Some(...))
		end)
	end)

	test("returns None if both options are Some", function()
		randomTuple(function(tuple, ...)
			assertEq(Option.Some(...):xor(Option.Some(tuple.other())), Option.None)
		end)
	end)
end)

describe("match", function()
	test("calls the Some and returns the result if the option is Some", function()
		randomTuple(function(tuple, ...)
			tuple.assertEqMapped(Option.Some(...):match({
				Some = tuple.map,
				None = tuple.other,
			}))
		end)
	end)

	test("calls the None and returns the result if the option is None", function()
		randomTuple(function(tuple)
			tuple.assertEqOther(Option.None:match({
				Some = tuple.map,
				None = tuple.other,
			}))
		end)
	end)
end)

describe("join", function()
	test("joins the values if both options are Some", function()
		randomTuple(function(tuple, ...)
			assertEq(Option.Some(...):join(Option.Some(tuple.other())), Option.Some(..., tuple.other()))
		end)
	end)

	test("returns None if the first option is None", function()
		randomTuple(function(_, ...)
			assertEq(Option.None:join(Option.Some(...)), Option.None)
		end)
	end)

	test("returns None if the second option is None", function()
		randomTuple(function(_, ...)
			assertEq(Option.Some(...):join(Option.None), Option.None)
		end)
	end)

	test("returns None if both options are None", function()
		assertEq(Option.None:join(Option.None), Option.None)
	end)
end)

describe("__tostring", function()
	test("returns a string with the value when Some", function()
		assertEq(tostring(Option.Some(5)), "Option::Some(5)")
		assertEq(tostring(Option.Some(5, "hello")), 'Option::Some(5, "hello")')
		assertEq(tostring(Option.Some("hello", true, 5, 20)), 'Option::Some("hello", true, 5, 20)')
		assertEq(tostring(Option.Some({ a = 5 })), `Option::Some({stdio.format({ a = 5 })})`)
	end)

	test("returns a string when None", function()
		assertEq(tostring(Option.None), "Option::None")
	end)
end)

describe("__eq", function()
	local none: Option.Option<number> = Option.None

	test("returns true if both options are None", function()
		assertEq(none == none, true)
	end)

	test("returns false if only one option is None", function()
		randomTuple(function(_, ...)
			assertEq(none == Option.Some(...), false)
			assertEq(Option.Some(...) == none, false)
		end)
	end)

	test("returns true if both options are Some and equal", function()
		randomTuple(function(_, ...)
			assertEq(Option.Some(...) == Option.Some(...), true)
		end)
	end)

	test("returns false if both options are Some and not equal", function()
		randomTuple(function(tuple, ...)
			assertEq(Option.Some(...) == Option.Some(tuple.other()), false)

			local a = { ... }
			randomTuple(function(_, ...)
				assertEq(Option.Some(unpack(a)) == Option.Some(...), false)
			end)
		end)
	end)
end)


export type Result<T, E> = typeof(setmetatable(
	{} :: {
		_ok: boolean,
		_v: T,
		_e: E,
	},
	{} :: ResultImpl
))

export type ResultImpl = {
	__index: ResultImpl,
	__tostring: <T, E>(self: Result<T, E>) -> string,
	__eq: <T, E>(self: Result<T, E>, other: Result<T, E>) -> boolean,

	Ok: <T>(value: T) -> Result<T, any>,
	Err: <E>(err: E) -> Result<any, E>,
	try: <T, U...>(f: (U...) -> T, U...) -> Result<T, unknown>,
	is: (value: any) -> boolean,
	isOk: <T, E>(self: Result<T, E>) -> boolean,
	isOkAnd: <T, E>(self: Result<T, E>, f: (T) -> boolean) -> boolean,
	isErr: <T, E>(self: Result<T, E>) -> boolean,
	isErrAnd: <T, E>(self: Result<T, E>, f: (E) -> boolean) -> boolean,
	map: <T, E, U>(self: Result<T, E>, f: (T) -> U) -> Result<U, E>,
	mapOr: <T, E, U>(self: Result<T, E>, default: U, f: (T) -> U) -> U,
	mapOrElse: <T, E, U>(self: Result<T, E>, default: (E) -> U, f: (T) -> U) -> U,
	mapErr: <T, E, F>(self: Result<T, E>, f: (E) -> F) -> Result<T, F>,
	expect: <T, E>(self: Result<T, E>, msg: string) -> T,
	unwrap: <T, E>(self: Result<T, E>) -> T,
	expectErr: <T, E>(self: Result<T, E>, msg: string) -> E,
	unwrapErr: <T, E>(self: Result<T, E>) -> E,
	andRes: <T, E, U>(self: Result<T, E>, res: Result<U, E>) -> Result<U, E>,
	andThen: <T, E, U>(self: Result<T, E>, f: (T) -> Result<U, E>) -> Result<U, E>,
	orRes: <T, E, F>(self: Result<T, E>, res: Result<T, F>) -> Result<T, F>,
	orElse: <T, E, F>(self: Result<T, E>, f: (E) -> Result<T, F>) -> Result<T, F>,
	unwrapOr: <T, E>(self: Result<T, E>, default: T) -> T,
	unwrapOrElse: <T, E>(self: Result<T, E>, f: (E) -> T) -> T,
	match: <T, E, U>(self: Result<T, E>, f: { Ok: (T) -> U, Err: (E) -> U }) -> U,
	unpack: <T, E>(self: Result<T, E>) -> (T, E),
}

--[=[
  @class Result

  Represents a value that is either success (`Ok`) or failure (`Err`).

  Results can be used as an alternative to throwing errors. It requires
  you to eplicitly handle both cases, which can prevent unexpected
  errors from being thrown.

  `Result<T, E>` is a generic type with two type parameters: `T` and `E`:
    - `T` is the type of the success value.
    - `E` is the type of the error value.
]=]
local Result = {} :: ResultImpl
Result.__index = Result

--[=[
  Creates a new `Ok` with the given value.
]=]
function Result.Ok(value)
	local self = setmetatable({ _ok = true :: true, _v = value }, Result)
	return table.freeze(self) :: any
end

--[=[
  Creates a new `Err` with the given error.
]=]
function Result.Err(err)
	local self = setmetatable({ _ok = false :: false, _e = err }, Result)
	return table.freeze(self) :: any
end

--[=[
	Calls the given function and returns a `Result` with the result of the
	function. If the function throws an error, it will be caught and returned
	as an `Err`.
]=]
function Result.try(f, ...)
	local ok, res = pcall(f :: any, ...)
	if ok then
		return Result.Ok(res)
	end
	return Result.Err(res)
end

--[=[
	Returns true if the given value is a `Result`.
]=]
function Result.is(value)
	return typeof(value) == "table" and getmetatable(value) == Result
end

--[=[
  Returns true if the result is `Ok`.
]=]
function Result.isOk(self)
	return self._ok
end

--[=[
  Returns true if the result is `Ok` and the value matches the given
  predicate.
]=]
function Result.isOkAnd(self, f)
	return self._ok and f(self._v)
end

--[=[
  Returns true if the result is `Err`.
]=]
function Result.isErr(self)
	return not self._ok
end

--[=[
  Returns true if the result is `Err` and the error matches the given
  predicate.
]=]
function Result.isErrAnd(self, f)
	return not self._ok and f(self._e)
end

--[=[
	Calls the given function with the contained `Ok` value and returns a
	`Result` with the result of the function. Leaving an `Err` value untouched.
]=]
function Result.map(self, f)
	if not self._ok then
		return self :: Result<any, any>
	end
	return Result.Ok((f :: any)(self._v))
end

--[=[
	Calls the given function with the contained `Ok` value and returns the
	result, otherwise returns the given default value.
]=]
function Result.mapOr(self, default, f)
	if not self._ok then
		return default
	end
	return f(self._v)
end

--[=[
	Calls the given function with the contained `Ok` value and returns the
	result, otherwise calls the given function with the contained `Err` value.
]=]
function Result.mapOrElse(self, default, f)
	if not self._ok then
		return default(self._e)
	end
	return f(self._v)
end

--[=[
	Calls the given function with the contained `Err` value and returns a
	`Result` with the result of the function. Leaving an `Ok` value untouched.
]=]
function Result.mapErr(self, f)
	if self._ok then
		return self :: Result<any, any>
	end
	return Result.Err((f :: any)(self._e))
end

--[=[
	Returns the contained `Ok`. Otherwise an error is thrown with the given message.
]=]
function Result.expect(self, msg)
	if not self._ok then
		error(msg, 2)
	end
	return self._v
end

--[=[
	Returns the contained `Ok`. Otherwise the contained `Err` value is thrown.
]=]
function Result.unwrap(self)
	if not self._ok then
		error(self._e :: any, 2)
	end
	return self._v
end

--[=[
	Returns the contained `Err`. Otherwise an error is thrown with the given message.
]=]
function Result.expectErr(self, msg)
	if self._ok then
		error(msg, 2)
	end
	return self._e
end

--[=[
	Returns the contained `Err`. Otherwise the contained `Ok` value is thrown.
]=]
function Result.unwrapErr(self)
	if self._ok then
		error(self._v :: any, 2)
	end
	return self._e
end

--[=[
	Returns `res` if the result is `Ok`, otherwise returns the `Err`.
]=]
function Result.andRes(self, res)
	if self._ok then
		return res
	end
	return self :: Result<any, any>
end

--[=[
	Calls the given function with the contained `Ok` value and returns the
	result, otherwise returns the `Err`.
]=]
function Result.andThen(self, f)
	if self._ok then
		return f(self._v)
	end
	return self :: Result<any, any>
end

--[=[
	Returns `res` if the result is `Err`, otherwise returns the `Ok`.
]=]
function Result.orRes(self, res)
	if not self._ok then
		return res
	end
	return self :: Result<any, any>
end

--[=[
	Calls the given function with the contained `Err` value and returns the
	result, otherwise returns the `Ok`.
]=]
function Result.orElse(self, f)
	if not self._ok then
		return f(self._e)
	end
	return self :: Result<any, any>
end

--[=[
	Returns the contained `Ok` value, or the given default value.
]=]
function Result.unwrapOr(self, default)
	if self._ok then
		return self._v
	end
	return default
end

--[=[
	Returns the contained `Ok` value, or calls the given function with the
	contained `Err` value.
]=]
function Result.unwrapOrElse(self, f)
	if self._ok then
		return self._v
	end
	return f(self._e)
end

--[=[
	Takes a table containing an `Ok` and `Err` function. If the option
	is a `Ok`, calls the `Ok` function with the contained `Ok` value,
	otherwise calls the `Err` function with the contained `Err`. The
	result of the function call is returned.
]=]
function Result.match(self, f)
	if self._ok then
		return f.Ok(self._v)
	end
	return f.Err(self._e)
end

--[=[
	Returns a tuple where the first value is the contained `Ok` value or nil and
	the second value is the contained `Err` value or nil.
]=]
function Result.unpack(self)
	return self._v, self._e
end

if _G.UTIL_TARGET == "lune" then
	-- use `stdio.format` on Lune which pretty-prints tables

	local stdio = require("@lune/stdio")
	function Result.__tostring(self)
		if self._ok then
			if typeof(self._v) == "string" then
				return string.format("Result::Ok(%q)", self._v)
			else
				return `Result::Ok({stdio.format(self._v)})`
			end
		end

		if typeof(self._e) == "string" then
			return string.format("Result::Err(%q)", self._e)
		else
			return `Result::Err({stdio.format(self._e)})`
		end
	end
else
	function Result.__tostring(self)
		if self._ok then
			if typeof(self._v) == "string" then
				return string.format("Result::Ok(%q)", self._v)
			else
				return `Result::Ok({self._v})`
			end
		end

		if typeof(self._e) == "string" then
			return string.format("Result::Err(%q)", self._e)
		else
			return `Result::Err({self._e})`
		end
	end
end

--[=[
	Returns `true` if both results are `Ok` or `Err` and their values are equal.
	Othwerwise returns `false`.
]=]
function Result.__eq(self, other)
	if Result.is(other) then
		if self._ok and other._ok then
			return self._v == other._v
		elseif not self._ok and not other._ok then
			return self._e == other._e
		end
	end
	return false
end

return Result


local stdio = require("@lune/stdio")

local assertEq = require("@testing/assertEq")
local describe = require("@testing/describe")
local test = require("@testing/test")

local Result = require("./lib")

describe("Ok", function()
	test("returns a result containing the given Ok value", function()
		local result = Result.Ok(5)
		assertEq(result._ok, true)
		assertEq((result :: any)._v, 5)
	end)
end)

describe("Err", function()
	test("returns a result containing the given Err value", function()
		local result = Result.Err("error")
		assertEq(result._ok, false)
		assertEq((result :: any)._e, "error")
	end)
end)

describe("try", function()
	test("returns an Ok value when the function succeeds", function()
		local result = Result.try(function(v)
			return v
		end, 5)

		assertEq(result, Result.Ok(5))
	end)

	test("returns an Err value when the function fails", function()
		local result = Result.try(function()
			error("hello")
		end)

		assertEq(result._ok, false)
		assertEq((result :: any)._e:match("hello"), "hello")
	end)
end)

describe("is", function()
	test("returns true when given a result", function()
		assertEq(Result.is(Result.Ok(5)), true)
		assertEq(Result.is(Result.Err("error")), true)
	end)

	test("returns false when given a non-result", function()
		assertEq(Result.is(5), false)
		assertEq(Result.is("error"), false)
	end)
end)

describe("isOk", function()
	test("returns true when given an Ok result", function()
		assertEq(Result.Ok(5):isOk(), true)
	end)

	test("returns false when given an Err result", function()
		assertEq(Result.Err("error"):isOk(), false)
	end)
end)

describe("isOkAnd", function()
	test("returns true when the option is Ok and the predicate is true", function()
		assertEq(
			Result.Ok(5):isOkAnd(function(v)
				return v == 5
			end),
			true
		)
	end)

	test("returns false when the option is Ok and the predicate is false", function()
		assertEq(
			Result.Ok(5):isOkAnd(function(v)
				return v == 10
			end),
			false
		)
	end)

	test("returns false when the option is Err", function()
		assertEq(
			Result.Err("error"):isOkAnd(function(v)
				return v == 5
			end),
			false
		)
	end)
end)

describe("isErr", function()
	test("returns true when given an `Err` result", function()
		assertEq(Result.Err("error"):isErr(), true)
	end)

	test("returns false when given an `Ok` result", function()
		assertEq(Result.Ok(5):isErr(), false)
	end)
end)

describe("isErrAnd", function()
	test("returns true when the option is Err and the predicate is true", function()
		assertEq(
			Result.Err("error"):isErrAnd(function(e)
				return e == "error"
			end),
			true
		)
	end)

	test("returns false when the option is Err and the predicate is false", function()
		assertEq(
			Result.Err("error"):isErrAnd(function(e)
				return e == "hello"
			end),
			false
		)
	end)

	test("returns false when the option is Ok", function()
		assertEq(
			Result.Ok(5):isErrAnd(function(e)
				return e == "error"
			end),
			false
		)
	end)
end)

describe("map", function()
	test("returns Err when the result is Err", function()
		local result = Result.Err("error"):map(function(v: number)
			return v * 2
		end)

		assertEq(result, Result.Err("error"))
	end)

	test("returns Ok when the result is Ok", function()
		local result = Result.Ok(5):map(function(v: number)
			return v * 2
		end)

		assertEq(result, Result.Ok(10))
	end)
end)

describe("mapOr", function()
	test("returns the default value when the result is Err", function()
		local result = Result.Err("error"):mapOr(5, function(v: number)
			return v * 2
		end)

		assertEq(result, 5)
	end)

	test("returns the mapped value when the result is Ok", function()
		local result = Result.Ok(5):mapOr(5, function(v: number)
			return v * 2
		end)

		assertEq(result, 10)
	end)
end)

describe("mapOrElse", function()
	test("returns the default value when the result is Err", function()
		local result = Result.Err("error"):mapOrElse(function(e: string)
			return e:upper() :: number | string
		end, function(v: number)
			return v * 2
		end)

		assertEq(result, "ERROR")
	end)

	test("returns the mapped value when the result is Ok", function()
		local result = Result.Ok(5):mapOrElse(function(e: string)
			return e:upper() :: number | string
		end, function(v: number)
			return v * 2
		end)

		assertEq(result, 10)
	end)
end)

describe("mapErr", function()
	test("returns Ok when the result is Ok", function()
		local result = Result.Ok(5):mapErr(function(e: string)
			return e:upper()
		end)

		assertEq(result, Result.Ok(5))
	end)

	test("returns the mapped Err when the result is Err", function()
		local result = Result.Err("error"):mapErr(function(e: string)
			return e:upper()
		end)

		assertEq(result, Result.Err("ERROR"))
	end)
end)

describe("expect", function()
	test("returns the value when the result is Ok", function()
		local result = Result.Ok(5):expect("error")

		assertEq(result, 5)
	end)

	test("throws an error when the result is Err", function()
		local ok, err = pcall(function()
			Result.Err("error"):expect("error")
		end)

		assertEq(ok, false)
		assertEq(err:match("error"), "error")
	end)
end)

describe("unwrap", function()
	test("returns the value when the result is Ok", function()
		local result = Result.Ok(5):unwrap()

		assertEq(result, 5)
	end)

	test("throws an error when the result is Err", function()
		local ok, err = pcall(function()
			Result.Err("error"):unwrap()
		end)

		assertEq(ok, false)
		assertEq(err:match("error"), "error")
	end)
end)

describe("expectErr", function()
	test("returns the error when the result is Err", function()
		local result = Result.Err("error"):expectErr("error")

		assertEq(result, "error")
	end)

	test("throws an error when the result is Ok", function()
		local ok, err = pcall(function()
			Result.Ok(5):expectErr("error")
		end)

		assertEq(ok, false)
		assertEq(err:match("error"), "error")
	end)
end)

describe("unwrapErr", function()
	test("returns the error when the result is Err", function()
		local result = Result.Err("error"):unwrapErr()

		assertEq(result, "error")
	end)

	test("throws an error when the result is Ok", function()
		local ok, err = pcall(function()
			Result.Ok(5):unwrapErr()
		end)

		assertEq(ok, false)
		assertEq(err:match("5"), "5")
	end)
end)

describe("andRes", function()
	test("returns the other result when the result is Ok", function()
		local resultA = Result.Ok(5):andRes(Result.Ok(10))
		assertEq(resultA, Result.Ok(10))

		local resultB = Result.Ok(5):andRes(Result.Err("error"))
		assertEq(resultB, Result.Err("error"))
	end)

	test("returns the Err when the result is Err", function()
		local result = Result.Err("error"):andRes(Result.Ok(10))
		assertEq(result, Result.Err("error"))
	end)
end)

describe("andThen", function()
	test("returns the other result when the result is Ok", function()
		local resultA = Result.Ok(5):andThen(function(v: number)
			return Result.Ok(v * 2)
		end)
		assertEq(resultA, Result.Ok(10))

		local resultB = Result.Ok(5):andThen(function(v: number)
			return Result.Err(v)
		end)
		assertEq(resultB, Result.Err(5))
	end)

	test("returns the Err when the result is Err", function()
		local result = Result.Err("error"):andThen(function(v: number)
			return Result.Ok(v * 2)
		end)
		assertEq(result, Result.Err("error"))
	end)
end)

describe("orRes", function()
	test("returns the Ok when the result is Ok", function()
		local result = Result.Ok(5):orRes(Result.Ok(10))
		assertEq(result, Result.Ok(5))
	end)

	test("returns the other result when the result is Err", function()
		local resultA = Result.Err("error"):orRes(Result.Ok(10))
		assertEq(resultA, Result.Ok(10))

		local resultB = Result.Err("error"):orRes(Result.Err(10))
		assertEq(resultB, Result.Err(10))
	end)
end)

describe("orElse", function()
	test("returns the Ok when the result is Ok", function()
		local result = Result.Ok(5):orElse(function(e: string)
			return Result.Err(e:upper())
		end)
		assertEq(result, Result.Ok(5))
	end)

	test("returns the other result when the result is Err", function()
		local resultA = Result.Err("error"):orElse(function(e: string)
			return Result.Err(e:upper())
		end)
		assertEq(resultA, Result.Err("ERROR"))

		local resultB = Result.Err("error"):orElse(function(e: string)
			return Result.Err(e:upper())
		end)
		assertEq(resultB, Result.Err("ERROR"))
	end)
end)

describe("unwrapOr", function()
	test("returns the value when the result is Ok", function()
		local result = Result.Ok(5):unwrapOr(10)
		assertEq(result, 5)
	end)

	test("returns the default value when the result is Err", function()
		local result = Result.Err("error"):unwrapOr(10)
		assertEq(result, 10)
	end)
end)

describe("unwrapOrElse", function()
	test("returns the value when the result is Ok", function()
		local result = Result.Ok(5):unwrapOrElse(function()
			return 10
		end)
		assertEq(result, 5)
	end)

	test("returns the default value when the result is Err", function()
		local result = Result.Err("error"):unwrapOrElse(function(e: string)
			return e:upper()
		end)
		assertEq(result, "ERROR")
	end)
end)

describe("match", function()
	test("returns the result of the Ok function when the result is Ok", function()
		local result: string | number = Result.Ok(5):match({
			Ok = function(v: number)
				return v * 2
			end,
			Err = function(e)
				return e:upper()
			end,
		})

		assertEq(result, 10)
	end)

	test("returns the result of the Err function when the result is Err", function()
		local result = Result.Err("error"):match({
			Ok = function(v: number)
				return (v * 2) :: string | number
			end,
			Err = function(e: string)
				return e:upper()
			end,
		})

		assertEq(result, "ERROR")
	end)
end)

describe("unpack", function()
	test("returns the value when the result is Ok", function()
		local value, error = Result.Ok(5):unpack()
		assertEq(value, 5)
		assertEq(error, nil)
	end)

	test("returns the error when the result is Err", function()
		local value, error = Result.Err("error"):unpack()
		assertEq(value, nil)
		assertEq(error, "error")
	end)
end)

describe("__tostring", function()
	test("returns a string with the value when Ok", function()
		assertEq(tostring(Result.Ok(5)), "Result::Ok(5)")
		assertEq(tostring(Result.Ok({ a = 5 })), `Result::Ok({stdio.format({ a = 5 })})`)
	end)

	test("returns a string with the error when Err", function()
		assertEq(tostring(Result.Err("error")), 'Result::Err("error")')
		assertEq(tostring(Result.Err({ a = 5 })), `Result::Err({stdio.format({ a = 5 })})`)
	end)
end)

describe("__eq", function()
	test("returns true when both results are Ok and have the same value", function()
		assertEq(Result.Ok(5) == Result.Ok(5), true)
	end)

	test("returns false when both results are Ok and have different values", function()
		assertEq(Result.Ok(5) == Result.Ok(10), false)
	end)

	test("returns true when both results are Err and have the same error", function()
		assertEq(Result.Err("error") == Result.Err("error"), true)
	end)

	test("returns false when both results are Err and have different errors", function()
		assertEq(Result.Err("error") == Result.Err("hello"), false)
	end)

	test("returns false when the results are different", function()
		local resultA: Result.Result<number, string> = Result.Ok(5)
		local resultB: Result.Result<number, string> = Result.Err("error")
		assertEq(resultA == resultB, false)
	end)
end)


local runtime = require("../runtime")
local task = require("@lune/task")

return runtime.defineRuntime({
	name = "lune",
	libs = {
		task = {
			cancel = task.cancel,
			defer = task.defer,
			delay = task.delay,
			spawn = task.spawn,
			wait = task.wait,
		},
	},
})


local runtime = require("../runtime")

return runtime.defineRuntime({
	name = "roblox",
	libs = {
		task = {
			cancel = task.cancel,
			defer = task.defer,
			delay = task.delay,
			spawn = task.spawn,
			wait = task.wait,
		} :: any,
	},
})


local libs = require("./libs")
local runtime = require("./runtime")

export type Runtime = runtime.Runtime
export type RuntimeDefinition = runtime.RuntimeDefinition

export type TaskLib = libs.TaskLib

local std = {}

local function detectRuntime(): Runtime?
	if _G.UTIL_TARGET == "roblox" then
		return require("./default-runtimes/roblox")
	end

	if _G.UTIL_TARGET == "lune" then
		return require("./default-runtimes/lune")
	end

	if typeof(script) == "Instance" then
		return require("./default-runtimes/roblox")
	end

	if string.sub(_VERSION, 1, 5):lower() == "lune " then
		return require("./default-runtimes/lune")
	end

	return nil
end

local detectedRuntime = detectRuntime()
std.runtime = detectedRuntime

if detectedRuntime ~= nil then
	std.task = table.clone(detectedRuntime.libs.task)
else
	std.task = libs.defaultImplementation("task", libs.task)
end

function std.setRuntime(runtime: Runtime)
	std.runtime = runtime

	for libName, lib in runtime.libs do
		for k, v in lib :: any do
			std[libName][k] = v
		end
	end
end

std.defineRuntime = runtime.defineRuntime

return std


local assertEq = require("@testing/assertEq")
local describe = require("@testing/describe")
local test = require("@testing/test")

local std = require("./lib")

describe("detectRuntime", function()
	test("detects lune runtime", function()
		assertEq(std.runtime ~= nil, true)

		assert(std.runtime ~= nil, "make Luau typechecker happy")
		assertEq(std.runtime.name, "lune")
	end)
end)

describe("setRuntime", function()
	test("sets the runtime", function()
		local luneRuntime = std.runtime
		assert(luneRuntime ~= nil, "should have runtime")

		local runtime = std.defineRuntime({ name = "test", libs = {} })

		local prevTask = std.task

		std.setRuntime(runtime)
		assertEq(std.runtime, runtime)
		assertEq(std.task, prevTask)
		assertEq(std.task.spawn, runtime.libs.task.spawn)

		std.setRuntime(luneRuntime)
	end)
end)

describe("defineRuntime", function()
	test("fills in missing libraries", function()
		local runtime = std.defineRuntime({ name = "test", libs = {} })

		local success, result = pcall(runtime.libs.task.spawn, function() end)
		assertEq(success, false)
		assertEq(result :: any, "'task.spawn' is not implemented by runtime 'test'")
	end)
end)


local libs = {}

export type TaskLib = {
	cancel: (thread) -> (),
	defer: <T...>(functionOrThread: thread | (T...) -> (), T...) -> thread,
	delay: <T...>(duration: number, functionOrThread: thread | (T...) -> (), T...) -> thread,
	spawn: <T...>(functionOrThread: thread | (T...) -> (), T...) -> thread,
	wait: (duration: number) -> number,
}

libs.task = {
	"cancel",
	"defer",
	"delay",
	"spawn",
	"wait",
}

function libs.defaultImplementation(libName: string, keys: { string }): { [any]: any }
	local lib = {}
	for _, key in keys do
		lib[key] = function()
			error(`Called '{libName}.${key}' on an unsupported runtime.`)
		end
	end
	return lib
end

return libs


local libs = require("./libs")

export type Runtime = {
	name: string,
	libs: {
		task: libs.TaskLib,
	},
}

export type RuntimeDefinition = {
	name: string,
	libs: {
		task: {
			cancel: ((thread) -> ())?,
			defer: (<T...>(functionOrThread: thread | (T...) -> (), T...) -> thread)?,
			delay: (<T...>(duration: number, functionOrThread: thread | (T...) -> (), T...) -> thread)?,
			spawn: (<T...>(functionOrThread: thread | (T...) -> (), T...) -> thread)?,
			wait: ((duration: number) -> number)?,
		}?,
	},
}

local runtime = {}

local function notImplemented(runtimeName: string, functionName: string)
	return function()
		error(`'{functionName}' is not implemented by runtime '{runtimeName}'`, 2)
	end
end

local function fillMissing(runtimeName: string, lib: { [any]: any }?, libName: string, keys: { string }): { [any]: any }
	local out = {}

	for _, key in keys do
		if lib == nil or lib[key] == nil then
			out[key] = notImplemented(runtimeName, `{libName}.{key}`)
		else
			out[key] = lib[key]
		end
	end

	return out
end

function runtime.defineRuntime(definition: RuntimeDefinition): Runtime
	local runtime = {
		name = definition.name,
		libs = {
			task = fillMissing(definition.name, definition.libs.task, "task", libs.task),
		},
	}

	return runtime
end

return runtime


local std = require("../std/lib")
local task = std.task

local threadpool = {}

local freeThreads: { thread } = {}

local function run<T...>(f: (T...) -> (), thread: thread, ...)
	f(...)
	table.insert(freeThreads, thread)
end

local function yielder()
	while true do
		run(coroutine.yield())
	end
end

--[=[
  Executes the given function in a separate thread, threads are pooled and reused.
]=]
function threadpool.spawn<T...>(f: (T...) -> (), ...: T...)
	local thread
	if #freeThreads > 0 then
		thread = freeThreads[#freeThreads]
		freeThreads[#freeThreads] = nil
	else
		thread = coroutine.create(yielder)
		coroutine.resume(thread)
	end

	task.spawn(thread, f, thread, ...)
end

return threadpool


local assertEq = require("@testing/assertEq")
local describe = require("@testing/describe")
local test = require("@testing/test")

local threadpool = require("./lib")

describe("spawn", function()
	test("executes the functions in a thread", function()
		local count = 0
		local function f(v)
			assertEq(v, 10)
			count += 1
			coroutine.yield()
		end

		for _ = 1, 10 do
			threadpool.spawn(f, 10)
		end

		assertEq(count, 10)
	end)

	test("reuses pooled threads", function()
		local threads: { [thread]: boolean } = {}
		local function f()
			threads[coroutine.running()] = true
			coroutine.yield()
		end

		for _ = 1, 10 do
			threadpool.spawn(f)
		end

		for thread in pairs(threads) do
			coroutine.resume(thread)
		end

		local function check()
			assertEq(threads[coroutine.running()], true)
		end

		for _ = 1, 10 do
			threadpool.spawn(check)
		end
	end)
end)


local Future = require("../future/lib")
local std = require("../std/lib")
local threadpool = require("../threadpool/lib")

local task = std.task

local timer = {}

function timer.delay(duration: number): Future.Future<number>
	return Future.spawn(task.wait, duration)
end

function timer.interval<T...>(duration: number, f: (T...) -> (), ...: T...): () -> ()
	local function inner(duration: number, f: (T...) -> (), ...: T...)
		local nextTick = os.clock() + duration
		while true do
			task.wait(nextTick - os.clock())
			threadpool.spawn(f, ...)
			nextTick += duration
		end
	end

	local thread = task.spawn(inner, duration, f, ...)
	return function()
		task.cancel(thread)
	end
end

return timer


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local DevPackages = ReplicatedStorage.DevPackages
local JestGlobals = require(DevPackages.JestGlobals)

local describe = JestGlobals.describe
local it = JestGlobals.it
local expect = JestGlobals.expect

describe("math", function()
    it("should add two numbers", function()
        expect(1 + 2).toBe(3)
    end)
end)


local Integrations = {}

Integrations.InboundFilters = require(script.inboundfilter)

return Integrations


-- note: no upstream

type Array<T> = { T }

export type SourcemapEntry = {
    name: string,
    className: string,
    filePaths: Array<string>,
    children: Array<SourcemapEntry>?,
}

-- note: Moved here to avoid cyclic dependency between client.lua and stackparser.lua
export type RobloxStackParserOptions = {
    --- Rojo sourcemap for project, used for turning instance paths back to file system paths in stacktraces.
    projectSourcemap: SourcemapEntry?,
}

return {}


export type PromiseLike<T> = {
    andThen: (
        self: PromiseLike<T>,
        resolve: ((T) -> ...(nil | T | PromiseLike<T>))?,
        reject: ((any) -> ...(nil | T | PromiseLike<T>))?
    ) -> PromiseLike<T>,
}

type PromiseStatus = "Started" | "Resolved" | "Rejected" | "Cancelled"

export type Promise<T> = {
    andThen: (
        self: Promise<T>,
        resolve: ((T) -> ...(nil | T | PromiseLike<T>))?,
        reject: ((any) -> ...(nil | T | PromiseLike<T>))?
    ) -> Promise<T>,

    catch: (Promise<T>, (any) -> ...(nil | T | PromiseLike<nil>)) -> Promise<T>,

    onCancel: (Promise<T>, () -> ()?) -> boolean,

    expect: (Promise<T>) -> ...T,

    -- FIXME Luau: need union type packs to parse  (...T) | () | PromiseLike<T> here
    await: (Promise<T>) -> (boolean, ...(T | any)),

    getStatus: (self: Promise<T>) -> PromiseStatus,
    -- FIXME Luau: need union type packs to parse  (...T) | () | PromiseLike<T> here
    awaitStatus: (self: Promise<T>) -> (PromiseStatus, ...(T | any)),
}

type RejectionArgs = {
    context: string?,
    createdTick: number?,
    createdTrace: string?,
    error: string?,
    kind: string?,
    trace: string?,
}

export type MaybePromiseLibrary = {
    --- Registers a callback that runs when an unhandled rejection happens. An unhandled rejection happens when a Promise
    --- is rejected, and the rejection is not observed with `:catch`.

    --- The callback is called with the actual promise that rejected, followed by the rejection values.

    --- @since v3.2.0
    --- @param callback (promise: Promise, ...: any) -- A callback that runs when an unhandled rejection happens.
    --- @return () -> () -- Function that unregisters the `callback` when called
    onUnhandledRejection: (callback: (promise: PromiseLike<any>, args: RejectionArgs) -> ()) -> () -> (),
}

return {}


return {
    placeId = 98877362830092,
    universeId = 7864706368,

    testPlaceFile = "test-place.rbxl",
    rojoProjectFile = "test.project.json",
}


local MarketplaceService = game:GetService("MarketplaceService")
local RunService = game:GetService("RunService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local Selection = game:GetService("Selection")
local StudioService = game:GetService("StudioService")
local UserInputService = game:GetService("UserInputService")
local storageHandler = require(script.storageHandler)
local uiHandler = require(script.uiHandler)
local wakatime = require(script.wakatime)

if RunService:IsRunning() and RunService:IsClient() then
	return
end

local function activityCallback(force: boolean?)
	wakatime.onActivityCallback(plugin, force)
end

local function getBestProjectName()
	-- If there's already a saved project name we don't need to figure it out.
	if storageHandler.getSavedProjectName(plugin) then
		return
	end
	local ok, gameDetails = pcall(MarketplaceService.GetProductInfo, MarketplaceService, game.PlaceId)
	if ok then
		storageHandler.setProjectName(plugin, gameDetails.Name)
	else
		local inferredName = storageHandler.getProjectName(plugin)
		if inferredName then
			storageHandler.setProjectName(plugin, inferredName)
		end
	end
end

do
	-- While the plugin runs in playtest, we can't get the proper game name, so we'll save it in the plugin settings for it to be read later on.
	if not RunService:IsRunning() and not storageHandler.getSavedProjectName(plugin) then
		getBestProjectName()
	end

	uiHandler.init(plugin)

	UserInputService.InputBegan:Connect(function(inputObject)
		if inputObject.UserInputType == Enum.UserInputType.MouseMovement then
			return
		end
		activityCallback()
	end)
	StudioService:GetPropertyChangedSignal("ActiveScript"):Connect(activityCallback)
	ScriptEditorService.TextDocumentDidChange:Connect(activityCallback)
	Selection.SelectionChanged:Connect(activityCallback)

	-- While in playtesting, the plugin can't run clientside because we can't access HttpService. However, the server code still runs, and we can mark this playtime.
	if RunService:IsRunning() then
		-- Accurately mark the end segment of the playtest.
		game:BindToClose(function()
			activityCallback(true)
		end)

		-- The possibility that someone pressed play and left the game running in the background, with no focus on the window, or falling asleep, is still there. So we are not doing an indefinite loop, and instead limiting this to 8 iterations. After 16 minutes, playtime would no longer be on autopilot.
		-- Of course, if the developer focuses on the server view (either in play solo or in server mode), then input events would be registered, and the activity would continue even after this loop ends.
		for i = 1, 8 do
			activityCallback()
			task.wait(121)
		end
	end
end


local util = require(script.Parent.util)
local module = {}

local projectSettingKey = `WakaTimeProject_{game.GameId}`
function module.setProjectName(plugin: Plugin, value: string)
	value = util.trimString(value)
	-- Setting the project name to "" is a valid state, used for marking that it should be purposefully not sent.
	plugin:SetSetting(projectSettingKey, value)
end

function module.getSavedProjectName(plugin: Plugin)
	return plugin:GetSetting(projectSettingKey)
end

function module.getProjectName(plugin: Plugin): string?
	local existingName = module.getSavedProjectName(plugin)
	if existingName then
		return existingName
	end

	if game.Name == "Server" or game.Name == "Game" then
		if game.GameId == 0 then
			return nil
		end
		return `{game.GameId}`
	end

	return game.Name
end

function module.getWakaTimeKey(plugin: Plugin)
	return plugin:GetSetting("WakaTimeKey") :: string?
end

function module.setWakaTimeKey(plugin: Plugin, value: string)
	plugin:SetSetting("WakaTimeKey", value)
end

function module.getWakaTimeUrl(plugin: Plugin)
	local value = plugin:GetSetting("WakaTimeUrl") :: string?
	return if value == nil or value == "" then "https://api.wakatime.com/api/v1" else value
end

function module.setWakaTimeUrl(plugin: Plugin, value: string)
	plugin:SetSetting("WakaTimeUrl", value)
end

function module.getShouldLogPlayTime(plugin: Plugin)
	local value = plugin:GetSetting("WakaTimeShouldLogPlayTime")
	return if value == nil then true else value
end

function module.setShouldLogPlayTime(plugin: Plugin, value: boolean)
	plugin:SetSetting("WakaTimeShouldLogPlayTime", value)
end

function module.getShouldLogEditTime(plugin: Plugin)
	local value = plugin:GetSetting("WakaTimeShouldLogEditTime")
	return if value == nil then true else value
end

function module.setShouldLogEditTime(plugin: Plugin, value: boolean)
	plugin:SetSetting("WakaTimeShouldLogEditTime", value)
end

function module.getShouldLogCodingTime(plugin: Plugin)
	local value = plugin:GetSetting("WakaTimeShouldLogCodingTime")
	return if value == nil then true else value
end

function module.setShouldLogCodingTime(plugin: Plugin, value: boolean)
	plugin:SetSetting("WakaTimeShouldLogCodingTime", value)
end

return module


local React = require(script.Parent.Packages.React)
local ReactRoblox = require(script.Parent.Packages.ReactRoblox)
local StudioComponents = require(script.Parent.Packages.StudioComponents)
local storageHandler = require(script.Parent.storageHandler)
local util = require(script.Parent.util)
local e = React.createElement
local module = {}

local function getIcon()
	return if settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground).R < 0.5
		then "rbxassetid://121239006179116"
		else "rbxassetid://70875562655977",
		"Settings"
end

local function HeaderText(props: {
	Text: string,
})
	return e(StudioComponents.Label, {
		Text = `<b>{props.Text}</b>`,
		RichText = true,
		TextXAlignment = Enum.TextXAlignment.Left,
		Size = UDim2.new(1, 0, 0, 20),
		TextColorStyle = Enum.StudioStyleGuideColor.BrightText,
	})
end

local function Checkbox(props: {
	Label: string,
	InitialValue: boolean,
	SetValue: (value: boolean) -> (),
})
	local state, setState = React.useState(props.InitialValue)

	return e(StudioComponents.Checkbox, {
		Label = props.Label,
		Value = state,
		OnChanged = function()
			local newValue = not state
			setState(newValue)
			props.SetValue(newValue)
		end,
	})
end

local function VerticalSpace(props: { Height: number })
	return e("Frame", {
		Size = UDim2.new(1, 0, 0, props.Height),
		BackgroundTransparency = 1,
		BorderSizePixel = 0,
	})
end

local function MainUI(props: {
	initialProject: string,
	setProject: (value: string) -> (),

	initialApiKey: string,
	setApiKey: (value: string) -> (),

	initialApiUrl: string,
	setApiUrl: (value: string) -> (),

	initialShouldLogPlayTime: boolean,
	setShouldLogPlayTime: (value: boolean) -> (),

	initialShouldLogEditTime: boolean,
	setShouldLogEditTime: (value: boolean) -> (),

	initialShouldLogCodingTime: boolean,
	setShouldLogCodingTime: (value: boolean) -> (),
})
	local projectText, setProjectText = React.useState(props.initialProject)
	local keyText, setKeyText = React.useState(props.initialApiKey)
	local urlText, setUrlText = React.useState(props.initialApiUrl)

	return e(StudioComponents.ScrollFrame, {
		PaddingLeft = UDim.new(0, 10),
		PaddingRight = UDim.new(0, 10),
		PaddingTop = UDim.new(0, 10),
		PaddingBottom = UDim.new(0, 10),
	}, {
		e(HeaderText, {
			Text = "Experience settings",
		}),

		e(StudioComponents.Label, {
			Text = "Project name",
			TextXAlignment = Enum.TextXAlignment.Left,
			Size = UDim2.new(1, 0, 0, 30),
		}),
		e(StudioComponents.TextInput, {
			Text = projectText,
			OnChanged = function(newText)
				setProjectText(newText)
				props.setProject(newText)
			end,
			ClearTextOnFocus = false,
			PlaceholderText = "Get it at wakatime.com/api-key",
		}),

		VerticalSpace({
			Height = 15,
		}),
		e(HeaderText, {
			Text = "Global settings",
		}),

		e(StudioComponents.Label, {
			Text = "WakaTime API URL",
			TextXAlignment = Enum.TextXAlignment.Left,
			Size = UDim2.new(1, 0, 0, 30),
			RichText = true,
		}),
		e(StudioComponents.TextInput, {
			Text = urlText,
			OnChanged = function(newText)
				setUrlText(newText)
				props.setApiUrl(newText)
			end,
			ClearTextOnFocus = false,
			PlaceholderText = "https://api.wakatime.com/api/v1"
		}),

		-- VerticalSpace({ Height = 10 }),

		e(StudioComponents.Label, {
			Text = `WakaTime API key{if #keyText > 0
				then ""
				else " <b>(Please paste your API key here)</b>"}`,
			TextXAlignment = Enum.TextXAlignment.Left,
			Size = UDim2.new(1, 0, 0, 30),
			RichText = true,
		}),
		e(StudioComponents.TextInput, {
			Text = keyText,
			OnChanged = function(newText)
				setKeyText(newText)
				props.setApiKey(newText)
			end,
			ClearTextOnFocus = false,
			PlaceholderText = "Get it from wakatime.com or use your key from alternative source",
		}),

		VerticalSpace({ Height = 10 }),

		e(Checkbox, {
			Label = "Log time in the editor",
			InitialValue = props.initialShouldLogEditTime,
			SetValue = props.setShouldLogEditTime,
		}),

		e(Checkbox, {
			Label = "Log time in playtesting",
			InitialValue = props.initialShouldLogPlayTime,
			SetValue = props.setShouldLogPlayTime,
		}),

		e(Checkbox, {
			Label = "Log time in the script editor",
			InitialValue = props.initialShouldLogCodingTime,
			SetValue = props.setShouldLogCodingTime,
		}),
	})
end

function module.init(plugin: Plugin)
	local settingsWidget = plugin:CreateDockWidgetPluginGui(
		"wakatime_widget",
		DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Right, false, false, 200, 300, 50, 50)
	)
	settingsWidget.Title = "WakaTime"

	local toolbar = plugin:CreateToolbar("WakaTime")
	local settingsButton =
		toolbar:CreateButton("wakatime_main", "Open the Roblox Studio WakaTime configuration.", getIcon())
	settings().Studio.ThemeChanged:Connect(function()
		settingsButton.Icon = getIcon()
	end)
	settingsButton.Click:Connect(function()
		settingsWidget.Enabled = not settingsWidget.Enabled
	end)
	settingsButton:SetActive(settingsWidget.Enabled)
	settingsWidget:GetPropertyChangedSignal("Enabled"):Connect(function()
		settingsButton:SetActive(settingsWidget.Enabled)
	end)

	local root = ReactRoblox.createRoot(settingsWidget)
	root:render(e(MainUI, {
		initialProject = storageHandler.getProjectName(plugin),
		setProject = function(value)
			storageHandler.setProjectName(plugin, value)
		end,

		initialApiKey = storageHandler.getWakaTimeKey(plugin) or "",
		setApiKey = function(value)
			storageHandler.setWakaTimeKey(plugin, value)
		end,

		initialApiUrl = storageHandler.getWakaTimeUrl(plugin) or "https://api.wakatime.com/api/v1",
		setApiUrl = function(value)
			storageHandler.setWakaTimeUrl(plugin, value)
		end,

		initialShouldLogPlayTime = storageHandler.getShouldLogPlayTime(plugin),
		setShouldLogPlayTime = function(value)
			storageHandler.setShouldLogPlayTime(plugin, value)
		end,

		initialShouldLogEditTime = storageHandler.getShouldLogEditTime(plugin),
		setShouldLogEditTime = function(value)
			storageHandler.setShouldLogEditTime(plugin, value)
		end,

		initialShouldLogCodingTime = storageHandler.getShouldLogCodingTime(plugin),
		setShouldLogCodingTime = function(value)
			storageHandler.setShouldLogCodingTime(plugin, value)
		end,
	}))
end

return module


local module = {}

function module.instanceToPath(instance: Instance): string
	local path

	local parent = instance
	while parent do
		local name = if parent == game then "game" else parent.Name
		path = if path then `{name}/{path}` else name
		parent = parent.Parent
	end

	return path
end

function module.trimString(s: string)
	return s:match("^%s*(.-)%s*$")
end

-- Thanks for amazing work of xDeltaXen (https://devforum.roblox.com/t/base64-encoding-and-decoding-in-lua/1719860)
function module.encode(data: string): string
    local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    return ((data:gsub('.', function(x)
        local r,b='',x:byte()
        for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end
        return r;
    end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
        if (#x < 6) then return '' end
        local c=0
        for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
        return b:sub(c+1,c+1)
    end)..({ '', '==', '=' })[#data%3+1])
end

return module


local ReplicatedStorage = game:GetService('ReplicatedStorage')

local jest = require('@pkg/@jsdotlua/jest')

local jestRoots = {
    ReplicatedStorage:FindFirstChild('TestTarget'),
}

local success, result = jest.runCLI(ReplicatedStorage, {}, jestRoots):await()

if not success then
    error(result)
end

task.wait(0.5)


local chroma = require('./chroma')

export type Color = chroma.Color
export type AsColor = chroma.AsColor
export type ColorBrewerName = chroma.ColorBrewerName
export type ColorSpace = chroma.ColorSpace
export type InterpolationMode = chroma.InterpolationMode
export type Cubehelix = chroma.Cubehelix
export type Scale = chroma.Scale

-- feel free to comment out anything to rollup
-- a smaller chroma.js built

-- io --> convert colors
require('./io/cmyk')
require('./io/css')
require('./io/gl')
require('./io/hcg')
require('./io/hex')
require('./io/hsi')
require('./io/hsl')
require('./io/hsv')
require('./io/lab')
require('./io/lch')
require('./io/named')
require('./io/num')
require('./io/rgb')
require('./io/temp')
require('./io/oklab')
require('./io/oklch')
-- deviation: add roblox Color3 support
if _G.LUA_ENV == 'roblox' then
    require('./io/roblox')
end

-- operators --> modify existing Colors
require('./ops/alpha')
require('./ops/clipped')
require('./ops/darken')
require('./ops/get')
require('./ops/luminance')
require('./ops/mix')
require('./ops/premultiply')
require('./ops/saturate')
require('./ops/set')

-- interpolators
require('./interpolator/rgb')
require('./interpolator/lrgb')
require('./interpolator/lab')
require('./interpolator/lch')
require('./interpolator/num')
require('./interpolator/hcg')
require('./interpolator/hsi')
require('./interpolator/hsl')
require('./interpolator/hsv')
require('./interpolator/oklab')
require('./interpolator/oklch')

-- generators -- > create new colors
chroma.average = require('./generator/average')
chroma.bezier = require('./generator/bezier')
chroma.blend = require('./generator/blend')
chroma.cubehelix = require('./generator/cubehelix')
chroma.mix = require('./generator/mix')
chroma.interpolate = require('./generator/mix')
chroma.random = require('./generator/random')
chroma.scale = require('./generator/scale')

-- other utility methods
chroma.analyze = require('./utils/analyze').analyze
chroma.contrast = require('./utils/contrast')
chroma.deltaE = require('./utils/delta-e')
chroma.distance = require('./utils/distance')
chroma.limits = require('./utils/analyze').limits
chroma.valid = require('./utils/valid')

-- scale
chroma.scales = require('./utils/scales')

-- colors
chroma.colors = require('./colors/w3cx11')
chroma.brewer = require('./colors/colorbrewer')

return chroma


export type Format = {
    -- cmyk
    cmyk: (...any) -> { number },
    -- css
    css: (css: string) -> { number }?,
    -- gl
    gl: (...any) -> { number },
    -- hcg
    hcg: (...any) -> { number },

    -- hex
    hex: (string) -> { number },
    -- hsi
    hsi: (...any) -> { number },
    -- hsl
    hsl: (...any) -> { number },
    -- hsv
    hsv: (...any) -> { number },

    -- lab
    lab: (...any) -> { number },
    -- lch
    lch: (...any) -> { number },
    hcl: (...any) -> { number },
    -- named
    named: (...any) -> { number },
    -- num
    num: (...any) -> { number },
    -- oklab
    oklab: (...any) -> { number },
    -- oklch
    oklch: (...any) -> { number },
    -- rgb
    rgb: (...any) -> { number },
    -- temp
    temperature: (number) -> { number },
    kelvin: (number) -> { number },
    temp: (number) -> { number },
    -- roblox
    roblox: (Color3) -> { number },
}

export type AutoDetect = { p: number, test: (...any) -> string? }

return {
    sorted = false,
    format = {} :: Format,
    autodetect = {} :: { AutoDetect },
}


local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local chroma = require('../../..')

local expect = jestGlobals.expect
local test = jestGlobals.test
local describe = jestGlobals.describe

if _G.LUA_ENV ~= 'roblox' then
    return
end

test('build a color from a Roblox Color3', function()
    expect(chroma(Color3.fromRGB(25, 50, 100)):rgb()).toEqual({ 25, 50, 100 })
end)

test('build a color with opacity from a Roblox Color3', function()
    expect(chroma.roblox(Color3.fromRGB(25, 200, 75), 0.5):rgba()).toEqual({ 25, 200, 75, 0.5 })
end)

describe('convert w3cx11 colors to Roblox', function()
    for name, hexCode in chroma.colors :: { [string]: string } do
        test(`convert '{name}' to Roblox Color3`, function()
            local color = chroma.hex(hexCode)
            expect(color:roblox()).toEqual(Color3.fromHex(hexCode))
        end)
    end
end)