local UserInputService = game:GetService("UserInputService")

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

export type source<T> = internal.source<T>
export type value<T> = T | source<T>
export type props = internal.props

type function _partial(object: type)
	local tag = object.tag

	if tag == "union" or tag == "intersection" then
		local result

		for _, component in object:components() do
			if component.tag == "nil" then continue end
			if result == nil then
				result = component
				continue
			end

			result = types.unionof(result, component)
		end

		return types.optional(result :: type)
	elseif tag == "table" then
		local result = types.newtable()

		for key, prop in object:properties() do
			if prop.read ~= prop.write then continue end
			result:setproperty(key, _partial(prop.read))
		end

		return result
	end

	return types.optional(object)
end
export type partial<T> = _partial<T>

type function _dissolve(object: type)
	local tag = object.tag

	if tag == "union" then
		local non_nil = {}
		for _, component in object:components() do
			if component.tag == "nil" then continue end
			table.insert(non_nil, component)
		end

		if #non_nil <= 1 then
			return non_nil[1] or types.never
		else
			return types.unionof(unpack(non_nil))
		end
	elseif tag == "table" then
		local result = types.newtable()

		for key, prop in object:properties() do
			local unpartialedType = _dissolve(prop.read)
			result:setproperty(key, unpartialedType)
		end

		return result
	end

	return object
end
export type dissolve<T> = _dissolve<T>

local kit = {
	gen_id = internal.gen_id,
	throw = internal.throw,
	assert = internal.assert,
	read = internal.read,
}

function kit.get_tick()
	return internal._tick
end

function kit.create(class_name: string)
	return function(props: internal.props)
		local id = internal.gen_id(2, "create")
		return internal.create(class_name, id)(props)
	end
end

function kit.gen_id(ignore_levels: number?, prefix: string?)
	ignore_levels = (ignore_levels or 0) + 1
	return internal.gen_id(ignore_levels, prefix)
end

function kit.source<T>(default: T): source<T>
	local id = internal.gen_id(1, "create")
	return internal.source(default, id)
end

function kit.from<T>(value: value<T>): source<T>
	if typeof(value) == "function" then
		return value
	end
	return kit.source(value)
end

function kit.source_changed<T>(value: source<T>): boolean
	local val: () -> any = value
	local last_value = kit.source( val() )
	if last_value() ~= val() then
		last_value(val())
		return true
	end
	return false
end

function kit.force<T>(value: source<T>): T
	return kit.read(value)
end

kit.derive = function<T>(fn)
	local src = internal.source()
	local value = internal.read(fn) :: T & any
	if value ~= src() then src(value) end
	return src
end ::
	& (<T>(fn: () -> T) -> source<T>)
	& (<T>(value: T) -> source<T>)

function kit.default<T>(value: any?, default: T): T
	local ref_type = typeof(default)
	local value_type = typeof(value)

	if value_type ~= ref_type then return default end
	if ref_type ~= "table" and value_type ~= "table" then
		return if value ~= nil then value else default
	elseif value_type ~= "table" then return default end

	local result = value :: any
	for key, value in default :: any do
		if result[key] ~= nil then continue end
		result[key] = value
	end
	return result
end

function kit.once<A...>(fn: (A...) -> (), ...: A...)
	local started = internal.source(false)
	if started() == false then
		fn(...)
		started(true)
	end
end

function kit.changed<T>(property: string, fn: (self: Instance, T) -> ())
	return {
		Changed = function(self: Instance, prop: string)
			if prop ~= property then return end
			fn( self, internal.read(property) )
		end,
	}
end

function kit.open_scope(target: Instance)
	internal.push_parent(target)
end

function kit.close_scope(depth: number?)
	internal.pop_parent(depth)
end

function kit.conditional<T, R, A...>(callback: value<T> | T, fn: (A...) -> R, ...: A...): R
	if internal.read(callback) ~= true then return nil end
	return fn(...)
end

function kit.mount(object: Instance | () -> Instance, target: (Instance | "root")?)
	if typeof(object) == "function" then
		internal.mount(object(), target)
	else
		internal.mount(object, target)
	end
end

function kit.created(fn: (self: Instance) -> ())
	return function(inst)
		local id = internal._instance_to_id[inst]
		local run = internal._vdom[id] ~= nil and internal._last_vdom[id] == nil
		if run then fn( inst ) end
	end
end

function kit.destroyed(fn: (self: Instance) -> ())
	return { Destroying = fn, } :: any
end

function kit.apply(instance: Instance, properties: internal.props)
	return internal.apply(instance, properties)
end

function kit.event(): { fire: () -> (), fired: () -> boolean }
	local last_updated = internal.source(-1)
	return {
		fire = function()
			last_updated(internal._tick)
		end,
		fired = function()
			return last_updated() == internal._tick
		end
	}
end

function kit.keys_pressed(...: Enum.KeyCode | string): boolean
	for _, key: Enum.KeyCode in { ... } do
		if key == "any" then return true end
		if key == "none" then return false end

		if typeof(key) == "string" then
			key = Enum.KeyCode:FromName(key) :: Enum.KeyCode
		end
		if key == Enum.KeyCode.Unknown then continue end

		if UserInputService:IsKeyDown(key :: Enum.KeyCode) then
			return true
		end
	end
	return false
end

function kit.mouse_clicked(mouse_button: string): boolean
	local mouse = Enum.UserInputType:FromName(`Mouse{mouse_button}`)
	return UserInputService:IsMouseButtonPressed(mouse :: Enum.UserInputType)
end

kit.push_id = internal.push_id
kit.pop_id = internal.pop_id

return kit


local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")

local internal = require("../internal")
local root = require("./rootkit")
local color4 = require("./color4")

type source<T> = internal.source<T>
type value<T> = source<T> | T
type color4 = color4.color4

export type scope<T> = (scope: T) -> ()
export type state<T={}> = T
export type scoped_state<T={}> = state<T> & { with: (scope: scope<T>) -> () }

export type fillDirection = "Vertical" | "Horizontal"
export type mouseButton = "Button1" | "Button2" | "Button3"
export type flexMode = "Fill" | "Grow" | "Shrink"
export type widgetConstructor<T, A...> = (A...) -> state<T>

local function get_root()
	assert(internal._target, "no root found")

	local ui_root = internal._target:FindFirstChild("ui_root")
	if not ui_root then
		ui_root = Instance.new("Folder")
		ui_root.Name = "ui_root"
		ui_root.Parent = internal._target
	end
	return ui_root
end

local kit = {
	drag_target = nil,
	window_amount = 0,
}

function kit.mouse_position()
	local pos = UserInputService:GetMouseLocation()
	return vector.create(pos.X, pos.Y)
end

function kit.inset()
	local inset = GuiService:GetGuiInset()
	return vector.create(inset.X, inset.Y)
end

function kit.is_mouse_over(object: GuiObject)
	local inset = GuiService:GetGuiInset()
	local position = kit.mouse_position()
	local player = Players.LocalPlayer :: Player
	local playergui = player:FindFirstChildOfClass("PlayerGui")

	if not playergui then return false end

	local px, py = position.x - inset.X, position.y - inset.Y
	local instances = playergui:GetGuiObjectsAtPosition(px, py)
	local clicked_object = instances[1]

	if object:IsAncestorOf(clicked_object) or object == clicked_object then
		return true
	else
		return false
	end
end

function kit.clicked(mouse_button: value<mouseButton>, callback: (self: GuiObject, x: number, y: number) -> ())
	mouse_button = internal.read(mouse_button) :: mouseButton

	local input_type = Enum.UserInputType:FromName(`Mouse{mouse_button}`) or Enum.UserInputType.MouseButton1
	return {
		InputEnded = function(self, input: InputObject)
			if input.UserInputType ~= input_type then return end
			if not kit.is_mouse_over(self) then return end
			callback(self, input.Position.X, input.Position.Y)
		end,
	}
end

function kit.apply_color(property: value<string>, color: value<color4>)
	local key = internal.read(property) :: string
	local value = internal.read(color) :: color4

	local colorIndex = `{key}Color3`
	local transparencyIndex = `{key}Transparency`

	return {
		[transparencyIndex] = color4.read_alpha(value),
		[colorIndex] = color4.read_color3(value) :: any,
	}
end

function kit.list_layout(direction: fillDirection, padding: number)
	local fill_direction = Enum.FillDirection:FromName(direction)
	local vertical_alignment
	local horiztonal_alignment

	if direction == "Vertical" then
		vertical_alignment = Enum.VerticalAlignment.Top
		horiztonal_alignment = Enum.HorizontalAlignment.Left
	elseif direction == "Horizontal" then
		vertical_alignment = Enum.VerticalAlignment.Center
		horiztonal_alignment = Enum.HorizontalAlignment.Left
	end

	return internal.create("UIListLayout") {
		FillDirection = fill_direction,
		SortOrder = Enum.SortOrder.LayoutOrder,
		VerticalAlignment = vertical_alignment,
		HorizontalAlignment = horiztonal_alignment,
		Padding = UDim.new(0, padding),
	}
end

function kit.corner(radius: number)
	return internal.create("UICorner") {
		CornerRadius = UDim.new(0, radius),
	}
end

function kit.circle()
	return internal.create("UICorner") {
		CornerRadius = UDim.new(1, 0),
	}
end

function kit.flexitem(name: flexMode)
	return internal.create("UIFlexItem") {
		FlexMode = Enum.UIFlexMode:FromName(name)
	}
end

function kit.constrain(min_x: number?, min_y: number?, max_x: number?, max_y: number?)
	return internal.create("UISizeConstraint") {
		MinSize = Vector2.new(min_x or 0, min_y or 0),
		MaxSize = Vector2.new(max_x or math.huge, max_y or math.huge),
	}
end

function kit.aspect_ratio(x: number, y: number?)
	return internal.create("UIAspectRatioConstraint") {
		AspectRatio = x / (y or 1),
		DominantAxis = Enum.DominantAxis.Height
	}
end

function kit.hovered(fn: (boolean) -> ())
	return function(self)
		local hovered = kit.is_mouse_over(self)
		fn(hovered)
	end
end

kit.padding = function(...)
	local t,b,l,r
	local args = { ... }

	if #args == 1 then
		t, b, l, r = args[1], args[1], args[1], args[1]
	elseif #args == 2 then
		t, b, l, r = args[1], args[1], args[2], args[2]
	elseif #args == 4 then
		t, b, l, r = args[1], args[2], args[3], args[4]
	else
		internal.throw("Invalid number of arguments", 0)
	end

	return internal.create("UIPadding") {
		PaddingTop = UDim.new(0, t),
		PaddingBottom = UDim.new(0, b),
		PaddingLeft = UDim.new(0, l),
		PaddingRight = UDim.new(0, r),
	}
end ::
& ((offset: number) -> UIPadding)
& ((vertical: number, horizontal: number) -> UIPadding)
& ((top: number, bottom: number, left: number, right: number) -> UIPadding)

function kit.widget<C, T, A...>(name: string, constructor: widgetConstructor<T, A...> & C): C
	local widget_id = internal.gen_id(1, `widget::{name}`)

	return function(...: A...)
		internal.push_id(widget_id)
		local object: any = constructor(...)
		internal.pop_id()
		return object
	end :: any
end

function kit.start_scope<T>(target: Instance, state: (T & any)?): scoped_state<T>
	local st = if type(state) == "table" then state else {} :: T
	(st::any).with = function(fn)
		return internal.scope(function() fn(st) end)
	end
	internal.push_parent(target)
	return st
end

function kit.mount(object: Instance | () -> Instance, target: (Instance | "root")?)
	if internal._parent_stack_length == 0 or target == "root" then
		target = get_root()
	end
	return root.mount(object, target)
end

return kit


local config = require("../config")
local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	clicked: source<boolean>,
	right_clicked: source<boolean>,
	middle_clicked: source<boolean>,

	hovered: source<boolean>,
}
export type arguments = {
	text: string,
	padding: number,
}

return ui.widget("button", function(arguments: partial<arguments>?)
	local args = root.default(arguments, { text = "Button", padding = config.padding }) :: arguments
	local last_clicked = root.source(-1)
	local last_right_clicked = root.source(-1)
	local last_middle_clicked = root.source(-1)

	local state: state = {
		hovered = root.source(false),
		clicked = root.derive(function() return last_clicked() >= root.get_tick() end),
		right_clicked = root.derive(function() return last_right_clicked() >= root.get_tick() end),
		middle_clicked = root.derive(function() return last_middle_clicked() >= root.get_tick() end),
	}

	local clicked = root.derive(function()
		return state.clicked() or state.right_clicked() or state.middle_clicked()
	end)

	ui.mount(function()
		return root.create("TextLabel") {
			Active = true,
			AutomaticSize = Enum.AutomaticSize.XY,

			Text = args.text,
			ui.padding(args.padding),

			paint.text(),
			paint.button(clicked, state.hovered),

			ui.hovered(state.hovered),
			ui.clicked("Button1", function()
				last_clicked( root.get_tick() )
			end),
			ui.clicked("Button2", function()
				last_right_clicked( root.get_tick() )
			end),
			ui.clicked("Button3", function()
				last_middle_clicked( root.get_tick() )
			end)
		}
	end)

	return state
end)


local config = require("../config")
local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")

local row = require("./format/row")
local text = require("./text")

local assets = config.assets

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	hovered: source<boolean>,
	checked: source<boolean>,
	clicked: source<boolean>,
}
export type arguments = {
	text: string?
}

return ui.widget("checkbox", function(arguments: partial<arguments>?)
	local args = root.default(arguments, {}) :: arguments
	local last_click = root.source( root.get_tick() )

	local state: state = {
		hovered = root.source(false),
		checked = root.source(false),
		clicked = root.derive(function()
			return last_click() >= root.get_tick()
		end),
	}

	row().with(function()
		local checkbox = root.create("Frame") {
			Active = true,
			Size = UDim2.fromOffset(18, 18),

			root.create("ImageLabel") {
				BackgroundTransparency = 1,
				Image = assets.checkMark,
				Size = UDim2.fromScale(1, 1),

				ImageTransparency = function()
					return if state.checked() then 0 else 1
				end,

				paint.image(),
			},

			paint.button(state.clicked, state.hovered),

			ui.padding(config.padding / 2),
			ui.hovered(state.hovered),
			ui.clicked("Button1", function()
				state.checked( not state.checked() )
				last_click( root.get_tick() )
			end),
		}

		if args.text then
			text { text = args.text }
		end

		ui.mount(checkbox)
	end)
	return state
end)


local config = require("../../config")
local input = require("../../utils/input")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")

type source<T> = root.source<T>
type value<T> = root.value<T>
type partial<T> = root.partial<T>

export type state = {
    value: source<Color3>,
    hovered: source<boolean>,
    changed: source<boolean>,
}
export type arguments = {
    value: value<Color3>,
    text: string,
    increment: number,

    noPreview: boolean?,
    hsv: boolean?,
}

local DEFAULT_MIN = 0
local DEFAULT_MAX = 255
local DEFAULT_VALUE = Color3.new(1, 1, 1)

return {
    base = ui.widget("color3_input", function(arguments: partial<arguments>?)
        local args = root.default(arguments, {
            text = "color3 input", value = DEFAULT_VALUE,
            increment = config.default_value_increment
        }) :: arguments

        local state: state = {
            hovered = root.source(false),
            changed = root.source(false),
            value = root.from(args.value) :: source<Color3>,
        }

        input.row({ fixed_size = true }).with(function()
            local ctrl_pressed = root.keys_pressed("LeftControl", "RightControl")
            local increment = args.increment
            if ctrl_pressed then increment *= 100 end

            local inputs = input.create_data_type_inputs(state.value, {
                min = DEFAULT_MIN,
                max = DEFAULT_MAX,
                increment = increment,
                id_suffix = if args.hsv then "_HSV" else "",

                has_color_preview = not args.noPreview,
            })

            state.changed(inputs.changed())
            input.create_input_label(args.text)
        end)

        return state
    end)
}


local input = require("../../utils/input")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")

type source<T> = root.source<T>
type value<T> = root.value<T>
type partial<T> = root.partial<T>

export type state = {
    value: source<number>,
    hovered: source<boolean>,
    changed: source<boolean>,
}
export type arguments = {
    value: value<number>,

    text: string,
    min: number,
    max: number,
    increment: number,

    noButtons: boolean?,
}

local DEFAULT_MIN = -1e6
local DEFAULT_MAX = 1e6
local DEFAULT_VALUE = 0

return {
    base = ui.widget("number_input", function(arguments: partial<arguments>?)
        local args = root.default(arguments, {
            text = "number input", min = DEFAULT_MIN, max = DEFAULT_MAX,
            value = DEFAULT_VALUE, increment = 1
        }) :: arguments

        local state: state = {
            hovered = root.source(false),
            changed = root.source(false),
            value = root.from(args.value) :: source<number>,
        }

        input.row({ fixed_size = true }).with(function()
            local ctrl_pressed = root.keys_pressed("LeftControl", "RightControl")
            local increment = args.increment
            if ctrl_pressed then increment *= 100 end

            local inputs = input.create_data_type_inputs(state.value, {
                min = args.min,
                max = args.max,
                increment = increment,
                decimal_places = input.get_decimal_places(increment),

                has_buttons = not args.noButtons,
            })

            state.changed(inputs.changed())
            input.create_input_label(args.text)
        end)

        return state
    end)
}


local config = require("../../config")
local input = require("../../utils/input")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")

type source<T> = root.source<T>
type value<T> = root.value<T>
type partial<T> = root.partial<T>

export type state = {
    value: source<Rect>,
    hovered: source<boolean>,
    changed: source<boolean>,
}
export type arguments = {
    value: value<Rect>,
    increment: number,

    text: string,
    min: number,
    max: number,
}

local DEFAULT_MIN = -1e6
local DEFAULT_MAX = 1e6
local DEFAULT_VALUE = Rect.new(0, 0, 0, 0)

return {
    base = ui.widget("rect_input", function(arguments: partial<arguments>?)
        local args = root.default(arguments, {
            text = "rect input", min = DEFAULT_MIN, max = DEFAULT_MAX,
            value = DEFAULT_VALUE, increment = config.default_value_increment
        }) :: arguments

        local state: state = {
            hovered = root.source(false),
            changed = root.source(false),
            value = root.from(args.value) :: source<Rect>,
        }

        input.row({ fixed_size = true }).with(function()
            local increment = config.default_value_increment
            local inputs = input.create_data_type_inputs(state.value, {
                min = args.min,
                max = args.max,
                increment = increment,
                decimal_places = input.get_decimal_places(increment),
            })

            state.changed(inputs.changed())
            input.create_input_label(args.text)
        end)

        return state
    end)
}


local config = require("../../config")
local input = require("../../utils/input")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")

type source<T> = root.source<T>
type value<T> = root.value<T>
type partial<T> = root.partial<T>

export type state = {
    value: source<UDim>,
    hovered: source<boolean>,
    changed: source<boolean>,
}
export type arguments = {
    value: value<UDim>,
    increment: number,

    text: string,
    min: number,
    max: number,
}

local DEFAULT_MIN = -1e6
local DEFAULT_MAX = 1e6
local DEFAULT_VALUE = UDim.new(0, 0)

return {
    base = ui.widget("udim_input", function(arguments: partial<arguments>?)
        local args = root.default(arguments, {
            text = "udim input", min = DEFAULT_MIN, max = DEFAULT_MAX,
            value = DEFAULT_VALUE, increment = config.default_value_increment
        }) :: arguments

        local state: state = {
            hovered = root.source(false),
            changed = root.source(false),
            value = root.from(args.value) :: source<UDim>,
        }

        input.row({ fixed_size = true }).with(function()
            local increment = config.default_value_increment
            local inputs = input.create_data_type_inputs(state.value, {
                min = args.min,
                max = args.max,
                increment = increment,
            })

            state.changed(inputs.changed())
            input.create_input_label(args.text)
        end)

        return state
    end)
}


local config = require("../../config")
local input = require("../../utils/input")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")

type source<T> = root.source<T>
type value<T> = root.value<T>
type partial<T> = root.partial<T>

export type state = {
    value: source<UDim2>,
    hovered: source<boolean>,
    changed: source<boolean>,
}
export type arguments = {
    value: value<UDim2>,
    increment: number,

    text: string,
    min: number,
    max: number,
}

local DEFAULT_MIN = -1e6
local DEFAULT_MAX = 1e6
local DEFAULT_VALUE = UDim2.new(0, 0, 0, 0)

return {
    base = ui.widget("udim2_input", function(arguments: partial<arguments>?)
        local args = root.default(arguments, {
            text = "udim2 input", min = DEFAULT_MIN, max = DEFAULT_MAX,
            value = DEFAULT_VALUE, increment = config.default_value_increment
        }) :: arguments

        local state: state = {
            hovered = root.source(false),
            changed = root.source(false),
            value = root.from(args.value) :: source<UDim2>,
        }

        input.row({ fixed_size = true }).with(function()
            local increment = config.default_value_increment
            local inputs = input.create_data_type_inputs(state.value, {
                min = args.min,
                max = args.max,
                increment = increment,
            })

            state.changed(inputs.changed())
            input.create_input_label(args.text)
        end)

        return state
    end)
}


local config = require("../../config")
local input = require("../../utils/input")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")

type source<T> = root.source<T>
type value<T> = root.value<T>
type partial<T> = root.partial<T>

export type state = {
    value: source<Vector2>,
    hovered: source<boolean>,
    changed: source<boolean>,
}
export type arguments = {
    value: value<Vector2>,
    increment: number,

    text: string,
    min: number,
    max: number,
}

local DEFAULT_MIN = -1e6
local DEFAULT_MAX = 1e6
local DEFAULT_VALUE = Vector2.zero

return {
    base = ui.widget("vector2_input", function(arguments: partial<arguments>?)
        local args = root.default(arguments, {
            text = "vector2 input", min = DEFAULT_MIN, max = DEFAULT_MAX,
            value = DEFAULT_VALUE, increment = config.default_value_increment
        }) :: arguments

        local state: state = {
            hovered = root.source(false),
            changed = root.source(false),
            value = root.from(args.value) :: source<Vector2>,
        }

        input.row({ fixed_size = true }).with(function()
            local increment = config.default_value_increment
            local inputs = input.create_data_type_inputs(state.value, {
                min = args.min,
                max = args.max,
                increment = increment,
            })

            state.changed(inputs.changed())
            input.create_input_label(args.text)
        end)

        return state
    end)
}


local config = require("../../config")
local input = require("../../utils/input")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")

type source<T> = root.source<T>
type value<T> = root.value<T>
type partial<T> = root.partial<T>

export type state = {
    value: source<Vector3 | vector>,
    hovered: source<boolean>,
    changed: source<boolean>,
}
export type arguments = {
    value: value<Vector3 | vector>,
    increment: number,

    text: string,
    min: number,
    max: number,
}

local DEFAULT_MIN = -1e6
local DEFAULT_MAX = 1e6
local DEFAULT_VALUE = Vector3.zero

return {
    base = ui.widget("vector3_input", function(arguments: partial<arguments>?)
        local args = root.default(arguments, {
            text = "vector3 input", min = DEFAULT_MIN, max = DEFAULT_MAX,
            value = DEFAULT_VALUE, increment = config.default_value_increment
        }) :: arguments

        local state: state = {
            hovered = root.source(false),
            changed = root.source(false),
            value = root.from(args.value) :: source<Vector3 | vector>,
        }

        input.row({ fixed_size = true }).with(function()
            local increment = config.default_value_increment
            local inputs = input.create_data_type_inputs(state.value, {
                min = args.min,
                max = args.max,
                increment = increment,
            })

            state.changed(inputs.changed())
            input.create_input_label(args.text)
        end)

        return state
    end)
}


local config = require("../config")
local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")

local row = require("./format/row")
local text = require("./text")
local assets = config.assets

type source<T> = root.source<T>
type value<T> = root.value<T>
type partial<T> = root.partial<T>

export type state = {
	opened: source<boolean>,
	hovered: source<boolean>,
}
export type arguments = {
    text: string,
    index: value<any>,
    noButton: boolean?,
    noPreview: boolean?,
}

return ui.widget("dropdown", function(arguments: partial<arguments>?)
	local args = root.default(arguments, { text = "dropdown" }) :: arguments
    local clicked = root.event()

	local state: state = {
		opened = root.source(false),
		hovered = root.source(false),
	}

    local content_size = config.textSize
    local size = content_size + config.padding * 2
    local preview = root.create("TextLabel") {
        Text = tostring(root.read(args.index)),
        Size = UDim2.new(config.dropdown_width, 0, 0, size),

        ui.padding(config.padding),
        ui.hovered(state.hovered),
        ui.constrain(100, nil, nil, nil),

        paint.text(),
        paint.dropdown(state.hovered),

        ui.clicked("Button1", function()
            state.opened(not state.opened())
            clicked.fire()
        end),
    } :: TextLabel

    local button = root.create("Frame") {
        AnchorPoint = Vector2.new(1, 0.5),

        Position = UDim2.new(1, config.padding, 0.5, 0),
        Size = function()
            local size = preview.AbsoluteSize
            return UDim2.fromOffset(size.Y, size.Y)
        end,

        paint.button(state.hovered),

        root.create("ImageLabel") {
            AnchorPoint = Vector2.new(0.5, 0.5),
            BackgroundTransparency = 1,

            Position = UDim2.fromScale(0.5, 0.5),
            Size = UDim2.fromOffset(content_size, content_size),

            Image = assets.downPointingTriangle,

            paint.image(),
        },
    }

    local contents = root.create("Frame") {
        AutomaticSize = Enum.AutomaticSize.Y,
        Size = UDim2.new(0, preview.AbsoluteSize.X, 0, 0),

        Visible = state.opened,
        Position = function()
            local px = preview.AbsolutePosition.X
            local py = preview.AbsolutePosition.Y
            local sy = preview.AbsoluteSize.Y

            return UDim2.fromOffset(px, py + sy)
        end,

        paint.tooltip(),

        ui.padding(config.padding),
        ui.list_layout("Vertical", 0),
    } :: Frame

    local timer = root.source(-1)
    local delay = 2

    if state.opened() and root.mouse_clicked("Button1") then
        timer( root.get_tick() + delay )
    end

    if timer() == root.get_tick() then
        state.opened(false)
        timer(-1)
    end

    row({ fixed_size = true }).with(function()
        ui.mount(button, preview)
        ui.mount(preview)

        text { text = args.text }
    end)
    ui.mount(contents, "root")
    return ui.start_scope(contents, state)
end)


local config = require("../../config")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type arguments = {
	align: "Left" | "Center" | "Right",
	padding: number,
}

return ui.widget("group", function(arguments: root.partial<arguments>?)
	local args = root.default(arguments, { align = "Left", padding = config.spacing }) :: arguments

	local alignment = Enum.HorizontalAlignment:FromName(args.align) :: Enum.HorizontalAlignment
	local list = ui.list_layout("Vertical", args.padding) :: UIListLayout
	list.HorizontalAlignment = alignment

	local object = root.create("Frame") {
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 0, 0),

		list,
	}

	ui.mount(object)
	return ui.start_scope(object)
end)


local config = require("../../config")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")
local paint = require("../../utils/paint")

local assets = config.assets

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	open: source<boolean>,
    clicked: source<boolean>,
    hovered: source<boolean>,
}
export type arguments = {
	text: string,
}

local function header(args: arguments, state: state)
    local content_size = config.textSize
    local clicked = root.event()
    state.clicked(clicked.fired())

    return root.create("Frame") {
        AutomaticSize = Enum.AutomaticSize.Y,
        Size = UDim2.new(1, 0, 0, content_size),
        LayoutOrder = 1,

        paint.header(state.clicked, state.hovered),
        ui.hovered(state.hovered),
        ui.padding(config.padding),
        ui.list_layout("Horizontal", config.padding),

        ui.clicked("Button1", function()
            state.open(not state.open())
            clicked.fire()
        end),

        root.create("ImageLabel") {
            Active = true,
            BackgroundTransparency = 1,
            Size = UDim2.fromOffset(content_size, math.floor(content_size * 0.7)),
            paint.image(),

            Image = function()
                return
                    if state.open() then assets.downPointingTriangle
                    else assets.rightPointingTriangle
            end,
        },
        root.create("TextLabel") {
            AutomaticSize = Enum.AutomaticSize.XY,
            BackgroundTransparency = 1,

            Text = args.text,
            paint.text(),
        },
    }
end

local function contents(args: arguments, state: state)
    return root.create("Frame") {
        AutomaticSize = Enum.AutomaticSize.Y,
        LayoutOrder = 2,
        Size = UDim2.fromScale(1, 0),
        Visible = state.open,

        paint.no_background(),
        ui.padding(config.padding / 2),
        ui.list_layout("Vertical", config.spacing),
    }
end

return ui.widget("header", function(arguments: partial<arguments>?)
	local args = root.default(arguments, {}) :: arguments

	local state: state = {
        clicked = root.source(false),
        hovered = root.source(false),
		open = root.source(false),
	}

    local contents = contents(args, state)
    local main = root.create("Frame") {
        AutomaticSize = Enum.AutomaticSize.Y,
        Size = UDim2.fromScale(1, 0),

        contents,
        header(args, state),

        ui.list_layout("Vertical", config.padding),
        paint.no_background(),
    }

    ui.mount(main)
	return ui.start_scope(contents, state)
end)


local config = require("../../config")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type arguments = {
    indent: number,
    padding: number,
}

return ui.widget("indent", function(arguments: root.partial<arguments>?)
    local args = root.default(arguments, { padding = 0, indent = 25 }) :: arguments

    local indent = root.create("Frame") {
        AutomaticSize = Enum.AutomaticSize.XY,
        BackgroundTransparency = 1,
        Size = UDim2.new(0, 0, 0, 0),

        ui.padding(0, 0, args.indent, 0),
        ui.list_layout("Vertical", config.spacing),
    }

    ui.mount(indent)
    return ui.start_scope(indent)
end)


local config = require("../../config")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type arguments = {
	height: number,
	padding: number,
	fixed_size: boolean,
}

return ui.widget("row", function(arguments: root.partial<arguments>?)
	local args = root.default(arguments, { height = config.rowHeight, padding = config.spacing }) :: arguments

	local mode = if args.fixed_size then "Y" else "XY"
	local object = root.create("Frame") {
		AutomaticSize = Enum.AutomaticSize:FromName(mode),
		BackgroundTransparency = 1,
		Size = function()
			local scale = args.fixed_size and 1 or 0
			return UDim2.new(scale, 0, 0, args.height)
		end,

		ui.list_layout("Horizontal", args.padding),
	}

	ui.mount(object)
	return ui.start_scope(object)
end)


local config = require("../../config")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")
local paint = require("../../utils/paint")

local assets = config.assets

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	open: source<boolean>,
    clicked: source<boolean>,
    hovered: source<boolean>,
}
export type arguments = {
	text: string,
}

local function header(args: arguments, state: state)
    local content_size = config.textSize
    local clicked = root.event()
    state.clicked(clicked.fired())

    return root.create("Frame") {
        AutomaticSize = Enum.AutomaticSize.X,
        Size = UDim2.new(0, 0, 0, content_size),
        LayoutOrder = 1,

        paint.no_background(),
        ui.hovered(state.hovered),
        ui.padding(0, 0, config.padding / 2, 0),
        ui.list_layout("Horizontal", config.padding),

        ui.clicked("Button1", function()
            state.open(not state.open())
            clicked.fire()
        end),

        root.create("ImageLabel") {
            Active = true,
            BackgroundTransparency = 1,
            Size = UDim2.fromOffset(content_size, math.floor(content_size * 0.7)),
            paint.image(),

            Image = function()
                return
                    if state.open() then assets.downPointingTriangle
                    else assets.rightPointingTriangle
            end,
        },
        root.create("TextLabel") {
            AutomaticSize = Enum.AutomaticSize.XY,
            BackgroundTransparency = 1,

            Text = args.text,
            paint.text(),
        },
    }
end

local function contents(args: arguments, state: state)
    return root.create("Frame") {
        AutomaticSize = Enum.AutomaticSize.Y,
        LayoutOrder = 2,
        Size = UDim2.fromScale(1, 0),
        Visible = state.open,

        paint.no_background(),
        ui.padding(config.padding, 0, 21, 0),
        ui.list_layout("Vertical", config.spacing),
    }
end

return ui.widget("section", function(arguments: partial<arguments>?)
	local args = root.default(arguments, {}) :: arguments

	local state: state = {
        clicked = root.source(false),
        hovered = root.source(false),
		open = root.source(false),
	}

    local contents = contents(args, state)
    local main = root.create("Frame") {
        AutomaticSize = Enum.AutomaticSize.Y,
        Size = UDim2.fromScale(1, 0),

        contents,
        header(args, state),

        ui.list_layout("Vertical", config.padding),
        paint.no_background(),
    }

    ui.mount(main)
	return ui.start_scope(contents, state)
end)


local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")
local paint = require("../../utils/paint")

type partial<T> = root.partial<T>
type source<T> = root.source<T>

export type arguments = { text: string? }

return ui.widget("seperator", function(arguments: partial<arguments>?)
	local args = root.default(arguments, {}) :: arguments
	local has_text = args.text ~= nil
	local thickness = 1

	ui.mount(function()
		return root.create("Frame") {
			AutomaticSize = Enum.AutomaticSize.Y,
			BackgroundTransparency = 1,
			Size = UDim2.new(1, 0, 0, 0),

			ui.padding(5, 0),
			ui.list_layout("Horizontal", 0),

			-- left
			root.create("Frame") {
				Size = UDim2.new(0, 10, 0, thickness),
				LayoutOrder = 1,

				paint.seperator(),
			},

			-- text
			root.create("TextLabel") {
				AutomaticSize = Enum.AutomaticSize.X,
				LayoutOrder = 2,

				Text = root.default(args.text, ""),

				paint.text(),
				root.conditional(has_text, function()
					return ui.padding(0, 3, 5, 5)
				end),
			},

			-- right
			root.create("Frame") {
				LayoutOrder = 3,
				Size = UDim2.new(0, 0, 0, thickness),

				paint.seperator(),
				ui.flexitem("Grow"),
			}
		}
	end)

	return {}
end)


local config = require("../config")
local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")

local assets = config.assets

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	hovered: source<boolean>,
}
export type arguments = {
	image_id: string,
	padding: number,
	size: Vector2,
	rect: Rect?,
	scale_type: ("Fit" | "Crop" | "Fill" | "Stretch"),
	resample_mode: ("Default" | "Pixelated"),
	tile_size: Vector2?,
	slice_center: Vector2?,
	slice_scale: number?,
}

return ui.widget("image", function(arguments: partial<arguments>?)
	local args = root.default(arguments, {
		image_id = assets.placeholderImage, padding = config.padding,
		size = Vector2.new(32, 32),
		scale_type = "Fit",
		resample_mode = "Default",
	}) :: arguments

	local state: state = {
		hovered = root.source(false),
	}

	ui.mount(function()
		return root.create("ImageLabel") {
			Size = UDim2.fromOffset(args.size.X, args.size.Y),

			Image = args.image_id,
			ImageRect = args.rect,
			ImageRectOffset = args.rect and Vector2.new(args.rect.Min.X, args.rect.Min.Y) or nil,
			ImageRectSize = args.rect and Vector2.new(args.rect.Max.X - args.rect.Min.X, args.rect.Max.Y - args.rect.Min.Y) or nil,
			ScaleType = Enum.ScaleType:FromName(args.scale_type),
			ResampleMode = args.resample_mode and Enum.ResamplerMode:FromName(args.resample_mode) or nil,
			TileSize = args.tile_size,
			SliceCenter = args.slice_center,
			SliceScale = args.slice_scale,

			paint.image(),

			ui.padding(args.padding),
			ui.hovered(state.hovered),
		}
	end)

	return state
end)


local config = require("../config")
local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")

local assets = config.assets

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	clicked: source<boolean>,
	right_clicked: source<boolean>,
	middle_clicked: source<boolean>,

	hovered: source<boolean>,
}
export type arguments = {
	image_id: string,
	padding: number,
	size: Vector2,
	rect: Rect?,
	scale_type: ("Fit" | "Crop" | "Fill" | "Stretch"),
	resample_mode: ("Default" | "Pixelated"),
	tile_size: Vector2?,
	slice_center: Vector2?,
	slice_scale: number?,
}

return ui.widget("image_button", function(arguments: partial<arguments>?)
	local args = root.default(arguments, {
		image_id = assets.placeholderImage, padding = config.padding,
		size = Vector2.new(32, 32),
		scale_type = "Fit",
		resample_mode = "Default",
	}) :: arguments
	local last_clicked = root.source(-1)
	local last_right_clicked = root.source(-1)
	local last_middle_clicked = root.source(-1)

	local state: state = {
		hovered = root.source(false),
		clicked = root.derive(function() return last_clicked() >= root.get_tick() end),
		right_clicked = root.derive(function() return last_right_clicked() >= root.get_tick() end),
		middle_clicked = root.derive(function() return last_middle_clicked() >= root.get_tick() end),
	}

	local clicked = root.derive(function()
		return state.clicked() or state.right_clicked() or state.middle_clicked()
	end)

	ui.mount(function()
		return root.create("ImageLabel") {
			Active = true,
			Size = UDim2.fromOffset(args.size.X, args.size.Y),

			Image = args.image_id,
			ImageRect = args.rect,
			ImageRectOffset = args.rect and Vector2.new(args.rect.Min.X, args.rect.Min.Y) or nil,
			ImageRectSize = args.rect and Vector2.new(args.rect.Max.X - args.rect.Min.X, args.rect.Max.Y - args.rect.Min.Y) or nil,
			ScaleType = Enum.ScaleType:FromName(args.scale_type),
			ResampleMode = args.resample_mode and Enum.ResamplerMode:FromName(args.resample_mode) or nil,
			TileSize = args.tile_size,
			SliceCenter = args.slice_center,
			SliceScale = args.slice_scale,

			paint.image(),
			paint.button(clicked, state.hovered),

			ui.padding(args.padding),
			ui.hovered(state.hovered),
			ui.clicked("Button1", function()
				last_clicked( root.get_tick() )
			end),
			ui.clicked("Button2", function()
				last_right_clicked( root.get_tick() )
			end),
			ui.clicked("Button3", function()
				last_middle_clicked( root.get_tick() )
			end)
		}
	end)

	return state
end)


local config = require("../../config")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")
local paint = require("../../utils/paint")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	opened: source<boolean>,
	hovered: source<boolean>,
	clicked: source<boolean>,
}

local menu_manager = {
	active_menu = nil :: state?,
	menus = {} :: {[Instance]: state},
}

function menu_manager.register(object: TextButton, state: state)
	menu_manager.menus[object] = state
end

function menu_manager.unregister(object: TextButton)
	local menu = menu_manager.menus[object]
	menu_manager.menus[object] = nil

	menu.opened(false)
end

function menu_manager.set_active(object: TextButton?)
	if menu_manager.active_menu then
		local last_menu = menu_manager.active_menu
		last_menu.opened(false)
	end

	local menu = menu_manager.menus[object :: any]
	menu_manager.active_menu = menu

	if not menu then return end
	menu.opened(true)
end

function menu_manager.handle(object: TextButton, state: state, contents: Frame)
	local can_switch =
		menu_manager.active_menu ~= nil and
		menu_manager.active_menu ~= menu_manager.menus[object]

	if state.hovered() and can_switch then
		menu_manager.set_active(object)
	end

	if state.clicked() then
		menu_manager.set_active(object)
	end

	if root.mouse_clicked("Button1") and menu_manager.active_menu then
		local in_content_area = ui.is_mouse_over(contents)
		local in_button_area = ui.is_mouse_over(object)

		if not in_content_area and not in_button_area then
			menu_manager.set_active(nil)
		end
	end
end

local function create_contents(object: TextButton, state: state)
	local visible = root.source(false)
	local contents = root.create("Frame") {
        AutomaticSize = Enum.AutomaticSize.XY,

        Visible = visible,
        Position = function()
			local pos = object.AbsolutePosition
			local size = object.AbsoluteSize

			local position = vector.create(pos.X, pos.Y + size.Y)
			return UDim2.fromOffset(position.x, position.y)
        end,

        paint.tooltip(),

        ui.padding(config.padding),
        ui.constrain(nil, nil, 250, nil),
		ui.list_layout("Vertical", 0),
    } :: Frame

	local base_children = root.source(#contents:GetChildren())
	visible( state.opened() and #contents:GetChildren() > base_children() )
	return contents
end

return ui.widget("menu", function(name: string?)
	name = root.default(name, "Menu")
    local clicked = root.event()

	local state: state = {
		hovered = root.source(false),
		opened = root.source(false),
		clicked = root.source(false),
	}

	local main = root.create("TextButton") {
		AutomaticSize = Enum.AutomaticSize.X,
		Size = UDim2.new(0, 0, 1, 0),

		Text = name,

        paint.text(),
		paint.menu(state.opened, state.hovered),

        ui.padding(0, config.spacing),
		ui.hovered(state.hovered),
		ui.clicked("Button1", function()
			clicked.fire()
		end),

		root.created(function(self)
			menu_manager.register(self :: TextButton, state)
		end),

		root.destroyed(function(self)
			menu_manager.unregister(self :: TextButton)
		end)
	} :: TextButton

	local contents = create_contents(main, state)
	menu_manager.handle(main, state, contents)

	ui.mount(main)
	ui.mount(contents, "root")

	state.clicked(clicked.fired())
	return ui.start_scope(contents, state)
end)


local config = require("../../config")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")
local paint = require("../../utils/paint")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

return ui.widget("menubar", function()
    local menubar = root.create("Frame") {
        Size = UDim2.new(1, 0, 0, 22),

        LayoutOrder = 1,
        paint.menubar(),

        ui.padding(0, config.spacing),
        ui.list_layout("Horizontal", 0),
    }

    ui.mount(menubar)
    return ui.start_scope(menubar)
end)


local config = require("../../config")
local root = require("../../utils/rootkit")
local ui = require("../../utils/uikit")
local paint = require("../../utils/paint")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
    clicked: source<boolean>,
    hovered: source<boolean>,
}

return ui.widget("menuitem", function(text: string)
    local clicked = root.event()
    local state: state = {
        clicked = root.source(false),
        hovered = root.source(false),
    }

    local button = root.create("TextButton") {
        AutomaticSize = Enum.AutomaticSize.XY,
        Size = UDim2.new(0, config.menu_item_width, 0, 0),

        Text = text,
        RichText = true,

        paint.text(),
        paint.menuitem(state.clicked, state.hovered),

        ui.padding(config.padding/2, config.padding),
        ui.hovered(state.hovered),
        ui.clicked("Button1", function()
            clicked.fire()
        end),
    } :: TextButton

    ui.mount(button)

    state.clicked(clicked.fired())
    return state
end)


local config = require("../config")
local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	hovered: source<boolean>,
	selected: source<boolean>,
	clicked: source<boolean>,
}
export type arguments = {
	text: string,
	value: any,
	state: source<any>,
}

return ui.widget("option", function(arguments: partial<arguments>?)
	local args = root.default(arguments, {}) :: arguments
    local clicked = root.event()

	local state: state = {
		hovered = root.source(false),
		clicked = root.derive(clicked.fired),
		selected = root.derive(function()
            return args.state() == args.value
        end),
	}

	local main = root.create("TextButton") {
		AutomaticSize = Enum.AutomaticSize.Y,
		Size = UDim2.new(1, 0, 0, 0),

		Text = args.text,

        paint.text(),
        paint.option(state.selected, state.clicked, state.hovered),

        ui.padding(config.padding),
		ui.hovered(state.hovered),
		ui.clicked("Button1", function()
			args.state(args.value)
			clicked.fire()
		end),
	}

	ui.mount(main)
	return state
end)


local config = require("../config")
local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")

local text = require("./text")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	hovered: source<boolean>,
    selected: source<boolean>,
	clicked: source<boolean>,
}
export type arguments = {
    index: any,
    text: string?,
	state: source<any>,
}

return ui.widget("radiobutton", function(arguments: partial<arguments>?)
	local args = root.default(arguments, { state = root.source(nil) }) :: arguments
	local last_click = root.source( root.get_tick() )

	local state: state = {
		hovered = root.source(false),
		selected = root.derive(function()
            return args.state() == args.index
        end),
		clicked = root.derive(function()
			return last_click() >= root.get_tick()
		end),
	}

    local button = root.create("Frame") {
        Active = true,
        Size = UDim2.fromOffset(18, 18),

        root.create("Frame") {
            Size = UDim2.fromScale(1, 1),
            Visible = state.selected(),

            ui.circle(),
            paint.radialcenter(),
        },

        ui.circle(),
        ui.padding(3),
        paint.background(state.clicked, state.hovered),
    }

    local main = root.create("Frame") {
		AutomaticSize = Enum.AutomaticSize.XY,
		BackgroundTransparency = 1,

        button,
        ui.list_layout("Horizontal", config.padding),

        ui.hovered(state.hovered),
        ui.clicked("Button1", function()
            last_click( root.get_tick() )
            args.state( args.index )
        end),
    }

    root.open_scope(main)
    root.conditional(args.text ~= nil, function()
        return text { text = args.text }
    end)

    root.close_scope()

    ui.mount(main)
    return state
end)


local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	hovered: source<boolean>,
}
export type arguments = {
	text: string?,
	richText: boolean?,
	color: Color3?,
	wrapped: boolean?,
}

return ui.widget("text", function(arguments: partial<arguments>?)
	local args = root.default(arguments, {}) :: arguments
	local state: state = {
		hovered = root.source(false),
	}

	ui.mount(function()
		return root.create("TextLabel") {
			AutomaticSize = Enum.AutomaticSize.XY,

			BackgroundTransparency = 1,
			Size = UDim2.new(0, 0, 0, 16),
			TextTruncate = Enum.TextTruncate.None,

			paint.text(),
			ui.hovered(state.hovered),

			Text = root.default(args.text, "text"),
			TextWrapped = root.default(args.wrapped, false),
			RichText = root.default(args.richText, false),

			root.conditional(args.color ~= nil, function()
				return { TextColor3 = args.color }
			end),
		}
	end)

	return state
end)


local config = require("../config")
local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
	text_changed: source<boolean>,
	focused: source<boolean>,
	hovered: source<boolean>,
	text: source<string>,
}
export type arguments = {
	text: string,
	placeholder: string,
	multiLine: boolean?,
    wrapped: boolean?,
    readOnly: boolean?,
}

return ui.widget("textinput", function(arguments: partial<arguments>?)
	local args = root.default(arguments, {
        multiLine = false,
        readOnly = false,
        placeholder = "",
        text = "",
        wrapped = false,
    }) :: arguments

    local last_text_change = root.source(-1)
	local state: state = {
		focused = root.source(false),
        hovered = root.source(false),
		text = root.source(args.text),
		text_changed = root.derive(function()
            return last_text_change() >= root.get_tick()
		end),
	}

	ui.mount(function()
		return root.create("TextBox") {
            AutomaticSize = Enum.AutomaticSize.XY,
            Size = UDim2.new(1, 0, 0, 16),

			ClearTextOnFocus = false,
            TextYAlignment = Enum.TextYAlignment.Top,

            Text = args.text,
			PlaceholderText = args.placeholder,
            TextEditable = not args.readOnly,
            MultiLine = args.multiLine,
            TextWrapped = args.multiLine or args.wrapped,

            Focused = function(self: TextBox)
                state.focused( true )
            end,
            FocusLost = function(self: TextBox)
                state.text( self.Text )
                state.focused( false )
                last_text_change( root.get_tick() )
            end,

            ui.padding(config.padding),
            ui.hovered(state.hovered),

            paint.text(),
            paint.background(),
		}
	end)

	return state
end)


local config = require("../config")
local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")

local TOOLTIP_OFFSET = vector.create(10, 15)

type source<T> = root.source<T>
type partial<T> = root.partial<T>

export type state = {
    active: source<boolean>,
	hovered: source<boolean>,
}
export type arguments = {
	text: string?,
    richText: boolean?,
    color: Color3?,
}

return ui.widget("tooltip", function(arguments: partial<arguments>?)
	local args = root.default(arguments, {}) :: arguments
	local state: state = {
		active = root.source(false),
        hovered = root.source(false),
	}

	local main = root.create("TextLabel") {
        AutomaticSize = Enum.AutomaticSize.XY,

        Position = function()
            local mouse_pos = ui.mouse_position() - ui.inset()
            mouse_pos += TOOLTIP_OFFSET
            return UDim2.fromOffset(mouse_pos.x, mouse_pos.y)
        end,

        paint.text(),
        paint.tooltip(),

        ui.padding(config.spacing),
        ui.hovered(state.hovered),
        ui.constrain(nil, nil, 250, nil),

        TextWrapped = true,
		TextTruncate = Enum.TextTruncate.None,
        Text = root.default(args.text, "text"),
        RichText = root.default(args.richText, false),

        root.conditional(args.color ~= nil, function()
            return { TextColor3 = args.color }
        end),
    }

    ui.mount(main, "root")
	return state
end)


local UserInputService = game:GetService("UserInputService")

local config = require("../config")
local root = require("../utils/rootkit")
local ui = require("../utils/uikit")
local paint = require("../utils/paint")
local extern_menubar = require("./menu/menubar")

local START_POSITION = vector.create(200, 200)
local DEFAULT_SIZE = vector.create(400, 400)
local OFFSET_INCREMENT = vector.create(30, 30)

local assets = config.assets

type source<T> = root.source<T>
type partial<T> = root.partial<T>

type state = {
	active: source<boolean>,
	collasped: source<boolean>,
	closed: source<boolean>,

	position: source<vector>,
	size: source<vector>,
	zindex: source<number>,

	menubar: () -> ui.scoped_state,
}
export type arguments = {
	title: string,

	closeable: boolean?,
	noMinimize: boolean?,
	noTitle: boolean?,
	noTopbar: boolean?,
	noResize: boolean?,
	noDrag: boolean?,
}

local wm = {
	drag_connection = nil,
	window_stack = {},
	last_update = -1,
	last_tick = -1,
}

function wm.register(applier: (x: number, y: number) -> (), ignore_zindex: boolean?)
	local input_type = Enum.UserInputType.MouseButton1

	return {
		InputBegan = function(object: GuiObject, input: InputObject)
			if input.UserInputType ~= input_type then return end
			if wm.drag_connection ~= nil then return end

			if ignore_zindex ~= true then wm.push_to_top(object) end

			local last_position = input.Position
			wm.drag_connection = UserInputService.InputChanged:Connect(function(input)
				if input.UserInputType ~= Enum.UserInputType.MouseMovement then return end

				local delta = last_position - input.Position
				last_position = input.Position
				applier(delta.X, delta.Y)
			end)
		end,

		InputEnded = function(_, input: InputObject)
			if input.UserInputType ~= input_type then return end
			if wm.drag_connection == nil then return end

			wm.drag_connection:Disconnect()
			wm.drag_connection = nil
		end,

		Destroying = function(object: GuiObject)
			if wm.drag_connection and wm.drag_connection.Connected then
				wm.drag_connection:Disconnect()
			end

			wm.unregister(object)
		end,
	}
end

function wm.get_next_position()
	local start_instance = wm.window_stack[#wm.window_stack] :: any
	local start_pos = start_instance and start_instance.Parent.AbsolutePosition :: Vector2

	if start_pos == nil then
		return START_POSITION
	else
		return vector.create(start_pos.X, start_pos.Y) + OFFSET_INCREMENT
	end
end

function wm.unregister(object: GuiObject)
	for index, obj in wm.window_stack do
		if obj ~= object then continue end
		table.remove(wm.window_stack, index :: any)
		break
	end
end

function wm.get_index(object: GuiObject)
	for index, obj in wm.window_stack do
		if obj ~= object then continue end
		return index
	end
	return -1
end

function wm.push_to_top(object: GuiObject)
	wm.unregister(object)
	table.insert(wm.window_stack, object)

	if wm.updated() then return end
	wm.last_update = root.get_tick()
end

function wm.get_active()
	return wm.window_stack[#wm.window_stack]
end

function wm.updated()
	return wm.last_update == root.get_tick()
end

local function topbar(state: state, args: arguments)
	local title_button_size = config.textSize - 1.5

	return root.create("Frame") {
		function(object) state.zindex( wm.get_index(object) ) end,

		Size = UDim2.new(1, 0, 0, 28),
		Position = UDim2.fromOffset(0, 0),

		wm.register(function(x, y)
			local last_position = state.position()
			local target_position = last_position - vector.create(x, y)
			state.position(target_position)
		end),

		ui.padding(config.spacing / 2, config.spacing),
		ui.list_layout("Horizontal", config.spacing),
		paint.title(state.active, state.collasped),

		root.conditional(not args.noMinimize, function()
			return root.create("ImageLabel") {
				Active = true,

				AnchorPoint = Vector2.new(0, 0.5),
				Size = UDim2.fromOffset(title_button_size, title_button_size),
				Position = UDim2.fromScale(0, 0.5),
				BackgroundTransparency = 1,
				LayoutOrder = 1,

				Image = function()
					return
						if state.collasped() then assets.rightPointingTriangle
						else assets.downPointingTriangle
				end,

				paint.image(),
				ui.clicked("Button1", function()
					state.collasped(not state.collasped())
				end),
			}
		end),

		root.conditional(not args.noTitle, function()
			return root.create("TextLabel") {
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, title_button_size),
				LayoutOrder = 2,
				Interactable = false,

				Text = root.default(args.title, "Window"),
				TextSize = config.textSize,
				Font = config.font,

				ui.flexitem("Fill"),
				paint.text(state.active),
			}
		end),

		root.conditional(args.closeable, function()
			return root.create("ImageLabel") {
				Active = true,

				AnchorPoint = Vector2.new(0, 0.5),
				Size = UDim2.fromOffset(title_button_size, title_button_size),
				Position = UDim2.fromScale(0, 0.5),
				BackgroundTransparency = 1,
				LayoutOrder = 3,

				Image = assets.xCloseIcon,

				paint.image(),
				--TODO: implement closing listeners
			}
		end),
	}
end

local function resize_grip(state: state, args: arguments)
	return root.create("ImageLabel") {
		AnchorPoint = Vector2.new(1, 1),
		Position = UDim2.new(1, 0, 1, 0),
		Size = UDim2.fromOffset(25, 25),

		BackgroundTransparency = 1,
		Rotation = 90,
		Image = assets.bottomRightCorner,

		Visible = not state.collasped(),
		ZIndex = 2,

		paint.resize_grip(),
		wm.register(function(x, y)
			local last_size = state.size()
			local target_size = last_size - vector.create(x, y)

			local new_x = math.max(config.minWindowSize.x, target_size.x)
			local new_y = math.max(config.minWindowSize.y, target_size.y)

			state.size(vector.create(new_x, new_y))
		end, true),
	}
end

local function user_contents(state: state, args: arguments)
	return root.create("ScrollingFrame") {
		Size = UDim2.new(1, 0, 1, 0),
		BackgroundTransparency = 1,

		LayoutOrder = 2,
		Visible = not state.collasped(),

		AutomaticCanvasSize = Enum.AutomaticSize.Y,
		CanvasSize = UDim2.fromOffset(0, 0),
		ScrollBarThickness = 0,

		ui.padding(10),
		ui.flexitem("Shrink"),
		ui.list_layout("Vertical", config.spacing)
	}
end

local function menubar(state: state, args: arguments)
	return root.create("Frame") {
		AutomaticSize = Enum.AutomaticSize.Y,
		Size = UDim2.new(1, 0, 0, 0),

		BackgroundTransparency = 1,
		LayoutOrder = 1,
	}
end

local function contents(state: state, args: arguments)
	local menubar = menubar(state, args)
	local user_contents = user_contents(state, args)
	local contents = root.create("Frame") {
		Position = UDim2.new(0, 0, 0, 28),
		Size = UDim2.new(1, 0, 1, -28),
		BackgroundTransparency = 1,

		LayoutOrder = 1,
		Visible = not state.collasped(),
		ui.list_layout("Vertical", 0),

		menubar,
		user_contents,
	}

	return {
		contents = contents,
		menubar = menubar,
		user_contents = user_contents,
	}
end

return ui.widget("window", function(arguments: partial<arguments>?)
	local args = root.default(arguments, { noClose = true }) :: arguments
	local state = {
		closed = root.source(false),
		collasped = root.source(false),
		active = root.source(true),

		position = root.source(START_POSITION),
		size = root.source(DEFAULT_SIZE),
		zindex = root.source(-1),
	} :: state

	local contents = contents(state, args)
	local topbar = root.conditional(not args.noTopbar, topbar, state, args)::any
	(state::any).menubar = function()
		local menubar = contents.menubar
		root.open_scope(menubar)
		local extern = extern_menubar()
		root.close_scope(1)
		return extern
	end

	local main = root.create("Frame") {
		ClipsDescendants = true,
		ZIndex = state.zindex(),

		Size = function()
			local size = state.size()
			local collapsed = state.collasped()

			return if collapsed
				then UDim2.fromOffset(size.x, 28)
				else UDim2.fromOffset(size.x, size.y)
		end,

		Position = function()
			local pos = state.position()
			return UDim2.fromOffset(pos.x, pos.y)
		end,

		contents.contents,
		topbar,

		root.conditional(not args.noResize, resize_grip, state, args),
		paint.window(state.collasped),

		ui.clicked("Button1", function()
			wm.push_to_top(topbar)
		end),

		root.created(function()
			state.position(wm.get_next_position())
			wm.push_to_top(topbar)
		end),

		root.destroyed(function()
			wm.unregister(topbar)
		end)
	}

	if wm.get_active() == topbar then
		state.active(true)
	else
		state.active(false)
	end

	ui.mount(main)
	return ui.start_scope(contents.user_contents, state)
end)


--!strict
--[=[
	@function copyDeep
	@within Array

	@param array {T} -- The array to copy.
	@return {T} -- The copied array.

	Copies an array, with deep copies of all nested arrays.

	```lua
	local array = { 1, 2, 3, { 4, 5 } }

	local result = CopyDeep(array) -- { 1, 2, 3, { 4, 5 } }

	print(result == array) -- false
	print(result[4] == array[4]) -- false
	```
]=]
local function copyDeep<T>(array: { T }): { T }
	local result = table.clone(array)

	for index, value in array do
		if type(value) == "table" then
			result[index] = copyDeep(value) :: any
		end
	end

	return result
end

return copyDeep


--!strict
local Sift = script.Parent.Parent

local Util = require(Sift.Util)

--[=[
	@function count
	@within Array

	@param array {T} -- The array to count the number of items in.
	@param predicate? (value: T, index: number, array: {T}) -> any -- The predicate to use to filter the array.
	@return number -- The number of items in the array.

	Counts the number of items in an array.

	```lua
	local array = { 1, 2, 3 }

	local value = Count(array) -- 3
	local value = Count(array, function(item, index)
		return item == 2
	end) -- 1
	```
]=]
local function count<T>(
	array: { T },
	predicate: ((value: T, index: number, array: { T }) -> any)?
): number
	local counter = 0

	predicate = if type(predicate) == "function" then predicate else Util.func.truthy

	for index, value in ipairs(array) do
		if predicate(value, index, array) then
			counter += 1
		end
	end

	return counter
end

return count


--!strict
local T = require(script.Parent.Parent.Types)

local toSet = require(script.Parent.toSet)
local toArray = require(script.Parent.Parent.Set.toArray)
local setDifference = require(script.Parent.Parent.Set.difference)

--[=[
  @function difference
  @within Array

  @param array Array<V> -- The array to compare.
  @param ... ...Array<V> -- The arrays to compare against.
  @return Array<V> -- The difference between the arrays.

  Returns an array of values that are in the first array, but not in the other arrays.

  ```lua
  local array1 = { "hello", "world" }
  local array2 = { "cat", "dog", "hello" }

  local difference = Difference(array1, array2) -- { "world" }
  ```
]=]
local function difference<V>(array: T.Array<V>, ...: T.Array<V>): T.Array<V>
	local arraySet = toSet(array)
	local otherSets = {}

	for _, nextArray in { ... } do
		if typeof(nextArray) ~= "table" then
			continue
		end

		table.insert(otherSets, toSet(nextArray))
	end

	local differenceSet = setDifference(arraySet, unpack(otherSets))

	return toArray(differenceSet)
end

return difference


--!strict
local T = require(script.Parent.Parent.Types)

local toSet = require(script.Parent.toSet)
local toArray = require(script.Parent.Parent.Set.toArray)
local setDifferenceSymmetric = require(script.Parent.Parent.Set.differenceSymmetric)

--[=[
  @function differenceSymmetric
  @within Array

  @param array Array<V> -- The array to compare.
  @param ... ...Array<V> -- The arrays to compare against.
  @return Array<V> -- The symmetric difference between the arrays.

  Returns an array of values that are in the first array, but not in the other arrays, and vice versa.

  ```lua
  local array1 = { "hello", "world" }
  local array2 = { "cat", "dog", "hello" }

  local difference = DifferenceSymmetric(array1, array2) -- { "world", "cat", "dog" }
  ```
]=]
local function differenceSymmetric<V>(array: T.Array<V>, ...: T.Array<V>): T.Array<V>
	local arraySet = toSet(array)
	local otherSets = {}

	for _, nextArray in { ... } do
		if typeof(nextArray) ~= "table" then
			continue
		end

		table.insert(otherSets, toSet(nextArray))
	end

	local differenceSet = setDifferenceSymmetric(arraySet, unpack(otherSets))

	return toArray(differenceSet)
end

return differenceSymmetric


--!strict
--[=[
	@function every
	@within Array

	@param array {T} -- The array to check.
	@param predicate (value: T, index: number, array: {T}) -> any -- The predicate to use to check the array.
	@return boolean -- Whether every item in the array passes the predicate.

	Checks whether every item in the array passes the predicate.

	```lua
	local array = { 1, 2, 3 }

	local value = Every(array, function(item, index)
		return item > 0
	end) -- true

	local value = Every(array, function(item, index)
		return item > 1
	end) -- false
	```
]=]
local function every<T>(
	array: { T },
	predicate: (value: T, index: number, array: { T }) -> any
): boolean
	for index, value in ipairs(array) do
		if not predicate(value, index, array) then
			return false
		end
	end

	return true
end

return every


--!strict
local Sift = script.Parent.Parent

local Util = require(Sift.Util)

--[=[
	@function filter
	@within Array

	@param array {T} -- The array to filter.
	@param filterer? (value: T, index: number, array: {T}) -> any -- The callback to use to filter the array.
	@return {T} -- The filtered array.

	Filters an array using a filterer callback. Any items that do not pass the filterer will be removed from the array.

	If no filterer is provided, all items will be kept.

	```lua
	local array = { 1, 2, 3 }

	local result = Filter(array, function(item, index)
		return item > 1
	end) -- { 2, 3 }
	```
]=]
local function filter<T>(
	array: { T },
	predicate: ((value: T, index: number, array: { T }) -> boolean?)?
): { T }
	local result = {}

	predicate = if type(predicate) == "function" then predicate else Util.func.truthy

	for index, value in ipairs(array) do
		if predicate(value, index, array) then
			table.insert(result, value)
		end
	end

	return result
end

return filter


--!strict
--[=[
	@class Array

	An array is a table consisting of index-value pairs. You don't need to
	manually specify the indices when you create an array.

	```lua
	local array = {
		"hello",
		"world",
	}
	```

	#### Aliases

	`List`
]=]
local Array = {
	at = require(script.at),
	concat = require(script.concat),
	concatDeep = require(script.concatDeep),
	copy = require(script.copy),
	copyDeep = require(script.copyDeep),
	count = require(script.count),
	create = require(script.create),
	difference = require(script.difference),
	differenceSymmetric = require(script.differenceSymmetric),
	equals = require(script.equals),
	equalsDeep = require(script.equalsDeep),
	every = require(script.every),
	filter = require(script.filter),
	find = require(script.find),
	findLast = require(script.findLast),
	findWhere = require(script.findWhere),
	findWhereLast = require(script.findWhereLast),
	first = require(script.first),
	flatten = require(script.flatten),
	freeze = require(script.freeze),
	freezeDeep = require(script.freezeDeep),
	includes = require(script.includes),
	insert = require(script.insert),
	is = require(script.is),
	last = require(script.last),
	map = require(script.map),
	pop = require(script.pop),
	push = require(script.push),
	reduce = require(script.reduce),
	reduceRight = require(script.reduceRight),
	removeIndex = require(script.removeIndex),
	removeIndices = require(script.removeIndices),
	removeValue = require(script.removeValue),
	removeValues = require(script.removeValues),
	reverse = require(script.reverse),
	set = require(script.set),
	shift = require(script.shift),
	shuffle = require(script.shuffle),
	slice = require(script.slice),
	some = require(script.some),
	sort = require(script.sort),
	splice = require(script.splice),
	toSet = require(script.toSet),
	unshift = require(script.unshift),
	update = require(script.update),
	zip = require(script.zip),
	zipAll = require(script.zipAll),
}

Array.join = Array.concat
Array.merge = Array.concat
Array.joinDeep = Array.concatDeep
Array.mergeDeep = Array.concatDeep
Array.append = Array.push
Array.prepend = Array.unshift
Array.indexOf = Array.find
Array.has = Array.includes
Array.contains = Array.includes
Array.isArray = Array.is

return Array


--!strict
--[=[
  @function is
  @within Array

  @param object any -- The object to check.
  @return boolean -- Whether the object is an array.

  Checks if the given object is an array.

  ```lua
  local array = { 1, 2, 3 }
  local dictionary = { hello = "world" }
  local mixed = { 1, 2, hello = "world" }

  Array.is(array) -- true
  Array.is(dictionary) -- false
  Array.is(mixed) -- false
  ```
]=]
local function is(object: any): boolean
	return typeof(object) == "table" and #object > 0 and next(object, #object) == nil
end

return is


--!strict
--[=[
	@function push
	@within Array

	@param array {T} -- The array to push an element to.
	@param ... ...T -- The elements to push.
	@return {T} -- The array with the pushed elements.

	Adds elements to the end of the array.

	#### Aliases

	`append`

	```lua
	local array = { 1, 2, 3 }

	local new = Push(array, 4, 5, 6) -- { 1, 2, 3, 4, 5, 6 }
	```
]=]
local function push<T>(array: { T }, ...: T): { T }
	local result = {}

	for _, value in ipairs(array) do
		table.insert(result, value)
	end

	for _, value in ipairs({ ... }) do
		table.insert(result, value)
	end

	return result
end

return push


--!strict
--[=[
	@function reduce
	@within Array

	@param array {T} -- The array to reduce.
	@param reducer (accumulator: U, value: T, index: number, array: {T}) -> U -- The reducer to use.
	@param initialReduction? U = {T}[1] -- The initial accumulator value.
	@return U -- The final accumulator value.

	Reduces the array using the given reducer and initial accumulator value.
	If no `initialReduction` value is given, the first item in the array is used.

	```lua
	local array = { 1, 2, 3 }

	local value = Reduce(array, function(accumulator, item, index)
		return accumulator - item
	end) -- -4

	local value = Reduce(array, function(accumulator, item, index)
		table.insert(accumulator, item)
		return accumulator
	end, {}) -- { 1, 2, 3 }
	```
]=]
local function reduce<T, U>(
	array: { T },
	reducer: (accumulator: U, value: T, index: number, array: { T }) -> U,
	initReduction: U?
): U
	local result = initReduction
	local start = 1

	if result == nil then
		result = array[1]
		start = 2
	end

	for index = start, #array do
		result = reducer(result, array[index], index, array)
	end

	return result
end

return reduce


--!strict
--[=[
	@function reduceRight
	@within Array

	@param array {T} -- The array to reduce.
	@param reducer (accumulator: U, value: T, index: number, array: {T}) -> U -- The reducer to use.
	@param initialReduction? U = {T}[#{T}] -- The initial accumulator value.
	@return U -- The final accumulator value.

	Reduces the array using the given reducer and initial accumulator value,
	starting from the end of the array. If no `initialReduction` value is given,
	the last item in the array is used.

	```lua
	local array = { 1, 2, 3 }

	local value = ReduceRight(array, function(accumulator, item, index)
		return accumulator - item
	end) -- 0

	local value = ReduceRight(array, function(accumulator, item, index)
		table.insert(accumulator, item)
		return accumulator
	end, {}) -- { 3, 2, 1 }
	```
]=]
local function reduceRight<T, U>(
	array: { T },
	reducer: (accumulator: U, value: T, index: number, array: { T }) -> U,
	initReduction: U?
): U
	local result = initReduction
	local start = #array

	if result == nil then
		result = array[start]
		start -= 1
	end

	for index = start, 1, -1 do
		result = reducer(result, array[index], index, array)
	end

	return result
end

return reduceRight


--!strict
--[=[
	@function removeValue
	@within Array

	@param array {T} -- The array to remove the value from.
	@param value T -- The value to remove.
	@return {T} -- The array with the value removed.

	Removes a value from an array.

	```lua
	local array = { 1, 2, 3 }

	local new = RemoveValue(array, 2) -- { 1, 3 }
	```
]=]
local function removeValue<T>(array: { T }, value: T): { T }
	local result = {}

	for _, arrValue in ipairs(array) do
		if arrValue ~= value then
			table.insert(result, arrValue)
		end
	end

	return result
end

return removeValue


--!strict
--[=[
	@function some
	@within Array

	@param array {T} -- The array to check.
	@param predicate (value: T, index: number, array: {T}) -> any -- The predicate to use to check the array.
	@return boolean -- Whether some item in the array passes the predicate.

	Checks whether some item in the array passes the predicate.

	```lua
	local array = { 1, 2, 3 }

	local value = Some(array, function(item, index)
		return item > 1
	end) -- true

	local value = Some(array, function(item, index)
		return item > 3
	end) -- false
	```
]=]
local function some<T>(
	array: { T },
	predicate: (value: T, index: number, array: { T }) -> any
): boolean
	for index, value in ipairs(array) do
		if predicate(value, index, array) then
			return true
		end
	end

	return false
end

return some


--!strict
local Sift = script.Parent.Parent

local Util = require(Sift.Util)
local Copy = require(script.Parent.copy)

type Callback<T> = (index: number) -> T
type Updater<T> = (currentValue: T, index: number) -> T

local function call<T>(callback: Callback<T>, index: number)
	if type(callback) == "function" then
		return callback(index)
	end
end

--[=[
	@function update
	@within Array

	@param array {T} -- The array to update.
	@param index number -- The index to update.
	@param updater? (value: T, index: number) -> T -- The updater function.
	@param callback? (index: number) -> T -- The callback function.
	@return {T} -- The updated array.

	Updates an array at the given index. If the value at the given index does
	not exist, `callback` will be called, and its return value will be used
	as the value at the given index.

	```lua
	local array = { 1, 2, 3 }

	local new = Update(array, 2, function(value)
		return value + 1
	end) -- { 2, 3, 3 }

	local new = Update(array, 4, function(value)
		return value + 1
	end, function(value)
		return 10
	end) -- { 1, 2, 3, 10 }
	```
]=]
local function update<T>(
	array: { T },
	index: number,
	updater: Updater<T>?,
	callback: Callback<T>?
): { T }
	local length = #array
	local result = Copy(array)

	if index < 1 then
		index += length
	end

	updater = if type(updater) == "function" then updater else Util.func.returned

	if result[index] ~= nil then
		result[index] = updater(result[index], index)
	else
		result[index] = call(callback, index)
	end

	return result
end

return update


--!strict
--[=[
  @function copyDeep
  @within Dictionary

  @param dictionary T -- The dictionary to copy.
  @return T -- The copied dictionary.

  Copies a dictionary recursively.

  ```lua
  local dictionary = { hello = { world = "goodbye" } }

  local new = CopyDeep(dictionary) -- { hello = { world = "goodbye" } }

  print(new == dictionary) -- false
  print(new.hello == dictionary.hello) -- false
  ```
]=]
local function copyDeep<T>(dictionary: T): T
	local new = table.clone(dictionary)

	for key, value in pairs(dictionary) do
		if type(value) == "table" then
			new[key] = copyDeep(value)
		end
	end

	return new
end

return copyDeep


--!strict
--[=[
	@class Dictionary

	Dictionaries are a type of data structure that can be used to store key-value pairs.

	```lua
	local dictionary = {
		cats = 2,
		dogs = 1
	}

	print(dictionary.cats) -- 2
	```
]=]
local Dictionary = {
	copy = require(script.copy),
	copyDeep = require(script.copyDeep),
	count = require(script.count),
	entries = require(script.entries),
	equals = require(script.equals),
	equalsDeep = require(script.equalsDeep),
	every = require(script.every),
	filter = require(script.filter),
	flatten = require(script.flatten),
	flip = require(script.flip),
	freeze = require(script.freeze),
	freezeDeep = require(script.freezeDeep),
	fromArrays = require(script.fromArrays),
	fromEntries = require(script.fromEntries),
	has = require(script.has),
	includes = require(script.includes),
	keys = require(script.keys),
	map = require(script.map),
	merge = require(script.merge),
	mergeDeep = require(script.mergeDeep),
	removeKey = require(script.removeKey),
	removeKeys = require(script.removeKeys),
	removeValue = require(script.removeValue),
	removeValues = require(script.removeValues),
	set = require(script.set),
	some = require(script.some),
	update = require(script.update),
	values = require(script.values),
	withKeys = require(script.withKeys),
}

Dictionary.join = Dictionary.merge
Dictionary.joinDeep = Dictionary.mergeDeep

return Dictionary


--!strict
--[=[
  @function some
  @within Dictionary

  @param dictionary {[K]: V} -- The dictionary to check.
  @param predicate (value: V, key: K, dictionary: { [K]: V }) -> any -- The predicate to check against.
  @return boolean -- Whether or not the predicate returned true for any value.

  Checks whether or not the predicate returned true for any value in the dictionary.

  ```lua
  local dictionary = { hello = "world", cat = "meow", unicorn = "rainbow" }

  local hasMeow = Some(dictionary, function(value)
    return value == "meow"
  end) -- true

  local hasDog = Some(dictionary, function(_, key)
    return key == "dog"
  end) -- false
  ```
]=]
local function some<K, V>(
	dictionary: { [K]: V },
	predicate: (value: V, key: V, dictionary: { [K]: V }) -> any
): boolean
	for key, value in pairs(dictionary) do
		if predicate(value, key, dictionary) then
			return true
		end
	end

	return false
end

return some


--!strict

--[=[
  @function withKeys
  @within Dictionary

  @param dictionary {[K]: V} -- The dictionary to select the keys from.
  @param keys ...K -- The keys to keep.
  @return {[K]: V} -- The dictionary with only the given keys.

  Returns a dictionary with the given keys.

  ```lua
  local dictionary = { hello = "world", cat = "meow", dog = "woof", unicorn = "rainbow" }

  local withoutCatDog = WithKeys(dictionary, "cat", "dog") -- { cat = "meow", dog = "woof" }
  ```
]=]
local function withKeys<K, V>(dictionary: { [K]: V }, ...: K): { [K]: V }
	local result = {}

	for _, key in ipairs({ ... }) do
		result[key] = dictionary[key]
	end

	return result
end

return withKeys


--!strict
local Sift = script.Parent.Parent

local Util = require(Sift.Util)

--[=[
  @function count
  @within Set

  @param set { [T]: boolean } -- The set to count.
  @param predicate? (item: T, set: { [T]: boolean }) -> boolean? -- The predicate to use to count.
  @return number -- The number of items in the set.

  Counts the number of items in a set.

  ```lua
  local set = { hello = true, world = true }

  local count = Count(set) -- 2
  local count = Count(set, function(item)
    return item == "hello"
  end) -- 1
  ```
]=]
local function count<T>(
	set: { [T]: boolean },
	predicate: ((item: T, set: { [T]: boolean }) -> boolean?)?
): number
	local counter = 0

	predicate = if type(predicate) == "function" then predicate else Util.func.truthy

	for item, _ in pairs(set) do
		if predicate(item, set) then
			counter += 1
		end
	end

	return counter
end

return count


--!strict
local T = require(script.Parent.Parent.Types)

--[=[
  @function difference
  @within Set

  @param set Set<V> -- The set to compare.
  @param ... ...Set<V> -- The sets to compare against.
  @return Set<V> -- The difference between the sets.

  Returns a set of values that are in the first set, but not in the other sets.

  ```lua
  local set1 = { hello = true, world = true }
  local set2 = { cat = true, dog = true, hello = true }

  local difference = Difference(set1, set2) -- { world = true }
  ```
]=]
local function difference<V>(set: T.Set<V>, ...: T.Set<V>): T.Set<V>
	local diff = table.clone(set)

	for _, nextSet in { ... } do
		if typeof(nextSet) ~= "table" then
			continue
		end

		for value in nextSet do
			diff[value] = nil
		end
	end

	return diff
end

return difference


--!strict
local T = require(script.Parent.Parent.Types)

--[=[
  @function differenceSymmetric
  @within Set

  @param set Set<V> -- The set to compare.
  @param ... ...Set<V> -- The sets to compare against.
  @return Set<V> -- The symmetric difference between the sets.

  Returns a set of values that are in the first set, but not in the other sets, and vice versa.

  ```lua
  local set1 = { hello = true, world = true }
  local set2 = { cat = true, dog = true, hello = true }

  local differenceSymmetric = DifferenceSymmetric(set1, set2) -- { world = true, cat = true, dog = true }
  ```
]=]
local function differenceSymmetric<V>(set: T.Set<V>, ...: T.Set<V>): T.Set<V>
	local diff = table.clone(set)

	for _, nextSet in { ... } do
		if typeof(nextSet) ~= "table" then
			continue
		end

		for value in nextSet do
			diff[value] = if diff[value] == nil then true else false
		end
	end

	for value, keep in diff do
		diff[value] = if keep then true else nil
	end

	return diff
end

return differenceSymmetric


--!strict
local Sift = script.Parent.Parent

local Util = require(Sift.Util)

--[=[
  @function filter
  @within Set

  @param set { [T]: boolean } -- The set to filter.
  @param predicate? (item: T, set: { [T]: boolean }) -> any -- The function to filter the set with.
  @return { [T]: boolean } -- The filtered set.

  Filters a set using a predicate. Any items that do not pass the predicate will be removed from the set.

  ```lua
  local set = { hello = true, world = true }

  local newSet = Filter(set, function(value)
    return value ~= "hello"
  end) -- { world = true }
  ```
]=]
local function filter<T>(
	set: { [T]: boolean },
	predicate: ((T, { [T]: boolean }) -> any)?
): { [T]: boolean }
	local result = {}

	predicate = if type(predicate) == "function" then predicate else Util.func.truthy

	for key, _ in pairs(set) do
		if predicate(key, set) then
			result[key] = true
		end
	end

	return result
end

return filter


--!strict
--[=[
	@class Set

	Sets are a collection of values. They are used to store unique values.
	They are essentially a dictionary, but each value is stored as a boolean.
	This means that a value can only be in a set once.

	```lua
	local set = { hello = true }

	local newSet = Add(set, "world") -- { hello = true, world = true }
	```
]=]
local set = {
	add = require(script.add),
	copy = require(script.copy),
	count = require(script.count),
	delete = require(script.delete),
	difference = require(script.difference),
	differenceSymmetric = require(script.differenceSymmetric),
	filter = require(script.filter),
	fromArray = require(script.fromArray),
	has = require(script.has),
	intersection = require(script.intersection),
	isSubset = require(script.isSubset),
	isSuperset = require(script.isSuperset),
	map = require(script.map),
	merge = require(script.merge),
	toArray = require(script.toArray),
}

set.fromList = set.fromArray
set.join = set.merge
set.subtract = set.delete
set.union = set.merge

return set


--selene: allow(unused_variable)
local None = require(script.Parent.None)

--[=[
  @type None None
  @within Sift
]=]
export type None = typeof(None)
export type Dictionary<K, V> = { [K]: V }
export type Array<T> = Dictionary<number, T>
export type Set<T> = Dictionary<T, boolean>
export type Table = Dictionary<any, any>

export type AnyDictionary = Dictionary<any, any>

return nil


--[[
	Allows creation of expectation statements designed for behavior-driven
	testing (BDD). See Chai (JS) or RSpec (Ruby) for examples of other BDD
	frameworks.

	The Expectation class is exposed to tests as a function called `expect`:

		expect(5).to.equal(5)
		expect(foo()).to.be.ok()

	Expectations can be negated using .never:

		expect(true).never.to.equal(false)

	Expectations throw errors when their conditions are not met.
]]

local Expectation = {}

--[[
	These keys don't do anything except make expectations read more cleanly
]]
local SELF_KEYS = {
	to = true,
	be = true,
	been = true,
	have = true,
	was = true,
	at = true,
}

--[[
	These keys invert the condition expressed by the Expectation.
]]
local NEGATION_KEYS = {
	never = true,
}

--[[
	Extension of Lua's 'assert' that lets you specify an error level.
]]
local function assertLevel(condition, message, level)
	message = message or "Assertion failed!"
	level = level or 1

	if not condition then
		error(message, level + 1)
	end
end

--[[
	Returns a version of the given method that can be called with either . or :
]]
local function bindSelf(self, method)
	return function(firstArg, ...)
		if firstArg == self then
			return method(self, ...)
		else
			return method(self, firstArg, ...)
		end
	end
end

local function formatMessage(result, trueMessage, falseMessage)
	if result then
		return trueMessage
	else
		return falseMessage
	end
end

--[[
	Create a new expectation
]]
function Expectation.new(value)
	local self = {
		value = value,
		successCondition = true,
		condition = false,
		matchers = {},
		_boundMatchers = {},
	}

	setmetatable(self, Expectation)

	self.a = bindSelf(self, self.a)
	self.an = self.a
	self.ok = bindSelf(self, self.ok)
	self.equal = bindSelf(self, self.equal)
	self.throw = bindSelf(self, self.throw)
	self.near = bindSelf(self, self.near)

	return self
end

function Expectation.checkMatcherNameCollisions(name)
	if SELF_KEYS[name] or NEGATION_KEYS[name] or Expectation[name] then
		return false
	end

	return true
end

function Expectation:extend(matchers)
	self.matchers = matchers or {}

	for name, implementation in pairs(self.matchers) do
		self._boundMatchers[name] = bindSelf(self, function(_self, ...)
			local result = implementation(self.value, ...)
			local pass = result.pass == self.successCondition

			assertLevel(pass, result.message, 3)
			self:_resetModifiers()
			return self
		end)
	end

	return self
end

function Expectation.__index(self, key)
	-- Keys that don't do anything except improve readability
	if SELF_KEYS[key] then
		return self
	end

	-- Invert your assertion
	if NEGATION_KEYS[key] then
		local newExpectation = Expectation.new(self.value):extend(self.matchers)
		newExpectation.successCondition = not self.successCondition

		return newExpectation
	end

	if self._boundMatchers[key] then
		return self._boundMatchers[key]
	end

	-- Fall back to methods provided by Expectation
	return Expectation[key]
end

--[[
	Called by expectation terminators to reset modifiers in a statement.

	This makes chains like:

		expect(5)
			.never.to.equal(6)
			.to.equal(5)

	Work as expected.
]]
function Expectation:_resetModifiers()
	self.successCondition = true
end

--[[
	Assert that the expectation value is the given type.

	expect(5).to.be.a("number")
]]
function Expectation:a(typeName)
	local result = (type(self.value) == typeName) == self.successCondition

	local message = formatMessage(self.successCondition,
		("Expected value of type %q, got value %q of type %s"):format(
			typeName,
			tostring(self.value),
			type(self.value)
		),
		("Expected value not of type %q, got value %q of type %s"):format(
			typeName,
			tostring(self.value),
			type(self.value)
		)
	)

	assertLevel(result, message, 3)
	self:_resetModifiers()

	return self
end

-- Make alias public on class
Expectation.an = Expectation.a

--[[
	Assert that our expectation value is truthy
]]
function Expectation:ok()
	local result = (self.value ~= nil) == self.successCondition

	local message = formatMessage(self.successCondition,
		("Expected value %q to be non-nil"):format(
			tostring(self.value)
		),
		("Expected value %q to be nil"):format(
			tostring(self.value)
		)
	)

	assertLevel(result, message, 3)
	self:_resetModifiers()

	return self
end

--[[
	Assert that our expectation value is equal to another value
]]
function Expectation:equal(otherValue)
	local result = (self.value == otherValue) == self.successCondition

	local message = formatMessage(self.successCondition,
		("Expected value %q (%s), got %q (%s) instead"):format(
			tostring(otherValue),
			type(otherValue),
			tostring(self.value),
			type(self.value)
		),
		("Expected anything but value %q (%s)"):format(
			tostring(otherValue),
			type(otherValue)
		)
	)

	assertLevel(result, message, 3)
	self:_resetModifiers()

	return self
end

--[[
	Assert that our expectation value is equal to another value within some
	inclusive limit.
]]
function Expectation:near(otherValue, limit)
	assert(type(self.value) == "number", "Expectation value must be a number to use 'near'")
	assert(type(otherValue) == "number", "otherValue must be a number")
	assert(type(limit) == "number" or limit == nil, "limit must be a number or nil")

	limit = limit or 1e-7

	local result = (math.abs(self.value - otherValue) <= limit) == self.successCondition

	local message = formatMessage(self.successCondition,
		("Expected value to be near %f (within %f) but got %f instead"):format(
			otherValue,
			limit,
			self.value
		),
		("Expected value to not be near %f (within %f) but got %f instead"):format(
			otherValue,
			limit,
			self.value
		)
	)

	assertLevel(result, message, 3)
	self:_resetModifiers()

	return self
end

--[[
	Assert that our functoid expectation value throws an error when called.
	An optional error message can be passed to assert that the error message
	contains the given value.
]]
function Expectation:throw(messageSubstring)
	local ok, err = pcall(self.value)
	local result = ok ~= self.successCondition

	if messageSubstring and not ok then
		if self.successCondition then
			result = err:find(messageSubstring, 1, true) ~= nil
		else
			result = err:find(messageSubstring, 1, true) == nil
		end
	end

	local message

	if messageSubstring then
		message = formatMessage(self.successCondition,
			("Expected function to throw an error containing %q, but it %s"):format(
				messageSubstring,
				err and ("threw: %s"):format(err) or "did not throw."
			),
			("Expected function to never throw an error containing %q, but it threw: %s"):format(
				messageSubstring,
				tostring(err)
			)
		)
	else
		message = formatMessage(self.successCondition,
			"Expected function to throw an error, but it did not throw.",
			("Expected function to succeed, but it threw an error: %s"):format(
				tostring(err)
			)
		)
	end

	assertLevel(result, message, 3)
	self:_resetModifiers()

	return self
end

return Expectation


local Expectation = require(script.Parent.Expectation)
local checkMatcherNameCollisions = Expectation.checkMatcherNameCollisions

local function copy(t)
	local result = {}

	for key, value in pairs(t) do
		result[key] = value
	end

	return result
end

local ExpectationContext = {}
ExpectationContext.__index = ExpectationContext

function ExpectationContext.new(parent)
	local self = {
		_extensions = parent and copy(parent._extensions) or {},
	}

	return setmetatable(self, ExpectationContext)
end

function ExpectationContext:startExpectationChain(...)
	return Expectation.new(...):extend(self._extensions)
end

function ExpectationContext:extend(config)
	for key, value in pairs(config) do
		assert(self._extensions[key] == nil, string.format("Cannot reassign %q in expect.extend", key))
		assert(checkMatcherNameCollisions(key), string.format("Cannot overwrite matcher %q; it already exists", key))

		self._extensions[key] = value
	end
end

return ExpectationContext