-- Author: iGottic

local ForPairs = {}

-- Imports
local Modules = script.Parent.Parent.Parent.Modules
local Types = require(Modules.Types)
local Symbol = require(Modules.Symbol)
local CreateDeepTraceback = require(Modules.CreateDeepTraceback)
local Computed = require(script.Parent.Computed)
local Value = require(script.Parent.Value)

-- Variables
local ClassSymbol = Symbol.new("ForPairs")

-- Types Extended
export type ForPairsInstance<T> = {} & Types.BaseState<T>
export type ForPairsConstructor<T> = (TrackedValue : Value.ValueInstance<any>, Callback : ((Value : Value.ValueInstance<T>) -> any) -> any?) -> ForPairsInstance<T>

function ForPairs:__call(TrackedValue : Value.ValueInstance<any>, Callback : ((Value : Value.ValueInstance<any>) -> any) -> any?)
    local LastValues = {}
    local OutputtedValues = {}

    -- ForPairs is literally just Computed behind the scenes, with the exception that it checks for key differences
    local ActiveComputation = Computed(function(Use)
        local ThisTable = Use(TrackedValue)
        local ThisOuput = {}

        if typeof(ThisTable) ~= "table" then
            error("ForPairs needs a table value to work\n" .. CreateDeepTraceback())
        end

        for Index, NewValue in ThisTable do
            if LastValues[Index] ~= NewValue then
                LastValues[Index] = NewValue
                ThisOuput[Index] = Callback(Use, Index, NewValue)
            end
        end

        for Index, LastValue in LastValues do
            if ThisTable[Index] ~= LastValue then
                LastValues[Index] = ThisTable[Index]

                if ThisTable[Index] then
                    ThisOuput[Index] = Callback(Use, Index, ThisTable[Index])
                end
            elseif not ThisOuput[Index] then
                ThisOuput[Index] = OutputtedValues[Index]
            end
        end

        OutputtedValues = ThisOuput

        return ThisOuput
    end)

    return ActiveComputation :: ForPairsInstance<any>
end

function ForPairs:__index(Key : string)
    if Key == "__SEAM_INDEX" then
        return ClassSymbol
    elseif Key == "__SEAM_CAN_BE_SCOPED" then
        return true
    else
        return nil
    end
end

local Meta = setmetatable({}, ForPairs)

return Meta :: ForPairsConstructor<any>


-- Author: iGottic

local Rendered = {}

-- Imports
local Modules = script.Parent.Parent.Parent.Modules
local StateManager = require(Modules.StateManager)
local Trove = require(Modules.Trove)
local Signal = require(Modules.Signal)
local Types = require(Modules.Types)
local Symbol = require(Modules.Symbol)

-- Variables
local ClassSymbol = Symbol.new("Rendered")

-- Types Extended
export type RenderedInstance<T> = {} & Types.BaseState<T>
export type RenderedConstructor<T> = (Callback : (number) -> any?) -> RenderedInstance<T>

function Rendered:__call(Callback : (number) -> any?)
    -- This is MUCH simpler than computed, since it just force-updates every frame
    local TroveInstance = Trove.new()
    local AttachedSignal = Signal.new()
    local InstanceSymbol = Symbol.new("RenderedInstance")
    local LastFrame = os.clock()

    local WrappedCallback = function()
        local DeltaTime = os.clock() - LastFrame
        LastFrame = os.clock()

        return Callback(DeltaTime)
    end

    local ActiveComputation; ActiveComputation = setmetatable({
        Destroy = function()
            TroveInstance:Destroy()
        end
    }, {
        __call = function(_, Object : Instance, Index : string)
            AttachedSignal:Fire(Object)

            TroveInstance:Add(StateManager:AttachStateToObject(Object, {
                Value = WrappedCallback,
                PropertyName = Index
            }))

            return ActiveComputation
        end,

        __index = function(_, Index : string)
            if Index == "__SEAM_OBJECT" then
                return InstanceSymbol
            elseif Index == "Value" then
                return WrappedCallback()
            elseif Index == "AttachedToInstance" then
                return AttachedSignal
            end

            return nil
        end
    })

    return ActiveComputation :: RenderedInstance<any>
end

function Rendered:__index(Key : string)
    if Key == "__SEAM_INDEX" then
        return ClassSymbol
    elseif Key == "__SEAM_CAN_BE_SCOPED" then
        return true
    else
        return nil
    end
end

local Meta = setmetatable({}, Rendered)

return Meta :: RenderedConstructor<any>


-- Author: iGottic

local Value = {}

-- Imports
local Modules = script.Parent.Parent.Parent.Modules
local StateManager = require(Modules.StateManager)
local Trove = require(Modules.Trove)
local Signal = require(Modules.Signal)
local Types = require(Modules.Types)
local IsValueChanged = require(Modules.IsValueChanged)
local Symbol = require(Modules.Symbol)

-- Variables
local ClassSymbol = Symbol.new("Value")

-- Types Extended
export type ValueInstance<T> = {
    Changed : RBXScriptSignal,
} & Types.BaseState<T>

export type ValueConstructor<T> = (Value : T) -> ValueInstance<T>

function Value:__call(ThisValue : any)
    local TroveInstance = Trove.new()
    local ChangedSignal = Signal.new()
    local AttachedSignal = Signal.new()
    local InstanceSymbol = Symbol.new("Value")
    local IsLocked = false
    local ValueType = if ThisValue ~= nil then typeof(ThisValue) else nil

    if ValueType == "table" then
        -- Seam values clone tables before using them
        ThisValue = table.clone(ThisValue)
    end

    --[[
        local This = Value(...)
        This.Value = x OR x = This.Value
        This.Changed -> As rbxscriptsignal
    --]]

    local ActiveValue; ActiveValue = setmetatable({
        Destroy = function(_)
            TroveInstance:Destroy()
        end
    }, {
        __index = function(_, Index : string)
            if Index == "__SEAM_OBJECT" then
                return InstanceSymbol
            elseif Index == "Value" then
                return ThisValue
            elseif Index == "Changed" then
                return ChangedSignal
            elseif Index == "AttachedToInstance" then
                return AttachedSignal
            end

            return nil
        end,

        __newindex = function(_, Index : string, NewValue : any)
            if Index == "Value" then
                if IsLocked then
                    error("Attempt to modify value when locked.")
                end

                if ValueType ~= nil then
                    if NewValue ~= nil and typeof(NewValue) ~= ValueType then
                        error("Invalid value type! Expected " .. ValueType .. ", got " .. typeof(NewValue))
                    end
                elseif NewValue ~= nil then
                    ValueType = typeof(NewValue)
                end

                if not IsValueChanged(ThisValue, NewValue) then
                    return
                end

                ThisValue = NewValue

                -- Make sure to fire the changed signal for other states
                ChangedSignal:Fire("Value", ThisValue)
                return
            elseif Index == "__LOCKED" then
                if NewValue == true then
                    IsLocked = true
                    ActiveValue:Destroy()
                    return
                else
                    error("Can not unlock values.")
                end
            elseif Index == "__SEAM_OBJECT" then
                return InstanceSymbol
            else
                error("Ran into an unexpected error")
            end
        end,

        __call = function(_, Object, Index : string)
            if not Object then
                return
            end

            if not Index then
                return
            end

            if typeof(ThisValue) == "table" and ThisValue ~= nil then
                Object[Index] = table.clone(ThisValue)
            else
                Object[Index] = ThisValue
            end

            AttachedSignal:Fire(Object)

            TroveInstance:Add(StateManager:AttachStateToObject(Object, {
                Value = function()
                    if typeof(ThisValue) == "table" and ThisValue ~= nil then
                        return table.clone(ThisValue)
                    else
                        return ThisValue
                    end
                end,

                PropertyName = Index
            }))
        end
    })

    return ActiveValue :: ValueInstance<any>
end

function Value:__index(Index : string)
    if Index == "__SEAM_INDEX" then
        return ClassSymbol
    elseif Index == "__SEAM_CAN_BE_SCOPED" then
        return true
    else
        return nil
    end
end

local Meta = setmetatable({}, Value)

return Meta :: ValueConstructor<any>


-- Author: iGottic

local GetValue = {}

-- Types
export type GetValue = (State : any?) -> any?

-- Imports
local IsState = require(script.Parent.IsState)

function GetValue:__call(State : any?) : any?
    -- Nothing exists? No problem.
    if State == nil then
        return nil
    end

    -- Is it a state? Return the value of that state.
    if IsState(State) then
        return State.Value
    end

    -- Just normal userdata? That's cool too.
    return State
end

function GetValue:__index(Key : string)
    if Key == "__SEAM_CAN_BE_SCOPED" then
        return false
    end

    return nil
end

local Meta = setmetatable({}, GetValue)

return Meta :: GetValue


-- Author: iGottic

local Inspect = {}

-- Types
export type Inspect = (Object : any, DebugName : string) -> RBXScriptConnection

function Inspect:__call(Object : any, DebugName : string)
    if not DebugName then
        error("Expected DebugName, got nil")
    end

    return (Object.Changed :: RBXScriptSignal):Connect(function(PropertyName : string)
        if not PropertyName then
            print(`SEAM_INSPECT | {DebugName} | Value changed, unknown property that changed`)
            return
        end

        print(`SEAM_INSPECT | {DebugName} | Value changed to {Object[PropertyName]}`)
    end)
end

function Inspect:__index(Index : string)
    if Index == "__SEAM_CAN_BE_SCOPED" then
        return true
    else
        return nil
    end
end

local Meta = setmetatable({}, Inspect)

return Meta :: Inspect


-- Author: iGottic

local IsComponent = {}

-- Types
export type IsComponent = (CheckedValue : any?) -> boolean

function IsComponent:__call(CheckedValue : any?) : boolean
    if typeof(CheckedValue) ~= "table" then
        return false -- Not a component since it's not a table
    end

    if CheckedValue.__SEAM_COMPONENT then
        return true -- Is a state since the value has a component symbol
    end

    return false -- Default to false
end

function IsComponent:__index(Key : string)
    if Key == "__SEAM_CAN_BE_SCOPED" then
        return false
    end

    return nil
end

local Meta = setmetatable({}, IsComponent)

return Meta :: IsComponent


-- Author: iGottic

local IsState = {}

-- Types
export type IsState = (CheckedValue : any?) -> boolean

-- Constants
local RECOGNIZED_STATE_SYMBOLS = {"Spring", "Tween", "ComputedInstance", "RenderedInstance", "Value"}

function IsState:__call(CheckedValue : any?) : boolean
    if typeof(CheckedValue) ~= "table" then
        return false -- Not a state since it's not a table
    end

    if not CheckedValue.__SEAM_OBJECT then
        return false -- Not a state since it's not from Seam or not a Seam object
    end

    if table.find(RECOGNIZED_STATE_SYMBOLS, tostring(CheckedValue.__SEAM_OBJECT)) then
        return true -- Is a state since the symbol can be coerced into a string from the recognized state symbols table
    end

    return false -- Default to false
end

function IsState:__index(Key : string)
    if Key == "__SEAM_CAN_BE_SCOPED" then
        return false
    end

    return nil
end

local Meta = setmetatable({}, IsState)

return Meta :: IsState


-- Author: iGottic

local LockValue = {}

-- Imports
local Value = require(script.Parent.Parent.States.Value)

-- Types
export type LockValue = (LockedValue : Value.ValueInstance<any>) -> nil

function LockValue:__call(LockedValue : Value.ValueInstance<any>) : nil
    if typeof(LockedValue) ~= "table" or not LockedValue.__SEAM_OBJECT or tostring(LockedValue.__SEAM_OBJECT) ~= "Value" then
        error("You can only lock a value state")
    end

    LockedValue.__LOCKED = true
end

function LockValue:__index(Key : string)
    if Key == "__SEAM_CAN_BE_SCOPED" then
        return false
    end

    return nil
end

local Meta = setmetatable({}, LockValue)

return Meta :: LockValue


-- Author: iGottic

local SetValue = {}

-- Types
export type SetValue = (State : any?, NewValue : any?) -> any?

function SetValue:__call(State : any?, NewValue : any?) : any?
    if State == nil then
        return NewValue
    end

    if typeof(State) == "table" and State.Value ~= nil then
        State.Value = NewValue
    end

    return NewValue
end

function SetValue:__index(Key : string)
    if Key == "__SEAM_CAN_BE_SCOPED" then
        return false
    end

    return nil
end

local Meta = setmetatable({}, SetValue)

return Meta :: SetValue


-- Author: iGottic

-- Constants
local DEPTH = 10

return function()
    local Traceback = ""

	for Index = 1, DEPTH do
		local ThisTraceback = debug.traceback(nil, Index + 1)

		if ThisTraceback:len() == 0 then
			break
		end

		Traceback ..= ThisTraceback .. "\n"
	end

	return Traceback
end


-- Author: iGottic

return function(OldValue : any, NewValue : any) : boolean
    if typeof(OldValue) ~= typeof(NewValue) then
        return true
    end

    return OldValue ~= NewValue
end


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

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

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

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

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

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

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

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

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

Connection.Destroy = Connection.Disconnect

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

local Signal = {}
Signal.__index = Signal

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

	return self
end

function Signal.Wrap<T...>(rbxScriptSignal: RBXScriptSignal): Signal<T...>
	assert(
		typeof(rbxScriptSignal) == "RBXScriptSignal",
		"Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal)
	)

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

	return signal
end

function Signal.Is(obj: any): boolean
	return type(obj) == "table" and getmetatable(obj) == Signal
end

function Signal:Connect(fn)
	local connection = setmetatable({
		Connected = true,
		_signal = self,
		_fn = fn,
		_next = false,
	}, Connection)

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

	return connection
end

function Signal:ConnectOnce(fn)
	return self:Once(fn)
end

function Signal:Once(fn)
	local connection
	local done = false

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

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

	return connection
end

function Signal:GetConnections()
	local items = {}

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

	return items
end

function Signal:DisconnectAll()
	local item = self._handlerListHead
	while item do
		item.Connected = false
		item = item._next
	end
	self._handlerListHead = false

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

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

function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item.Connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end

function Signal:FireDeferred(...)
	local item = self._handlerListHead
	while item do
		local conn = item
		task.defer(function(...)
			if conn.Connected then
				conn._fn(...)
			end
		end, ...)
		item = item._next
	end
end

function Signal:Wait()
	local yieldedThreads = rawget(self, "_yieldedThreads")
	if not yieldedThreads then
		yieldedThreads = {}
		rawset(self, "_yieldedThreads", yieldedThreads)
	end

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

	self:Once(function(...)
		yieldedThreads[thread] = nil

		if coroutine.status(thread) == "suspended" then
			task.spawn(thread, ...)
		end
	end)

	return coroutine.yield()
end

function Signal:Destroy()
	self:DisconnectAll()

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

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

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


-- Author: iGottic

local StateManager = {}

-- Imports
local Trove = require(script.Parent.Trove)
local IsValueChanged = require(script.Parent.IsValueChanged)
local UpdateSignals = require(script.Parent.UpdateSignals)
local CreateDeepTraceback = require(script.Parent.CreateDeepTraceback)

local function GetObjectType(Object : any) : string
    if typeof(Object) == "Instance" then
        return "Instance"
    elseif typeof(Object) == "table" then
        if Object.__SEAM_OBJECT then
            return "SeamObject"
        end
    end

    return "unknown"
end

function StateManager:AttachStateToObject(Object : any, StateInstance : any)
    local ObjectType = GetObjectType(Object)
    local TroveInstance = Trove.new()

    if ObjectType == "Instance" then
        local LastValue = nil

        TroveInstance:Add(UpdateSignals.OnFrameUpdate:Connect(function()
            local NewValue = StateInstance.Value

            if typeof(NewValue) == "function" then
                NewValue = NewValue()
            end

            if LastValue ~= nil and not IsValueChanged(LastValue, NewValue) then
                return
            end

            if typeof(Object[StateInstance.PropertyName]) ~= typeof(NewValue) then
                warn(`{Object:GetFullName()} expected type {typeof(Object[StateInstance.PropertyName])} for attached state value, got a value of type {typeof(NewValue)}{"\n"}{CreateDeepTraceback()}`)
                return
            end

            Object[StateInstance.PropertyName] = NewValue
            LastValue = NewValue
        end))

        task.defer(function()
            TroveInstance:Add(Object.AncestryChanged:Connect(function()
                if not Object:IsDescendantOf(game) then
                    TroveInstance:Destroy()
                end
            end))
        end)
    elseif ObjectType == "SeamObject" then
        local LastValue = nil

        TroveInstance:Add(UpdateSignals.OnFrameUpdate:Connect(function()
            local NewValue = StateInstance.Value

            if typeof(NewValue) == "function" then
                NewValue = NewValue()
            end

            if LastValue ~= nil and not IsValueChanged(LastValue, NewValue) then
                return
            end

            Object[StateInstance.PropertyName] = NewValue
            LastValue = NewValue
        end))
    else
        error("Attempted to attach non-state to object:\n" .. CreateDeepTraceback())
    end

    return TroveInstance
end

return StateManager


-- Author: iGottic

local Symbol = {}

-- Types
export type Symbol = typeof(newproxy(true))

function Symbol.new(Name : string)
    local self = newproxy(true)

    getmetatable(self).__tostring = function()
        return Name
    end

    return self
end

return Symbol


--!strict

local RunService = game:GetService("RunService")

export type Trove = {
	Extend: (self: Trove) -> Trove,
	Clone: <T>(self: Trove, instance: T & Instance) -> T,
	Construct: <T, A...>(self: Trove, class: Constructable<T, A...>, A...) -> T,
	Connect: (
		self: Trove,
		signal: SignalLike | SignalLikeMetatable | RBXScriptSignal,
		fn: (...any) -> ...any
	) -> ConnectionLike | ConnectionLikeMetatable,
	BindToRenderStep: (self: Trove, name: string, priority: number, fn: (dt: number) -> ()) -> (),
	AddPromise: <T>(self: Trove, promise: (T & PromiseLike) | (T & PromiseLikeMetatable)) -> T,
	Add: <T>(self: Trove, object: T & Trackable, cleanupMethod: string?) -> T,
	Remove: <T>(self: Trove, object: T & Trackable) -> boolean,
	Pop: <T>(self: Trove, object: T & Trackable) -> boolean,
	Clean: (self: Trove) -> (),
	WrapClean: (self: Trove) -> () -> (),
	AttachToInstance: (self: Trove, instance: Instance) -> RBXScriptConnection,
	Destroy: (self: Trove) -> (),
}

type TroveInternal = Trove & {
	_objects: { any },
	_cleaning: boolean,
	_findAndRemoveFromObjects: (self: TroveInternal, object: any, cleanup: boolean) -> boolean,
	_cleanupObject: (self: TroveInternal, object: any, cleanupMethod: string?) -> (),
}

export type Trackable =
	| Instance
	| RBXScriptConnection
	| ConnectionLike
	| ConnectionLikeMetatable
	| PromiseLike
	| PromiseLikeMetatable
	| thread
	| ((...any) -> ...any)
	| Destroyable
	| DestroyableMetatable
	| DestroyableLowercase
	| DestroyableLowercaseMetatable
	| Disconnectable
	| DisconectableMetatable
	| DisconnectableLowercase
	| DisconnectableLowercaseMetatable
	| SignalLike
	| SignalLikeMetatable

type ConnectionLike = {
	Connected: boolean,
	Disconnect: (self: ConnectionLike) -> (),
}

type ConnectionLikeMetatable = typeof(setmetatable(
	{},
	{} :: { Connected: boolean, Disconnect: (self: ConnectionLikeMetatable) -> () }
))

type SignalLike = {
	Connect: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike | ConnectionLikeMetatable,
	Once: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike | ConnectionLikeMetatable,
}

type SignalLikeMetatable = typeof(setmetatable(
	{},
	{} :: {
		Connect: (self: SignalLikeMetatable, callback: (...any) -> ...any) -> ConnectionLike | ConnectionLikeMetatable,
		Once: (self: SignalLikeMetatable, callback: (...any) -> ...any) -> ConnectionLike | ConnectionLikeMetatable,
	}
))

type PromiseLike = {
	getStatus: (self: PromiseLike) -> string,
	finally: (self: PromiseLike, callback: (...any) -> ...any) -> PromiseLike | PromiseLikeMetatable,
	cancel: (self: PromiseLike) -> (),
}

type PromiseLikeMetatable = typeof(setmetatable(
	{},
	{} :: {
		getStatus: (self: any) -> string,
		finally: (self: PromiseLikeMetatable, callback: (...any) -> ...any) -> PromiseLike | PromiseLikeMetatable,
		cancel: (self: PromiseLikeMetatable) -> (),
	}
))

type Constructable<T, A...> = { new: (A...) -> T } | (A...) -> T

type Destroyable = {
	Destroy: (self: Destroyable) -> (),
}

type DestroyableMetatable = typeof(setmetatable({}, {} :: { Destroy: (self: DestroyableMetatable) -> () }))

type DestroyableLowercase = {
	destroy: (self: DestroyableLowercase) -> (),
}

type DestroyableLowercaseMetatable = typeof(setmetatable(
	{},
	{} :: { destroy: (self: DestroyableLowercaseMetatable) -> () }
))

type Disconnectable = {
	Disconnect: (self: Disconnectable) -> (),
}

type DisconectableMetatable = typeof(setmetatable({}, {} :: { Disconnect: (self: DisconectableMetatable) -> () }))

type DisconnectableLowercase = {
	disconnect: (self: DisconnectableLowercase) -> (),
}

type DisconnectableLowercaseMetatable = typeof(setmetatable(
	{},
	{} :: { disconnect: (self: DisconnectableLowercaseMetatable) -> () }
))

local FN_MARKER = newproxy()
local THREAD_MARKER = newproxy()
local GENERIC_OBJECT_CLEANUP_METHODS = table.freeze({ "Destroy", "Disconnect", "destroy", "disconnect" })

local function getObjectCleanupFunction(object: any, cleanupMethod: string?)
	local t = typeof(object)

	if t == "function" then
		return FN_MARKER
	elseif t == "thread" then
		return THREAD_MARKER
	end

	if cleanupMethod then
		return cleanupMethod
	end

	if t == "Instance" then
		return "Destroy"
	elseif t == "RBXScriptConnection" then
		return "Disconnect"
	elseif t == "table" then
		for _, genericCleanupMethod in GENERIC_OBJECT_CLEANUP_METHODS do
			if typeof(object[genericCleanupMethod]) == "function" then
				return genericCleanupMethod
			end
		end
	end

	error(`failed to get cleanup function for object {t}: {object}`, 3)
end

local function assertPromiseLike(object: any)
	if
		typeof(object) ~= "table"
		or typeof(object.getStatus) ~= "function"
		or typeof(object.finally) ~= "function"
		or typeof(object.cancel) ~= "function"
	then
		error("did not receive a promise as an argument", 3)
	end
end

local function assertSignalLike(object: any)
	if
		typeof(object) ~= "RBXScriptSignal"
		and (typeof(object) ~= "table" or typeof(object.Connect) ~= "function" or typeof(object.Once) ~= "function")
	then
		error("did not receive a signal as an argument", 3)
	end
end

local Trove = {}
Trove.__index = Trove

function Trove.new(): Trove
	local self = setmetatable({}, Trove)

	self._objects = {}
	self._cleaning = false

	return (self :: any) :: Trove
end

function Trove.Add(self: TroveInternal, object: Trackable, cleanupMethod: string?): any
	if self._cleaning then
		error("cannot call trove:Add() while cleaning", 2)
	end

	local cleanup = getObjectCleanupFunction(object, cleanupMethod)
	table.insert(self._objects, { object, cleanup })

	return object
end

function Trove.Clone(self: TroveInternal, instance: Instance): Instance
	if self._cleaning then
		error("cannot call trove:Clone() while cleaning", 2)
	end

	return self:Add(instance:Clone())
end

function Trove.Construct<T, A...>(self: TroveInternal, class: Constructable<T, A...>, ...: A...)
	if self._cleaning then
		error("Cannot call trove:Construct() while cleaning", 2)
	end

	local object = nil
	local t = type(class)
	if t == "table" then
		object = (class :: any).new(...)
	elseif t == "function" then
		object = (class :: any)(...)
	end

	return self:Add(object)
end

function Trove.Connect(
	self: TroveInternal,
	signal: SignalLike | SignalLikeMetatable | RBXScriptSignal,
	fn: (...any) -> ...any
)
	if self._cleaning then
		error("Cannot call trove:Connect() while cleaning", 2)
	end
	assertSignalLike(signal)

	local confirmedSignal = signal :: SignalLike

	return self:Add(confirmedSignal:Connect(fn))
end

function Trove.Once(
	self: TroveInternal,
	signal: SignalLike | SignalLikeMetatable | RBXScriptSignal,
	fn: (...any) -> ...any
)
	if self._cleaning then
		error("Cannot call trove:Connect() while cleaning", 2)
	end
	assertSignalLike(signal)

	local confirmedSignal = signal :: SignalLike

	local conn
	conn = confirmedSignal:Once(function(...)
		fn(...)
		self:Pop(conn)
	end)

	return self:Add(conn)
end

function Trove.BindToRenderStep(self: TroveInternal, name: string, priority: number, fn: (dt: number) -> ())
	if self._cleaning then
		error("cannot call trove:BindToRenderStep() while cleaning", 2)
	end

	RunService:BindToRenderStep(name, priority, fn)

	self:Add(function()
		RunService:UnbindFromRenderStep(name)
	end)
end

function Trove.AddPromise(self: TroveInternal, promise: PromiseLike | PromiseLikeMetatable)
	if self._cleaning then
		error("cannot call trove:AddPromise() while cleaning", 2)
	end
	assertPromiseLike(promise)
	local confirmedPromise = promise :: PromiseLike

	if confirmedPromise:getStatus() == "Started" then
		confirmedPromise:finally(function()
			if self._cleaning then
				return
			end
			self:_findAndRemoveFromObjects(confirmedPromise, false)
		end)

		self:Add(confirmedPromise, "cancel")
	end

	return confirmedPromise
end

function Trove.Remove(self: TroveInternal, object: Trackable): boolean
	if self._cleaning then
		error("cannot call trove:Remove() while cleaning", 2)
	end

	return self:_findAndRemoveFromObjects(object, true)
end

function Trove.Pop(self: TroveInternal, object: Trackable): boolean
	if self._cleaning then
		error("cannot call trove:Pop() while cleaning", 2)
	end

	return self:_findAndRemoveFromObjects(object, false)
end

function Trove.Extend(self: TroveInternal)
	if self._cleaning then
		error("cannot call trove:Extend() while cleaning", 2)
	end

	return self:Construct(Trove)
end

function Trove.Clean(self: TroveInternal)
	if self._cleaning then
		return
	end

	self._cleaning = true

	for _, obj in self._objects do
		self:_cleanupObject(obj[1], obj[2])
	end

	table.clear(self._objects)
	self._cleaning = false
end

function Trove.WrapClean(self: TroveInternal)
	return function()
		self:Clean()
	end
end

function Trove._findAndRemoveFromObjects(self: TroveInternal, object: any, cleanup: boolean): boolean
	local objects = self._objects

	for i, obj in objects do
		if obj[1] == object then
			local n = #objects
			objects[i] = objects[n]
			objects[n] = nil

			if cleanup then
				self:_cleanupObject(obj[1], obj[2])
			end

			return true
		end
	end

	return false
end

function Trove._cleanupObject(_self: TroveInternal, object: any, cleanupMethod: string?)
	if cleanupMethod == FN_MARKER then
		task.spawn(object)
	elseif cleanupMethod == THREAD_MARKER then
		pcall(task.cancel, object)
	else
		object[cleanupMethod](object)
	end
end

function Trove.AttachToInstance(self: TroveInternal, instance: Instance)
	if self._cleaning then
		error("cannot call trove:AttachToInstance() while cleaning", 2)
	elseif not instance:IsDescendantOf(game) then
		error("instance is not a descendant of the game hierarchy", 2)
	end

	return self:Connect(instance.Destroying, function()
		self:Destroy()
	end)
end

function Trove.Destroy(self: TroveInternal)
	self:Clean()
end

return {
	new = Trove.new,
}


-- Author: iGottic

export type BaseState<T> = {
    Value : T,
}

export type Child = Instance | BaseState<any> | {[unknown] : Child}

return nil


-- Author: iGottic

local UpdateSignals = {}

-- Types
type UpdateSignals = {
    OnFrameUpdate : RBXScriptSignal,
    OnFramePreUpdate : RBXScriptSignal,
}

-- Services
local RunService = game:GetService("RunService")

if RunService:IsClient() then
    UpdateSignals.OnFrameUpdate = RunService.RenderStepped
    UpdateSignals.OnFramePreUpdate = RunService.PreRender
else -- If we're using the server, always use RunService.Heartbeat
    UpdateSignals.OnFrameUpdate = RunService.Heartbeat
    UpdateSignals.OnFramePreUpdate = RunService.Heartbeat
end

return UpdateSignals :: UpdateSignals


-- Author: iGottic
-- Based on UnpackType and PackType from Elttob

local ValuePacker = {}

-- Types
type UnpackedValue = {[number]: number}

-- Imports
local Oklab = require(script.Oklab) -- We use Oklab color space to make animating colors look more natural

-- Variables
local Converter : {[string] : {Pack : (UnpackedValue : UnpackedValue) -> any, Unpack : (Value : any) -> UnpackedValue}} = {}

Converter.number = {
	Pack = function(UnpackedValue : UnpackedValue)
		return UnpackedValue[1]
	end,

	Unpack = function(Value : number)
		return {Value}
	end,
}

Converter.CFrame = {
	Pack = function(UnpackedValue : UnpackedValue)
		return CFrame.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3]) *
			CFrame.fromAxisAngle(Vector3.new(UnpackedValue[4], UnpackedValue[5], UnpackedValue[6]).Unit, UnpackedValue[7])
	end,

	Unpack = function(Value : CFrame)
		local Axis, Angle = Value:ToAxisAngle()
		return {Value.X, Value.Y, Value.Z, Axis.X, Axis.Y, Axis.Z, Angle}
	end,
}

Converter.Color3 = {
	Pack = function(UnpackedValue : UnpackedValue)
		return Oklab.from(Vector3.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3]), false)
	end,

	Unpack = function(Value : Color3)
		local Lab = Oklab.to(Value)
		return {Lab.X, Lab.Y, Lab.Z}
	end,
}

Converter.ColorSequenceKeypoint = {
	Pack = function(UnpackedValue : UnpackedValue)
		return ColorSequenceKeypoint.new(UnpackedValue[4], Oklab.from(Vector3.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3]), false))
	end,

	Unpack = function(Value : ColorSequenceKeypoint)
		local Lab = Oklab.to(Value.Value)
		return {Lab.X, Lab.Y, Lab.Z, Value.Time}
	end,
}

Converter.DateTime = {
	Pack = function(UnpackedValue : UnpackedValue)
		return DateTime.fromUnixTimestampMillis(UnpackedValue[1])
	end,

	Unpack = function(Value : DateTime)
		return {Value.UnixTimestampMillis}
	end,
}

Converter.NumberRange = {
	Pack = function(UnpackedValue : UnpackedValue)
		return NumberRange.new(UnpackedValue[1], UnpackedValue[2])
	end,

	Unpack = function(Value : NumberRange)
		return {Value.Min, Value.Max}
	end,
}

Converter.NumberSequenceKeypoint = {
	Pack = function(UnpackedValue : UnpackedValue)
		return NumberSequenceKeypoint.new(UnpackedValue[2], UnpackedValue[1], UnpackedValue[3])
	end,

	Unpack = function(Value : NumberSequenceKeypoint)
		return {Value.Value, Value.Time, Value.Envelope}
	end,
}

Converter.PhysicalProperties = {
	Pack = function(UnpackedValue : UnpackedValue)
		return PhysicalProperties.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3], UnpackedValue[4], UnpackedValue[5])
	end,

	Unpack = function(Value : PhysicalProperties)
		return {Value.Density, Value.Friction, Value.Elasticity, Value.FrictionWeight, Value.ElasticityWeight}
	end,
}

Converter.Ray = {
	Pack = function(UnpackedValue : UnpackedValue)
		return Ray.new(
			Vector3.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3]),
			Vector3.new(UnpackedValue[4], UnpackedValue[5], UnpackedValue[6])
		)
	end,

	Unpack = function(Value : Ray)
		return {Value.Origin.X, Value.Origin.Y, Value.Origin.Z, Value.Direction.X, Value.Direction.Y, Value.Direction.Z}
	end,
}

Converter.Rect = {
	Pack = function(UnpackedValue : UnpackedValue)
		return Rect.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3], UnpackedValue[4])
	end,

	Unpack = function(Value : Rect)
		return {Value.Min.X, Value.Min.Y, Value.Max.X, Value.Max.Y}
	end,
}

Converter.Region3 = {
	Pack = function(UnpackedValue : UnpackedValue)
		local Position = Vector3.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3])
		local HalfSize = Vector3.new(UnpackedValue[4] / 2, UnpackedValue[5] / 2, UnpackedValue[6] / 2)

		return Region3.new(Position - HalfSize, Position + HalfSize)
	end,

	Unpack = function(Value : Region3)
		return {Value.CFrame.X, Value.CFrame.Y, Value.CFrame.Z, Value.Size.X, Value.Size.Y, Value.Size.Z}
	end,
}

Converter.Region3int16 = {
	Pack = function(UnpackedValue : UnpackedValue)
		return Region3int16.new(
			Vector3int16.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3]),
			Vector3int16.new(UnpackedValue[4], UnpackedValue[5], UnpackedValue[6])
		)
	end,

	Unpack = function(Value : Region3int16)
		return {Value.Min.X, Value.Min.Y, Value.Min.Z, Value.Max.X, Value.Max.Y, Value.Max.Z}
	end,
}

Converter.UDim = {
	Pack = function(UnpackedValue : UnpackedValue)
		return UDim.new(UnpackedValue[1], UnpackedValue[2])
	end,

	Unpack = function(Value : UDim)
		return {Value.Scale, Value.Offset}
	end,
}

Converter.UDim2 = {
	Pack = function(UnpackedValue : UnpackedValue)
		return UDim2.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3], UnpackedValue[4])
	end,

	Unpack = function(Value : UDim2)
		return {Value.X.Scale, Value.X.Offset, Value.Y.Scale, Value.Y.Offset}
	end,
}

Converter.Vector2 = {
	Pack = function(UnpackedValue : UnpackedValue)
		return Vector2.new(UnpackedValue[1], UnpackedValue[2])
	end,

	Unpack = function(Value : Vector2)
		return {Value.X, Value.Y}
	end,
}

Converter.Vector2int16 = {
	Pack = function(UnpackedValue : UnpackedValue)
		return Vector2int16.new(UnpackedValue[1], UnpackedValue[2])
	end,

	Unpack = function(Value : Vector2int16)
		return {Value.X, Value.Y}
	end,
}

Converter.Vector3 = {
	Pack = function(UnpackedValue : UnpackedValue)
		return Vector3.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3])
	end,

	Unpack = function(Value : Vector3)
		return {Value.X, Value.Y, Value.Z}
	end,
}

Converter.Vector3int16 = {
	Pack = function(UnpackedValue : UnpackedValue)
		return Vector3int16.new(UnpackedValue[1], UnpackedValue[2], UnpackedValue[3])
	end,

	Unpack = function(Value : Vector3int16)
		return {Value.X, Value.Y, Value.Z}
	end,
}

-- Converts a value into a table
function ValuePacker.UnpackValue(Value : any, ValueType : string) : UnpackedValue
	if Converter[ValueType] then
		return Converter[ValueType].Unpack(Value)
	end

	return {}
end

-- Converts a table into a value
function ValuePacker.PackValue(UnpackedValue : UnpackedValue, ValueType : string) : any
	if Converter[ValueType] then
		return Converter[ValueType].Pack(UnpackedValue)
	end

	return nil
end

return ValuePacker


--[=[
    Documentation: https://seam.igottic.com/

    @class Seam
]=]

-- Services
local RunService = game:GetService("RunService")

-- Imports
-- Note: I opt for WaitForChild to load stuff; this helps prevent edge case errors where children don't load in time, particularly when loading from ReplicatedStorage in a script located in ReplicatedFirst
local Constructors = script:WaitForChild("Constructors")
local Connections = Constructors:WaitForChild("Connections")
local Declarations = Constructors:WaitForChild("Declarations")
local Memory = Constructors:WaitForChild("Memory")
local States = Constructors:WaitForChild("States")
local Utilities = Constructors:WaitForChild("Utilities")
local Modules = script:WaitForChild("Modules")
local Types = require(Modules:WaitForChild("Types"))
local New = require(Declarations:WaitForChild("New"))
local Children = require(Declarations:WaitForChild("Children"))
local Value = require(States:WaitForChild("Value"))
local Computed = require(States:WaitForChild("Computed"))
local Spring = require(States.Animation:WaitForChild("Spring"))
local Tween = require(States.Animation:WaitForChild("Tween"))
local Scope = require(Memory:WaitForChild("Scope"))
local OnEvent = require(Connections:WaitForChild("OnEvent"))
local OnChanged = require(Connections:WaitForChild("OnChanged"))
local Attribute = require(Declarations:WaitForChild("Attribute"))
local OnAttributeChanged = require(Connections:WaitForChild("OnAttributeChanged"))
local Lifetime = require(Declarations:WaitForChild("Lifetime"))
local Rendered = require(States:WaitForChild("Rendered"))
local FollowProperty = require(Declarations:WaitForChild("FollowProperty"))
local FollowAttribute = require(Declarations:WaitForChild("FollowAttribute"))
local GetValue = require(Utilities:WaitForChild("GetValue"))
local Component = require(Declarations:WaitForChild("Component"))
local Tags = require(Declarations:WaitForChild("Tags"))
local SetValue = require(Utilities:WaitForChild("SetValue"))
local IsState = require(Utilities:WaitForChild("IsState"))
local IsComponent = require(Utilities:WaitForChild("IsComponent"))
local LockValue = require(Utilities:WaitForChild("LockValue"))
local Inspect = require(Utilities:WaitForChild("Inspect"))
local OnAttached = require(Connections:WaitForChild("OnAttached"))
local Destroyed = require(Declarations:WaitForChild("Destroyed"))
local EventSequence = require(Declarations:WaitForChild("EventSequence"))
local ForPairs = require(States:WaitForChild("ForPairs"))

-- Types extended
type Seam = {
    New : New.NewConstructor,
    Children : Children.Children,
    Value : Value.ValueConstructor<any>,
    Computed : Computed.ComputedConstructor<any>,
    Spring : Spring.SpringConstructor<any>,
    Tween : Tween.TweenConstructor<any>,
    Scope : Scope.ScopeConstructor,
    OnEvent : OnEvent.OnEvent,
    OnChanged : OnChanged.OnChanged,
    Attribute : Attribute.Attribute,
    OnAttributeChanged : OnAttributeChanged.OnAttributeChanged,
    Lifetime : Lifetime.Lifetime,
    Rendered : Rendered.RenderedConstructor<any>,
    FollowProperty : FollowProperty.FollowProperty,
    FollowAttribute : FollowAttribute.FollowAttribute,
    GetValue : GetValue.GetValue,
    Component : Component.Component,
    Tags : Tags.Tags,
    SetValue : SetValue.SetValue,
    IsState : IsState.IsState,
    IsComponent : IsComponent.IsComponent,
    LockValue : LockValue.LockValue,
    Inspect : Inspect.Inspect,
    OnAttached : OnAttached.OnAttached,
    Destroyed : Destroyed.Destroyed,
    EventSequence : EventSequence.EventSequence,
    ForPairs : ForPairs.ForPairsConstructor<any>
}

export type BaseState<T> = Types.BaseState<T>
export type Child = Types.Child
export type NewConstructor = New.NewConstructor
export type Children = Children.Children
export type Value<T> = Value.ValueInstance<T>
export type ValueConstructor<T> = Value.ValueConstructor<T>
export type Computed<T> = Computed.ComputedInstance<T>
export type ComputedConstructor<T> = Computed.ComputedConstructor<T>
export type Spring<T> = Spring.SpringInstance<T>
export type SpringConstructor<T> = Spring.SpringConstructor<T>
export type Tween<T> = Tween.TweenInstance<T>
export type TweenConstructor<T> = Tween.TweenConstructor<T>
export type Scope = Scope.ScopeInstance
export type ScopeConstructor = Scope.ScopeConstructor
export type OnEvent = OnEvent.OnEvent
export type OnChanged = OnChanged.OnChanged
export type Attribute = Attribute.Attribute
export type OnAttributeChanged = OnAttributeChanged.OnAttributeChanged
export type Lifetime = Lifetime.Lifetime
export type Rendered<T> = Rendered.RenderedInstance<T>
export type RenderedConstructor<T> = Rendered.RenderedConstructor<T>
export type FollowProperty = FollowProperty.FollowProperty
export type FollowAttribute = FollowAttribute.FollowAttribute
export type GetValue = GetValue.GetValue
export type Component = Component.Component
export type Tags = Tags.Tags
export type SetValue = SetValue.SetValue
export type IsState = IsState.IsState
export type IsComponent = IsComponent.IsComponent
export type LockValue = LockValue.LockValue
export type Inspect = Inspect.Inspect
export type OnAttached = OnAttached.OnAttached
export type Destroyed = Destroyed.Destroyed
export type EventSequence = EventSequence.EventSequence
export type ForPairs<T> = ForPairs.ForPairsInstance<T>
export type ForPairsInstance<T> = ForPairs.ForPairsInstance<T>

-- Declaration
local Seam : Seam = {} :: Seam

local function Init()
    if RunService:IsServer() then
        return -- Don't wait for game.Loaded on server
    end

    if not game:IsLoaded() then
        game.Loaded:Wait()

        -- There is this weird bug where, when loaded from ReplicatedFirst, the framework loads *too* fast and animations don't work on the frontend.
        -- This is a hacky workaround for this while I figure out a better solution.
    end
end

--[=[
    @function New
    @within Seam
    @tag Seam Declaration

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.New = New

--[=[
    @function Children
    @within Seam
    @tag Seam Declaration

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Children = Children

--[=[
    @function Value
    @within Seam
    @tag Seam State

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Value = Value

--[=[
    @function Computed
    @within Seam
    @tag Seam State

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Computed = Computed

--[=[
    @function Spring
    @within Seam
    @tag Seam State

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Spring = Spring

--[=[
    @function Tween
    @within Seam
    @tag Seam State

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Tween = Tween

--[=[
    @function Scope
    @within Seam
    @tag Seam Memory

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Scope = Scope

--[=[
    @function OnEvent
    @within Seam
    @tag Seam Connection

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.OnEvent = OnEvent

--[=[
    @function OnChange
    @within Seam
    @tag Seam Connection

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.OnChanged = OnChanged

--[=[
    @function Attribute
    @within Seam
    @tag Seam Declaration
]=]

Seam.Attribute = Attribute

--[=[
    @function OnAttributeChanged
    @within Seam
    @tag Seam Connection

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.OnAttributeChanged = OnAttributeChanged

--[=[
    @function Lifetime
    @within Seam
    @tag Seam Declaration

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Lifetime = Lifetime

--[=[
    @function Rendered
    @within Seam
    @tag Seam State

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Rendered = Rendered

--[=[
    @function FollowProperty
    @within Seam
    @tag Seam Declaration

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.FollowProperty = FollowProperty

--[=[
    @function FollowAttribute
    @within Seam
    @tag Seam Declaration

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.FollowAttribute = FollowAttribute

--[=[
    @function GetValue
    @within Seam
    @tag Seam Utility

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.GetValue = GetValue

--[=[
    @function Component
    @within Seam
    @tag Seam Declaration

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Component = Component

--[=[
    @function Tags
    @within Seam
    @tag Seam Declaration

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Tags = Tags

--[=[
    @function SetValue
    @within Seam
    @tag Seam Utility

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.SetValue = SetValue

--[=[
    @function IsState
    @within Seam
    @tag Seam Utility

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.IsState = IsState

--[=[
    @function IsComponent
    @within Seam
    @tag Seam Utility

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.IsComponent = IsComponent

--[=[
    @function LockValue
    @within Seam
    @tag Seam Utility

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.LockValue = LockValue

--[=[
    @function Inspect
    @within Seam
    @tag Seam Utility

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Inspect = Inspect

--[=[
    @function OnAttached
    @within Seam
    @tag Seam Connection

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.OnAttached = OnAttached

--[=[
    @function Destroyed
    @within Seam
    @tag Seam Declaration

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.Destroyed = Destroyed

--[=[
    @function EventSequence
    @within Seam
    @tag Seam Declaration

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.EventSequence = EventSequence

--[=[
    @function ForPairs
    @within Seam
    @tag Seam State

    Visit https://seam.igottic.com/ for more info.
]=]

Seam.ForPairs = ForPairs

Init()

return Seam :: Seam


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

export type Asset = {
    IconName: string, -- "icon-name"
    Id: number, -- 123456789
    Url: string, -- "rbxassetid://123456789"
    ImageRectSize: Vector2, -- Vector2.new(48, 48)
    ImageRectOffset: Vector2, -- Vector2.new(648, 266)
}

local Icons = require(script.Icons)
local VersionInfo = require(script.VersionInfo)

local Type = typeof or type
local function CheckArgTypes(funcName: string, inputArgs: {any}, typeEntries: {[number]: {string}})
    for ArgIndex, TypeEntryArray in typeEntries do
        local ArgName = TypeEntryArray[1]
        local ExpectedType = TypeEntryArray[2]

        local InputArg = inputArgs[ArgIndex]
        local InputArgType = Type(InputArg)

        if InputArgType ~= ExpectedType then
            error(funcName .. ": Argument " .. ArgIndex .. " (" .. ArgName .. "): expected type `" .. ExpectedType .. "`, got `" .. InputArgType .. "`", 3)
        end
    end
end

local function TrimIconIdentifier(inputIconIdentifier: string): string
    return string.match(string.lower(inputIconIdentifier), "^%s*(.*)%s*$") :: string
end

local function ApplyToInstance(object: Instance, properties: {[string]: any}): Instance
    if properties then
        for Property, Value in properties do
            if Property ~= "Parent" then
                object[Property] = Value
            end
        end

        if properties.Parent then
            object.Parent = properties.Parent
        end
    end

    return object
end

-- See /version-info.luau
local Lucide = {
    PackageVersion = VersionInfo.PackageVersion,
    LucideVersion = VersionInfo.LucideVersion,
}

-- Add all icon names to an array
do
    local IconNames: {string} = {}
    local _, FirstIconIndex = next(Icons) -- If it's actually `{}`, we're in trouble..
    FirstIconIndex = FirstIconIndex or {}

    for IconName in FirstIconIndex do
        table.insert(IconNames, IconName)
    end

    table.sort(IconNames)
    table.freeze(IconNames)
    Lucide.IconNames = IconNames
end

--[[
    Attempts to retrieve and wrap an asset object from a specified icon name, with
    an optional target icon size argument, fetching the closest to what's supported

    *Will* throw an error if the icon name provided is invalid/not found

    *Example:*
    ```lua
    local Asset = Lucide.GetAsset("server", 48) -- iconSize will default to `256` if not provided
    assert(Asset, "Failed to fetch asset!")

    print(Asset.IconName) -- "server"
    print(Asset.Id) -- 15269177520
    print(Asset.Url) -- "rbxassetid://15269177520"
    print(Asset.ImageRectSize) -- Vector2.new(48, 48)
    print(Asset.ImageRectOffset) -- Vector2.new(0, 771)
    ```
]]
function Lucide.GetAsset(iconName: string, iconSize: number?): Asset
    local IconSize = if iconSize == nil then 256 else iconSize

    CheckArgTypes("Lucide.GetAsset", {iconName, IconSize}, {
        [1] = {"iconName", "string"},
        [2] = {"iconSize", "number"},
    })

    local IconName = TrimIconIdentifier(iconName)

    -- If reading directly from a UI obj w/ a negative size..?
    if IconSize < 0 then
        IconSize = -IconSize
    end

    local RealSizeIndex = if IconSize <= 48 then "48px" else "256px"
    local IconIndexDict = Icons[RealSizeIndex]

    if not IconIndexDict then
        error("Lucide.GetAsset: Internal error: Failed to find icon index for specified size")
    end

    local RawAsset = IconIndexDict[IconName]
    if not RawAsset then
        error("Lucide.GetAsset: Failed to find icon by the name of \"" .. IconName .. "\" (@" .. RealSizeIndex .. "), perhaps a spelling mistake?", 2)
    end

    local Id = RawAsset[1]
    local RawImageRectSize = RawAsset[2]
    local RawImageRectOffset = RawAsset[3]

    if type(Id) ~= "number" or type(RawImageRectSize) ~= "table" or type(RawImageRectOffset) ~= "table" then
        error("Lucide.GetAsset: Internal error: Invalid auto-generated asset entry")
    end

    local Url = "rbxassetid://" .. Id
    local ImageRectSize = Vector2.new(RawImageRectSize[1], RawImageRectSize[2])
    local ImageRectOffset = Vector2.new(RawImageRectOffset[1], RawImageRectOffset[2])

    local Asset: Asset = {
        IconName = IconName,
        Id = Id,
        Url = Url,
        ImageRectSize = ImageRectSize,
        ImageRectOffset = ImageRectOffset,
    }

    return Asset
end

--[[
    Returns a dictionary of every `Asset` from every icon name in `Lucide.IconNames`

    This could also be useful for, for example, working with a custom asset
    preloading system via `ContentProvider:PreloadAsync()` etc

    *Example:*
    ```lua
    local AllAssets = Lucide.GetAllAssets(256) -- Also defaults to `256`, just like `Lucide.GetAsset()`

    for _, Asset in AllAssets do
        print(Asset.IconName, Asset.Url)
    end
    ```
]]
function Lucide.GetAllAssets(inputSize: number?): {Asset}
    local InputSize = if inputSize == nil then 256 else inputSize

    CheckArgTypes("Lucide.GetAllAssets", {InputSize}, {
        [1] = {"inputSize", "number"},
    })

    local Assets = {}

    for _, IconName in Lucide.IconNames do
        local Asset = Lucide.GetAsset(IconName, InputSize)
        if Asset then
            table.insert(Assets, Asset)
        end
    end

    -- `Lucide.IconNames` is already pre-sorted
    return Assets
end

--[[
    Wrapper around `Lucide.GetAsset()` that fetches asset info for the specified
    icon name and size, anc creates an `ImageLabel` Instance. Accepts an additional
    optional argument for providing a table of properties to automatically apply
    after the asset has been applied to said `ImageLabel`

    Without providing any extra property overrides, the icon is colored to its
    default of #FFFFFF, and theinput from the `imageSize` argument is the
    offset value of `ImageLabel.Size`

    Throws an error under the same terms as `Lucide.GetAsset()`

    *Example:*
    ```lua
    local PlayerGui = game:GetService("Players").LocalPlayer.PlayerGui
    local ScreenGui = Instance.new("ScreenGui")

    Lucide.ImageLabel("server-crash", 256, {
        AnchorPoint = Vector2.new(0.5, 0.5),
        Position = UDim2.fromScale(0.5, 0.5),

        Parent = ScreenGui,
    })

    ScreenGui.Parent = PlayerGui
    ```
]]
function Lucide.ImageLabel(iconName: string, imageSize: number?, propertyOverrides: {[string]: any}?): ImageLabel
    local ImageSize = if imageSize == nil then 256 else imageSize
    local PropertyOverrides = if propertyOverrides == nil then {} else propertyOverrides

    CheckArgTypes("Lucide.ImageLabel", {iconName, imageSize, propertyOverrides}, {
        [1] = {"iconName", "string"},
        [2] = {"imageSize", "number"},
        [3] = {"propertyOverrides", "table"}
    })

    local Asset = Lucide.GetAsset(iconName, ImageSize)

    local ImageLabel = ApplyToInstance(Instance.new("ImageLabel"), {
        Name = Asset.IconName,

        Size = UDim2.fromOffset(ImageSize, ImageSize),

        BackgroundColor3 = Color3.new(0, 0, 0),
        BackgroundTransparency = 1,
        BorderSizePixel = 0,

        Image = Asset.Url,
        ImageRectSize = Asset.ImageRectSize,
        ImageRectOffset = Asset.ImageRectOffset,
        ImageColor3 = Color3.new(1, 1, 1), -- #FFFFFF
        ScaleType = Enum.ScaleType.Fit,
    })

    -- Apply any provided overrides
    ApplyToInstance(ImageLabel, PropertyOverrides)

    return ImageLabel
end

table.freeze(Lucide)
return Lucide


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

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

local Log = require("modules/Log")
local CommandUtils = require("modules/CommandUtils")
local StringUtils = require("modules/StringUtils")
local Run = require("modules/Run")
local ParseArgs = require("modules/ParseArgs")

local SizesToExport = {"48", "256"}

CommandUtils.CheckCommands({"inkscape", "convert"})

if process.os ~= "linux" then
    Log.Warn("Due to the command syntax used, this build script was designed with only Linux use supported; you don't appear to be running Linux, do you still wish to proceed?")
    local ShouldContinue = stdio.prompt("confirm", "")

    if not ShouldContinue then
        return
    end
end

local Options = ParseArgs(process.args, {
    ["icons-folder"] = {"string", "icons"},
    ["skip-existing"] = {"boolean", true},
})

local IconsFolder = Options["icons-folder"]
local SkipExisting = Options["skip-existing"]

local SourceFolder = `{IconsFolder}/src`
local CompiledFolder = `{IconsFolder}/compiled`

assert(fs.isDir(IconsFolder), `Icons folder not found at "{process.cwd}{IconsFolder}"`)
assert(fs.isDir(SourceFolder), `Icon SVG sources not found at "{process.cwd}{SourceFolder}"`)

local NumberOfSourceIcons = #fs.readDir(SourceFolder)

if not SkipExisting and fs.isDir(SourceFolder) then
    Log.Warn(`The "{SourceFolder}" directory already exists, do you want to remove it and continue?`)
    local ShouldRemove = stdio.prompt("confirm", "")

    if not ShouldRemove then
        return
    end

    fs.removeDir(SourceFolder)
end

if not fs.isDir(SourceFolder) then
    fs.writeDir(SourceFolder)
end

-- Loop through source SVGs
for _, SizeToExport in SizesToExport do
    local SizeFolder = `{CompiledFolder}/{SizeToExport}px`
    if not fs.isDir(SizeFolder) then
        fs.writeDir(SizeFolder)
    end

    for _, FileName in fs.readDir(SourceFolder) do
        if StringUtils.FileType(FileName) ~= "svg" then
            continue
        end

        local IconName = StringUtils.FileNameWithoutExtension(FileName)
        local PNGPath = `{SizeFolder}/{IconName}.png`

        if SkipExisting and fs.isFile(PNGPath) then
            continue
        end

        Run(`inkscape -w {SizeToExport} -h {SizeToExport} --export-type="png" -o - {SourceFolder}/{FileName}`
        ..  ` | convert - -channel RGB -negate {SizeFolder}/{IconName}.png`)

        Log.Info(`Progress (Of @{SizeToExport}px): {#fs.readDir(SizeFolder)}/{NumberOfSourceIcons}`)
    end
end


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

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

local StringUtils = require("modules/StringUtils")

local IconSourcesDir = "icons/src"
local IconDisplayDir = "icons/compiled/48px"
local IconDisplayFileType = "png"
local OutputPath = "md/icon-index.md"

local Output = [[
# Icon Index

This file contains an auto-generated index of icons, paired with their internal identifier that you can use with `lucide-roblox`'s API

| Icon | Identifier |
| :--: | ---------- |
]]

local RelativeUpString = ""
for _ in StringUtils.SplitFilePath(StringUtils.DirectoryOfFilePath(OutputPath) :: string) do
    RelativeUpString ..= "../"
end

local IconSourcesDirFileNames = fs.readDir(IconSourcesDir)
table.sort(IconSourcesDirFileNames)

for _, FileName in IconSourcesDirFileNames do
    local IconName = StringUtils.FileNameWithoutExtension(FileName)
    local IconDisplayFilePath = `{RelativeUpString}{IconDisplayDir}/{IconName}.{IconDisplayFileType}`

    Output ..= `| <img width="24px" src="{StringUtils.SerializeStringData(IconDisplayFilePath)}" /> | \`{IconName}\` |\n`
end

fs.writeFile(OutputPath, Output)


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

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

local Log = require("modules/Log")
local Run = require("modules/Run")

local VersionInfo = require("../version-info")

local RojoOutputPath = "build/lucide-roblox.rbxm"
local WaxOutputPath = "build/lucide-roblox.luau"

local CodegenHeader = "--!nolint\n--!nocheck\n--!optimize 2\n--!native\n\n" ..
                      "--[[\n" ..
                      "    lucide-roblox - Library to Use the Lucide Icon Set (https://lucide.dev) in Roblox\n" ..
                      "    MIT License | Copyright (c) 2023 Latte Softworks <https://latte.to>\n\n" ..
                      `    Package Version: {VersionInfo.PackageVersion}\n` ..
                      `    Lucide Version: {VersionInfo.LucideVersion}\n` ..
                      "]]\n\n"

if fs.isDir("build") then
    Log.Info("Removing old \"build\" dir..")
    fs.removeDir("build")
end

local StartTime = os.clock()

fs.writeDir("build")

Log.Info("Copying \"LICENSE.md\" to build dir")
fs.copy("LICENSE.md", "build/LICENSE.md")

local function BadExit(message: string)
    Log.Error(message)
    Log.Error("Aborting..")
    process.exit(1)
end

Run("rojo", {"build", "-o", `{RojoOutputPath}`}, false, BadExit)
Run("lune", {"run", "wax", "bundle", `output={WaxOutputPath}`, "minify=true", `extra-offset-lines={#CodegenHeader - 1}`, "ci-mode=true", "verbose=true"}, false, BadExit)

fs.writeFile(WaxOutputPath, CodegenHeader .. fs.readFile(WaxOutputPath) .. "\n")

Log.Info(`Finished build process in {string.format("%.4f", os.clock() - StartTime)} (seconds)`)


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

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

local CommandUtils = {}

-- We'll check some required commands in a sec
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 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 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 Latte Softworks <https://latte.to>

local StringUtils = {}

function StringUtils.FileType(filePath: string): string?
    local Match = string.match(filePath, ".%.([^%.]+)$")
    if Match then
        Match = string.lower(Match)
    end

    return Match
end

function StringUtils.FileNameFromPath(filePath: string): string?
    return string.match(filePath, "[^/\\]+$")
end

function StringUtils.FileNameWithoutExtension(fileName: string, onlyTheLastDot: boolean?): string?
    onlyTheLastDot = if onlyTheLastDot == nil then true else onlyTheLastDot

    if onlyTheLastDot then
        return string.match(fileName, "^(.+)%.")
    else
        return string.match(fileName, "^%.?[^%.]+")
    end
end

function StringUtils.FilePathWithoutExtension(filePath: string, onlyTheLastDot: boolean?): string?
    local FilePathDirectory = StringUtils.DirectoryOfFilePath(filePath)
    local FileName = StringUtils.FileNameFromPath(filePath)

    if not FilePathDirectory or not FileName then
        return
    end

    local FileNameStripped = StringUtils.FileNameWithoutExtension(FileName, onlyTheLastDot)
    if not FileNameStripped then
        return
    end

    return FilePathDirectory .. FileNameStripped
end

function StringUtils.DirectoryOfFilePath(filePath: string): string?
    local Directory = string.match(filePath, "^.*[/\\]")
    if not Directory or Directory == "" then
        Directory = "./"
    end

    return Directory
end

function StringUtils.SplitStringByPattern(originalString: string, pattern: string)
    local SplitArray = {}
    for Match in string.gmatch(originalString, pattern) do
        table.insert(SplitArray, Match)
    end

    return SplitArray
end

function StringUtils.SplitFilePath(filePath: string)
    return StringUtils.SplitStringByPattern(filePath, "([^/\\]+)[/\\]?")
end

function StringUtils.LineCount(input: string): number
    local Lines = 1
    for _ in string.gmatch(input, "\n") do
        Lines += 1
    end

    return Lines
end

function StringUtils.Replace(inputString: string, replacements: {[string]: string}): string
    return string.gsub(inputString, "${([A-Za-z_][A-Za-z0-9_]*)}", replacements)
end

do
    -- These are control characters to be encoded in a certain way in Lua rather than just a byte
    -- escape (e.g. "\n" -> "\10")
    local SpecialCharacters = {
        ["\""] = "\\\"", -- Double-Quote
        ["\\"] = "\\\\", -- (Literal) Backslash
        -- Special ASCII control char codes
        ["\a"] = "\\a", -- Bell; ASCII #7
        ["\b"] = "\\b", -- Backspace; ASCII #8
        ["\t"] = "\\t", -- Horizontal-Tab; ASCII #9
        ["\n"] = "\\n", -- Newline; ASCII #10
        ["\v"] = "\\v", -- Vertical-Tab; ASCII #11
        ["\f"] = "\\f", -- Form-Feed; ASCII #12
        ["\r"] = "\\r", -- Carriage-Return; ASCII #13
    }

    -- We need to assign all extra normal byte escapes for runtime optimization
    for Index = 0, 255 do
        local Character = string.char(Index)

        if not SpecialCharacters[Character] and (Index < 32 or Index > 126) then
            SpecialCharacters[Character] = "\\" .. Index
        end
    end

    function StringUtils.SerializeStringData(inputString: string): string
        -- FYI; We can't do "\0-\31" in Lua 5.1 (Only Luau/Lua 5.2+) due to an embedded zeros in pattern
        -- issue. See: https://stackoverflow.com/a/22962409
        local Serialized = string.gsub(inputString, "[%z\\\"\1-\31\127-\255]", SpecialCharacters)
        return Serialized
    end
end

return StringUtils


-- MIT License | Copyright (c) 2023 Latte Softworks <https://latte.to>
-- Yes, this gets a bit hacky; we're reading from the manifest directly, so..

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

local Log = require("modules/Log")
local StringUtils = require("modules/StringUtils")
local LuaEncode = require("libraries/LuaEncode")

local CodegenPath = "lib/Icons.luau"
local CodegenBasePath = "icons/compiled/"
local IconNameDir = "icons/src/"

local TarmacManifest = serde.decode("toml", fs.readFile("tarmac-manifest.toml"))
local Inputs = TarmacManifest.inputs

assert(Inputs, "Failed to parse tarmac-manifest.toml; `inputs` table not found")

local function RemoveFromTable(inputTable: {[any]: any}, amountToRemove: number)
    for _ = 1, amountToRemove do
        table.remove(inputTable, 1)
    end
end

-- Now let's go thru and read the manifest
local Icons = {}

for FilePath, ImageInfo in Inputs do
    local SplitImagePath = StringUtils.SplitFilePath(FilePath)
    local CodegenLocation = Icons

    if string.match(FilePath, CodegenBasePath) then
        RemoveFromTable(SplitImagePath, #StringUtils.SplitFilePath(CodegenBasePath))
    end

    -- Now we'll loop through the split path and recursively move the pointer ref
    local CurrentLocation: any = CodegenLocation
    for _, Location in SplitImagePath do
        local LocationFileType = StringUtils.FileType(Location)

        if not LocationFileType then -- Directory; no file type
            -- Set dir as a path in codegen when needed
            if not CurrentLocation[Location] then
                CurrentLocation[Location] = {}
            end

            CurrentLocation = CurrentLocation[Location]
        else -- png/jpg etc
            local ImageName = StringUtils.FileNameWithoutExtension(Location)

            local ImageObject = {
                [1] = ImageInfo.id, -- ImageId
            }

            if ImageInfo.slice then
                local ImageRectOffset = ImageInfo.slice[1]
                local ImageRectOffsetBounds = ImageInfo.slice[2]

                local OffsetX, OffsetY = ImageRectOffset[1], ImageRectOffset[2]

                local SizeX = ImageRectOffsetBounds[1] - OffsetX
                local SizeY = ImageRectOffsetBounds[2] - OffsetY

                ImageObject[2] = {SizeX, SizeY} -- ImageRectSize
                ImageObject[3] = {OffsetX, OffsetY} -- ImageRectOffset
            end

            CurrentLocation[ImageName] = ImageObject
        end
    end
end

local IconNames = {}
for _, IconFileName in fs.readDir(IconNameDir) do
    table.insert(IconNames, StringUtils.FileNameWithoutExtension(IconFileName))
end

Log.Info(`Total # of icons: {#IconNames}`)

fs.writeFile(
    CodegenPath,
    `-- This file was automatically @generated and not intended for manual editing\n--!nocheck\n\nreturn ` ..
    LuaEncode(Icons) ..
    "\n"
)

Log.Info(`Wrote to file "{CodegenPath}"`)


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

local ServerStorage = game:GetService("ServerStorage")

local Lucide = require(ServerStorage.Lucide)

local function ApplyToInstance(object: Instance, properties: {[string]: any}): Instance
    if properties then
        for Property, Value in properties do
            if Property ~= "Parent" then
                object[Property] = Value
            end
        end

        if properties.Parent then
            object.Parent = properties.Parent
        end
    end

    return object
end

return function(target: Frame)
    local InnerFrame = ApplyToInstance(Instance.new("ScrollingFrame"), {
        Position = UDim2.fromOffset(20, 20),
        Size = UDim2.new(1, -40, 1, 0),

        BackgroundTransparency = 1,
        BorderSizePixel = 0,

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

    ApplyToInstance(Instance.new("UIGridLayout"), {
        Parent = InnerFrame,

        CellSize = UDim2.fromOffset(48, 48),
        CellPadding = UDim2.fromOffset(10, 10),

        HorizontalAlignment = Enum.HorizontalAlignment.Center,
    })

    InnerFrame.Parent = target

    for _, IconName in Lucide.IconNames do
        Lucide.ImageLabel(IconName, 48, {
            Parent = InnerFrame
        })
    end
end


return {
    PackageVersion = "0.1.3", -- The version of the `lucide-roblox` package
    LucideVersion = "0.363.0", -- The version of the Lucide icon set itself the package ver noted above is on
}


--!nonstrict
--// Initialization

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

--[=[
	@class Overture

	The core of Overture revolves around the management of libraries.

	In Overture, a library is any ModuleScript which is tagged with the CollectionService `oLibrary` tag.
	Overture itself will make no effort to replicate ModuleScripts, so remember to ensure that they are
	accessible to both the server and the client; when this behavior is desired.

	:::info
	Remember, all library names in Overture must be unique!
	:::
]=]
local Overture = {}
local LibraryThreadCache = {}
local Libraries: {[string]: ModuleScript} = {}

--// Functions

--[=[
	@within Overture
	@ignore
]=]
local function Retrieve(InstanceName: string, InstanceClass: string, InstanceParent: Instance, ForceWait: boolean?): Instance
	if ForceWait and RunService:IsRunning() then
		return InstanceParent:WaitForChild(InstanceName)
	end

	local SearchInstance = InstanceParent:FindFirstChild(InstanceName)

	if not SearchInstance then
		SearchInstance = Instance.new(InstanceClass)
		SearchInstance.Name = InstanceName
		SearchInstance.Parent = InstanceParent
	end

	return SearchInstance
end

--[=[
	@within Overture
	@ignore

	@param Tag -- The CollectionService tag
	@param Function -- The function to call
]=]
local function BindToTag(Tag: string, Function: (Instance) -> ()): RBXScriptConnection
	for _, Value in CollectionService:GetTagged(Tag) do
		task.spawn(Function, Value)
	end

	return CollectionService:GetInstanceAddedSignal(Tag):Connect(Function)
end

--[=[
	@within Overture
	@ignore

	@param Module -- The ModuleScript to require
	@param NamedImports -- When provided, returns the named variables instead of the entire ModuleScript
	@return any?
]=]
local function RequireModule(Module: ModuleScript, NamedImports: {string}?)
	if NamedImports then
		local Exports = require(Module)
		local Imports = {}

		for ImportIndex, ImportName in ipairs(NamedImports) do
			Imports[ImportIndex] = Exports[ImportName]
		end

		return unpack(Imports)
	else
		return require(Module)
	end
end

--[=[
	Finds a ModuleScript with the CollectionService `oLibrary` tag,
	and returns the value that was returned by the given ModuleScript,
	running it if it has not been run yet.

	A tagged ModuleScript will only be required when LoadLibrary is called on it.

	The behaviour of this function differs between the server and the client.
	In the server, if no ModuleScript is found with the name provided in `Index`,
	an error is thrown. This is because **modules are expected to be tagged
	before runtime**. In the client, if no ModuleScript is found in the initial
	search, the function will yield until a ModuleScript with a matching name is
	tagged or replicated.

	This function makes use of coroutines and Roblox's task library to yield its
	own thread. This eliminates the processing implications of periodically
	polling every time a module is not yet present in the client, and speeds up
	the process of requiring a library once it is replicated.

	:::caution
	This method will yield when called on the client, but only if the library has
	not been replicated and indexed yet.
	:::

	:::danger
	If the library cannot be found when called on the server, this method will error.
	:::

	@tag Library Management

	@yields
	@param Index -- The name of the ModuleScript
	@param NamedImports -- When provided, returns the named variables instead of the entire ModuleScript
	@return any?

	@error The library `Index` does not exist! -- Thrown on the server, if no ModuleScript exists with the given name.
]=]
function Overture:LoadLibrary(Index: string, NamedImports: {string}?)
	if Libraries[Index] then
		return RequireModule(Libraries[Index], NamedImports)
	else
		assert(not RunService:IsServer(), "The library \"" .. Index .. "\" does not exist!")

		table.insert(LibraryThreadCache, {Thread = coroutine.running(), RequestedIndex = Index, RequestedAt = time()})
		return RequireModule(coroutine.yield(), NamedImports)
	end
end

--[=[
	See [Overture:LoadLibrary] for the arguments to this method.
	This function will return nil when called on the server, regradless if the ModuleScript exists.

	Sugar for:
	```lua
		if RunService:IsClient() then
			Overture:LoadLibrary(...)
		end
	```

	@tag Library Management

	@yields
	@client
	@param ... any
	@return any?
]=]
function Overture:LoadLibraryOnClient(...)
	if RunService:IsClient() then
		return self:LoadLibrary(...)
	end
end

--[=[
	See [Overture:LoadLibrary] for the arguments to this method.
	This function will return nil when called on the client, regradless if the ModuleScript exists.

	Sugar for:
	```lua
		if RunService:IsServer() then
			Overture:LoadLibrary(...)
		end
	```

	@tag Library Management

	@server
	@param ... any
	@return any?
]=]
function Overture:LoadLibraryOnServer(...)
	if RunService:IsServer() then
		return self:LoadLibrary(...)
	end
end

--[=[
	Returns an instance of the specified class.

	If an instance of the same class and name already exists, it will be returned.
	Otherwise, when called from the client, the method will yield until one exists; if called from the server, a new one will be created.

	@tag Instance Retrieval

	@yields
	@param InstanceClass -- The class of the Instance
	@param InstanceName -- The name of the Instance
	@param Parent -- An optional override parent Instance. Useful for retrieving dependencies.
]=]
function Overture:Get(InstanceClass: string, InstanceName: string, Parent: Instance?): Instance
	local SetFolder = (Parent or Retrieve(InstanceClass, "Folder", script, RunService:IsClient()))
	local Item = SetFolder:FindFirstChild(InstanceName)

	if Item then
		return Item
	elseif RunService:IsServer() or not RunService:IsRunning() then
		return Retrieve(InstanceName, InstanceClass, SetFolder)
	else
		return SetFolder:WaitForChild(InstanceName)
	end
end

--[=[
	Returns an instance of the specified class.
	If an instance of the same class and name already exists, it will be returned. Otherwise, a new one will be created.

	This function can be particularly useful to create and manage BindableEvent and BindableFunction instances for client-side communication.

	:::info
	Note that instances created by this function in the client will not replicate to the server, or other clients.
	:::

	@tag Instance Retrieval

	@param InstanceClass -- The class of the Instance
	@param InstanceName -- The name of the Instance
	@param Parent -- An optional override parent Instance. Useful for retrieving dependencies.
]=]
function Overture:GetLocal(InstanceClass: string, InstanceName: string, Parent: Instance?): Instance
	return Retrieve(InstanceName, InstanceClass, (Parent or Retrieve("Local" .. InstanceClass, "Folder", script)))
end

--[=[
	Returns an instance of the specified class.
	If an instance of the same class and name already exists, it will be returned. Otherwise, the method will yield until one exists.

	@tag Instance Retrieval

	@yields
	@param InstanceClass -- The class of the Instance
	@param InstanceName -- The name of the Instance
	@param Parent -- An optional override parent Instance. Useful for retrieving dependencies.
]=]
function Overture:WaitFor(InstanceClass: string, InstanceName: string, Parent: Instance?): Instance
	local IsClient = RunService:IsClient()
	return (Parent or Retrieve(InstanceClass, "Folder", script, IsClient)):WaitForChild(InstanceName, if IsClient then math.huge else nil)
end

task.spawn(BindToTag, "oLibrary", function(Object)
	Libraries[Object.Name] = Object

	for _, Cached in LibraryThreadCache do
		if Object.Name == Cached.RequestedIndex then
			task.defer(Cached.Thread, Object)
			task.delay(1, function()
				table.remove(LibraryThreadCache, table.find(LibraryThreadCache, Cached))
			end)
		end
	end
end)

task.spawn(function()
	while script:GetAttribute("Debug") do
		task.wait(1)

		for _, Cached in LibraryThreadCache do
			if Cached.WarningEmitted then continue end
			if (time() - Cached.RequestedAt) > 5 then
				warn(string.format([[Infinite yield possible on Overture:LoadLibrary("%s").]], Cached.RequestedIndex))
				Cached.WarningEmitted = true
			end
		end
	end
end)

--// Triggers

return Overture


export type ALL_EVENTS =
	"WriteActivityHistoryEventFromStudio"
	| "AnimationPlayed"
	| "Stopped"
	| "DidLoop"
	| "Ended"
	| "KeyframeReached"
	| "UserGuiRenderingChanged"
	| "TopbarTransparencyChangedSignal"
	| "CoreGuiChangedSignal"
	| "OnClientEvent"
	| "OnServerEvent"
	| "Event"
	| "ActionActivatedSignal"
	| "FindVariationsSignal"
	| "SetMaterialSettingsSignal"
	| "SettingChangedSignal"
	| "ShiftToAssetIdSignal"
	| "ShiftVariationSignal"
	| "MouseClick"
	| "MouseHoverEnter"
	| "MouseHoverLeave"
	| "RightMouseClick"
	| "mouseClick"
	| "DragContinue"
	| "DragEnd"
	| "DragStart"
	| "ReceiverConnected"
	| "ReceiverDisconnected"
	| "EventConnected"
	| "EventDisconnected"
	| "SourceValueChanged"
	| "ExpressionAdded"
	| "ExpressionsCleared"
	| "SelectionChanged"
	| "DragBegin"
	| "DragStopped"
	| "InputBegan"
	| "InputChanged"
	| "InputEnded"
	| "MouseEnter"
	| "MouseLeave"
	| "MouseMoved"
	| "MouseWheelBackward"
	| "MouseWheelForward"
	| "SelectionGained"
	| "SelectionLost"
	| "TouchLongPress"
	| "TouchPan"
	| "TouchPinch"
	| "TouchRotate"
	| "TouchSwipe"
	| "TouchTap"
	| "Activated"
	| "MouseButton1Click"
	| "MouseButton1Down"
	| "MouseButton1Up"
	| "MouseButton2Click"
	| "MouseButton2Down"
	| "MouseButton2Up"
	| "FocusLost"
	| "Focused"
	| "ReturnPressedFromOnScreenKeyboard"
	| "Loaded"
	| "Paused"
	| "Played"
	| "PluginDragDropped"
	| "PluginDragEntered"
	| "PluginDragLeft"
	| "PluginDragMoved"
	| "WindowFocusReleased"
	| "WindowFocused"
	| "adGuiStateChanged"
	| "BrowserWindowClosed"
	| "CloseInspectMenuRequest"
	| "CoreGuiRenderOverflowed"
	| "EmotesMenuOpenChanged"
	| "ErrorMessageChanged"
	| "GuiVisibilityChangedSignal"
	| "InspectMenuEnabledChangedSignal"
	| "InspectPlayerFromHumanoidDescriptionRequest"
	| "InspectPlayerFromUserIdWithCtxRequest"
	| "KeyPressed"
	| "MenuClosed"
	| "MenuOpened"
	| "NativeClose"
	| "NetworkPausedEnabledChanged"
	| "Open9SliceEditor"
	| "OpenStyleEditor"
	| "PurchasePromptShown"
	| "SafeZoneOffsetsChanged"
	| "ShowLeaveConfirmation"
	| "SpecialKeyPressed"
	| "UiMessageChanged"
	| "LocalSimulationTouched"
	| "OutfitChanged"
	| "StoppedTouching"
	| "TouchEnded"
	| "Touched"
	| "OnConvertToPackageResult"
	| "OnOpenConvertToPackagePlugin"
	| "PromptButtonHoldBegan"
	| "PromptButtonHoldEnded"
	| "PromptHidden"
	| "PromptShown"
	| "TriggerEnded"
	| "Triggered"
	| "PromptTriggerEnded"
	| "PromptTriggered"
	| "TrackerError"
	| "TrackerPrompt"
	| "ActiveUserSignedOut"
	| "PageEnter"
	| "PageLeave"
	| "Changed"
	| "changed"
	| "Destroying"

return 0


local storybook = {
	name = "Examples",
	storyRoots = {
		script.Parent,
	},
}

return storybook


-- elements created by functions can take parameters
-- Visible is a state that that tells the tooltip to show
return function(Scope, tool_text: string?, Hovering)
	local New = Scope.New
	local Spring = Scope.Animations.Spring
	local text = tool_text or "ToolTip"

	local Visible = Scope.State(function(use)
		if use(Hovering) == 1 then
			for i = 1, 30 do
				task.wait()
				if Hovering() == 0 then
					return 0
				end
			end
			if Hovering() == 1 then
				return 1
			end
		end

		return 0
	end)

	Visible(0)

	local Tip = New "TextLabel" {
		Text = "",
		TextSize = 10,
		Size = UDim2.fromOffset(100, 25),
		TextColor3 = Color3.new(1, 1, 1),
		BackgroundColor3 = Color3.new(),
		BackgroundTransparency = 0.5,
		AnchorPoint = Vector2.new(0, 0.5),

		children = New "UICorner" {
			CornerRadius = UDim.new(0, 5),
		},
	}

	Visible.connect(function(value)
		if value == 1 then
			Tip.Text = text
		else
			Tip.Text = ""
		end
	end)

	Spring(0, Visible, 0.9, 0.1):BindAndPlay(function(value: number)
		Tip.Size = UDim2.fromScale(1 + (0.05 * #text), value / 2)
		Tip.Position = UDim2.fromScale(value * 1.1, 0.5)
	end)

	return Tip
end


local ReplicatedStorage = game:GetService "ReplicatedStorage"

local Button = require(script.Button)
local Quark = require(ReplicatedStorage.Shared.Quark)
local New = Quark.New

-- The Story
return function(Parent)
	local ButtonData: { [string]: string } = {
		["Continue"] = "Continue the game",
		["New Game"] = "Start a new adventure",
		["Quit"] = "Commit die!",
	}
	local SpawnedButtons = 0

	-- Main frame to contain all the buttons
	New "Frame" {
		Parent = Parent,
		Position = UDim2.fromOffset(20, 20),
		Size = UDim2.fromOffset(500, 200),
		BackgroundTransparency = 1,

		-- Children (list layout)
		children = {
			New "UIListLayout" {
				Padding = UDim.new(0, 10),
				SortOrder = "LayoutOrder",
			},
			-- Tip text
			New "TextLabel" {
				Text = "Hover over the buttons. Wait a little to see the tooltip.",
				TextColor3 = Color3.new(1, 1, 1),
				Size = UDim2.new(1, 0, 0, 20),
				BackgroundTransparency = 0.5,
				BackgroundColor3 = Color3.new(),
				TextXAlignment = "Left",
			},

			-- Spawn buttons using Entries
			Quark.Entries(ButtonData, function(Scope, i, v)
				SpawnedButtons += 1
				return Button(Scope, i, v, (SpawnedButtons - 1) / 10)
			end):Push(New "UIListLayout" {
				Padding = UDim.new(0, 10),
				SortOrder = "LayoutOrder",
			}),
		},
	}

	-- CLEANUP
	return Quark.Cleanup
end


local ReplicatedStorage = game:GetService "ReplicatedStorage"

local Quark = require(ReplicatedStorage.Shared.Quark)
local New = Quark.New
local State = Quark.State
local Hook = Quark.Hook

return function(parent)
	local Counter = State(0)

	-- Tip text
	New "TextLabel" {
		Text = "Click the buttons. These update automatically using the State.",
		TextColor3 = Color3.new(1, 1, 1),
		Size = UDim2.new(1, 0, 0, 20),
		BackgroundTransparency = 0.5,
		BackgroundColor3 = Color3.new(),
		TextXAlignment = "Left",
		Parent = parent,
	}

	New "TextButton" {
		Parent = parent,
		Size = UDim2.fromOffset(200, 50),
		Position = UDim2.fromOffset(0, 25),

		-- Computeds are states that `use` other states.
		-- Automatically update when the `used` state updates.
		Text = State(function(use)
			return `Counter: {use(Counter)}`
		end),

		-- Click event increments counter
		Hook("MouseButton1Click", function()
			Counter(Counter() + 1)
		end),
	}

	New "TextButton" {
		Parent = parent,
		Size = UDim2.fromOffset(200, 50),
		Position = UDim2.fromOffset(0, 85),

		-- Same thing as the computed, just only for string formats.
		-- it replaces `%s` with the value of the Counter.
		-- all other arguments after the first translate to extra `string.format` arguments.
		Text = Counter.compString "[computed state] Counter: %s",

		-- Click event increments counter
		Hook("MouseButton1Click", function()
			Counter(Counter() + 1)
		end),
	}

	return Quark.Cleanup
end


local ReplicatedStorage = game:GetService "ReplicatedStorage"

local Quark = require(ReplicatedStorage.Shared.Quark)
local New = Quark.New
local Entries = Quark.Entries
local Hook = Quark.Hook

return function(parent: Instance)
	local E = Entries(10, function(Scope, i: number)
		return Scope.New "TextLabel" {
			Position = UDim2.fromOffset(0, 20 * i + 50),
			Size = UDim2.fromOffset(100, 20),
			Text = `Label: {i}`,
			BackgroundColor3 = Color3.new(1, 0.5, 0.2),
		}
	end)

	New "Frame" {
		Parent = parent,
		children = {
			New "TextButton" {
				Text = "Click me!",
				Size = UDim2.fromOffset(200, 50),
				Hook("Activated", E.Push),
				BackgroundColor3 = Color3.new(0.196078, 0.705882, 1.000000),
			},
			E,
		},
	}

	return Quark.Cleanup
end


return function(Scope)
	local New = Scope.New
    local Main = New "Frame" {
        Size = UDim2.new(1, 0, 0, 30),
        BackgroundTransparency = 1
    }

    local Button = New "TextButton" {
        Text = "New Note",
        BackgroundTransparency = 0.9,
        TextColor3 = Color3.new(1,1,1),
        Size = UDim2.fromScale(1, 1)
    }

    Main:PushChildren {
        Button
    }
    return Main
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Quark = require(ReplicatedStorage.Shared.Quark)

return function(Scope: Quark.Scope, text: string?)
	local New = Scope.New
	local Spring = Scope.Animations.Spring
	local Hook = Scope.Hook

	local Main = New "Frame" {
		Size = UDim2.new(1, -10, 0, 50),
		BackgroundTransparency = 1,
	}

	local Hovering = Scope.State(0)
    local hover_spring: Quark.Spring<any> = nil

	local Note = New "TextButton" {
		Text = "",
        AutoButtonColor = false,
		Size = UDim2.fromScale(0, 0),
		BackgroundTransparency = 0.9,
		AnchorPoint = Vector2.one * 0.5,
		Position = UDim2.fromScale(0.5, 0.5),
		Parent = Main,

        Hook("MouseEnter", Hovering.setLambda(1)),
        Hook("MouseLeave", Hovering.setLambda(0)),

        Hook("MouseButton1Down", function(self)
			self.BackgroundColor3 = Color3.new(1,0.1,0.2)
			Hovering(0)
		end),
        Hook("MouseButton1Up", Hovering.setLambda(1)),

        Hook("MouseButton1Click", function(self)
            Main.Object:Destroy()
            hover_spring:DisconnectAll()
        end),

        children = {
            New "TextLabel" {
				Text = text or "Note Title",
                Size = UDim2.new(1, 0, 0, 20),
                BackgroundTransparency = 1,
                TextSize = 10,
                TextColor3 = Color3.new(1,1,1),

                children = {
                    New "UIPadding" {
                        PaddingTop = UDim.new(0, 10)
                    }
                }
            }
        }
	}

	coroutine.wrap(function()
		local InitAnim = Spring(0, 1, 0.5, 0.4)
        InitAnim:BindAndPlay(function(value: number)
			Note.Size = UDim2.fromScale(1, value)
		end)

        task.wait(0.2)
        InitAnim:DisconnectAll()

        hover_spring = Spring(0, Hovering, 0.5, 0.5):BindAndPlay(function(value)
            Note.Size = UDim2.fromScale(1 + value/15, 1 + value/10)
            Note.BackgroundTransparency = 0.9 - value / 10
        end)
	end)()

	return Main
end


local Note = require(script.Parent.Note)

return function(Scope)
	local New = Scope.New
    local Main = New "Frame" {
        Size = UDim2.fromScale(1, 1),
        BackgroundColor3 = Color3.new(),
        BackgroundTransparency = 0.85,
        BorderSizePixel = 0,

        children = {
            New "UIListLayout" {
                Padding = UDim.new(0, 10),
                HorizontalAlignment = Enum.HorizontalAlignment.Center,
            },
            New "UIPadding" {
                PaddingTop = UDim.new(0, 5)
            }
        }
    }

	local NoteScope = Scope.Scope()
    local function AddNote(text: string?)
        local note = Note(NoteScope, text)
        Main:Push(note)
    end

    return Main, AddNote
end


local ReplicatedStorage = game:GetService "ReplicatedStorage"

local Quark = require(ReplicatedStorage.Shared.Quark)
local New = Quark.New

local NewNote = require(script.NewNote)
local NoteHolder = require(script.NoteHolder)

-- Slightly more advanced, I suggest you look into `Button_ToolTip.story` first.

local controls = {
	Title = "Hello World",
}
local story = function(props)
	-- Tip text
	New "TextLabel" {
		Text = "Click `New Note` to add a note. Set the text using the Story Controls.",
		TextColor3 = Color3.new(1, 1, 1),
		Size = UDim2.new(1, 0, 0, 20),
		BackgroundTransparency = 0.5,
		BackgroundColor3 = Color3.new(),
		TextXAlignment = "Left",
		Parent = props.target,
	}

	local Background = New "Frame" {
		Size = UDim2.new(0, 200, 1, 0),
		BackgroundColor3 = Color3.new(0.1, 0.1, 0.1),
		Position = UDim2.fromOffset(0, 25),

		Parent = props.target,
		children = {
			New "UIListLayout" {
				Padding = UDim.new(0, 10),
			},
			New "UIPadding" {
				PaddingLeft = UDim.new(0, 10),
				PaddingRight = UDim.new(0, 10),
				PaddingTop = UDim.new(0, 10),
				PaddingBottom = UDim.new(0, 10),
			},
		},
	}

	local NoteScope = Quark.Scope()
	local NewNoteButton = NewNote(NoteScope)
	local NoteHolder, AddNote = NoteHolder(NoteScope)

	Background:PushChildren {
		NewNoteButton,
		NoteHolder,
	}

	props.subscribe(function(values, infos)
		controls.Title = infos.Title.__new
	end)
	NewNoteButton.TextButton.MouseButton1Click:Connect(function()
		AddNote(controls.Title .. ` | {#NoteHolder.Children - 1}`)
	end)

	return Quark.Cleanup
end

return {
	controls = controls,
	render = story,
}


local ReplicatedStorage = game:GetService "ReplicatedStorage"

local Quark = require(ReplicatedStorage.Shared.Quark)
local New = Quark.New
local Hook = Quark.Hook

return function(parent: Instance)
	-- This state controls text.
	-- Text will auto-update when set to this state.
	local Hovering = Quark.State "Hover over me"

	-- Tip text
	New "TextLabel" {
		Text = "Set a `State` as a property and it will update when the State changes.",
		TextColor3 = Color3.new(1, 1, 1),
		Size = UDim2.new(1, 0, 0, 20),
		BackgroundTransparency = 0.5,
		BackgroundColor3 = Color3.new(),
		TextXAlignment = "Left",
		Parent = parent,
	}

	New "TextLabel" {
		Text = "Use `setLambda(value)` to return a lambda function for value setting.",
		Position = UDim2.fromOffset(0, 20),
		TextColor3 = Color3.new(1, 1, 1),
		Size = UDim2.new(1, 0, 0, 20),
		BackgroundTransparency = 0.5,
		BackgroundColor3 = Color3.new(),
		TextXAlignment = "Left",
		Parent = parent,
	}

	New "TextLabel" {
		Size = UDim2.fromOffset(150, 50),
		Position = UDim2.fromOffset(0, 45),
		BackgroundColor3 = Color3.new(1, 1, 1),
		Text = Hovering,
		Parent = parent,

		-- `setLambda()` returns a function which updates Hovering.
		-- It also takes the new value.
		Hook("MouseEnter", Hovering.setLambda "OMG HI!"),
		Hook("MouseLeave", Hovering.setLambda "Hover over me"),
	}

	return Quark.Cleanup
end


local ReplicatedStorage = game:GetService "ReplicatedStorage"

local Quark = require(ReplicatedStorage.Shared.Quark)
local Spring = Quark.Animations.Spring
local New = Quark.New
local State = Quark.State
local Hook = Quark.Hook

-- Spring settings can be States
local SpringSettings = {
	Elasticity = State(0.1),
	Stiffness = State(0.7),
}

-- Control initial definitions
local controls = {
	Elasticity = 0.1,
	Stiffness = 0.7,
}

-- Since we're using controls, let's make an advanced story.
return {
	controls = controls,
	render = function(props)
		-- Hovering will be a state so that it updates automatically
		local Hovering = State(UDim2.fromScale(0.05, 0.1))

		local size_spring = Spring(
			Hovering(), -- Set initial start to state VALUE
			Hovering, -- Set target to the STATE (So it updates automatically)
			SpringSettings.Elasticity, -- We use the States defined in SpringSettings
			SpringSettings.Stiffness
		)

		-- Tip text
		New "TextLabel" {
			Text = "Custom Quark Springs update when the target state updates. (They use Elasticity, Stiffness)",
			TextColor3 = Color3.new(1, 1, 1),
			Size = UDim2.new(1, 0, 0, 20),
			BackgroundTransparency = 0.5,
			BackgroundColor3 = Color3.new(),
			TextXAlignment = "Left",
			Parent = props.target,
		}

		-- Create a button
		New "TextButton" {
			Position = UDim2.fromOffset(0, 25),
			Parent = props.target,
			Size = size_spring:Play(),

			-- setLamda returns a function telling it to update
			Hook("MouseEnter", Hovering.setLambda(UDim2.fromScale(0.1, 0.2))),
			-- Use Hovering's initial value (by calling it)
			Hook("MouseLeave", Hovering.setLambda(Hovering())),
		}

		-- subscribe to the story controls
		props.subscribe(function(_, info)
			SpringSettings.Elasticity(info.Elasticity.__new)
			SpringSettings.Stiffness(info.Stiffness.__new)
		end)

		-- CLEANUP
		return Quark.Cleanup
	end,
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Quark = require(ReplicatedStorage.Shared.Quark)
local New = Quark.New

return Quark.CreateStory(function()
	local baseSCOPE = Quark.Scope()
	local ScopeA = baseSCOPE.Scope()
	local ScopeB = baseSCOPE.Scope()

	print(Quark._CreatedScopes)

	return New "Frame" {
		children = {
			ScopeA.New "TextButton" {
				Size = UDim2.fromOffset(50, 50),
				BackgroundColor3 = Color3.new(1),

				ScopeA.Hook("Activated", ScopeA.Cleanup)
			},

			ScopeB.New "Frame" {
				Position = UDim2.fromOffset(100, 0),
				BackgroundColor3 = Color3.new(0, 0, 1)
			}
		}
	}
end)