local easing = require("../easing")
local test = require("../../../../tests/test")
local tween = require("../tween")

local createTween = tween.createTween
local scheduler = tween.scheduler

test("should have expected api", function()
	local tween = createTween(0)

	assert(type(tween.setPosition) == "function", "missing setPosition")
	assert(type(tween.setGoal) == "function", "missing setGoal")

	assert(type(tween.getPosition) == "function", "missing getPosition")
	assert(type(tween.getFrom) == "function", "missing getFrom")
	assert(type(tween.getGoal) == "function", "missing getGoal")

	assert(type(tween.onChange) == "function", "missing onChange")
	assert(type(tween.onComplete) == "function", "missing onComplete")

	assert(type(tween.step) == "function", "missing step")
	assert(type(tween.idle) == "function", "missing idle")
	assert(type(tween.configure) == "function", "missing configure")

	assert(type(tween.start) == "function", "missing start")
	assert(type(tween.stop) == "function", "missing stop")
	assert(type(tween.destroy) == "function", "missing destroy")
end)

test("should get properties", function()
	local tween = createTween(vector.one)

	assert(tween:getPosition() == vector.one, "invalid position")
	assert(tween:getFrom() == vector.one, "invalid from")
	assert(tween:getGoal() == vector.one, "invalid target")
	assert(tween:idle(), "invalid complete")
end)

test("should update properties", function()
	local tween = createTween(vector.zero)

	tween:setPosition(vector.one)
	tween:setGoal(vector.one)

	assert(tween:getPosition() == vector.one, "invalid position")
	assert(tween:getFrom() == tween:getPosition(), "invalid from")
	assert(tween:getGoal() == vector.one, "invalid target")
	assert(not tween:idle(), "invalid complete")
end)

test("should handle tables", function()
	local tween = createTween({ 0, 0, 0 })
	local initialPosition = tween:getPosition()
	local initialGoal = tween:getGoal()

	tween:setPosition({ 0, 0, 0 })
	tween:setGoal({ 0, 0, 0 })

	assert(tween:getPosition() == initialPosition, "unnecessary position copy")
	assert(tween:getGoal() == initialGoal, "unnecessary goal copy")

	tween:setPosition({ [1] = 1, [3] = 1 })
	tween:setGoal({ [1] = 1, [3] = 1 })

	assert(table.concat(tween:getPosition()) == "101", "did not merge position")
	assert(table.concat(tween:getGoal()) == "101", "did not merge goal")
end)

test("should complete if duration passes", function()
	local result
	local tween = createTween(vector.one, { duration = 1 })

	tween:onComplete(function(value)
		result = value
	end)
	tween:setPosition(vector.zero)
	tween:step(0.5)

	assert(not tween:idle(), "invalid complete")
	assert(not result, "invalid result")
	assert(tween:getPosition() == vector.one / 2, "invalid position")
	assert(tween:getFrom() == vector.zero, "invalid from")

	tween:step(0.5)

	assert(tween:idle(), "invalid complete")
	assert(result == vector.one, "invalid result")
	assert(tween:getPosition() == vector.one, "invalid position")
	assert(tween:getFrom() == vector.zero, "invalid from")
end)

test("should start from new position", function()
	local result
	local tween = createTween(vector.one, { duration = 1 })

	tween:onComplete(function(value)
		result = value
	end)

	for _ = 1, 2 do
		tween:setPosition(vector.zero)
		tween:step(0.5)

		assert(not tween:idle(), "invalid complete")
		assert(not result, "invalid result")
		assert(tween:getPosition() == vector.one / 2, "invalid position")
		assert(tween:getFrom() == vector.zero, "invalid from")
	end

	tween:step(0.5)

	assert(tween:idle(), "invalid complete")
	assert(result == vector.one, "invalid result")
	assert(tween:getPosition() == vector.one, "invalid position")
	assert(tween:getFrom() == vector.zero, "invalid from")
end)

test("should connect while active", function()
	local tweens = {}
	for index = 1, 10 do
		tweens[index] = createTween(vector.zero, { start = true, duration = 1 })
	end
	assert(#scheduler.states == 0, "start with 0 connections")

	for _ = 1, 2 do
		for _, tween in tweens do
			tween:setGoal(tween:getGoal() + vector.one)
		end
		assert(#scheduler.states == #tweens, "connect on update")

		scheduler.step(0.5)
		assert(#scheduler.states == #tweens, "stay connected on partial step")

		scheduler.step(0.5)
		assert(#scheduler.states == 0, "disconnect on complete")
	end
end)

test("should repeat", function()
	local cases = {
		[3] = { 0.5, 0, 0.5, 0, 0.5, 1 },
		[4] = { 0.5, 0, 0.5, 0, 0.5, 0, 0.5, 1 },
	}
	local result
	local tween = createTween(vector.one, { duration = 1 })

	tween:onComplete(function(value)
		result = value
	end)

	for repeats, case in cases do
		tween:configure({ position = vector.zero, repeats = repeats })

		for i = 1, repeats * 2 do
			tween:step(0.5)
			assert(
				tween:getPosition() == vector.one * case[i],
				`{repeats} expected {case[i]}, got {tween:getPosition()}`
			)
		end

		assert(tween:idle(), "did not complete")
		assert(result == vector.one * case[#case], `{repeats} expected {case[#case]}, got {result}`)
	end
end)

test("should repeat in reverse", function()
	local cases = {
		[3] = { 0.5, 1, 0.5, 0, 0.5, 1 },
		[4] = { 0.5, 1, 0.5, 0, 0.5, 1, 0.5, 0 },
	}
	local result
	local tween = createTween(vector.one, { duration = 1 })

	tween:onComplete(function(value)
		result = value
	end)

	for repeats, case in cases do
		tween:configure({ position = vector.zero, repeats = repeats, reverses = true })

		for i = 1, repeats * 2 do
			tween:step(0.5)
			assert(
				tween:getPosition() == vector.one * case[i],
				`{repeats} expected {case[i]}, got {tween:getPosition()}`
			)
		end

		assert(tween:idle(), "did not complete")
		assert(result == vector.one * case[#case], `{repeats} expected {case[#case]}, got {result}`)
	end
end)

test("should wake after update on complete", function()
	local changes = 0
	local completes = 0

	local tween = createTween(0, { duration = 1 })
	tween:setGoal(1)
	tween:onChange(function()
		changes += 1
	end)
	tween:onComplete(function()
		completes += 1
		if tween:getGoal() ~= 0 then
			tween:setGoal(0)
			assert(not tween:idle(), "did not uncomplete")
		end
	end)

	tween:step(0.5)
	assert(changes == 1, "did not step")
	assert(completes == 0, "completed early")

	tween:step(0.5)
	assert(changes == 2, "did not step")
	assert(completes == 1, "did not complete")

	tween:step(0.5)
	assert(changes == 3, "did not step")
	assert(completes == 1, "completed early")

	tween:step(0.5)
	assert(changes == 4, "did not step")
	assert(completes == 2, "did not finally complete")
	assert(tween:idle(), "did not complete")

	tween:step(0.5)
	assert(changes == 4, "stepped after complete")
	assert(completes == 2, "completed after complete")
end)

test("should have correct start and end", function()
	local tween = createTween(1)

	for name in pairs(easing) do
		tween:configure({ position = 0, easing = name })
		local x0 = tween:step(0)
		local x1 = tween:step(1)
		assert(x0 == 0, `{name}(0) should be 0, got {x0}`)
		assert(x1 == 1, `{name}(1) should be 1, got {x1}`)
	end
end)

test("should fire change with full state", function()
	local state = {}
	local tween = createTween({ 0, 0, 0 })
	tween:onChange(function(newState)
		state = newState
	end)

	tween:setPosition({ 1, 1, 1 })
	assert(table.concat(state) == "111", "did not fire change with position")

	tween:setPosition({ [1] = 2, [3] = 2 })
	assert(table.concat(state) == "212", "did not fire change with position merge")
end)

test("should not be nan when step and duration are zero", function()
	local tween = createTween(0, { duration = 0 })
	tween:setGoal(1)
	tween:step(0)
	assert(tween:getPosition() == tween:getPosition(), "position is nan on zero step")
end)

return {}


local interpolate = require("../../utils/interpolate")
local test = require("../../../../../tests/test")

local cases: { { any } } = {
	{ 0, 1, 0.5 },
	{ vector.zero, vector.one, vector.create(0.5, 0.5, 0.5) },
	{ { 0, 0, 0 }, { 1, 1, 1 }, { 0.5, 0.5, 0.5 } },
}

if game then
	table.insert(cases, { Color3.new(), Color3.new(1, 1, 1) }) -- Just check if the result is a Color3
	table.insert(cases, { UDim2.new(0, 0, 0, 0), UDim2.new(1, 100, 1, 100), UDim2.new(0.5, 50, 0.5, 50) })
	table.insert(cases, { UDim.new(0, 0), UDim.new(1, 100), UDim.new(0.5, 50) })
	table.insert(cases, { Rect.new(0, 0, 0, 0), Rect.new(100, 100, 200, 200), Rect.new(50, 50, 100, 100) })
	table.insert(cases, { CFrame.new(0, 0, 0), CFrame.new(10, 10, 10), CFrame.new(5, 5, 5) })
end

for _, case in cases do
	local a, b, expected = unpack(case)

	test(`should return correct type for {typeof(a)}`, function()
		local result = interpolate(a, b, 0.5)
		assert(typeof(result) == typeof(a), `Expected type {typeof(a)}, got {typeof(result)}`)
	end)

	if not expected then
		continue
	end

	test(`should return correct midpoint for {typeof(a)}`, function()
		local result = interpolate(a, b, 0.5)

		if type(result) == "table" then
			for key, value in expected do
				assert(result[key] == value, `Expected {value} at index {key}, got {result[key]}`)
			end
		else
			assert(result == expected, `Expected {expected}, got {result}`)
		end
	end)
end

test("should return original table if nothing changed", function()
	local input = { 1 }
	assert(interpolate(input, { 2 }, 0) == input, "Expected the same table for step 0")
	assert(interpolate(input, { 1 }, 1) == input, "Expected the same table for step to same value")
end)

return {}


local oklab = require("../../utils/oklab")
local test = require("../../../../../tests/test")

local function testColors(callback: (vector) -> ())
	for r = 0, 4 do
		for g = 0, 4 do
			for b = 0, 4 do
				callback(vector.create(r, g, b) / 4)
			end
		end
	end
end

local function fuzzyEqual(a: vector, b: vector): boolean
	local abs = vector.abs(a - b)
	return math.max(abs.x, abs.y, abs.z) < 1e-4
end

test("should be reversible lab to rgb", function()
	testColors(function(vec)
		local lab = oklab.fromSRGB(vec)
		local rgb = oklab.toSRGB(lab)
		assert(fuzzyEqual(rgb, vec), `expected {rgb} to be equal to {vec}`)
	end)
end)

test("should be reversible rgb to lab", function()
	testColors(function(vec)
		local rgb = oklab.toSRGB(vec)
		local lab = oklab.fromSRGB(rgb)
		assert(fuzzyEqual(lab, vec), `expected {lab} to be equal to {vec}`)
	end)
end)

test("should not be nan for negative numbers", function()
	local lab = oklab.fromSRGB(-vector.one)
	assert(lab == lab, "expected output to not be NaN")
end)

return {}


local spring = require("./spring")

type SpringOptions = spring.SpringOptions

return {
	default = { tension = 170, friction = 26 } :: SpringOptions,
	gentle = { tension = 120, friction = 14 } :: SpringOptions,
	wobbly = { tension = 180, friction = 12 } :: SpringOptions,
	stiff = { tension = 210, friction = 20 } :: SpringOptions,
	slow = { tension = 280, friction = 60 } :: SpringOptions,
	molasses = { tension = 280, friction = 120 } :: SpringOptions,
	figmaGentle = { tension = 100, friction = 15 } :: SpringOptions,
	figmaQuick = { tension = 300, friction = 20 } :: SpringOptions,
	figmaBouncy = { tension = 600, friction = 15 } :: SpringOptions,
	figmaSlow = { tension = 80, friction = 20 } :: SpringOptions,
}


local easing = require("./easing")
local interpolate = require("./utils/interpolate")
local merge = require("./utils/merge")
local scheduler = require("./utils/scheduler")
local signal = require("./utils/signal")
local types = require("./types")

type Animatable = types.Animatable
type Easing = easing.Easing

export type TweenState<Value = any> = {
	position: Value,
	from: Value,
	goal: Value,
	easingFunction: (number) -> number,
	duration: number,
	repeats: number,
	reverses: boolean,
	elapsed: number,
	started: boolean,
	complete: boolean,
	onChange: signal.SubscribeSignal<(Value, number)>,
	fireChange: signal.FireSignal<(Value, number)>,
	onComplete: signal.SubscribeSignal<Value>,
	fireComplete: signal.FireSignal<Value>,
}

export type TweenOptions<Value = any> = {
	start: boolean?,
	easing: Easing?,
	duration: number?,
	repeats: number?,
	reverses: boolean?,
	position: Value?,
}

export type Tween<Value = any> = {
	state: TweenState<Value>,

	setPosition: (self: Tween<Value>, value: Value) -> (),
	setGoal: (self: Tween<Value>, value: Value, options: TweenOptions<Value>?) -> (),

	getPosition: (self: Tween<Value>) -> Value,
	getFrom: (self: Tween<Value>) -> Value,
	getGoal: (self: Tween<Value>) -> Value,

	onChange: (self: Tween<Value>, callback: (value: Value, deltaTime: number) -> ()) -> () -> (),
	onComplete: (self: Tween<Value>, callback: (value: Value) -> ()) -> () -> (),

	step: (self: Tween<Value>, deltaTime: number) -> Value,
	idle: (self: Tween<Value>) -> boolean,
	configure: (self: Tween<Value>, options: TweenOptions<Value>) -> (),

	start: (self: Tween<Value>) -> (),
	stop: (self: Tween<Value>) -> (),
	destroy: (self: Tween<Value>) -> (),

	resumeFromCurrentPosition: (self: Tween<Value>) -> (),
}

local Tween = {} :: Tween<Animatable>
(Tween :: any).__index = Tween

local function getAlpha(progress: number, repeats: number, reverses: boolean): number
	if repeats < 0 then
		repeats = math.huge
	end

	if repeats > 1 and progress >= 1 then
		return if reverses then math.abs((progress - 1) % 2 - 1) elseif progress < repeats then progress % 1 else 1
	end

	return progress
end

local tweenScheduler = scheduler("Tween", function<T>(state: TweenState<T>, dt, remove): T
	if state.complete then
		remove(state)
		return state.position
	end

	state.elapsed += dt

	local progress = math.clamp(state.elapsed / state.duration, 0, state.repeats)

	if math.isnan(progress) then
		progress = state.repeats
	end

	local alpha = getAlpha(progress, state.repeats, state.reverses)
	local alphaEased = if alpha > 0 and alpha < 1 then state.easingFunction(alpha) else alpha
	local value = interpolate(state.from, state.goal, alphaEased)

	state.position = value
	state.fireChange(value, dt)

	if progress == state.repeats then
		remove(state)
		state.complete = true
		state.fireComplete(value)
	end

	return value
end)

local function createTween<Value>(initialValue: Value, inputOptions: TweenOptions<Value>?): Tween<Value>
	local options: TweenOptions<Value> = inputOptions or {}
	local position = options.position or initialValue

	local onChange, fireChange = signal()
	local onComplete, fireComplete = signal()

	local state: TweenState<Value> = {
		position = position,
		from = position,
		goal = position,
		easingFunction = easing.linear,
		duration = 1,
		repeats = 1,
		reverses = false,
		elapsed = 0,
		started = false,
		complete = true,
		onChange = onChange,
		fireChange = fireChange,
		onComplete = onComplete,
		fireComplete = fireComplete,
	}

	local self: Tween<Value> = setmetatable({
		state = state,
	}, Tween) :: any

	self:configure(options)

	if options.start then
		self:start()
	end

	return self
end

function Tween:resumeFromCurrentPosition()
	local state = self.state

	state.complete = false
	state.elapsed = 0
	state.from = state.position

	if state.started then
		tweenScheduler.add(state)
	end
end

function Tween:start()
	self.state.started = true

	if not self.state.complete then
		tweenScheduler.add(self.state)
	end
end

function Tween:stop()
	self.state.started = false
	tweenScheduler.remove(self.state)
end

function Tween:idle(): boolean
	return self.state.complete
end

function Tween:step(dt: number): Animatable
	return tweenScheduler.update(self.state, dt)
end

function Tween:configure(options: TweenOptions)
	local state = self.state

	state.easingFunction = easing[options.easing] or state.easingFunction
	state.duration = options.duration or state.duration
	state.repeats = options.repeats or state.repeats
	state.reverses = options.reverses or state.reverses

	if options.position then
		self:setPosition(options.position)
	end

	if not state.complete and state.elapsed ~= 0 then
		self:resumeFromCurrentPosition()
	end
end

function Tween:getPosition(): Animatable
	return self.state.position
end

function Tween:getFrom(): Animatable
	return self.state.from
end

function Tween:getGoal(): Animatable
	return self.state.goal
end

function Tween:setPosition(value: Animatable)
	local position = if type(value) == "table" then merge(self.state.position, value) else value

	if self.state.position ~= position then
		self.state.position = position
		self:resumeFromCurrentPosition()
		self.state.fireChange(position, 0)
	end
end

function Tween:setGoal(value: Animatable, options: TweenOptions?)
	if options then
		self:configure(options)
	end

	local goal = if type(value) == "table" then merge(self.state.goal, value) else value

	if self.state.goal ~= goal then
		self.state.goal = goal
		self:resumeFromCurrentPosition()
	end
end

function Tween:onChange(callback: (value: Animatable, deltaTime: number) -> ()): () -> ()
	return self.state.onChange(callback)
end

function Tween:onComplete(callback: (value: Animatable) -> ()): () -> ()
	return self.state.onComplete(callback)
end

function Tween:destroy()
	self:stop()
end

return {
	scheduler = tweenScheduler,
	createTween = createTween,
}


export type AnimatablePrimitive = number | vector | Vector3

export type AnimatableType = AnimatablePrimitive | Vector2 | CFrame | Color3 | UDim | UDim2 | Rect

export type Animatable = AnimatableType | { [any]: AnimatablePrimitive }

return {}


local merge = require("./merge")
local oklab = require("./oklab")

export type Components = { number | vector | Vector3 }

export type Intermediate<T = any> = {
	components: Components,
	value: T,
	dirty: boolean?,
	encode: (components: Components, value: T) -> (),
	decode: (components: Components) -> T,
}

local encoders: { [string]: (Components, any) -> () } = {
	number = function(components: Components, value: number)
		components[1] = value
	end,
	vector = function(components: Components, value: vector)
		components[1] = value
	end,
	Vector3 = function(components: Components, value: Vector3)
		components[1] = value
	end,
	table = function(components: Components, value: { [any]: any })
		for k, v in value do
			components[k] = v
		end
	end,
	Vector2 = function(components: Components, value: Vector2)
		components[1] = vector.create(value.X, value.Y, 0)
	end,
	CFrame = function(components: Components, value: CFrame)
		components[1] = value.Position
		components[2] = value.XVector
		components[3] = value.YVector
		components[4] = value.ZVector
	end,
	Color3 = function(components: Components, value: Color3)
		components[1] = oklab.fromSRGB(vector.create(value.R, value.G, value.B))
	end,
	UDim = function(components: Components, value: UDim)
		components[1] = vector.create(value.Scale, value.Offset, 0)
	end,
	UDim2 = function(components: Components, value: UDim2)
		components[1] = vector.create(value.X.Scale, value.X.Offset, value.Y.Scale)
		components[2] = value.Y.Offset
	end,
	Rect = function(components: Components, value: Rect)
		components[1] = vector.create(value.Min.X, value.Min.Y, value.Max.X)
		components[2] = value.Max.Y
	end,
}

local decoders: { [string]: (components: { any }) -> any } = {
	number = function(components: { number }): number
		return components[1]
	end,
	vector = function(components: { vector }): vector
		return components[1]
	end,
	Vector3 = function(components: { Vector3 }): Vector3
		return components[1]
	end,
	table = function(components: Components): Components
		return table.clone(components)
	end,
	Vector2 = function(components: { vector }): Vector2
		local xy = components[1]
		return Vector2.new(xy.x, xy.y)
	end,
	CFrame = function(components: { Vector3 }): CFrame
		return CFrame.fromMatrix(components[1], components[2], components[3], components[4]):Orthonormalize()
	end,
	Color3 = function(components: { vector }): Color3
		local rgb = vector.max(oklab.toSRGB(components[1]), vector.zero)
		return Color3.new(rgb.x, rgb.y, rgb.z)
	end,
	UDim = function(components: { vector }): UDim
		local so = components[1]
		return UDim.new(so.x, math.round(so.y))
	end,
	UDim2 = function(components: { vector | number }): UDim2
		local abc = components[1] :: vector
		local d = components[2] :: number
		return UDim2.new(abc.x, math.round(abc.y), abc.z, math.round(d))
	end,
	Rect = function(components: { vector | number }): Rect
		local abc = components[1] :: vector
		local d = components[2] :: number
		return Rect.new(abc.x, abc.y, abc.z, d)
	end,
}

local function create<T>(value: T): Intermediate<T>
	local kind = typeof(value)
	local components: Components = {}
	local encode = encoders[kind]
	local decode = decoders[kind]

	if not encode or not decode then
		error(`Unsupported type {kind} for Ripple animation value`)
	end

	encode(components, value)

	return {
		components = components,
		value = value,
		encode = encode,
		decode = decode,
	}
end

local function copy<T>(intermediate: Intermediate<T>): Intermediate<T>
	return {
		components = table.clone(intermediate.components),
		value = intermediate.value,
		encode = intermediate.encode,
		decode = intermediate.decode,
	}
end

local function zero<T>(intermediate: Intermediate<T>): Intermediate<T>
	local components = intermediate.components

	for key in components do
		components[key] *= 0 :: any
	end

	intermediate.dirty = true

	return intermediate
end

local function recomputeValue<T>(intermediate: Intermediate<T>): T
	local value = intermediate.decode(intermediate.components)
	intermediate.value = value
	intermediate.dirty = nil
	return value
end

local function getValue<T>(intermediate: Intermediate<T>): T
	return if intermediate.dirty then recomputeValue(intermediate) else intermediate.value
end

local function setValue<T>(intermediate: Intermediate<T>, value: T): boolean
	local currentValue = getValue(intermediate)

	if type(value) == "table" then
		value = merge(currentValue, value)
	end

	if value == currentValue then
		return false
	end

	intermediate.encode(intermediate.components, value)
	intermediate.value = value
	intermediate.dirty = nil

	return true
end

local function setValueFast<T>(intermediate: Intermediate<T>, encoded: Intermediate<T>, decoded: T)
	for key, component in encoded.components do
		intermediate.components[key] = component
	end

	intermediate.value = decoded
	intermediate.dirty = nil
end

local function addValue<T>(intermediate: Intermediate<T>, value: T)
	local components: Components = {}

	intermediate.encode(components, value)

	for key, component in components do
		intermediate.components[key] += component :: any
	end

	intermediate.dirty = true
end

return {
	create = create,
	copy = copy,
	zero = zero,
	setValue = setValue,
	setValueFast = setValueFast,
	addValue = addValue,
	getValue = getValue,
	recomputeValue = recomputeValue,
}


local oklab = require("./oklab")
local types = require("../types")

type Animatable = types.Animatable

local function interpolate<T>(from: any, to: any, alpha: number): any
	local kind = typeof(from)

	if kind == "number" or kind == "vector" or kind == "Vector3" or kind == "Vector2" then
		return from + (to - from) * alpha
	elseif kind == "Color3" then
		local a = oklab.fromSRGB(vector.create(from.R, from.G, from.B))
		local b = oklab.fromSRGB(vector.create(to.R, to.G, to.B))
		local v = vector.max(oklab.toSRGB(a + (b - a) * alpha), vector.zero)

		return Color3.new(v.x, v.y, v.z)
	elseif kind == "UDim2" or kind == "CFrame" then
		return from:Lerp(to, alpha)
	elseif kind == "UDim" then
		return UDim.new(math.lerp(from.Scale, to.Scale, alpha), math.lerp(from.Offset, to.Offset, alpha))
	elseif kind == "Rect" then
		return Rect.new(
			math.lerp(from.Min.X, to.Min.X, alpha),
			math.lerp(from.Min.Y, to.Min.Y, alpha),
			math.lerp(from.Max.X, to.Max.X, alpha),
			math.lerp(from.Max.Y, to.Max.Y, alpha)
		)
	elseif kind == "table" then
		local result: { number | vector } = from

		for key, b in to do
			local a = from[key]

			if not a then
				continue
			end

			local value = a + (b - a) * alpha

			if result[key] ~= value then
				if result == from then
					result = table.clone(from)
				end
				result[key] = value
			end
		end

		return result
	else
		error(`Unsupported type for interpolation: {kind}`)
	end
end

return interpolate :: <T>(from: T, to: T, alpha: number) -> T


local function merge<T>(target: any, source: any): T
	local result = target

	for key, value in source do
		if target[key] == value then
			continue
		end

		if result == target then
			result = table.clone(target)
		end

		result[key] = value
	end

	return result
end

return merge


export type Scheduler<State> = {
	states: { State },
	update: (state: State, time: number) -> any,
	step: (deltaTime: number) -> (),
	add: (state: State) -> (),
	remove: (state: State) -> (),
	clear: () -> (),
}

local function scheduler<State>(
	label: string,
	updater: (state: State, time: number, remove: (State) -> ()) -> any
): Scheduler<State>
	local states: { State } = {}
	local count = 0
	local connection: RBXScriptConnection?

	local function remove(state: State)
		local index = table.find(states, state)

		if index then
			states[index] = states[count]
			states[count] = nil
			count -= 1
		end
	end

	local function step(deltaTime: number)
		for index = count, 1, -1 do
			updater(states[index], deltaTime, remove)
		end
	end

	local function update(state: State, time: number)
		return updater(state, time, remove)
	end

	local function add(state: State)
		if table.find(states, state) then
			return
		end

		table.insert(states, state)
		count += 1

		if not game or connection then
			return
		end

		connection = game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
			debug.profilebegin(`Ripple{label}`)
			step(deltaTime)
			debug.profileend()

			if connection and count == 0 then
				connection:Disconnect()
				connection = nil
			end
		end)
	end

	local function clear()
		table.clear(states)
		count = 0
	end

	return {
		states = states,
		update = update,
		step = step,
		add = add,
		remove = remove,
		clear = clear,
	}
end

return scheduler


local spawn = require("./spawn")

export type FireSignal<T...> = (T...) -> ()
export type SubscribeSignal<T...> = (callback: FireSignal<T...>) -> () -> ()

local function signal<T...>(): (SubscribeSignal<T...>, FireSignal<T...>)
	local listeners: { (...any) -> () } = {}
	local count = 0

	local function subscribe(callback: (T...) -> ()): () -> ()
		table.insert(listeners, callback)
		count += 1

		return function()
			local index = table.find(listeners, callback)

			if index then
				listeners[index] = listeners[count]
				listeners[count] = nil
				count -= 1
			end
		end
	end

	local function fire(...: T...)
		for index = count, 1, -1 do
			spawn(listeners[index], ...)
		end
	end

	return subscribe, fire
end

return signal


local freeThread: thread?

local function run<T...>(thread: thread, fn: (T...) -> (), ...: T...)
	freeThread = nil
	fn(...)
	freeThread = thread
end

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

local function spawn<T...>(callback: (T...) -> (), ...: T...)
	local thread = freeThread or task.spawn(runner)
	task.spawn(thread, thread, callback, ...)
end

local function call<T...>(callback: (T...) -> (), ...: T...)
	callback(...)
end

return (if task then spawn else call) :: typeof(spawn)


local Ripple = require("../../Ripple")
local Vide = require("../../Vide")
local test = require("../../../../tests/test")
local useMotion = require("../useMotion")

test("should update binding when goal is set", function()
	local src, motion

	Vide.root(function()
		src, motion = useMotion(0)
		motion:setGoal(1, { tween = { duration = 0 } })
	end)

	Ripple.motionScheduler.step(1)
	assert(src() == 1, `Expected motion to reach goal, got {src()}`)
end)

return {}


local Ripple = require("../../Ripple")
local Vide = require("../../Vide")
local test = require("../../../../tests/test")
local useTween = require("../useTween")

test("should update binding when goal is set", function()
	local src, tween

	Vide.root(function()
		src, tween = useTween(0)
		tween:setGoal(1, { tween = { duration = 0 } })
	end)

	Ripple.tweenScheduler.step(1)
	assert(src() == 1, `Expected tween to reach goal, got {src()}`)
end)

return {}


local Ripple = require(script.Parent.Parent.Ripple)
local Vide = require(script.Parent.Parent.Vide)

type Animatable = Ripple.Animatable
type Motion<T> = Ripple.Motion<T>
type MotionOptions<T> = Ripple.MotionOptions<T>

local function useMotion<T>(initialValue: T, initialOptions: MotionOptions<T>?): (() -> T, Motion<T>)
	local motion = Ripple.createMotion(initialValue, initialOptions)
	local value = Vide.source(initialValue)

	if not (initialOptions and initialOptions.start == false) then
		motion:start()
	end

	local disconnect = motion:onChange(value)

	Vide.cleanup(function()
		disconnect()
		motion:stop()
	end)

	return value, motion
end

return useMotion


local Ripple = require(script.Parent.Parent.Ripple)
local Vide = require(script.Parent.Parent.Vide)

type Animatable = Ripple.Animatable
type Tween<T> = Ripple.Tween<T>
type TweenOptions<T> = Ripple.TweenOptions<T>

local function useTween<T>(initialValue: T, initialOptions: TweenOptions<T>?): (() -> T, Tween<T>)
	local tween = Ripple.createTween(initialValue, initialOptions)
	local value = Vide.source(initialValue)

	if not (initialOptions and initialOptions.start == false) then
		tween:start()
	end

	local disconnect = tween:onChange(value)

	Vide.cleanup(function()
		disconnect()
		tween:stop()
	end)

	return value, tween
end

return useTween


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local React = require(ReplicatedStorage["@rbxts-js"].React)

type GraphProps = {
	name: string,
	precision: number,
	step: (dt: number) -> number,
	idle: () -> boolean,
	showRuntime: boolean?,
}

local MAX_POINTS = 50

local function Graph(props: GraphProps)
	local pathRef = React.useRef((nil :: any) :: Path2D)
	local runtime, setRuntime = React.useState(0)

	React.useEffect(function()
		local path = pathRef.current
		local samples: { number } = { props.step(0) }
		local controlPoints: { Path2DControlPoint } = table.create(MAX_POINTS)

		for _ = 1, 2000 do
			table.insert(samples, props.step(props.precision))
			if props.idle() then
				break
			end
		end

		local sampleCount = #samples
		local pointCount = math.min(sampleCount, MAX_POINTS)

		for pointIndex = 0, pointCount - 1 do
			local sampleIndex = math.floor(pointIndex / (pointCount - 1) * (sampleCount - 1))
			local value = samples[sampleIndex + 1]
			local point = Path2DControlPoint.new(UDim2.fromScale(pointIndex / (pointCount - 1), value))
			table.insert(controlPoints, point)
		end

		path:SetControlPoints(controlPoints)

		if props.showRuntime then
			setRuntime(#samples * props.precision)
		end
	end, { props.step })

	return React.createElement("Frame", {
		BackgroundColor3 = Color3.fromRGB(0, 0, 0),
		BackgroundTransparency = 0.4,
	}, {
		Corner = React.createElement("UICorner"),
		Padding = React.createElement("UIPadding", {
			PaddingTop = UDim.new(0, 8),
			PaddingBottom = UDim.new(0, 4),
			PaddingLeft = UDim.new(0, 8),
			PaddingRight = UDim.new(0, 8),
		}),
		Text = React.createElement("TextLabel", {
			Text = if runtime > 0 then `{props.name} ({string.format("%.2f", runtime)}s)` else props.name,
			TextSize = 7,
			TextColor3 = Color3.fromRGB(255, 255, 255),
			TextXAlignment = Enum.TextXAlignment.Center,
			TextYAlignment = Enum.TextYAlignment.Bottom,
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(1, 1),
		}),
		PathFrame = React.createElement("Frame", {
			BackgroundTransparency = 1,
			Size = UDim2.new(1, 0, 1, -12),
		}, {
			RulerTop = React.createElement("Frame", {
				BackgroundColor3 = Color3.fromRGB(255, 255, 255),
				BackgroundTransparency = 0.8,
				Size = UDim2.new(1, 0, 0, 1),
			}),
			RulerBottom = React.createElement("Frame", {
				BackgroundColor3 = Color3.fromRGB(255, 255, 255),
				BackgroundTransparency = 0.8,
				Size = UDim2.new(1, 0, 0, 1),
				Position = UDim2.fromScale(0, 1),
			}),
			Path = React.createElement("Path2D", {
				ref = pathRef,
				Color3 = Color3.fromRGB(180, 180, 180),
				Thickness = 2,
				ZIndex = 2,
			}),
		}),
	})
end

return Graph


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Graph = require(script.Parent.Parent.components.Graph)
local React = require(ReplicatedStorage["@rbxts-js"].React)
local ReactRoblox = require(ReplicatedStorage["@rbxts-js"].ReactRoblox)
local Ripple = require(ReplicatedStorage.packages.ripple.src)

type EasingFunctionProps = {
	easing: Ripple.Easing,
}

local function EasingFunction(props: EasingFunctionProps)
	local tween = React.useMemo(function()
		local source = Ripple.createTween(1, { duration = 1, easing = props.easing })
		source:setGoal(0)
		return source
	end, { props.easing })

	return React.createElement(Graph, {
		name = props.easing :: string,
		precision = 0.005,
		step = function(dt)
			return tween:step(dt)
		end,
		idle = function()
			return tween:idle()
		end,
	})
end

local function Story()
	local children: { [string]: React.ReactNode } = {}

	for name: Ripple.Easing in pairs(Ripple.easing) do
		children[name] = React.createElement(EasingFunction, {
			easing = name,
			duration = 1,
		})
	end

	return React.createElement("Frame", {
		BackgroundTransparency = 1,
		Size = UDim2.fromScale(1, 1),
	}, {
		GridLayout = React.createElement("UIGridLayout", {
			CellSize = UDim2.new(1 / 5, -8 * 5, 1 / 5, -8 * 5),
			CellPadding = UDim2.fromOffset(8, 8),
			HorizontalAlignment = Enum.HorizontalAlignment.Center,
			VerticalAlignment = Enum.VerticalAlignment.Center,
			FillDirection = Enum.FillDirection.Horizontal,
		}),
	}, children)
end

return function(container: Instance)
	local root = ReactRoblox.createRoot(container)

	root:render(React.createElement(Story))

	return function()
		root:unmount()
	end
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local React = require(ReplicatedStorage["@rbxts-js"].React)
local ReactRipple = require(ReplicatedStorage.packages["react-ripple"].src)
local ReactRoblox = require(ReplicatedStorage["@rbxts-js"].ReactRoblox)

local function Story()
	local _, tween = ReactRipple.useTween(Color3.new(), { start = false })

	local keypoints = React.useMemo(function()
		local keypoints = {}
		local points = 16

		tween:setPosition(Color3.new(1, 1, 1))
		tween:setGoal(Color3.new(0, 0, 1))

		for point = 0, points do
			local color = tween:getPosition()
			local colorClamped =
				Color3.new(math.clamp(color.R, 0, 1), math.clamp(color.G, 0, 1), math.clamp(color.B, 0, 1))
			table.insert(keypoints, ColorSequenceKeypoint.new(point / points, colorClamped))
			tween:step(1 / points)
		end

		return keypoints
	end, {})

	return React.createElement("Frame", {
		BackgroundColor3 = Color3.new(1, 1, 1),
		Size = UDim2.fromScale(1, 1),
	}, {
		React.createElement("UIGradient", {
			Color = ColorSequence.new(keypoints),
		}),
	})
end

return function(container: Instance)
	local root = ReactRoblox.createRoot(container)

	root:render(React.createElement(Story))

	return function()
		root:unmount()
	end
end


local test = require("./test")

local describeInRoblox = test.describeIf(function()
	return game :: any ~= nil
end)

test.describe("ripple", function()
	require("../packages/ripple/src/__tests__")
end)

describeInRoblox("react-ripple", function()
	require("../packages/react-ripple/src/__tests__")
end)

describeInRoblox("vide-ripple", function()
	require("../packages/vide-ripple/src/__tests__")
end)

test.finish({ verbose = false })

return {}


local DateTime = require("./datetime")
type DateTime = DateTime.DateTime

export type MetadataKind = "file" | "dir" | "symlink"

--[=[
	@interface MetadataPermissions
	@within FS

	Permissions for the given file or directory.

	This is a dictionary that will contain the following values:

	* `readOnly` - If the target path is read-only or not
]=]
export type MetadataPermissions = {
	readOnly: boolean,
}

-- FIXME: We lose doc comments here below in Metadata because of the union type

--[=[
	@interface Metadata
	@within FS

	Metadata for the given file or directory.

	This is a dictionary that will contain the following values:

	* `kind` - If the target path is a `file`, `dir` or `symlink`
	* `exists` - If the target path exists
	* `createdAt` - The timestamp represented as a `DateTime` object at which the file or directory was created
	* `modifiedAt` - The timestamp represented as a `DateTime` object at which the file or directory was last modified
	* `accessedAt` - The timestamp represented as a `DateTime` object at which the file or directory was last accessed
	* `permissions` - Current permissions for the file or directory

	Note that timestamps are relative to the unix epoch, and
	may not be accurate if the system clock is not accurate.
]=]
export type Metadata = {
	kind: MetadataKind,
	exists: true,
	createdAt: DateTime,
	modifiedAt: DateTime,
	accessedAt: DateTime,
	permissions: MetadataPermissions,
} | {
	kind: nil,
	exists: false,
	createdAt: nil,
	modifiedAt: nil,
	accessedAt: nil,
	permissions: nil,
}

--[=[
	@interface WriteOptions
	@within FS

	Options for filesystem APIs what write to files and/or directories.

	This is a dictionary that may contain one or more of the following values:

	* `overwrite` - If the target path should be overwritten or not, in the case that it already exists
]=]
export type WriteOptions = {
	overwrite: boolean?,
}

--[=[
	@class FS

	Built-in library for filesystem access

	### Example usage

	```lua
	local fs = require("@lune/fs")

	-- Reading a file
	local myTextFile: string = fs.readFile("myFileName.txt")

	-- Reading entries (files & dirs) in a directory
	for _, entryName in fs.readDir("myDirName") do
		if fs.isFile("myDirName/" .. entryName) then
			print("Found file " .. entryName)
		elseif fs.isDir("myDirName/" .. entryName) then
			print("Found subdirectory " .. entryName)
		end
	end
	```
]=]
local fs = {}

--[=[
	@within FS
	@tag must_use

	Reads a file at `path`.

	An error will be thrown in the following situations:

	* `path` does not point to an existing file.
	* The current process lacks permissions to read the file.
	* Some other I/O error occurred.

	@param path The path to the file to read
	@return The contents of the file
]=]
function fs.readFile(path: string): string
	return nil :: any
end

--[=[
	@within FS
	@tag must_use

	Reads entries in a directory at `path`.

	An error will be thrown in the following situations:

	* `path` does not point to an existing directory.
	* The current process lacks permissions to read the contents of the directory.
	* Some other I/O error occurred.

	@param path The directory path to search in
	@return A list of files & directories found
]=]
function fs.readDir(path: string): { string }
	return {}
end

--[=[
	@within FS

	Writes to a file at `path`.

	An error will be thrown in the following situations:

	* The file's parent directory does not exist.
	* The current process lacks permissions to write to the file.
	* Some other I/O error occurred.

	@param path The path of the file
	@param contents The contents of the file
]=]
function fs.writeFile(path: string, contents: buffer | string) end

--[=[
	@within FS

	Creates a directory and its parent directories if they are missing.

	An error will be thrown in the following situations:

	* `path` already points to an existing file or directory.
	* The current process lacks permissions to create the directory or its missing parents.
	* Some other I/O error occurred.

	@param path The directory to create
]=]
function fs.writeDir(path: string) end

--[=[
	@within FS

	Removes a file.

	An error will be thrown in the following situations:

	* `path` does not point to an existing file.
	* The current process lacks permissions to remove the file.
	* Some other I/O error occurred.

	@param path The file to remove
]=]
function fs.removeFile(path: string) end

--[=[
	@within FS

	Removes a directory and all of its contents.

	An error will be thrown in the following situations:

	* `path` is not an existing and empty directory.
	* The current process lacks permissions to remove the directory.
	* Some other I/O error occurred.

	@param path The directory to remove
]=]
function fs.removeDir(path: string) end

--[=[
	@within FS
	@tag must_use

	Gets metadata for the given path.

	An error will be thrown in the following situations:

	* The current process lacks permissions to read at `path`.
	* Some other I/O error occurred.

	@param path The path to get metadata for
	@return Metadata for the path
]=]
function fs.metadata(path: string): Metadata
	return nil :: any
end

--[=[
	@within FS
	@tag must_use

	Checks if a given path is a file.

	An error will be thrown in the following situations:

	* The current process lacks permissions to read at `path`.
	* Some other I/O error occurred.

	@param path The file path to check
	@return If the path is a file or not
]=]
function fs.isFile(path: string): boolean
	return nil :: any
end

--[=[
	@within FS
	@tag must_use

	Checks if a given path is a directory.

	An error will be thrown in the following situations:

	* The current process lacks permissions to read at `path`.
	* Some other I/O error occurred.

	@param path The directory path to check
	@return If the path is a directory or not
]=]
function fs.isDir(path: string): boolean
	return nil :: any
end

--[=[
	@within FS

	Moves a file or directory to a new path.

	Throws an error if a file or directory already exists at the target path.
	This can be bypassed by passing `true` as the third argument, or a dictionary of options.
	Refer to the documentation for `WriteOptions` for specific option keys and their values.

	An error will be thrown in the following situations:

	* The current process lacks permissions to read at `from` or write at `to`.
	* The new path exists on a different mount point.
	* Some other I/O error occurred.

	@param from The path to move from
	@param to The path to move to
	@param overwriteOrOptions Options for the target path, such as if should be overwritten if it already exists
]=]
function fs.move(from: string, to: string, overwriteOrOptions: (boolean | WriteOptions)?) end

--[=[
	@within FS

	Copies a file or directory recursively to a new path.

	Throws an error if a file or directory already exists at the target path.
	This can be bypassed by passing `true` as the third argument, or a dictionary of options.
	Refer to the documentation for `WriteOptions` for specific option keys and their values.

	An error will be thrown in the following situations:

	* The current process lacks permissions to read at `from` or write at `to`.
	* Some other I/O error occurred.

	@param from The path to copy from
	@param to The path to copy to
	@param overwriteOrOptions Options for the target path, such as if should be overwritten if it already exists
]=]
function fs.copy(from: string, to: string, overwriteOrOptions: (boolean | WriteOptions)?) end

return fs


--[=[
	@class RegexMatch

	A match from a regular expression.

	Contains the following values:

	- `start` -- The start index of the match in the original string.
	- `finish` -- The end index of the match in the original string.
	- `text` -- The text that was matched.
	- `len` -- The length of the text that was matched.
]=]
local RegexMatch = {
	start = 0,
	finish = 0,
	text = "",
	len = 0,
}

type RegexMatch = typeof(RegexMatch)

--[=[
	@class RegexCaptures

	Captures from a regular expression.
]=]
local RegexCaptures = {}

--[=[
	@within RegexCaptures
	@tag Method

	Returns the match at the given index, if one exists.

	@param index -- The index of the match to get
	@return RegexMatch -- The match, if one exists
]=]
function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch?
	return nil :: any
end

--[=[
	@within RegexCaptures
	@tag Method

	Returns the match for the given named match group, if one exists.

	@param group -- The name of the group to get
	@return RegexMatch -- The match, if one exists
]=]
function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch?
	return nil :: any
end

--[=[
	@within RegexCaptures
	@tag Method

	Formats the captures using the given format string.

	### Example usage

	```lua
	local regex = require("@lune/regex")

	local re = regex.new("(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})")

	local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb.");
	assert(caps ~= nil, "Example pattern should match example text")

	local formatted = caps:format("year=$year, month=$month, day=$day")
	print(formatted) -- "year=2010, month=03, day=14"
	```

	@param format -- The format string to use
	@return string -- The formatted string
]=]
function RegexCaptures.format(self: RegexCaptures, format: string): string
	return nil :: any
end

export type RegexCaptures = typeof(RegexCaptures)

local Regex = {}

--[=[
	@within Regex
	@tag Method

	Check if the given text matches the regular expression.

	This method may be slightly more efficient than calling `find`
	if you only need to know if the text matches the pattern.

	@param text -- The text to search
	@return boolean -- Whether the text matches the pattern
]=]
function Regex.isMatch(self: Regex, text: string): boolean
	return nil :: any
end

--[=[
	@within Regex
	@tag Method

	Finds the first match in the given text.

	Returns `nil` if no match was found.

	@param text -- The text to search
	@return RegexMatch? -- The match object
]=]
function Regex.find(self: Regex, text: string): RegexMatch?
	return nil :: any
end

--[=[
	@within Regex
	@tag Method

	Finds all matches in the given text as a `RegexCaptures` object.

	Returns `nil` if no matches are found.

	@param text -- The text to search
	@return RegexCaptures? -- The captures object
]=]
function Regex.captures(self: Regex, text: string): RegexCaptures?
	return nil :: any
end

--[=[
	@within Regex
	@tag Method

	Splits the given text using the regular expression.

	@param text -- The text to split
	@return { string } -- The split text
]=]
function Regex.split(self: Regex, text: string): { string }
	return nil :: any
end

--[=[
	@within Regex
	@tag Method

	Replaces the first match in the given text with the given replacer string.

	@param haystack -- The text to search
	@param replacer -- The string to replace matches with
	@return string -- The text with the first match replaced
]=]
function Regex.replace(self: Regex, haystack: string, replacer: string): string
	return nil :: any
end

--[=[
	@within Regex
	@tag Method

	Replaces all matches in the given text with the given replacer string.

	@param haystack -- The text to search
	@param replacer -- The string to replace matches with
	@return string -- The text with all matches replaced
]=]
function Regex.replaceAll(self: Regex, haystack: string, replacer: string): string
	return nil :: any
end

export type Regex = typeof(Regex)

--[=[
	@class Regex

	Built-in library for regular expressions

	### Example usage

	```lua
	local Regex = require("@lune/regex")

	local re = Regex.new("hello")

	if re:isMatch("hello, world!") then
		print("Matched!")
	end

	local caps = re:captures("hello, world! hello, again!")

	print(#caps) -- 2
	print(caps:get(1)) -- "hello"
	print(caps:get(2)) -- "hello"
	print(caps:get(3)) -- nil
	```
]=]
local regex = {}

--[=[
	@within Regex
	@tag Constructor

	Creates a new `Regex` from a given string pattern.

	### Errors

	This constructor throws an error if the given pattern is invalid.

	@param pattern -- The string pattern to use
	@return Regex -- The new Regex object
]=]
function regex.new(pattern: string): Regex
	return nil :: any
end

return regex


--[=[
	@class Task

	Built-in task scheduler & thread spawning

	### Example usage

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

	-- Waiting for a certain amount of time
	task.wait(1)
	print("Waited for one second")

	-- Running a task after a given amount of time
	task.delay(2, function()
		print("Ran after two seconds")
	end)

	-- Spawning a new task that runs concurrently
	task.spawn(function()
		print("Running instantly")
		task.wait(1)
		print("One second passed inside the task")
	end)

	print("Running after task.spawn yields")
	```
]=]
local task = {}

--[=[
	@within Task

	Stops a currently scheduled thread from resuming.

	@param thread The thread to cancel
]=]
function task.cancel(thread: thread) end

--[=[
	@within Task

	Defers a thread or function to run at the end of the current task queue.

	@param functionOrThread The function or thread to defer
	@return The thread that will be deferred
]=]
function task.defer<T...>(functionOrThread: thread | (T...) -> ...any, ...: T...): thread
	return nil :: any
end

--[=[
	@within Task

	Delays a thread or function to run after `duration` seconds.

	@param functionOrThread The function or thread to delay
	@return The thread that will be delayed
]=]
function task.delay<T...>(
	duration: number,
	functionOrThread: thread | (T...) -> ...any,
	...: T...
): thread
	return nil :: any
end

--[=[
	@within Task

	Instantly runs a thread or function.

	If the spawned task yields, the thread that spawned the task
	will resume, letting the spawned task run in the background.

	@param functionOrThread The function or thread to spawn
	@return The thread that was spawned
]=]
function task.spawn<T...>(functionOrThread: thread | (T...) -> ...any, ...: T...): thread
	return nil :: any
end

--[=[
	@within Task

	Waits for *at least* the given amount of time.

	The minimum wait time possible when using `task.wait` is limited by the underlying OS sleep implementation.
	For most systems this means `task.wait` is accurate down to about 5 milliseconds or less.

	@param duration The amount of time to wait
	@return The exact amount of time waited
]=]
function task.wait(duration: number?): number
	return nil :: any
end

return task


-- MIT License | Copyright (c) 2023-2024 Latte Softworks <https://latte.to>

local luau = require("@lune/luau")
local roblox = require("@lune/roblox")

local LuneUtils = require("Libraries/LuneUtils")

local Log = LuneUtils.Log
local StringUtils = LuneUtils.StringUtils

local LuaEncode = require("Libraries/LuaEncode")

local Version = require("Data/Version")

-- Init script template for codegen (in this, we'll maintain 5.1 *syntax* compat
-- for optional compilation to vanilla Lua 5.1x bytecode format)
local InitScriptTemplate = require("Data/Template")

-- The line where the 1st closure's function will start in bundled codegen
local InitialLineOffset = 8

local ClassNameIdKeys = {
    Folder = 1,
    ModuleScript = 2,
    Script = 3,
    LocalScript = 4,
    StringValue = 5,
}

-- Building codegen directly from model data (from .rbxm/.rbxmx) or a deserialized
-- object using Lune's `roblox` library
local function BundleModel(modelData: string | {roblox.Instance}, envName: string?, minifyCodegen: boolean?, extraLinesToOffset: number?, verbose: boolean?): (string, number)
    local EnvName = envName or "WaxRuntime"
    local MinifyCodegen = if minifyCodegen == nil then true else minifyCodegen
    local ExtraLinesToOffset = extraLinesToOffset or 0
    local Verbose = if verbose == nil then true else verbose

    -- Same as in Wax CLI
    local function RunIfVerbose(f, ...)
        if Verbose then
            f(...)
        end
    end

    local LineDebugging = MinifyCodegen == false

    local ModelRoot = if type(modelData) == "string" then
        roblox.deserializeModel(modelData)
    else modelData

    -- We'll track how long it takes for us to read the entire object tree, with all
    -- other various processes included
    local ReadStartTime = os.clock()

    local FailedCompilations = 0

    -- We'll initialize the output object tree, then walk through what we need to
    local ObjectTree = {}
    local ClosureBindings = {} -- [RefId] = Closure
    local ClosureSourceLineCounts = {} -- [RefId] = LineCount

    local ScrapedInstanceTree = {} -- [RealRef] = {Child, ...}
    local RefIds = {} -- [RefId] = RealRef

    -- Recursive function to actually walk through the real instance tree, and assign refs
    local function ScrapeInstanceChildren(instance)
        -- Add a reference id for this instance
        table.insert(RefIds, instance)

        local ScrapedChildren = {}
        for _, Child in instance:GetChildren() do
            ScrapedChildren[Child] = ScrapeInstanceChildren(Child)
        end

        return ScrapedChildren
    end

    -- Initialize the scraped instance tree and assign all refs from root
    local ModelRootChildren = ModelRoot -- Using later aswell
    for _, RealInstance in ModelRootChildren do
        ScrapedInstanceTree[RealInstance] = ScrapeInstanceChildren(RealInstance)
    end

    -- Now, we'll recursively create the fake object tree
    local function CreateObjectTree(instance, children, currentPathString: string?)
        currentPathString = currentPathString or instance.Name

        local RefId = table.find(RefIds, instance)
        local ClassName = instance.ClassName

        local ClassNameId = ClassNameIdKeys[ClassName]
        if not ClassNameId then
            Log.Warn(`Skipping instance of ClassName "{ClassName}", as it isn't supported in bundling`)
            return
        end

        local InstanceIsABaseScript = ClassName == "LocalScript" or ClassName == "Script"
        local InstanceIsAScript = InstanceIsABaseScript or ClassName == "ModuleScript"

        --[[
            {
                [1] = RefId,
                [2] = ClassName,
                [3] = Properties,
                [4] = Children?
            }
        ]]

        local InstanceObject = {
            [1] = RefId,
            [2] = ClassNameId,
        }

        -- If it's statically disabled, we just won't include the closure to run
        if InstanceIsAScript and not (InstanceIsABaseScript and instance.Disabled) then
            local ScriptSource = instance.Source

            RunIfVerbose(Log.Info, `Compile-checking {instance.ClassName} "{currentPathString}"..`)
            local CompileSuccess, CompileError = pcall(luau.compile, ScriptSource)

            if CompileSuccess then
                -- The added line here is the "\nend" below every arbitrary closure
                ClosureSourceLineCounts[RefId] = StringUtils.LineCount(ScriptSource) + 1

                -- We're using `FunctionsReturnRaw` on LuaEncode later, this will set the return
                -- to the rew value, which is the script closure
                ClosureBindings[RefId] = function()
                    return "function()local wax,script,require=ImportGlobals(" .. RefId .. ")local ImportGlobals return (function(...)" .. ScriptSource .. "\nend)() end"
                end
            else
                local FirstLineOfError = string.match(tostring(CompileError), "%w* ?:%d*: ?([^\n]*)\n")
                Log.Warn(`WARNING: {instance.ClassName} "{currentPathString}" failed to compile: {FirstLineOfError or "[Failed to parse compiler error]"}`)
                FailedCompilations += 1

                ClosureSourceLineCounts[RefId] = 1 -- Guaranteed 1 line; see below lol

                ClosureBindings[RefId] = function()
                    return `function()error("[AOT COMPILER ERROR] {StringUtils.SerializeStringData(FirstLineOfError)}")end`
                end
            end
        end

        -- Add any properties
        local Properties = {[1] = instance.Name} -- For byte preservation (lol) the name is just set as the property index 1, and not "Name"

        if ClassName == "StringValue" then
            Properties.Value = instance.Value
        end

        -- The init script will assume the `Name` is the same as the `ClassName` if not included
        if instance.Name ~= ClassName then
            InstanceObject[3] = Properties
        end

        -- Recursively add children
        if next(children) then
            local ObjectChildren = {}

            for Child, ChildrenOfChild in children do
                local ChildObjectTree = CreateObjectTree(Child, ChildrenOfChild, `{currentPathString}.{Child.Name}`)
                if ChildObjectTree then
                    table.insert(ObjectChildren, ChildObjectTree)
                end
            end

            InstanceObject[4] = ObjectChildren
        end

        return InstanceObject
    end

    for RealInstance, Children in ScrapedInstanceTree do
        local ChildObjectTree = CreateObjectTree(RealInstance, Children)

        if ChildObjectTree then
            table.insert(ObjectTree, ChildObjectTree)
        end
    end

    local LineOffsets = {}
    if LineDebugging then
        -- Where the first closure func start should be
        local CurrentLineOffset = ExtraLinesToOffset + InitialLineOffset

        for RefId, LineCount in ClosureSourceLineCounts do
            LineOffsets[RefId] = CurrentLineOffset
            CurrentLineOffset += LineCount
        end
    end

    -- Now we're done reading everything!
    local ReadEndTime = os.clock()
    RunIfVerbose(Log.Info, `Finished bundling model data in {string.format("%.4f", ReadEndTime - ReadStartTime)} (seconds)`)

    local Prettify = if MinifyCodegen == false then true else false

    local SerializedObjectTree = LuaEncode(ObjectTree, {
        Prettify = Prettify,
        StackLimit = math.huge,
    })

    local SerializedClosureBindings = LuaEncode(ClosureBindings, {
        Prettify = Prettify,
        FunctionsReturnRaw = true, -- For Script.Source function closures
    })

    local SerializedLineOffsets = if not LineDebugging then
        "nil"
    else
        LuaEncode(LineOffsets, {
            Prettify = Prettify,
        })

    local CodegenOutput = StringUtils.Replace(InitScriptTemplate, {
        Version = StringUtils.SerializeStringData(Version),
        EnvName = StringUtils.SerializeStringData(EnvName),
        ObjectTree = SerializedObjectTree,
        ClosureBindings = SerializedClosureBindings,
        LineOffsets = SerializedLineOffsets,
    })

    -- If there's a top-level init modulescript, we'll return it from the output's closure directly
    -- It's better to impl this all AoT!
    if #ModelRootChildren == 1 and ModelRootChildren[1].ClassName == "ModuleScript" then
        CodegenOutput ..= "\n-- AoT adjustment: Load init module (MainModule behavior)\nreturn LoadScript(RealObjectRoot:GetChildren()[1])"
    else
        for _, Ref in next, ModelRootChildren do
            if Ref.ClassName == "ModuleScript" and Ref.Name == "MainModule" then
                CodegenOutput ..= "\n-- AoT adjustment: Load init module (MainModule behavior)\nreturn LoadScript(RealObjectRoot.MainModule)"
                break
            end
        end
    end

    return CodegenOutput, FailedCompilations
end

return BundleModel


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

local Config = {
    generator = {
        name = "dense",
        column_span = 120,
    },

    rules = {
        "convert_index_to_field",
        "compute_expression",
        "group_local_assignment",
        "filter_after_early_return",
        "remove_comments",
        "remove_empty_do",
        "remove_function_call_parens",
        "remove_nil_declaration",
        "remove_method_definition",
        "remove_spaces",
        "remove_unused_if_branch",
        "remove_unused_while",

        {
            rule = "rename_variables",
            include_functions = true,
        },
    },
}

return serde.encode("json", Config, false)


-- This codegen template *must* be 100% compatible with Lua 5.1x+, NOT just Luau
-- With that being said, this explains the weird cflow in some parts

return [[
-- ++++++++ WAX BUNDLED DATA BELOW ++++++++ --

-- Will be used later for getting flattened globals
local ImportGlobals

-- Holds direct closure data (defining this before the DOM tree for line debugging etc)
local ClosureBindings = ${ClosureBindings} -- [RefId] = Closure

-- Holds the actual DOM data
local ObjectTree = ${ObjectTree}

-- Line offsets for debugging (only included when minifyTables is false)
local LineOffsets = ${LineOffsets}

-- Misc AOT variable imports
local WaxVersion = "${Version}"
local EnvName = "${EnvName}"

-- ++++++++ RUNTIME IMPL BELOW ++++++++ --

-- Localizing certain libraries and built-ins for runtime efficiency
local string, task, setmetatable, error, next, table, unpack, coroutine, script, type, require, pcall, tostring, tonumber, _VERSION =
      string, task, setmetatable, error, next, table, unpack, coroutine, script, type, require, pcall, tostring, tonumber, _VERSION

local table_insert = table.insert
local table_remove = table.remove
local table_freeze = table.freeze or function(t) return t end -- lol

local coroutine_wrap = coroutine.wrap

local string_sub = string.sub
local string_match = string.match
local string_gmatch = string.gmatch

-- The Lune runtime has its own `task` impl, but it must be imported by its builtin
-- module path, "@lune/task"
if _VERSION and string_sub(_VERSION, 1, 4) == "Lune" then
    local RequireSuccess, LuneTaskLib = pcall(require, "@lune/task")
    if RequireSuccess and LuneTaskLib then
        task = LuneTaskLib
    end
end

local task_defer = task and task.defer

-- If we're not running on the Roblox engine, we won't have a `task` global
local Defer = task_defer or function(f, ...)
    coroutine_wrap(f)(...)
end

-- ClassName "IDs"
local ClassNameIdBindings = {
    [1] = "Folder",
    [2] = "ModuleScript",
    [3] = "Script",
    [4] = "LocalScript",
    [5] = "StringValue",
}

local RefBindings = {} -- [RefId] = RealObject

local ScriptClosures = {}
local ScriptClosureRefIds = {} -- [ScriptClosure] = RefId
local StoredModuleValues = {}
local ScriptsToRun = {}

-- wax.shared __index/__newindex
local SharedEnvironment = {}

-- We're creating 'fake' instance refs soley for traversal of the DOM for require() compatibility
-- It's meant to be as lazy as possible
local RefChildren = {} -- [Ref] = {ChildrenRef, ...}

-- Implemented instance methods
local InstanceMethods = {
    GetFullName = { {}, function(self)
        local Path = self.Name
        local ObjectPointer = self.Parent

        while ObjectPointer do
            Path = ObjectPointer.Name .. "." .. Path

            -- Move up the DOM (parent will be nil at the end, and this while loop will stop)
            ObjectPointer = ObjectPointer.Parent
        end

        return Path
    end},

    GetChildren = { {}, function(self)
        local ReturnArray = {}

        for Child in next, RefChildren[self] do
            table_insert(ReturnArray, Child)
        end

        return ReturnArray
    end},

    GetDescendants = { {}, function(self)
        local ReturnArray = {}

        for Child in next, RefChildren[self] do
            table_insert(ReturnArray, Child)

            for _, Descendant in next, Child:GetDescendants() do
                table_insert(ReturnArray, Descendant)
            end
        end

        return ReturnArray
    end},

    FindFirstChild = { {"string", "boolean?"}, function(self, name, recursive)
        local Children = RefChildren[self]

        for Child in next, Children do
            if Child.Name == name then
                return Child
            end
        end

        if recursive then
            for Child in next, Children do
                -- Yeah, Roblox follows this behavior- instead of searching the entire base of a
                -- ref first, the engine uses a direct recursive call
                return Child:FindFirstChild(name, true)
            end
        end
    end},

    FindFirstAncestor = { {"string"}, function(self, name)
        local RefPointer = self.Parent
        while RefPointer do
            if RefPointer.Name == name then
                return RefPointer
            end

            RefPointer = RefPointer.Parent
        end
    end},

    -- Just to implement for traversal usage
    WaitForChild = { {"string", "number?"}, function(self, name)
        return self:FindFirstChild(name)
    end},
}

-- "Proxies" to instance methods, with err checks etc
local InstanceMethodProxies = {}
for MethodName, MethodObject in next, InstanceMethods do
    local Types = MethodObject[1]
    local Method = MethodObject[2]

    local EvaluatedTypeInfo = {}
    for ArgIndex, TypeInfo in next, Types do
        local ExpectedType, IsOptional = string_match(TypeInfo, "^([^%?]+)(%??)")
        EvaluatedTypeInfo[ArgIndex] = {ExpectedType, IsOptional}
    end

    InstanceMethodProxies[MethodName] = function(self, ...)
        if not RefChildren[self] then
            error("Expected ':' not '.' calling member function " .. MethodName, 2)
        end

        local Args = {...}
        for ArgIndex, TypeInfo in next, EvaluatedTypeInfo do
            local RealArg = Args[ArgIndex]
            local RealArgType = type(RealArg)
            local ExpectedType, IsOptional = TypeInfo[1], TypeInfo[2]

            if RealArg == nil and not IsOptional then
                error("Argument " .. RealArg .. " missing or nil", 3)
            end

            if ExpectedType ~= "any" and RealArgType ~= ExpectedType and not (RealArgType == "nil" and IsOptional) then
                error("Argument " .. ArgIndex .. " expects type \"" .. ExpectedType .. "\", got \"" .. RealArgType .. "\"", 2)
            end
        end

        return Method(self, ...)
    end
end

local function CreateRef(className, name, parent)
    -- `name` and `parent` can also be set later by the init script if they're absent

    -- Extras
    local StringValue_Value

    -- Will be set to RefChildren later aswell
    local Children = setmetatable({}, {__mode = "k"})

    -- Err funcs
    local function InvalidMember(member)
        error(member .. " is not a valid (virtual) member of " .. className .. " \"" .. name .. "\"", 3)
    end
    local function ReadOnlyProperty(property)
        error("Unable to assign (virtual) property " .. property .. ". Property is read only", 3)
    end

    local Ref = {}
    local RefMetatable = {}

    RefMetatable.__metatable = false

    RefMetatable.__index = function(_, index)
        if index == "ClassName" then -- First check "properties"
            return className
        elseif index == "Name" then
            return name
        elseif index == "Parent" then
            return parent
        elseif className == "StringValue" and index == "Value" then
            -- Supporting StringValue.Value for Rojo .txt file conv
            return StringValue_Value
        else -- Lastly, check "methods"
            local InstanceMethod = InstanceMethodProxies[index]

            if InstanceMethod then
                return InstanceMethod
            end
        end

        -- Next we'll look thru child refs
        for Child in next, Children do
            if Child.Name == index then
                return Child
            end
        end

        -- At this point, no member was found; this is the same err format as Roblox
        InvalidMember(index)
    end

    RefMetatable.__newindex = function(_, index, value)
        -- __newindex is only for props fyi
        if index == "ClassName" then
            ReadOnlyProperty(index)
        elseif index == "Name" then
            name = value
        elseif index == "Parent" then
            -- We'll just ignore the process if it's trying to set itself
            if value == Ref then
                return
            end

            if parent ~= nil then
                -- Remove this ref from the CURRENT parent
                RefChildren[parent][Ref] = nil
            end

            parent = value

            if value ~= nil then
                -- And NOW we're setting the new parent
                RefChildren[value][Ref] = true
            end
        elseif className == "StringValue" and index == "Value" then
            -- Supporting StringValue.Value for Rojo .txt file conv
            StringValue_Value = value
        else
            -- Same err as __index when no member is found
            InvalidMember(index)
        end
    end

    RefMetatable.__tostring = function()
        return name
    end

    setmetatable(Ref, RefMetatable)

    RefChildren[Ref] = Children

    if parent ~= nil then
        RefChildren[parent][Ref] = true
    end

    return Ref
end

-- Create real ref DOM from object tree
local function CreateRefFromObject(object, parent)
    local RefId = object[1]
    local ClassNameId = object[2]
    local Properties = object[3] -- Optional
    local Children = object[4] -- Optional

    local ClassName = ClassNameIdBindings[ClassNameId]

    local Name = Properties and table_remove(Properties, 1) or ClassName

    local Ref = CreateRef(ClassName, Name, parent) -- 3rd arg may be nil if this is from root
    RefBindings[RefId] = Ref

    if Properties then
        for PropertyName, PropertyValue in next, Properties do
            Ref[PropertyName] = PropertyValue
        end
    end

    if Children then
        for _, ChildObject in next, Children do
            CreateRefFromObject(ChildObject, Ref)
        end
    end

    return Ref
end

local RealObjectRoot = CreateRef("Folder", "[" .. EnvName .. "]")
for _, Object in next, ObjectTree do
    CreateRefFromObject(Object, RealObjectRoot)
end

-- Now we'll set script closure refs and check if they should be ran as a BaseScript
for RefId, Closure in next, ClosureBindings do
    local Ref = RefBindings[RefId]

    ScriptClosures[Ref] = Closure
    ScriptClosureRefIds[Ref] = RefId

    local ClassName = Ref.ClassName
    if ClassName == "LocalScript" or ClassName == "Script" then
        table_insert(ScriptsToRun, Ref)
    end
end

local function LoadScript(scriptRef)
    local ScriptClassName = scriptRef.ClassName

    -- First we'll check for a cached module value (packed into a tbl)
    local StoredModuleValue = StoredModuleValues[scriptRef]
    if StoredModuleValue and ScriptClassName == "ModuleScript" then
        return unpack(StoredModuleValue)
    end

    local Closure = ScriptClosures[scriptRef]

    local function FormatError(originalErrorMessage)
        originalErrorMessage = tostring(originalErrorMessage)

        local VirtualFullName = scriptRef:GetFullName()

        -- Check for vanilla/Roblox format
        local OriginalErrorLine, BaseErrorMessage = string_match(originalErrorMessage, "[^:]+:(%d+): (.+)")

        if not OriginalErrorLine or not LineOffsets then
            return VirtualFullName .. ":*: " .. (BaseErrorMessage or originalErrorMessage)
        end

        OriginalErrorLine = tonumber(OriginalErrorLine)

        local RefId = ScriptClosureRefIds[scriptRef]
        local LineOffset = LineOffsets[RefId]

        local RealErrorLine = OriginalErrorLine - LineOffset + 1
        if RealErrorLine < 0 then
            RealErrorLine = "?"
        end

        return VirtualFullName .. ":" .. RealErrorLine .. ": " .. BaseErrorMessage
    end

    -- If it's a BaseScript, we'll just run it directly!
    if ScriptClassName == "LocalScript" or ScriptClassName == "Script" then
        local RunSuccess, ErrorMessage = xpcall(Closure, function(msg)
            return msg
        end)
        if not RunSuccess then
            error(FormatError(ErrorMessage), 0)
        end
    else
        local PCallReturn = {xpcall(Closure, function(msg)
            return msg
        end)}

        local RunSuccess = table_remove(PCallReturn, 1)
        if not RunSuccess then
            local ErrorMessage = table_remove(PCallReturn, 1)
            error(FormatError(ErrorMessage), 0)
        end

        StoredModuleValues[scriptRef] = PCallReturn
        return unpack(PCallReturn)
    end
end

-- We'll assign the actual func from the top of this output for flattening user globals at runtime
-- Returns (in a tuple order): wax, script, require
function ImportGlobals(refId)
    local ScriptRef = RefBindings[refId]

    local function RealCall(f, ...)
        local PCallReturn = {xpcall(f, function(msg)
            return debug.traceback(msg, 2)
        end, ...)}

        local CallSuccess = table_remove(PCallReturn, 1)
        if not CallSuccess then
            error(PCallReturn[1], 3)
        end

        return unpack(PCallReturn)
    end

    -- `wax.shared` index
    local WaxShared = table_freeze(setmetatable({}, {
        __index = SharedEnvironment,
        __newindex = function(_, index, value)
            SharedEnvironment[index] = value
        end,
        __len = function()
            return #SharedEnvironment
        end,
        __iter = function()
            return next, SharedEnvironment
        end,
    }))

    local Global_wax = table_freeze({
        -- From AOT variable imports
        version = WaxVersion,
        envname = EnvName,

        shared = WaxShared,

        -- "Real" globals instead of the env set ones
        script = script,
        require = require,
    })

    local Global_script = ScriptRef

    local function Global_require(module, ...)
        local ModuleArgType = type(module)

        local ErrorNonModuleScript = "Attempted to call require with a non-ModuleScript"
        local ErrorSelfRequire = "Attempted to call require with self"

        if ModuleArgType == "table" and RefChildren[module]  then
            if module.ClassName ~= "ModuleScript" then
                error(ErrorNonModuleScript, 2)
            elseif module == ScriptRef then
                error(ErrorSelfRequire, 2)
            end

            return LoadScript(module)
        elseif ModuleArgType == "string" and string_sub(module, 1, 1) ~= "@" then
            -- The control flow on this SUCKS

            if #module == 0 then
                error("Attempted to call require with empty string", 2)
            end

            local CurrentRefPointer = ScriptRef

            if string_sub(module, 1, 1) == "/" then
                CurrentRefPointer = RealObjectRoot
            elseif string_sub(module, 1, 2) == "./" then
                module = string_sub(module, 3)
            end

            local PreviousPathMatch
            for PathMatch in string_gmatch(module, "([^/]*)/?") do
                local RealIndex = PathMatch
                if PathMatch == ".." then
                    RealIndex = "Parent"
                end

                -- Don't advance dir if it's just another "/" either
                if RealIndex ~= "" then
                    local ResultRef = CurrentRefPointer:FindFirstChild(RealIndex)
                    if not ResultRef then
                        local CurrentRefParent = CurrentRefPointer.Parent
                        if CurrentRefParent then
                            ResultRef = CurrentRefParent:FindFirstChild(RealIndex)
                        end
                    end

                    if ResultRef then
                        CurrentRefPointer = ResultRef
                    elseif PathMatch ~= PreviousPathMatch and PathMatch ~= "init" and PathMatch ~= "init.server" and PathMatch ~= "init.client" then
                        error("Virtual script path \"" .. module .. "\" not found", 2)
                    end
                end

                -- For possible checks next cycle
                PreviousPathMatch = PathMatch
            end

            if CurrentRefPointer.ClassName ~= "ModuleScript" then
                error(ErrorNonModuleScript, 2)
            elseif CurrentRefPointer == ScriptRef then
                error(ErrorSelfRequire, 2)
            end

            return LoadScript(CurrentRefPointer)
        end

        return RealCall(require, module, ...)
    end

    -- Now, return flattened globals ready for direct runtime exec
    return Global_wax, Global_script, Global_require
end

for _, ScriptRef in next, ScriptsToRun do
    Defer(LoadScript, ScriptRef)
end
]]


-- MIT License | Copyright (c) 2023-2024 Latte Softworks <https://latte.to>

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

local CommandUtils = {}

function CommandUtils.CommandExists(binary: string): boolean
    if process.os ~= "windows" then
        -- Unix-compliance is simple!
        return process.spawn("type", {binary}, {shell = true}).ok -- Return from status code directly
    end

    -- I hate Windows
    return true
end

function CommandUtils.CheckCommands(requiredCommands: {string})
    -- Check required commands to continue script exec
    for _, Command in requiredCommands do
        assert(CommandUtils.CommandExists(Command), `Required command "{Command}" no found in your PATH environment variable, is it not installed?`)
    end
end

return CommandUtils


-- MIT License | Copyright (c) 2023-2024 Latte Softworks <https://latte.to>

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

--[[
    This kinda looks like a strange function, because of the format a "FileTree" accepts:

    ```lua
    EnsureFileTree({
        ["test"] = {
            ["test.txt"] = "hi",
            ["test.json"] = "[\"hi again\"]",
            ["another-sub-dir"] = {
                ["bye.txt"] = "bye"
            },
        },
        ["another-test"] = {
            ["another-example.txt"] = "real",
        },
    })
    ```
]]
local function EnsureFileTree(fileTree: {[string]: any}, _currentPath: string?)
    for FileName, Entry in fileTree do
        local RealPath = if _currentPath then `{_currentPath}/{FileName}` else FileName
        local EntryType = type(Entry)

        if EntryType == "table" then -- This is to be a directory
            if not fs.isDir(RealPath) then
                fs.writeDir(RealPath)
            end

            -- Recursively continue creating the file tree..
            EnsureFileTree(Entry, RealPath)
        elseif EntryType == "string" and not fs.isFile(RealPath) then -- This is to be a file
            fs.writeFile(RealPath, Entry)
        end
    end
end

return EnsureFileTree


-- MIT License | Copyright (c) 2023-2024 Latte Softworks <https://latte.to>

type OptionsInput = {[string]: {string | number | boolean | nil}}
type OptionsOutput = {[string]: string | number | boolean | nil}

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

local Log = require("Log")

local function ParseArgs(args: {string}, optionsTable: OptionsInput, usageMessage: string?): OptionsOutput
    local function ErrorExit()
        if usageMessage then
            print(`\n{usageMessage}`)
        end
        process.exit(1)
    end

    local OptionsOutput = {}

    for _, Arg in args do
        local Option, Value = string.match(Arg, "([%w%-_]+)=(.+)")

        if not Option then
            Log.Error(`Command argument given, "{Arg}" not in the proper \`option=value\` format, aborting..`)
            ErrorExit()
        end

        local OptionDetails = optionsTable[Option]

        if OptionDetails == nil then -- Then it isn't a defined arg
            Log.Error(`"{Option}" isn't a valid option"`)
            ErrorExit()
        end

        local OptionType = OptionDetails[1]

        if OptionType == "boolean" then
            Value = string.lower(Value)
            Value = if Value == "true" then true elseif Value == "false" then false else nil

            if Value == nil then
                Log.Error(`"{Value}" isn't a valid boolean type; expected "true" or "false"`)
                ErrorExit()
            end
        elseif OptionType == "number" then
            Value = tonumber(Value)
            if not Value then
                Log.Error(`Failed to convert given input "{Value}" to a number value`)
                ErrorExit()
            end
        end

        OptionsOutput[Option] = Value
    end

    for Option, Details in optionsTable do
        local DefaultValue = Details[2]
        local IsRequired = Details[3] or false

        if not OptionsOutput[Option] then
            if IsRequired then
                Log.Error(`Option "{Option}" required but not provided`)
                ErrorExit()
            end

            OptionsOutput[Option] = DefaultValue
        end
    end

    return OptionsOutput
end

return ParseArgs


-- MIT License | Copyright (c) 2023-2024 Latte Softworks <https://latte.to>

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

local function RecursiveReadDir(directory: string, _existingFileList: {string}?): {string}
    if not string.match(directory, "[/\\]$") then
        directory ..= "/"
    end

    local FileList = _existingFileList or {}
    for _, EntryName in fs.readDir(directory) do
        if fs.isFile(EntryName) then
            table.insert(FileList, directory .. EntryName)
        else -- Is dir
            RecursiveReadDir(directory .. EntryName .. "/", FileList)
        end
    end

    return FileList
end

return RecursiveReadDir


-- MIT License | Copyright (c) 2023-2024 Latte Softworks <https://latte.to>

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

local Log = require("Log")

local function Run(command: string, args: {string}?, directRun: boolean?, errorHandler: (string?) -> ()?): process.SpawnResult
    local Args = args or {}
    local DirectRun = if directRun == nil then false else directRun

    if not DirectRun then
        if #Args == 0 then
            Log.Info(`> {command}`)
        else
            Log.Info(`> {command} {table.concat(Args, " ")}`)
        end
    end

    local Result = process.spawn(command, args, {
        shell = if process.os ~= "windows" then true else nil,
        stdio = if DirectRun then "default" else "inherit",
    })

    if not Result.ok and errorHandler then
        errorHandler(`Command above failed with status code {Result.code}`)
    end

    return Result
end

return Run


-- MIT License | Copyright (c) 2023-2024 Latte Softworks <https://latte.to>

return {
    Log = require("Log"),
    StringUtils = require("StringUtils"),
    CommandUtils = require("CommandUtils"),
    WebhookQueue = require("WebhookQueue"),

    Run = require("Run"),
    ParseArgs = require("ParseArgs"),
    EnsureFileTree = require("EnsureFileTree"),
    RecursiveReadDir = require("RecursiveReadDir"),
}


wax.shared.Connections = {}

wax.shared.Connect = function(Connection)
	table.insert(wax.shared.Connections, Connection)
	return Connection
end

wax.shared.Disconnect = function(Connection)
	Connection:Disconnect()

	local Index = table.find(wax.shared.Connections, Connection)
	if Index then
		table.remove(wax.shared.Connections, Index)
	end

	return true
end

return {}


local FileHelper = {}
FileHelper.__index = FileHelper

function FileHelper.new(basePath: string)
    local self = setmetatable({
        BasePath = FileHelper.NormalizePath(basePath),
    }, FileHelper)

    self:EnsureDirectory()
    return self
end

function FileHelper:EnsureDirectory()
    local paths = {}
    local parts = self.BasePath:split("/")

    for idx = 1, #parts do
        paths[#paths + 1] = table.concat(parts, "/", 1, idx)
    end

    for i = 1, #paths do
        local str = paths[i]
        if isfolder(str) then continue end
        makefolder(str)
    end
end

function FileHelper:GetPath(relativePath: string)
    local normalizedPath = self.NormalizePath(relativePath or "")

    if self.BasePath:sub(-1) == "/" then
        return self.BasePath .. normalizedPath
    end

    if normalizedPath == "" then
        return self.BasePath
    end

    return self.BasePath .. "/" .. normalizedPath
end

function FileHelper.NormalizePath(path: string)
    if type(path) ~= "string" then return "" end
    return path:gsub("\\", "/")
end

function FileHelper:GetRelativePath(path: string)
    path = self.NormalizePath(path)
    local basePath = self.NormalizePath(self.BasePath)
    if basePath:sub(-1) ~= "/" then
        basePath = basePath .. "/"
    end

    local Start, End = string.find(path, basePath, 1, true)
    if Start then
        return string.sub(path, End + 1)
    end

    return path
end

function FileHelper:EnsureFile(relativePath: string, default: string)
    relativePath = self.NormalizePath(relativePath or "")

    if not isfile(self:GetPath(relativePath)) then
        writefile(self:GetPath(relativePath), default)
    end
end

function FileHelper:ReadFile(relativePath: string)
    relativePath = self.NormalizePath(relativePath or "")
    return readfile(self:GetPath(relativePath))
end

function FileHelper:ReadRawFile(path: string)
    return readfile(path)
end

function FileHelper:WriteFile(relativePath: string, data: string)
    relativePath = self.NormalizePath(relativePath or "")

    writefile(self:GetPath(relativePath), data)
end

function FileHelper:DeleteFile(relativePath: string)
    relativePath = self.NormalizePath(relativePath or "")

    if isfile(self:GetPath(relativePath)) then
        delfile(self:GetPath(relativePath))
    end
end

function FileHelper:DeleteDirectory(relativePath: string)
    relativePath = self.NormalizePath(relativePath or "")

    if isfolder(self:GetPath(relativePath)) then
        delfolder(self:GetPath(relativePath))
    end
end

function FileHelper:ListFiles(relativePath: string?)
    relativePath = self.NormalizePath(relativePath or "")

    local files = {}
    for _, file in listfiles(self:GetPath(relativePath)) do
        table.insert(files, self:GetRelativePath(file))
    end

    return files
end

function FileHelper:GetFileName(relativePath: string)
    relativePath = self.NormalizePath(relativePath or "")

    return relativePath:match("[^/]+$")
end

function FileHelper:ListFileNames(relativePath: string?)
    relativePath = self.NormalizePath(relativePath or "")

    local names = {}
    for _, file in self:ListFiles(relativePath) do
        table.insert(names, self:GetFileName(file))
    end

    return names
end

return FileHelper


-- Logger
-- ActualMasterOogway
-- December 8, 2024

--[=[
    A simple logging utility that writes messages to a file. Supports different log levels
    and can be configured to overwrite or append to the log file.

    Log Format:  2024-12-04T15:28:31.131Z,0.131060,MyThread,Warning [FLog::RobloxStarter] Roblox stage ReadyForFlagFetch completed
                 <timestamp>,<elapsed_time>,<thread_id>,<level> <message>
]=]

local Logger = {}
Logger.__index = Logger

Logger.LOG_LEVELS = {
	ERROR = 1,
	WARNING = 2,
	INFO = 3,
	DEBUG = 4,
}

local LOG_LEVEL_STRINGS = {
	[Logger.LOG_LEVELS.ERROR] = "ERROR",
	[Logger.LOG_LEVELS.WARNING] = "WARNING",
	[Logger.LOG_LEVELS.INFO] = "INFO",
	[Logger.LOG_LEVELS.DEBUG] = "DEBUG",
}

local startTime = tick()

local function createDirectoryRecursive(path)
	local currentPath = ""
	for part in path:gmatch("[^\\/]+") do
		currentPath = currentPath .. part
		if not isfolder(currentPath) then
			makefolder(currentPath)
		end
		currentPath = currentPath .. "/"
	end
end

--[=[
    Generates a unique file name for the log file. The file name is based on the current
    job ID, ensuring it is unique per server instance but consistent across multiple
    executions within the same server.

    @return string A unique file name for the log file.
]=]
function Logger:GenerateFileName()
	local JobIdNumber = game.JobId:gsub("%D", "")
	local timestamp = os.date("!%Y%m%d%H%M%S")

	return `{self.logFileDirectory}/{JobIdNumber * 1.7 // 1.8}_{timestamp}.log`
end

--[=[
    Creates a new Logger instance.

    @param logFilePath string The path to the log file.
    @param logLevel number The minimum log level to write to the file. Defaults to INFO.
    @param overwrite boolean Whether to overwrite the log file or append to it. Defaults to false (append).
    @return Logger A new Logger instance.
]=]
function Logger.new(logFilePath: string, logLevel: number?, overwrite: boolean?)
	local self = setmetatable({}, Logger)
	self.logFilePath = logFilePath
	self.logFileDirectory = logFilePath:match("(.+)/")
	self.logLevel = logLevel or Logger.LOG_LEVELS.INFO
	self.overwrite = overwrite or false

	local folderPath, fileName = logFilePath:match("(.*[\\/])(.*)")

	if folderPath and not isfolder(folderPath) then
		createDirectoryRecursive(folderPath)
	end

	if self.overwrite then
		local success, err = pcall(writefile, self.logFilePath, "")
		if not success then
			warn(debug.traceback(`Failed to clear log file: {self.logFilePath} - {err}`, 2))
		end
	end

	self:Info("Logger", "Logger initialized")

	return self
end

--[=[
    Logs a message to the file.

    @param level number The log level of the message.
    @param threadId string The ID of the thread or source of the log message.
    @param message string The message to log.
]=]
function Logger:Log(level: number, threadId: string, message: string)
	if level <= self.logLevel then
		local levelStr = LOG_LEVEL_STRINGS[level]

		local timestamp = `{os.date("!%Y-%m-%dT%H:%M:%S")}{("%.3f"):format(tick() % 1)}Z`
		local elapsedTime = ("%.6f"):format(tick() - startTime)

		local logMessage = `{timestamp},{elapsedTime},{threadId},{levelStr} {message}\n`

		local success, err = pcall(appendfile, self.logFilePath, logMessage)
		if not success then
			warn(debug.traceback(`Failed to write to log file: {self.logFilePath} - {err}`, 2))
		end
	end
end

--[=[
    Logs a debug message.

    @param threadId string The ID of the thread or source of the log message.
    @param message string The message to log.
]=]
function Logger:Debug(threadId: string, message: string)
	self:Log(Logger.LOG_LEVELS.DEBUG, threadId, message)
end

--[=[
    Logs an info message.

    @param threadId string The ID of the thread or source of the log message.
    @param message string The message to log.
]=]
function Logger:Info(threadId: string, message: string)
	self:Log(Logger.LOG_LEVELS.INFO, threadId, message)
end

--[=[
    Logs a warning message.

    @param threadId string The ID of the thread or source of the log message.
    @param message string The message to log.
]=]
function Logger:Warning(threadId: string, message: string)
	self:Log(Logger.LOG_LEVELS.WARNING, threadId, message)
end

--[=[
    Logs an error message.

    @param threadId string The ID of the thread or source of the log message.
    @param message string The message to log.
]=]
function Logger:Error(threadId: string, message: string)
	self:Log(Logger.LOG_LEVELS.ERROR, threadId, message)
end

return Logger


--[[

Pagination Module
made by deivid and turned into module by upio

]]

local Pagination = {}
Pagination.__index = Pagination

function Pagination.new(Options: {
	TotalItems: number,
	ItemsPerPage: number,
	CurrentPage: number?,
	SiblingCount: number?,
})
	return setmetatable({
		TotalItems = Options.TotalItems,
		ItemsPerPage = Options.ItemsPerPage,
		CurrentPage = Options.CurrentPage or 1,
		SiblingCount = Options.SiblingCount or 2,
	}, Pagination)
end

function Pagination:GetInfo()
	local TotalPages = math.ceil(self.TotalItems / self.ItemsPerPage)

	return {
		TotalItems = self.TotalItems,
		ItemsPerPage = self.ItemsPerPage,
		CurrentPage = self.CurrentPage,
		TotalPages = TotalPages,
	}
end

function Pagination:SetItemsPerPage(max: number)
	self.ItemsPerPage = max
end

function Pagination:GetIndexRanges(Page: number?)
	Page = Page or self.CurrentPage

	local TotalPages = math.ceil(self.TotalItems / self.ItemsPerPage)
	if TotalPages == 0 then
		return 1, 0
	end
	assert(
		Page <= TotalPages,
		"Attempted to get invalid page out of range, got " .. Page .. " but max is " .. TotalPages
	)

	local Start = (((Page or self.CurrentPage) - 1) * self.ItemsPerPage) + 1
	local End = Start + (self.ItemsPerPage - 1)

	return Start, End
end

function Pagination:SetPage(Page: number)
	local TotalPages = math.ceil(self.TotalItems / self.ItemsPerPage)
	if TotalPages == 0 then
		self.CurrentPage = 1
		return
	end
	assert(Page <= TotalPages, "Attempted to set page out of range, got " .. Page .. " but max is " .. TotalPages)

	self.CurrentPage = Page
end

function Pagination:Update(TotalItems: number?, ItemsPerPage: number?)
	self.TotalItems = TotalItems or self.TotalItems
	self.ItemsPerPage = ItemsPerPage or self.ItemsPerPage
end

function Pagination:GetVisualInfo(Page: number?)
	Page = Page or self.CurrentPage

	local TotalPages = math.ceil(self.TotalItems / self.ItemsPerPage)
	if TotalPages == 0 then
		return {}
	end

	assert(
		Page <= TotalPages,
		"Attempted to get invalid page out of range, got " .. Page .. " but max is " .. TotalPages
	)

	local MaxButtons = 5 + self.SiblingCount * 2
	local Result = table.create(MaxButtons, "none")
	if TotalPages <= MaxButtons then
		for i = 1, TotalPages do
			Result[i] = i
		end
		return Result
	end

	local LeftSibling = math.max(Page - self.SiblingCount, 1)
	local RightSibling = math.min(Page + self.SiblingCount, TotalPages)

	local FakeLeft = LeftSibling > 2
	local FakeRight = RightSibling < TotalPages - 1

	local TotalPageNumbers = math.min(2 * self.SiblingCount + 5, TotalPages)
	local ItemCount = TotalPageNumbers - 2

	if not FakeLeft and FakeRight then
		for i = 1, ItemCount do
			Result[i] = i
		end
		Result[ItemCount + 1] = "ellipsis"
		Result[ItemCount + 2] = TotalPages
		--return MergeTables(LeftRange, "ellipsis", TotalPages)
		return Result
	elseif FakeLeft and not FakeRight then
		--local RightRange = CreateArray(TotalPages - ItemCount + 1, TotalPages)
		Result[1] = 1
		Result[2] = "ellipsis"

		local Index = 3
		for i = TotalPages - ItemCount + 1, TotalPages do
			Result[Index] = i
			Index += 1
		end

		return Result
	elseif FakeLeft and FakeRight then
		--local MiddleRange = CreateArray(LeftSibling, RightSibling)
		Result[1] = 1
		Result[2] = "ellipsis"
		local Index = 3

		for i = LeftSibling, RightSibling do
			Result[Index] = i
			Index += 1
		end

		Result[Index] = "ellipsis"
		Result[Index + 1] = TotalPages

		return Result
		--return MergeTables(1, "ellipsis", MiddleRange, "ellipsis", TotalPages)
	end

	--return CreateArray(1, TotalPages)
	for i = 1, TotalPages do
		Result[i] = i
	end
	return Result
end

return Pagination


local FileHelperUtil = require(script.Parent.FileHelper)

local SaveManager = {
	State = {},
}

local FileHelper = FileHelperUtil.new("Cobalt")
FileHelper:EnsureFile("Settings.json", "{}")

local Success, Error = pcall(function()
	SaveManager.State = wax.shared.HttpService:JSONDecode(FileHelper:ReadFile("Settings.json"))
end)

if not Success then
	warn("Failed to load settings: " .. Error)
end

function SaveManager:SetState(Idx, Value)
	SaveManager.State[Idx] = Value
	pcall(FileHelper.WriteFile, FileHelper, "Settings.json", wax.shared.HttpService:JSONEncode(SaveManager.State))
end

function SaveManager:GetState(Idx, Default)
	if SaveManager.State[Idx] ~= nil then
		return SaveManager.State[Idx]
	end

	return Default
end

return SaveManager


local SignalHandler = {
	Signals = {},
}

function SignalHandler.New(self)
	local Signal = {
		Connections = {},
		Disconnect = nil,
	}

	function Signal:Connect(callback)
		local connection = {
			Connected = true,
			Enabled = true,
			Callback = callback,
		}

		function connection:Disconnect()
			connection.Connected = false
		end

		function connection:Reconnect()
			if not connection.Enabled then
				return
			end

			connection.Connected = true
		end

		function connection:Enable()
			connection.Enabled = true
		end

		function connection:Disable()
			connection.Enabled = false
		end

		table.insert(Signal.Connections, connection)

		return connection
	end

	function Signal:Once(callback)
		local connection
		connection = Signal:Connect(function(...)
			connection:Disconnect()
			callback(...)
		end)

		return connection
	end

	function Signal:Fire(...)
		for _, connection in Signal.Connections do
			if connection.Connected and connection.Enabled then
				task.spawn(connection.Callback, ...)
			end
		end
	end

	function Signal:Wait(...)
		local return_data = {}
		local finishedWaiting = false

		Signal:Once(function(...)
			return_data = { ... }
			finishedWaiting = true
		end)

		repeat
			task.wait()
		until finishedWaiting
		return table.unpack(return_data)
	end

	table.insert(SignalHandler.Signals, Signal)
	return Signal
end

function SignalHandler.StopAll(self: any)
	for Index, Signal in SignalHandler.Signals do
		for _, Connection in Signal.Connections do
			Connection:Disable()
			Connection:Disconnect()
		end
	end
end

return SignalHandler


local Animations = {
	TweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Exponential),
	Exclusions = {},
	Expectations = { In = {}, Out = {} },
}

local function GetTransparencyProperty(object)
	if table.find(Animations.Exclusions, object) then
		return nil
	end

	if object:IsA("TextButton") or object:IsA("TextLabel") or object:IsA("TextBox") then
		return { "TextTransparency" }
	elseif object:IsA("CanvasGroup") then
		return { "GroupTransparency" }
	elseif object:IsA("Frame") then
		return { "BackgroundTransparency" }
	elseif object:IsA("ScrollingFrame") then
		return { "ScrollBarImageTransparency" }
	elseif object:IsA("ImageLabel") or object:IsA("ImageButton") then
		return { "ImageTransparency", "BackgroundTransparency" }
	elseif object:IsA("UIStroke") then
		return { "Transparency" }
	end

	return nil
end

local function BuildPropertyTable(properties, type, object)
	if Animations.Expectations[type][object] then
		return Animations.Expectations[type][object]
	end

	local propTable = {}
	for _, prop in properties do
		propTable[prop] = type == "In" and 0 or 1
	end
	return propTable
end

function Animations.FadeOut(object, time)
	local tweenInfo = time and TweenInfo.new(time, Enum.EasingStyle.Exponential) or Animations.TweenInfo
	local properties = GetTransparencyProperty(object)

	if not properties then
		return
	end

	wax.shared.TweenService:Create(object, tweenInfo, BuildPropertyTable(properties, "Out", object)):Play()

	if object:IsA("CanvasGroup") then
		return
	end

	for _, child in object:GetDescendants() do
		local prop = GetTransparencyProperty(child)
		if not prop then
			continue
		end

		wax.shared.TweenService:Create(child, tweenInfo, BuildPropertyTable(prop, "Out", child)):Play()
	end
end

function Animations.FadeIn(object, time)
	local tweenInfo = time and TweenInfo.new(time, Enum.EasingStyle.Exponential) or Animations.TweenInfo
	local property = GetTransparencyProperty(object)

	if property then
		wax.shared.TweenService:Create(object, tweenInfo, BuildPropertyTable(property, "In", object)):Play()
	end

	if object:IsA("CanvasGroup") then
		return
	end

	for _, child in object:GetDescendants() do
		local prop = GetTransparencyProperty(child)
		if not prop then
			continue
		end

		wax.shared.TweenService:Create(child, tweenInfo, BuildPropertyTable(prop, "In", child)):Play()
	end
end

function Animations.AddFadeExclusion(object)
	local prop = GetTransparencyProperty(object)
	if not prop then
		return
	end

	table.insert(Animations.Exclusions, object)
end

function Animations.AddFadeExclusions(objects)
	for _, object in objects do
		local prop = GetTransparencyProperty(object)
		if not prop then
			continue
		end

		table.insert(Animations.Exclusions, object)
	end
end

function Animations.SetFadeExpectation(type: "In" | "Out", object: GuiBase2d, properties: { [string]: any })
	if not Animations.Expectations[type] then
		return
	end

	Animations.Expectations[type][object] = properties
end

return Animations